Added ability for commands to post properties to be used as params of other commands.

Starting to add monitor GUI.

Change-Id: I9fcf1568d0de27dfd1c19e875f8646fd731a1dfa
diff --git a/tools/test/bin/stc-launcher b/tools/test/bin/stc-launcher
index d5da4f1..6e473cd 100755
--- a/tools/test/bin/stc-launcher
+++ b/tools/test/bin/stc-launcher
@@ -3,6 +3,8 @@
 #   System Test Coordinator process launcher
 #-------------------------------------------------------------------------------
 
+#sleep 5 && exit 0;
+
 env=$1 && shift
 cwd=$1 && shift
 
diff --git a/tools/test/scenarios/example.xml b/tools/test/scenarios/example.xml
new file mode 100644
index 0000000..6580314
--- /dev/null
+++ b/tools/test/scenarios/example.xml
@@ -0,0 +1,19 @@
+<!--
+  ~ Copyright 2015 Open Networking Laboratory
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<scenario name="example" description="Example">
+    <step name="One" exec="echo @stc foo=bar"/>
+    <step name="Two" requires="One" exec="echo ${foo}"/>
+</scenario>
diff --git a/tools/test/scenarios/net-pingall.xml b/tools/test/scenarios/net-pingall.xml
index babdfa6..df1ae1f 100644
--- a/tools/test/scenarios/net-pingall.xml
+++ b/tools/test/scenarios/net-pingall.xml
@@ -28,7 +28,7 @@
         <step name="Check-Summary-For-Hosts" requires="~Ping-All-And-Verify"
               exec="onos-check-summary ${OC1} [0-9]* 25 140 25"/>
 
-        <step name="Config-Topo" requires="Check-Summary-For-Hosts"
+        <step name="Config-Topo" requires="~Check-Summary-For-Hosts"
               exec="onos-topo-cfg ${OC1} ${ONOS_ROOT}/tools/test/topos/attmpls.json"/>
     </group>
 </scenario>
\ No newline at end of file
diff --git a/utils/stc/pom.xml b/utils/stc/pom.xml
index 6785ce9..a3f9643 100644
--- a/utils/stc/pom.xml
+++ b/utils/stc/pom.xml
@@ -53,6 +53,19 @@
         </dependency>
 
         <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+            <version>2.4.2</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-annotations</artifactId>
+            <version>2.4.2</version>
+            <scope>compile</scope>
+        </dependency>
+
+        <dependency>
             <groupId>org.eclipse.jetty</groupId>
             <artifactId>jetty-server</artifactId>
             <version>8.1.17.v20150415</version>
diff --git a/utils/stc/src/main/java/org/onlab/stc/Compiler.java b/utils/stc/src/main/java/org/onlab/stc/Compiler.java
index 2d6fafa..162e8df 100644
--- a/utils/stc/src/main/java/org/onlab/stc/Compiler.java
+++ b/utils/stc/src/main/java/org/onlab/stc/Compiler.java
@@ -61,8 +61,8 @@
     private static final String FILE = "[@file]";
     private static final String NAMESPACE = "[@namespace]";
 
-    private static final String PROP_START = "${";
-    private static final String PROP_END = "}";
+    static final String PROP_START = "${";
+    static final String PROP_END = "}";
     private static final String HASH = "#";
 
     private final Scenario scenario;
@@ -230,7 +230,7 @@
     private void processStep(HierarchicalConfiguration cfg,
                              String namespace, Group parentGroup) {
         String name = expand(prefix(cfg.getString(NAME), namespace));
-        String command = expand(cfg.getString(COMMAND, parentGroup != null ? parentGroup.command() : null));
+        String command = expand(cfg.getString(COMMAND, parentGroup != null ? parentGroup.command() : null), true);
         String env = expand(cfg.getString(ENV, parentGroup != null ? parentGroup.env() : null));
         String cwd = expand(cfg.getString(CWD, parentGroup != null ? parentGroup.cwd() : null));
 
@@ -249,7 +249,7 @@
     private void processGroup(HierarchicalConfiguration cfg,
                               String namespace, Group parentGroup) {
         String name = expand(prefix(cfg.getString(NAME), namespace));
-        String command = expand(cfg.getString(COMMAND, parentGroup != null ? parentGroup.command() : null));
+        String command = expand(cfg.getString(COMMAND, parentGroup != null ? parentGroup.command() : null), true);
         String env = expand(cfg.getString(ENV, parentGroup != null ? parentGroup.env() : null));
         String cwd = expand(cfg.getString(CWD, parentGroup != null ? parentGroup.cwd() : null));
 
@@ -388,13 +388,14 @@
     }
 
     /**
-     * Expands any environment variables in the specified
-     * string. These are specified as ${property} tokens.
+     * Expands any environment variables in the specified string. These are
+     * specified as ${property} tokens.
      *
-     * @param string string to be processed
+     * @param string     string to be processed
+     * @param keepTokens true if the original unresolved tokens should be kept
      * @return original string with expanded substitutions
      */
-    private String expand(String string) {
+    private String expand(String string, boolean... keepTokens) {
         if (string == null) {
             return null;
         }
@@ -421,7 +422,11 @@
                     value = System.getenv(prop);
                 }
             }
-            sb.append(value != null ? value : "");
+            if (value == null && keepTokens.length == 1 && keepTokens[0]) {
+                sb.append("${").append(prop).append("}");
+            } else {
+                sb.append(value != null ? value : "");
+            }
             last = end + 1;
         }
         sb.append(pString.substring(last));
diff --git a/utils/stc/src/main/java/org/onlab/stc/Coordinator.java b/utils/stc/src/main/java/org/onlab/stc/Coordinator.java
index a26e9fe..6f79764 100644
--- a/utils/stc/src/main/java/org/onlab/stc/Coordinator.java
+++ b/utils/stc/src/main/java/org/onlab/stc/Coordinator.java
@@ -16,16 +16,24 @@
 package org.onlab.stc;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 
 import java.io.File;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutorService;
+import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
+import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 import static java.util.concurrent.Executors.newFixedThreadPool;
+import static org.onlab.stc.Compiler.PROP_END;
+import static org.onlab.stc.Compiler.PROP_START;
 import static org.onlab.stc.Coordinator.Directive.*;
 import static org.onlab.stc.Coordinator.Status.*;
 
@@ -44,6 +52,10 @@
     private final CountDownLatch latch;
     private final ScenarioStore store;
 
+    private static final Pattern PROP_ERE = Pattern.compile("^@stc ([a-zA-Z0-9_.]+)=(.*$)");
+    private final Map<String, String> properties = Maps.newConcurrentMap();
+    private final Function<String, String> substitutor = this::substitute;
+
     private final Set<StepProcessListener> listeners = Sets.newConcurrentHashSet();
     private File logDir;
 
@@ -208,10 +220,11 @@
             store.markStarted(step);
             if (step instanceof Group) {
                 Group group = (Group) step;
-                delegate.onStart(group);
+                delegate.onStart(group, null);
                 executeRoots(group);
             } else {
-                executor.execute(new StepProcessor(step, logDir, delegate));
+                executor.execute(new StepProcessor(step, logDir, delegate,
+                                                   substitutor));
             }
         } else if (directive == SKIP) {
             if (step instanceof Group) {
@@ -278,6 +291,43 @@
     }
 
     /**
+     * Expands the var references with values from the properties map.
+     *
+     * @param string string to perform substitutions on
+     */
+    private String substitute(String string) {
+        StringBuilder sb = new StringBuilder();
+        int start, end, last = 0;
+        while ((start = string.indexOf(PROP_START, last)) >= 0) {
+            end = string.indexOf(PROP_END, start + PROP_START.length());
+            checkArgument(end > start, "Malformed property in %s", string);
+            sb.append(string.substring(last, start));
+            String prop = string.substring(start + PROP_START.length(), end);
+            String value = properties.get(prop);
+            sb.append(value != null ? value : "");
+            last = end + 1;
+        }
+        sb.append(string.substring(last));
+        return sb.toString().replace('\n', ' ').replace('\r', ' ');
+    }
+
+    /**
+     * Scrapes the line of output for any variables to be captured and posted
+     * in the properties for later use.
+     *
+     * @param line line of output to scrape for property exports
+     */
+    private void scrapeForVariables(String line) {
+        Matcher matcher = PROP_ERE.matcher(line);
+        if (matcher.matches()) {
+            String prop = matcher.group(1);
+            String value = matcher.group(2);
+            properties.put(prop, value);
+        }
+    }
+
+
+    /**
      * Prints formatted output.
      *
      * @param format printf format string
@@ -291,10 +341,9 @@
      * Internal delegate to monitor the process execution.
      */
     private class Delegate implements StepProcessListener {
-
         @Override
-        public void onStart(Step step) {
-            listeners.forEach(listener -> listener.onStart(step));
+        public void onStart(Step step, String command) {
+            listeners.forEach(listener -> listener.onStart(step, command));
         }
 
         @Override
@@ -307,9 +356,9 @@
 
         @Override
         public void onOutput(Step step, String line) {
+            scrapeForVariables(line);
             listeners.forEach(listener -> listener.onOutput(step, line));
         }
-
     }
 
 }
diff --git a/utils/stc/src/main/java/org/onlab/stc/Main.java b/utils/stc/src/main/java/org/onlab/stc/Main.java
index 01ebe36..310c96e 100644
--- a/utils/stc/src/main/java/org/onlab/stc/Main.java
+++ b/utils/stc/src/main/java/org/onlab/stc/Main.java
@@ -54,6 +54,7 @@
     private String runToPatterns = "";
 
     private Coordinator coordinator;
+    private Monitor monitor;
     private Listener delegate = new Listener();
 
     private static boolean useColor = Objects.equals("true", System.getenv("stcColor"));
@@ -105,13 +106,16 @@
             Compiler compiler = new Compiler(scenario);
             compiler.compile();
 
-            // Execute process flow
+            // Setup the process flow coordinator
             coordinator = new Coordinator(scenario, compiler.processFlow(),
                                           compiler.logDir());
             coordinator.addListener(delegate);
 
-            startMonitorServer();
+            // Prepare the GUI monitor
+            monitor = new Monitor(coordinator, compiler);
+            startMonitorServer(monitor);
 
+            // Execute process flow
             processCommand();
 
         } catch (FileNotFoundException e) {
@@ -120,11 +124,12 @@
     }
 
     // Initiates a web-server for the monitor GUI.
-    private static void startMonitorServer() {
+    private static void startMonitorServer(Monitor monitor) {
         org.eclipse.jetty.util.log.Log.setLog(new NullLogger());
         Server server = new Server(9999);
         ServletHandler handler = new ServletHandler();
         server.setHandler(handler);
+        MonitorWebSocketServlet.setMonitor(monitor);
         handler.addServletWithMapping(MonitorWebSocketServlet.class, "/*");
         try {
             server.start();
@@ -187,8 +192,8 @@
      */
     private static class Listener implements StepProcessListener {
         @Override
-        public void onStart(Step step) {
-            logStatus(currentTimeMillis(), step.name(), IN_PROGRESS, step.command());
+        public void onStart(Step step, String command) {
+            logStatus(currentTimeMillis(), step.name(), IN_PROGRESS, command);
         }
 
         @Override
diff --git a/utils/stc/src/main/java/org/onlab/stc/Monitor.java b/utils/stc/src/main/java/org/onlab/stc/Monitor.java
new file mode 100644
index 0000000..4e6f63f
--- /dev/null
+++ b/utils/stc/src/main/java/org/onlab/stc/Monitor.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onlab.stc;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.collect.Maps;
+import org.onlab.stc.MonitorLayout.Box;
+
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Map;
+
+import static org.onlab.stc.Coordinator.Status.IN_PROGRESS;
+
+/**
+ * Scenario test monitor.
+ */
+public class Monitor implements StepProcessListener {
+
+    private final ObjectMapper mapper = new ObjectMapper();
+
+    private final Coordinator coordinator;
+    private final Compiler compiler;
+    private final MonitorLayout layout;
+
+    private MonitorDelegate delegate;
+
+    private Map<Step, Box> boxes = Maps.newHashMap();
+
+    /**
+     * Creates a new shared process flow monitor.
+     *
+     * @param coordinator process flow coordinator
+     * @param compiler    scenario compiler
+     */
+    Monitor(Coordinator coordinator, Compiler compiler) {
+        this.coordinator = coordinator;
+        this.compiler = compiler;
+        this.layout = new MonitorLayout(compiler);
+        coordinator.addListener(this);
+    }
+
+    /**
+     * Sets the process monitor delegate.
+     *
+     * @param delegate process monitor delegate
+     */
+    void setDelegate(MonitorDelegate delegate) {
+        this.delegate = delegate;
+    }
+
+    /**
+     * Notifies the process monitor delegate with the specified event.
+     *
+     * @param event JSON event data
+     */
+    public void notify(ObjectNode event) {
+        if (delegate != null) {
+            delegate.notify(event);
+        }
+    }
+
+    /**
+     * Returns the scenario process flow as JSON data.
+     *
+     * @return scenario process flow data
+     */
+    ObjectNode scenarioData() {
+        ObjectNode root = mapper.createObjectNode();
+        ArrayNode steps = mapper.createArrayNode();
+        ArrayNode requirements = mapper.createArrayNode();
+
+        ProcessFlow pf = compiler.processFlow();
+        pf.getVertexes().forEach(step -> add(step, steps));
+        pf.getEdges().forEach(requirement -> add(requirement, requirements));
+
+        root.set("steps", steps);
+        root.set("requirements", requirements);
+
+        try (FileWriter fw = new FileWriter("/tmp/data.json");
+             PrintWriter pw = new PrintWriter(fw)) {
+            pw.println(root.toString());
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        return root;
+    }
+
+
+    private void add(Step step, ArrayNode steps) {
+        Box box = layout.get(step);
+        ObjectNode sn = mapper.createObjectNode()
+                .put("name", step.name())
+                .put("isGroup", step instanceof Group)
+                .put("status", status(coordinator.getStatus(step)))
+                .put("tier", box.tier())
+                .put("depth", box.depth());
+        if (step.group() != null) {
+            sn.put("group", step.group().name());
+        }
+        steps.add(sn);
+    }
+
+    private String status(Coordinator.Status status) {
+        return status.toString().toLowerCase();
+    }
+
+    private void add(Dependency requirement, ArrayNode requirements) {
+        ObjectNode rn = mapper.createObjectNode();
+        rn.put("src", requirement.src().name())
+                .put("dst", requirement.dst().name())
+                .put("isSoft", requirement.isSoft());
+        requirements.add(rn);
+    }
+
+    @Override
+    public void onStart(Step step, String command) {
+        notify(event(step, status(IN_PROGRESS)));
+    }
+
+    @Override
+    public void onCompletion(Step step, Coordinator.Status status) {
+        notify(event(step, status(status)));
+    }
+
+    @Override
+    public void onOutput(Step step, String line) {
+
+    }
+
+    private ObjectNode event(Step step, String status) {
+        ObjectNode event = mapper.createObjectNode()
+                .put("name", step.name())
+                .put("status", status);
+        return event;
+    }
+
+}
diff --git a/utils/stc/src/main/java/org/onlab/stc/MonitorDelegate.java b/utils/stc/src/main/java/org/onlab/stc/MonitorDelegate.java
new file mode 100644
index 0000000..d11542a
--- /dev/null
+++ b/utils/stc/src/main/java/org/onlab/stc/MonitorDelegate.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onlab.stc;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+/**
+ * Delegate to which monitor can send notifications.
+ */
+public interface MonitorDelegate {
+
+    /**
+     * Issues JSON event to be sent to any connected monitor clients.
+     *
+     * @param event JSON event data
+     */
+    void notify(ObjectNode event);
+}
diff --git a/utils/stc/src/main/java/org/onlab/stc/MonitorLayout.java b/utils/stc/src/main/java/org/onlab/stc/MonitorLayout.java
new file mode 100644
index 0000000..1c0e731
--- /dev/null
+++ b/utils/stc/src/main/java/org/onlab/stc/MonitorLayout.java
@@ -0,0 +1,307 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onlab.stc;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+/**
+ * Computes scenario process flow layout for the Monitor GUI.
+ */
+public class MonitorLayout {
+
+    public static final int WIDTH = 210;
+    public static final int HEIGHT = 30;
+    public static final int W_GAP = 40;
+    public static final int H_GAP = 50;
+    public static final int SLOT_WIDTH = WIDTH + H_GAP;
+
+    private final Compiler compiler;
+    private final ProcessFlow flow;
+
+    private Map<Step, Box> boxes = Maps.newHashMap();
+
+    /**
+     * Creates a new shared process flow monitor.
+     *
+     * @param compiler scenario compiler
+     */
+    MonitorLayout(Compiler compiler) {
+        this.compiler = compiler;
+        this.flow = compiler.processFlow();
+
+        // Extract the flow and create initial bounding boxes.
+        boxes.put(null, new Box(null, 0));
+        flow.getVertexes().forEach(this::createBox);
+
+        computeLayout(null, 0, 1);
+    }
+
+    // Computes the graph layout giving preference to group associations.
+    private void computeLayout(Group group, int absoluteTier, int tier) {
+        Box box = boxes.get(group);
+
+        // Find all children of the group, or items with no group if at top.
+        Set<Step> children = group != null ? group.children() :
+                flow.getVertexes().stream().filter(s -> s.group() == null)
+                        .collect(Collectors.toSet());
+
+        children.forEach(s -> visit(s, absoluteTier, 1, group));
+
+        // Figure out what the group root vertexes are.
+        Set<Step> roots = findRoots(group);
+
+        // Compute the boxes for each of the roots.
+        roots.forEach(s -> updateBox(s, absoluteTier + 1, 1, group));
+
+        // Update the tier and depth of the group bounding box.
+        computeTiersAndDepth(group, box, absoluteTier, tier, children);
+
+        // Compute the minimum breadth of this group's bounding box.
+        computeBreadth(group, box, children);
+
+        // Compute child placements
+        computeChildPlacements(group, box, children);
+    }
+
+    // Updates the box for the specified step, given the tier number, which
+    // is relative to the parent.
+    private Box updateBox(Step step, int absoluteTier, int tier, Group group) {
+        Box box = boxes.get(step);
+        if (step instanceof Group) {
+            computeLayout((Group) step, absoluteTier, tier);
+        } else {
+            box.setTierAndDepth(absoluteTier, tier, 1, group);
+        }
+
+        // Follow the steps downstream of this one.
+        follow(step, absoluteTier + box.depth(), box.tier() + box.depth());
+        return box;
+    }
+
+    // Backwards follows edges leading towards the specified step to visit
+    // the source vertex and compute layout of those vertices that had
+    // sufficient number of visits to compute their tier.
+    private void follow(Step step, int absoluteTier, int tier) {
+        Group from = step.group();
+        flow.getEdgesTo(step).stream()
+                .filter(d -> visit(d.src(), absoluteTier, tier, from))
+                .forEach(d -> updateBox(d.src(), absoluteTier, tier, from));
+    }
+
+    // Visits each step, records maximum tier and returns true if this
+    // was the last expected visit.
+    private boolean visit(Step step, int absoluteTier, int tier, Group from) {
+        Box box = boxes.get(step);
+        return box.visitAndLatchMaxTier(absoluteTier, tier, from);
+    }
+
+    // Computes the absolute and relative tiers and the depth of the group
+    // bounding box.
+    private void computeTiersAndDepth(Group group, Box box,
+                                      int absoluteTier, int tier, Set<Step> children) {
+        int depth = children.stream().mapToInt(this::bottomMostTier).max().getAsInt();
+        box.setTierAndDepth(absoluteTier, tier, depth, group);
+    }
+
+    // Returns the bottom-most tier this step occupies relative to its parent.
+    private int bottomMostTier(Step step) {
+        Box box = boxes.get(step);
+        return box.tier() + box.depth();
+    }
+
+    // Computes breadth of the specified group.
+    private void computeBreadth(Group group, Box box, Set<Step> children) {
+        if (box.breadth() == 0) {
+            // Scan through all tiers and determine the maximum breadth of each.
+            IntStream.range(1, box.depth)
+                    .forEach(t -> computeTierBreadth(t, box, children));
+            box.latchBreadth(children.stream()
+                                     .mapToInt(s -> boxes.get(s).breadth())
+                                     .max().getAsInt());
+        }
+    }
+
+    // Computes tier width.
+    private void computeTierBreadth(int t, Box box, Set<Step> children) {
+        box.latchBreadth(children.stream().map(boxes::get)
+                                 .filter(b -> isSpanningTier(b, t))
+                                 .mapToInt(Box::breadth).sum());
+    }
+
+    // Computes the actual child box placements relative to the parent using
+    // the previously established tier, depth and breadth attributes.
+    private void computeChildPlacements(Group group, Box box,
+                                        Set<Step> children) {
+        // Order the root-nodes in alphanumeric order first.
+        List<Box> tierBoxes = Lists.newArrayList(boxesOnTier(1, children));
+        tierBoxes.sort((a, b) -> a.step().name().compareTo(b.step().name()));
+
+        // Place the boxes centered on the parent box; left to right.
+        int tierBreadth = tierBoxes.stream().mapToInt(Box::breadth).sum();
+        int slot = 1;
+        for (Box b : tierBoxes) {
+            b.updateCenter(1, slot(slot, tierBreadth));
+            slot += b.breadth();
+        }
+    }
+
+    // Returns the horizontal offset off the parent center.
+    private int slot(int slot, int tierBreadth) {
+        boolean even = tierBreadth % 2 == 0;
+        int multiplier = -tierBreadth / 2 + slot - 1;
+        return even ? multiplier * SLOT_WIDTH + SLOT_WIDTH / 2 : multiplier * SLOT_WIDTH;
+    }
+
+    // Returns a list of all child step boxes that start on the specified tier.
+    private List<Box> boxesOnTier(int tier, Set<Step> children) {
+        return boxes.values().stream()
+                .filter(b -> b.tier() == tier && children.contains(b.step()))
+                .collect(Collectors.toList());
+    }
+
+    // Determines whether the specified box spans, or occupies a tier.
+    private boolean isSpanningTier(Box b, int tier) {
+        return (b.depth() == 1 && b.tier() == tier) ||
+                (b.tier() <= tier && tier < b.tier() + b.depth());
+    }
+
+
+    // Determines roots of the specified group or of the entire graph.
+    private Set<Step> findRoots(Group group) {
+        Set<Step> steps = group != null ? group.children() : flow.getVertexes();
+        return steps.stream().filter(s -> isRoot(s, group)).collect(Collectors.toSet());
+    }
+
+    private boolean isRoot(Step step, Group group) {
+        if (step.group() != group) {
+            return false;
+        }
+
+        Set<Dependency> requirements = flow.getEdgesFrom(step);
+        return requirements.stream().filter(r -> r.dst().group() == group)
+                .collect(Collectors.toSet()).isEmpty();
+    }
+
+    /**
+     * Returns the bounding box for the specified step. If null is given, it
+     * returns the overall bounding box.
+     *
+     * @param step step or group; null for the overall bounding box
+     * @return bounding box
+     */
+    public Box get(Step step) {
+        return boxes.get(step);
+    }
+
+    /**
+     * Returns the bounding box for the specified step name. If null is given,
+     * it returns the overall bounding box.
+     *
+     * @param name name of step or group; null for the overall bounding box
+     * @return bounding box
+     */
+    public Box get(String name) {
+        return get(name == null ? null : compiler.getStep(name));
+    }
+
+    // Creates a bounding box for the specified step or group.
+    private void createBox(Step step) {
+        boxes.put(step, new Box(step, flow.getEdgesFrom(step).size()));
+    }
+
+    /**
+     * Bounding box data for a step or group.
+     */
+    final class Box {
+
+        private Step step;
+        private int remainingRequirements;
+
+        private int absoluteTier = 0;
+        private int tier;
+        private int depth = 1;
+        private int breadth;
+        private int center, top;
+
+        private Box(Step step, int remainingRequirements) {
+            this.step = step;
+            this.remainingRequirements = remainingRequirements + 1;
+            breadth = step == null || step instanceof Group ? 0 : 1;
+        }
+
+        private void latchTiers(int absoluteTier, int tier, Group from) {
+            this.absoluteTier = Math.max(this.absoluteTier, absoluteTier);
+            if (step == null || step.group() == from) {
+                this.tier = Math.max(this.tier, tier);
+            }
+        }
+
+        public void latchBreadth(int breadth) {
+            this.breadth = Math.max(this.breadth, breadth);
+        }
+
+        void setTierAndDepth(int absoluteTier, int tier, int depth, Group from) {
+            latchTiers(absoluteTier, tier, from);
+            this.depth = depth;
+        }
+
+        boolean visitAndLatchMaxTier(int absoluteTier, int tier, Group from) {
+            latchTiers(absoluteTier, tier, from);
+            --remainingRequirements;
+            return remainingRequirements == 0;
+        }
+
+        Step step() {
+            return step;
+        }
+
+        public int absoluteTier() {
+            return absoluteTier;
+        }
+
+        int tier() {
+            return tier;
+        }
+
+        int depth() {
+            return depth;
+        }
+
+        int breadth() {
+            return breadth;
+        }
+
+        int top() {
+            return top;
+        }
+
+        int center() {
+            return center;
+        }
+
+        public void updateCenter(int top, int center) {
+            this.top = top;
+            this.center = center;
+        }
+    }
+}
diff --git a/utils/stc/src/main/java/org/onlab/stc/MonitorWebSocket.java b/utils/stc/src/main/java/org/onlab/stc/MonitorWebSocket.java
index 3165dd3..cd14607 100644
--- a/utils/stc/src/main/java/org/onlab/stc/MonitorWebSocket.java
+++ b/utils/stc/src/main/java/org/onlab/stc/MonitorWebSocket.java
@@ -18,24 +18,24 @@
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import org.eclipse.jetty.websocket.WebSocket;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
 
+import static org.onlab.stc.Coordinator.print;
+
 /**
  * Web socket capable of interacting with the STC monitor GUI.
  */
 public class MonitorWebSocket implements WebSocket.OnTextMessage, WebSocket.OnControl {
 
-    private static final Logger log = LoggerFactory.getLogger(MonitorWebSocket.class);
-
     private static final long MAX_AGE_MS = 30_000;
 
     private static final byte PING = 0x9;
     private static final byte PONG = 0xA;
     private static final byte[] PING_DATA = new byte[]{(byte) 0xde, (byte) 0xad};
 
+    private final Monitor monitor;
+
     private Connection connection;
     private FrameConnection control;
 
@@ -44,6 +44,15 @@
     private long lastActive = System.currentTimeMillis();
 
     /**
+     * Creates a new monitor client GUI web-socket.
+     *
+     * @param monitor shared process flow monitor
+     */
+    MonitorWebSocket(Monitor monitor) {
+        this.monitor = monitor;
+    }
+
+    /**
      * Issues a close on the connection.
      */
     synchronized void close() {
@@ -62,13 +71,12 @@
         long quietFor = System.currentTimeMillis() - lastActive;
         boolean idle = quietFor > MAX_AGE_MS;
         if (idle || (connection != null && !connection.isOpen())) {
-            log.debug("IDLE (or closed) websocket [{} ms]", quietFor);
             return true;
         } else if (connection != null) {
             try {
                 control.sendControl(PING, PING_DATA, 0, PING_DATA.length);
             } catch (IOException e) {
-                log.warn("Unable to send ping message due to: ", e);
+                print("Unable to send ping message due to: %s", e);
             }
         }
         return false;
@@ -80,10 +88,10 @@
         this.control = (FrameConnection) connection;
         try {
             createHandlers();
-            log.info("GUI client connected");
+            sendMessage(message("flow", monitor.scenarioData()));
 
         } catch (Exception e) {
-            log.warn("Unable to open monitor connection: {}", e);
+            print("Unable to open monitor connection: %s", e);
             this.connection.close();
             this.connection = null;
             this.control = null;
@@ -93,8 +101,6 @@
     @Override
     public synchronized void onClose(int closeCode, String message) {
         destroyHandlers();
-        log.info("GUI client disconnected [close-code={}, message={}]",
-                 closeCode, message);
     }
 
     @Override
@@ -109,10 +115,9 @@
         try {
             ObjectNode message = (ObjectNode) mapper.reader().readTree(data);
             // TODO:
-            log.info("Got message: {}", message);
+            print("Got message: %s", message);
         } catch (Exception e) {
-            log.warn("Unable to parse GUI message {} due to {}", data, e);
-            log.debug("Boom!!!", e);
+            print("Unable to parse GUI message %s due to %s", data, e);
         }
     }
 
@@ -122,20 +127,14 @@
                 connection.sendMessage(message.toString());
             }
         } catch (IOException e) {
-            log.warn("Unable to send message {} to GUI due to {}", message, e);
-            log.debug("Boom!!!", e);
+            print("Unable to send message %s to GUI due to %s", message, e);
         }
     }
 
-    public synchronized void sendMessage(String type, long sid, ObjectNode payload) {
-        ObjectNode message = mapper.createObjectNode();
-        message.put("event", type);
-        if (sid > 0) {
-            message.put("sid", sid);
-        }
+    public ObjectNode message(String type, ObjectNode payload) {
+        ObjectNode message = mapper.createObjectNode().put("event", type);
         message.set("payload", payload);
-        sendMessage(message);
-
+        return message;
     }
 
     // Creates new message handlers.
diff --git a/utils/stc/src/main/java/org/onlab/stc/MonitorWebSocketServlet.java b/utils/stc/src/main/java/org/onlab/stc/MonitorWebSocketServlet.java
index 6796c6b..a870500 100644
--- a/utils/stc/src/main/java/org/onlab/stc/MonitorWebSocketServlet.java
+++ b/utils/stc/src/main/java/org/onlab/stc/MonitorWebSocketServlet.java
@@ -15,6 +15,7 @@
  */
 package org.onlab.stc;
 
+import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.google.common.io.ByteStreams;
 import com.google.common.net.MediaType;
 import org.eclipse.jetty.websocket.WebSocket;
@@ -34,11 +35,13 @@
 /**
  * Web socket servlet capable of creating web sockets for the STC monitor.
  */
-public class MonitorWebSocketServlet extends WebSocketServlet {
+public class MonitorWebSocketServlet extends WebSocketServlet
+        implements MonitorDelegate {
 
     private static final long PING_DELAY_MS = 5000;
     private static final String DOT = ".";
 
+    private static Monitor monitor;
     private static MonitorWebSocketServlet instance;
 
     private final Set<MonitorWebSocket> sockets = new HashSet<>();
@@ -46,6 +49,15 @@
     private final TimerTask pruner = new Pruner();
 
     /**
+     * Binds the shared process flow monitor.
+     *
+     * @param m process monitor reference
+     */
+    public static void setMonitor(Monitor m) {
+        monitor = m;
+    }
+
+    /**
      * Closes all currently open monitor web-sockets.
      */
     public static void closeAll() {
@@ -59,7 +71,7 @@
     public void init() throws ServletException {
         super.init();
         instance = this;
-        System.out.println("Yo!!!!");
+        monitor.setDelegate(this);
         timer.schedule(pruner, PING_DELAY_MS, PING_DELAY_MS);
     }
 
@@ -92,14 +104,20 @@
 
     @Override
     public WebSocket doWebSocketConnect(HttpServletRequest request, String protocol) {
-        System.out.println("Wazup????");
-        MonitorWebSocket socket = new MonitorWebSocket();
+        MonitorWebSocket socket = new MonitorWebSocket(monitor);
         synchronized (sockets) {
             sockets.add(socket);
         }
         return socket;
     }
 
+    @Override
+    public void notify(ObjectNode event) {
+        if (instance != null) {
+            instance.sockets.forEach(ws -> ws.sendMessage(event));
+        }
+    }
+
     // Task for pruning web-sockets that are idle.
     private class Pruner extends TimerTask {
         @Override
diff --git a/utils/stc/src/main/java/org/onlab/stc/StepProcessListener.java b/utils/stc/src/main/java/org/onlab/stc/StepProcessListener.java
index 421a606..a8222d0 100644
--- a/utils/stc/src/main/java/org/onlab/stc/StepProcessListener.java
+++ b/utils/stc/src/main/java/org/onlab/stc/StepProcessListener.java
@@ -23,9 +23,10 @@
     /**
      * Indicates that process step has started.
      *
-     * @param step subject step
+     * @param step    subject step
+     * @param command actual command executed; includes run-time substitutions
      */
-    default void onStart(Step step) {
+    default void onStart(Step step, String command) {
     }
 
     /**
diff --git a/utils/stc/src/main/java/org/onlab/stc/StepProcessor.java b/utils/stc/src/main/java/org/onlab/stc/StepProcessor.java
index 86315e9..1da9545 100644
--- a/utils/stc/src/main/java/org/onlab/stc/StepProcessor.java
+++ b/utils/stc/src/main/java/org/onlab/stc/StepProcessor.java
@@ -23,6 +23,7 @@
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.PrintWriter;
+import java.util.function.Function;
 
 import static java.lang.String.format;
 import static org.onlab.stc.Coordinator.Status.FAILED;
@@ -41,26 +42,32 @@
 
     private final Step step;
     private final File logDir;
+    private String command;
 
     private Process process;
     private StepProcessListener delegate;
+    private Function<String, String> substitutor;
 
     /**
      * Creates a process monitor.
      *
-     * @param step     step or group to be executed
-     * @param logDir   directory where step process log should be stored
-     * @param delegate process lifecycle listener
+     * @param step        step or group to be executed
+     * @param logDir      directory where step process log should be stored
+     * @param delegate    process lifecycle listener
+     * @param substitutor function to substitute var reference in command
      */
-    StepProcessor(Step step, File logDir, StepProcessListener delegate) {
+    StepProcessor(Step step, File logDir, StepProcessListener delegate,
+                  Function<String, String> substitutor) {
         this.step = step;
         this.logDir = logDir;
         this.delegate = delegate;
+        this.substitutor = substitutor;
     }
 
     @Override
     public void run() {
-        delegate.onStart(step);
+        command = substitutor != null ? substitutor.apply(command()) : command();
+        delegate.onStart(step, command);
         int code = execute();
         boolean ignoreCode = step.env() != null && step.env.equals(IGNORE_CODE);
         Status status = ignoreCode || code == 0 ? SUCCEEDED : FAILED;
@@ -74,7 +81,7 @@
      */
     private int execute() {
         try (PrintWriter pw = new PrintWriter(logFile())) {
-            process = Runtime.getRuntime().exec(command());
+            process = Runtime.getRuntime().exec(command);
             processOutput(pw);
 
             // Wait for the process to complete and get its exit code.
diff --git a/utils/stc/src/main/resources/data.json b/utils/stc/src/main/resources/data.json
new file mode 100644
index 0000000..f582374
--- /dev/null
+++ b/utils/stc/src/main/resources/data.json
@@ -0,0 +1,1087 @@
+{
+    "requirements": [
+        {
+            "dst": "Reactive-Forwarding.Ping-2",
+            "isSoft": false,
+            "src": "Reactive-Forwarding.Link-2-Down"
+        },
+        {
+            "dst": "Final-Check-Logs-2",
+            "isSoft": true,
+            "src": "Fetch-Logs-2"
+        },
+        {
+            "dst": "Host-Intent.Ping-4",
+            "isSoft": false,
+            "src": "Host-Intent.Link-2-Up"
+        },
+        {
+            "dst": "Install-1",
+            "isSoft": false,
+            "src": "Wait-for-Start-1"
+        },
+        {
+            "dst": "Host-Intent.Link-1-Down",
+            "isSoft": false,
+            "src": "Host-Intent.Ping-2"
+        },
+        {
+            "dst": "Host-Intent.Link-2-Up",
+            "isSoft": false,
+            "src": "Host-Intent.Ping-5"
+        },
+        {
+            "dst": "Host-Intent.Ping-2",
+            "isSoft": false,
+            "src": "Host-Intent.Link-2-Down"
+        },
+        {
+            "dst": "Reinstall-App-With-CLI",
+            "isSoft": false,
+            "src": "Verify-CLI"
+        },
+        {
+            "dst": "Create-App-UI-Overlay",
+            "isSoft": false,
+            "src": "Build-App-With-UI"
+        },
+        {
+            "dst": "Secure-SSH",
+            "isSoft": true,
+            "src": "Wait-for-Start-1"
+        },
+        {
+            "dst": "Pause-For-Masters",
+            "isSoft": true,
+            "src": "Check-Flows"
+        },
+        {
+            "dst": "Secure-SSH",
+            "isSoft": true,
+            "src": "Wait-for-Start-3"
+        },
+        {
+            "dst": "Uninstall-3",
+            "isSoft": false,
+            "src": "Kill-3"
+        },
+        {
+            "dst": "Balance-Masters",
+            "isSoft": false,
+            "src": "Pause-For-Masters"
+        },
+        {
+            "dst": "Reactive-Forwarding.Net-Pingall",
+            "isSoft": true,
+            "src": "Reactive-Forwarding.Net-Link-Down-Up"
+        },
+        {
+            "dst": "Wait-for-Start-3",
+            "isSoft": true,
+            "src": "Check-Logs-3"
+        },
+        {
+            "dst": "Wait-for-Start-2",
+            "isSoft": true,
+            "src": "Check-Components-2"
+        },
+        {
+            "dst": "Uninstall-Reactive-Forwarding",
+            "isSoft": false,
+            "src": "Find-Host-1"
+        },
+        {
+            "dst": "Wipe-Out-Data-Before",
+            "isSoft": true,
+            "src": "Initial-Summary-Check"
+        },
+        {
+            "dst": "Reactive-Forwarding.Ping-3",
+            "isSoft": false,
+            "src": "Reactive-Forwarding.Link-1-Up"
+        },
+        {
+            "dst": "Archetypes",
+            "isSoft": true,
+            "src": "Wrapup"
+        },
+        {
+            "dst": "Reactive-Forwarding.Ping-4",
+            "isSoft": false,
+            "src": "Reactive-Forwarding.Link-2-Up"
+        },
+        {
+            "dst": "Host-Intent-Connectivity",
+            "isSoft": true,
+            "src": "Net-Teardown"
+        },
+        {
+            "dst": "Host-Intent.Ping-3",
+            "isSoft": false,
+            "src": "Host-Intent.Link-1-Up"
+        },
+        {
+            "dst": "Host-Intent.Ping-1",
+            "isSoft": false,
+            "src": "Host-Intent.Link-1-Down"
+        },
+        {
+            "dst": "Install-App",
+            "isSoft": false,
+            "src": "Create-App-CLI-Overlay"
+        },
+        {
+            "dst": "Final-Check-Logs-3",
+            "isSoft": true,
+            "src": "Fetch-Logs-3"
+        },
+        {
+            "dst": "Install-App",
+            "isSoft": false,
+            "src": "Verify-App"
+        },
+        {
+            "dst": "Host-Intent.Link-2-Down",
+            "isSoft": false,
+            "src": "Host-Intent.Ping-3"
+        },
+        {
+            "dst": "Prerequisites",
+            "isSoft": false,
+            "src": "Setup"
+        },
+        {
+            "dst": "Verify-App",
+            "isSoft": true,
+            "src": "Reinstall-App-With-CLI"
+        },
+        {
+            "dst": "Net-Smoke",
+            "isSoft": true,
+            "src": "Archetypes"
+        },
+        {
+            "dst": "Setup",
+            "isSoft": true,
+            "src": "Wrapup"
+        },
+        {
+            "dst": "Start-Mininet",
+            "isSoft": false,
+            "src": "Wait-For-Mininet"
+        },
+        {
+            "dst": "Verify-UI",
+            "isSoft": false,
+            "src": "Uninstall-App"
+        },
+        {
+            "dst": "Kill-3",
+            "isSoft": false,
+            "src": "Install-3"
+        },
+        {
+            "dst": "Wait-for-Start-1",
+            "isSoft": true,
+            "src": "Check-Components-1"
+        },
+        {
+            "dst": "Wait-for-Start-1",
+            "isSoft": true,
+            "src": "Check-Nodes-1"
+        },
+        {
+            "dst": "Push-Topos",
+            "isSoft": false,
+            "src": "Start-Mininet"
+        },
+        {
+            "dst": "Reactive-Forwarding.Check-Summary-For-Hosts",
+            "isSoft": true,
+            "src": "Reactive-Forwarding.Config-Topo"
+        },
+        {
+            "dst": "Reactive-Forwarding.Install-Apps",
+            "isSoft": false,
+            "src": "Reactive-Forwarding.Check-Apps"
+        },
+        {
+            "dst": "Push-Bits",
+            "isSoft": false,
+            "src": "Install-2"
+        },
+        {
+            "dst": "Install-1",
+            "isSoft": false,
+            "src": "Secure-SSH"
+        },
+        {
+            "dst": "Create-Intent",
+            "isSoft": false,
+            "src": "Host-Intent.Net-Link-Down-Up"
+        },
+        {
+            "dst": "Verify-CLI",
+            "isSoft": true,
+            "src": "Reinstall-App-With-UI"
+        },
+        {
+            "dst": "Wait-for-Start-3",
+            "isSoft": true,
+            "src": "Check-Apps-3"
+        },
+        {
+            "dst": "Net-Smoke",
+            "isSoft": true,
+            "src": "Wrapup"
+        },
+        {
+            "dst": "Initial-Summary-Check",
+            "isSoft": false,
+            "src": "Start-Mininet"
+        },
+        {
+            "dst": "Install-3",
+            "isSoft": false,
+            "src": "Wait-for-Start-3"
+        },
+        {
+            "dst": "Reactive-Forwarding.Link-1-Up",
+            "isSoft": false,
+            "src": "Reactive-Forwarding.Ping-4"
+        },
+        {
+            "dst": "Check-Summary",
+            "isSoft": true,
+            "src": "Balance-Masters"
+        },
+        {
+            "dst": "Reactive-Forwarding.Net-Link-Down-Up",
+            "isSoft": true,
+            "src": "Host-Intent-Connectivity"
+        },
+        {
+            "dst": "Secure-SSH",
+            "isSoft": true,
+            "src": "Wait-for-Start-2"
+        },
+        {
+            "dst": "Build-App-With-CLI",
+            "isSoft": false,
+            "src": "Reinstall-App-With-CLI"
+        },
+        {
+            "dst": "Uninstall-1",
+            "isSoft": false,
+            "src": "Kill-1"
+        },
+        {
+            "dst": "Find-Host-1",
+            "isSoft": false,
+            "src": "Find-Host-2"
+        },
+        {
+            "dst": "Create-App-CLI-Overlay",
+            "isSoft": false,
+            "src": "Build-App-With-CLI"
+        },
+        {
+            "dst": "Net-Setup",
+            "isSoft": false,
+            "src": "Reactive-Forwarding.Net-Link-Down-Up"
+        },
+        {
+            "dst": "Kill-2",
+            "isSoft": false,
+            "src": "Install-2"
+        },
+        {
+            "dst": "Wait-for-Start-1",
+            "isSoft": true,
+            "src": "Check-Logs-1"
+        },
+        {
+            "dst": "Wait-for-Start-2",
+            "isSoft": true,
+            "src": "Check-Nodes-2"
+        },
+        {
+            "dst": "Reactive-Forwarding.Ping-All-And-Verify",
+            "isSoft": true,
+            "src": "Reactive-Forwarding.Check-Summary-For-Hosts"
+        },
+        {
+            "dst": "Clean-Up",
+            "isSoft": false,
+            "src": "Create-App"
+        },
+        {
+            "dst": "Host-Intent.Link-1-Up",
+            "isSoft": false,
+            "src": "Host-Intent.Ping-4"
+        },
+        {
+            "dst": "Build-App-With-UI",
+            "isSoft": false,
+            "src": "Reinstall-App-With-UI"
+        },
+        {
+            "dst": "Install-2",
+            "isSoft": false,
+            "src": "Secure-SSH"
+        },
+        {
+            "dst": "Wait-For-Mininet",
+            "isSoft": false,
+            "src": "Check-Summary"
+        },
+        {
+            "dst": "Host-Intent.Net-Link-Down-Up",
+            "isSoft": false,
+            "src": "Remove-Intent"
+        },
+        {
+            "dst": "Net-Setup",
+            "isSoft": false,
+            "src": "Host-Intent-Connectivity"
+        },
+        {
+            "dst": "Net-Setup",
+            "isSoft": false,
+            "src": "Reactive-Forwarding.Net-Pingall"
+        },
+        {
+            "dst": "Reactive-Forwarding.Link-2-Down",
+            "isSoft": false,
+            "src": "Reactive-Forwarding.Ping-3"
+        },
+        {
+            "dst": "Find-Host-2",
+            "isSoft": false,
+            "src": "Create-Intent"
+        },
+        {
+            "dst": "Wait-for-Start-2",
+            "isSoft": true,
+            "src": "Check-Apps-2"
+        },
+        {
+            "dst": "Final-Check-Logs-1",
+            "isSoft": true,
+            "src": "Fetch-Logs-1"
+        },
+        {
+            "dst": "Install-2",
+            "isSoft": false,
+            "src": "Wait-for-Start-2"
+        },
+        {
+            "dst": "Reactive-Forwarding.Ping-1",
+            "isSoft": false,
+            "src": "Reactive-Forwarding.Link-1-Down"
+        },
+        {
+            "dst": "Create-App",
+            "isSoft": false,
+            "src": "Build-App"
+        },
+        {
+            "dst": "Check-Summary",
+            "isSoft": true,
+            "src": "Check-Flows"
+        },
+        {
+            "dst": "Build-App",
+            "isSoft": false,
+            "src": "Install-App"
+        },
+        {
+            "dst": "Reinstall-App-With-UI",
+            "isSoft": false,
+            "src": "Verify-UI"
+        },
+        {
+            "dst": "Uninstall-2",
+            "isSoft": false,
+            "src": "Kill-2"
+        },
+        {
+            "dst": "Setup",
+            "isSoft": false,
+            "src": "Archetypes"
+        },
+        {
+            "dst": "Setup",
+            "isSoft": false,
+            "src": "Net-Smoke"
+        },
+        {
+            "dst": "Kill-1",
+            "isSoft": false,
+            "src": "Install-1"
+        },
+        {
+            "dst": "Reactive-Forwarding.Link-1-Down",
+            "isSoft": false,
+            "src": "Reactive-Forwarding.Ping-2"
+        },
+        {
+            "dst": "Wait-for-Start-2",
+            "isSoft": true,
+            "src": "Check-Logs-2"
+        },
+        {
+            "dst": "Wait-for-Start-3",
+            "isSoft": true,
+            "src": "Check-Components-3"
+        },
+        {
+            "dst": "Wait-for-Start-3",
+            "isSoft": true,
+            "src": "Check-Nodes-3"
+        },
+        {
+            "dst": "Stop-Mininet-If-Needed",
+            "isSoft": false,
+            "src": "Start-Mininet"
+        },
+        {
+            "dst": "Reactive-Forwarding.Link-2-Up",
+            "isSoft": false,
+            "src": "Reactive-Forwarding.Ping-5"
+        },
+        {
+            "dst": "Reactive-Forwarding.Check-Apps",
+            "isSoft": false,
+            "src": "Reactive-Forwarding.Ping-All-And-Verify"
+        },
+        {
+            "dst": "Install-3",
+            "isSoft": false,
+            "src": "Secure-SSH"
+        },
+        {
+            "dst": "Push-Bits",
+            "isSoft": false,
+            "src": "Install-3"
+        },
+        {
+            "dst": "Reinstall-App-With-CLI",
+            "isSoft": false,
+            "src": "Create-App-UI-Overlay"
+        },
+        {
+            "dst": "Push-Bits",
+            "isSoft": false,
+            "src": "Install-1"
+        },
+        {
+            "dst": "Wait-for-Start-1",
+            "isSoft": true,
+            "src": "Check-Apps-1"
+        }
+    ],
+    "steps": [
+        {
+            "group": "Net-Setup",
+            "isGroup": false,
+            "name": "Check-Summary",
+            "status": "waiting"
+        },
+        {
+            "group": "Net-Setup",
+            "isGroup": false,
+            "name": "Check-Flows",
+            "status": "waiting"
+        },
+        {
+            "group": "Wrapup",
+            "isGroup": false,
+            "name": "Final-Check-Logs-1",
+            "status": "waiting"
+        },
+        {
+            "group": "Wrapup",
+            "isGroup": false,
+            "name": "Final-Check-Logs-2",
+            "status": "waiting"
+        },
+        {
+            "group": "Archetypes",
+            "isGroup": false,
+            "name": "Clean-Up",
+            "status": "waiting"
+        },
+        {
+            "group": "Archetypes",
+            "isGroup": false,
+            "name": "Build-App-With-UI",
+            "status": "waiting"
+        },
+        {
+            "group": "Archetypes",
+            "isGroup": false,
+            "name": "Uninstall-App",
+            "status": "waiting"
+        },
+        {
+            "group": "Wrapup",
+            "isGroup": false,
+            "name": "Final-Check-Logs-3",
+            "status": "waiting"
+        },
+        {
+            "group": "Host-Intent.Net-Link-Down-Up",
+            "isGroup": false,
+            "name": "Host-Intent.Link-2-Down",
+            "status": "waiting"
+        },
+        {
+            "group": "Wrapup",
+            "isGroup": false,
+            "name": "Fetch-Logs-3",
+            "status": "waiting"
+        },
+        {
+            "group": "Wrapup",
+            "isGroup": false,
+            "name": "Fetch-Logs-2",
+            "status": "waiting"
+        },
+        {
+            "group": "Setup",
+            "isGroup": false,
+            "name": "Check-Components-3",
+            "status": "waiting"
+        },
+        {
+            "group": "Wrapup",
+            "isGroup": false,
+            "name": "Fetch-Logs-1",
+            "status": "waiting"
+        },
+        {
+            "group": "Net-Setup",
+            "isGroup": false,
+            "name": "Push-Topos",
+            "status": "waiting"
+        },
+        {
+            "group": "Reactive-Forwarding.Net-Pingall",
+            "isGroup": false,
+            "name": "Reactive-Forwarding.Check-Apps",
+            "status": "waiting"
+        },
+        {
+            "group": "Setup",
+            "isGroup": false,
+            "name": "Wait-for-Start-3",
+            "status": "waiting"
+        },
+        {
+            "group": "Setup",
+            "isGroup": false,
+            "name": "Wait-for-Start-2",
+            "status": "waiting"
+        },
+        {
+            "group": "Setup",
+            "isGroup": false,
+            "name": "Wait-for-Start-1",
+            "status": "waiting"
+        },
+        {
+            "group": "Net-Smoke",
+            "isGroup": true,
+            "name": "Host-Intent-Connectivity",
+            "status": "waiting"
+        },
+        {
+            "group": "Host-Intent-Connectivity",
+            "isGroup": false,
+            "name": "Create-Intent",
+            "status": "waiting"
+        },
+        {
+            "isGroup": true,
+            "name": "Prerequisites",
+            "status": "in_progress"
+        },
+        {
+            "group": "Setup",
+            "isGroup": false,
+            "name": "Push-Bits",
+            "status": "waiting"
+        },
+        {
+            "group": "Setup",
+            "isGroup": false,
+            "name": "Check-Logs-2",
+            "status": "waiting"
+        },
+        {
+            "group": "Setup",
+            "isGroup": false,
+            "name": "Check-Logs-3",
+            "status": "waiting"
+        },
+        {
+            "group": "Setup",
+            "isGroup": false,
+            "name": "Kill-1",
+            "status": "waiting"
+        },
+        {
+            "group": "Setup",
+            "isGroup": false,
+            "name": "Kill-3",
+            "status": "waiting"
+        },
+        {
+            "group": "Setup",
+            "isGroup": false,
+            "name": "Kill-2",
+            "status": "waiting"
+        },
+        {
+            "group": "Host-Intent-Connectivity",
+            "isGroup": true,
+            "name": "Host-Intent.Net-Link-Down-Up",
+            "status": "waiting"
+        },
+        {
+            "group": "Host-Intent.Net-Link-Down-Up",
+            "isGroup": false,
+            "name": "Host-Intent.Ping-1",
+            "status": "waiting"
+        },
+        {
+            "group": "Archetypes",
+            "isGroup": false,
+            "name": "Verify-UI",
+            "status": "waiting"
+        },
+        {
+            "group": "Host-Intent.Net-Link-Down-Up",
+            "isGroup": false,
+            "name": "Host-Intent.Ping-2",
+            "status": "waiting"
+        },
+        {
+            "group": "Host-Intent.Net-Link-Down-Up",
+            "isGroup": false,
+            "name": "Host-Intent.Ping-3",
+            "status": "waiting"
+        },
+        {
+            "group": "Setup",
+            "isGroup": false,
+            "name": "Uninstall-1",
+            "status": "waiting"
+        },
+        {
+            "group": "Setup",
+            "isGroup": false,
+            "name": "Check-Logs-1",
+            "status": "waiting"
+        },
+        {
+            "group": "Host-Intent.Net-Link-Down-Up",
+            "isGroup": false,
+            "name": "Host-Intent.Ping-4",
+            "status": "waiting"
+        },
+        {
+            "group": "Setup",
+            "isGroup": false,
+            "name": "Uninstall-3",
+            "status": "waiting"
+        },
+        {
+            "group": "Host-Intent.Net-Link-Down-Up",
+            "isGroup": false,
+            "name": "Host-Intent.Ping-5",
+            "status": "waiting"
+        },
+        {
+            "group": "Setup",
+            "isGroup": false,
+            "name": "Uninstall-2",
+            "status": "waiting"
+        },
+        {
+            "group": "Reactive-Forwarding.Net-Pingall",
+            "isGroup": false,
+            "name": "Reactive-Forwarding.Install-Apps",
+            "status": "waiting"
+        },
+        {
+            "group": "Net-Smoke",
+            "isGroup": true,
+            "name": "Reactive-Forwarding.Net-Link-Down-Up",
+            "status": "waiting"
+        },
+        {
+            "group": "Prerequisites",
+            "isGroup": false,
+            "name": "Check-ONOS-Bits",
+            "status": "in_progress"
+        },
+        {
+            "isGroup": true,
+            "name": "Wrapup",
+            "status": "waiting"
+        },
+        {
+            "group": "Setup",
+            "isGroup": false,
+            "name": "Install-2",
+            "status": "waiting"
+        },
+        {
+            "group": "Host-Intent-Connectivity",
+            "isGroup": false,
+            "name": "Find-Host-1",
+            "status": "waiting"
+        },
+        {
+            "group": "Setup",
+            "isGroup": false,
+            "name": "Install-1",
+            "status": "waiting"
+        },
+        {
+            "group": "Net-Setup",
+            "isGroup": false,
+            "name": "Wipe-Out-Data-Before",
+            "status": "waiting"
+        },
+        {
+            "group": "Net-Setup",
+            "isGroup": false,
+            "name": "Pause-For-Masters",
+            "status": "waiting"
+        },
+        {
+            "group": "Reactive-Forwarding.Net-Link-Down-Up",
+            "isGroup": false,
+            "name": "Reactive-Forwarding.Link-2-Up",
+            "status": "waiting"
+        },
+        {
+            "group": "Net-Smoke",
+            "isGroup": true,
+            "name": "Reactive-Forwarding.Net-Pingall",
+            "status": "waiting"
+        },
+        {
+            "group": "Setup",
+            "isGroup": false,
+            "name": "Check-Components-2",
+            "status": "waiting"
+        },
+        {
+            "group": "Setup",
+            "isGroup": false,
+            "name": "Check-Components-1",
+            "status": "waiting"
+        },
+        {
+            "group": "Archetypes",
+            "isGroup": false,
+            "name": "Reinstall-App-With-UI",
+            "status": "waiting"
+        },
+        {
+            "group": "Archetypes",
+            "isGroup": false,
+            "name": "Reinstall-App-With-CLI",
+            "status": "waiting"
+        },
+        {
+            "group": "Archetypes",
+            "isGroup": false,
+            "name": "Build-App-With-CLI",
+            "status": "waiting"
+        },
+        {
+            "group": "Host-Intent-Connectivity",
+            "isGroup": false,
+            "name": "Uninstall-Reactive-Forwarding",
+            "status": "waiting"
+        },
+        {
+            "group": "Host-Intent.Net-Link-Down-Up",
+            "isGroup": false,
+            "name": "Host-Intent.Link-2-Up",
+            "status": "waiting"
+        },
+        {
+            "group": "Net-Teardown",
+            "isGroup": false,
+            "name": "Stop-Mininet",
+            "status": "waiting"
+        },
+        {
+            "group": "Reactive-Forwarding.Net-Pingall",
+            "isGroup": false,
+            "name": "Reactive-Forwarding.Config-Topo",
+            "status": "waiting"
+        },
+        {
+            "group": "Archetypes",
+            "isGroup": false,
+            "name": "Create-App-CLI-Overlay",
+            "status": "waiting"
+        },
+        {
+            "group": "Reactive-Forwarding.Net-Link-Down-Up",
+            "isGroup": false,
+            "name": "Reactive-Forwarding.Link-1-Down",
+            "status": "waiting"
+        },
+        {
+            "isGroup": true,
+            "name": "Net-Smoke",
+            "status": "waiting"
+        },
+        {
+            "group": "Prerequisites",
+            "isGroup": false,
+            "name": "Check-Passwordless-Login-2",
+            "status": "in_progress"
+        },
+        {
+            "group": "Prerequisites",
+            "isGroup": false,
+            "name": "Check-Passwordless-Login-1",
+            "status": "in_progress"
+        },
+        {
+            "group": "Prerequisites",
+            "isGroup": false,
+            "name": "Check-Passwordless-Login-3",
+            "status": "in_progress"
+        },
+        {
+            "group": "Setup",
+            "isGroup": false,
+            "name": "Secure-SSH",
+            "status": "waiting"
+        },
+        {
+            "group": "Net-Smoke",
+            "isGroup": true,
+            "name": "Net-Setup",
+            "status": "waiting"
+        },
+        {
+            "group": "Setup",
+            "isGroup": false,
+            "name": "Check-Nodes-1",
+            "status": "waiting"
+        },
+        {
+            "group": "Setup",
+            "isGroup": false,
+            "name": "Install-3",
+            "status": "waiting"
+        },
+        {
+            "group": "Host-Intent-Connectivity",
+            "isGroup": false,
+            "name": "Find-Host-2",
+            "status": "waiting"
+        },
+        {
+            "group": "Net-Setup",
+            "isGroup": false,
+            "name": "Initial-Summary-Check",
+            "status": "waiting"
+        },
+        {
+            "group": "Archetypes",
+            "isGroup": false,
+            "name": "Create-App",
+            "status": "waiting"
+        },
+        {
+            "group": "Setup",
+            "isGroup": false,
+            "name": "Check-Nodes-3",
+            "status": "waiting"
+        },
+        {
+            "group": "Setup",
+            "isGroup": false,
+            "name": "Check-Nodes-2",
+            "status": "waiting"
+        },
+        {
+            "group": "Reactive-Forwarding.Net-Link-Down-Up",
+            "isGroup": false,
+            "name": "Reactive-Forwarding.Link-2-Down",
+            "status": "waiting"
+        },
+        {
+            "isGroup": true,
+            "name": "Setup",
+            "status": "waiting"
+        },
+        {
+            "group": "Archetypes",
+            "isGroup": false,
+            "name": "Verify-App",
+            "status": "waiting"
+        },
+        {
+            "group": "Reactive-Forwarding.Net-Link-Down-Up",
+            "isGroup": false,
+            "name": "Reactive-Forwarding.Ping-1",
+            "status": "waiting"
+        },
+        {
+            "group": "Reactive-Forwarding.Net-Link-Down-Up",
+            "isGroup": false,
+            "name": "Reactive-Forwarding.Ping-2",
+            "status": "waiting"
+        },
+        {
+            "group": "Net-Setup",
+            "isGroup": false,
+            "name": "Start-Mininet",
+            "status": "waiting"
+        },
+        {
+            "group": "Reactive-Forwarding.Net-Link-Down-Up",
+            "isGroup": false,
+            "name": "Reactive-Forwarding.Ping-3",
+            "status": "waiting"
+        },
+        {
+            "group": "Reactive-Forwarding.Net-Link-Down-Up",
+            "isGroup": false,
+            "name": "Reactive-Forwarding.Ping-4",
+            "status": "waiting"
+        },
+        {
+            "group": "Reactive-Forwarding.Net-Link-Down-Up",
+            "isGroup": false,
+            "name": "Reactive-Forwarding.Ping-5",
+            "status": "waiting"
+        },
+        {
+            "group": "Archetypes",
+            "isGroup": false,
+            "name": "Verify-CLI",
+            "status": "waiting"
+        },
+        {
+            "group": "Reactive-Forwarding.Net-Pingall",
+            "isGroup": false,
+            "name": "Reactive-Forwarding.Check-Summary-For-Hosts",
+            "status": "waiting"
+        },
+        {
+            "group": "Net-Smoke",
+            "isGroup": true,
+            "name": "Net-Teardown",
+            "status": "waiting"
+        },
+        {
+            "group": "Host-Intent.Net-Link-Down-Up",
+            "isGroup": false,
+            "name": "Host-Intent.Link-1-Up",
+            "status": "waiting"
+        },
+        {
+            "group": "Host-Intent-Connectivity",
+            "isGroup": false,
+            "name": "Remove-Intent",
+            "status": "waiting"
+        },
+        {
+            "group": "Archetypes",
+            "isGroup": false,
+            "name": "Install-App",
+            "status": "waiting"
+        },
+        {
+            "group": "Archetypes",
+            "isGroup": false,
+            "name": "Create-App-UI-Overlay",
+            "status": "waiting"
+        },
+        {
+            "group": "Reactive-Forwarding.Net-Link-Down-Up",
+            "isGroup": false,
+            "name": "Reactive-Forwarding.Link-1-Up",
+            "status": "waiting"
+        },
+        {
+            "group": "Net-Setup",
+            "isGroup": false,
+            "name": "Wait-For-Mininet",
+            "status": "waiting"
+        },
+        {
+            "group": "Setup",
+            "isGroup": false,
+            "name": "Check-Apps-3",
+            "status": "waiting"
+        },
+        {
+            "group": "Setup",
+            "isGroup": false,
+            "name": "Check-Apps-2",
+            "status": "waiting"
+        },
+        {
+            "group": "Setup",
+            "isGroup": false,
+            "name": "Check-Apps-1",
+            "status": "waiting"
+        },
+        {
+            "group": "Net-Setup",
+            "isGroup": false,
+            "name": "Stop-Mininet-If-Needed",
+            "status": "waiting"
+        },
+        {
+            "group": "Prerequisites",
+            "isGroup": false,
+            "name": "Check-Environment",
+            "status": "in_progress"
+        },
+        {
+            "isGroup": true,
+            "name": "Archetypes",
+            "status": "waiting"
+        },
+        {
+            "group": "Host-Intent.Net-Link-Down-Up",
+            "isGroup": false,
+            "name": "Host-Intent.Link-1-Down",
+            "status": "waiting"
+        },
+        {
+            "group": "Net-Setup",
+            "isGroup": false,
+            "name": "Balance-Masters",
+            "status": "waiting"
+        },
+        {
+            "group": "Reactive-Forwarding.Net-Pingall",
+            "isGroup": false,
+            "name": "Reactive-Forwarding.Ping-All-And-Verify",
+            "status": "waiting"
+        },
+        {
+            "group": "Archetypes",
+            "isGroup": false,
+            "name": "Build-App",
+            "status": "waiting"
+        }
+    ]
+}
diff --git a/utils/stc/src/main/resources/index.html b/utils/stc/src/main/resources/index.html
index 5a7cb81..c75bb8f 100644
--- a/utils/stc/src/main/resources/index.html
+++ b/utils/stc/src/main/resources/index.html
@@ -16,14 +16,14 @@
   -->
 <html>
 <head lang="en">
-    <meta charset="UTF-8">
+    <meta charset="utf-8">
     <title>Scenario Test Coordinator</title>
 
-    <script src="stc.js"></script>
     <link rel="stylesheet" href="stc.css">
+
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
+    <script src="stc.js"></script>
 </head>
 <body>
-<h1>Scenario Test Coordinator</h1>
-
 </body>
 </html>
\ No newline at end of file
diff --git a/utils/stc/src/main/resources/stc.css b/utils/stc/src/main/resources/stc.css
index a03dfca..8d94253 100644
--- a/utils/stc/src/main/resources/stc.css
+++ b/utils/stc/src/main/resources/stc.css
@@ -15,5 +15,23 @@
  */
 
 .body {
-    font-family: Helvetica, Arial;
+    font-family: Helvetica, Arial, sans-serif;
 }
+
+.node {
+    stroke: #fff;
+    stroke-width: 1.5px;
+}
+
+.link {
+    stroke: #999;
+    stroke-opacity: .6;
+}
+
+text {
+    font-family: 'DejaVu Sans', Arial, Helvetica, sans-serif;
+    stroke: #000;
+    stroke-width: 0.2;
+    font-weight: normal;
+    font-size: 0.6em;
+}
\ No newline at end of file
diff --git a/utils/stc/src/main/resources/stc.js b/utils/stc/src/main/resources/stc.js
index fed4272..215fd6e 100644
--- a/utils/stc/src/main/resources/stc.js
+++ b/utils/stc/src/main/resources/stc.js
@@ -15,4 +15,134 @@
  */
 (function () {
 
+    var ws, flow,
+        nodes = [],
+        links = [],
+        nodeIndexes = {};
+
+    var width = 2400,
+        height = 2400;
+
+    var color = d3.scale.category20();
+
+    var force = d3.layout.force()
+        .charge(-820)
+        .linkDistance(50)
+        .size([width, height]);
+
+    // Process flow graph layout
+    function createNode(n) {
+        nodeIndexes[n.name] = nodes.push(n) - 1;
+    }
+
+    function createLink(e) {
+        e.source = nodeIndexes[e.src];
+        e.target = nodeIndexes[e.dst];
+        links.push(e);
+    }
+
+    // Returns the newly computed bounding box of the rectangle
+    function adjustRectToFitText(n) {
+        var text = n.select('text'),
+            box = text.node().getBBox();
+
+        text.attr('text-anchor', 'left')
+            .attr('y', 2)
+            .attr('x', 4);
+
+        // add padding
+        box.x -= 4;
+        box.width += 8;
+        box.y -= 2;
+        box.height += 4;
+
+        n.select("rect").attr(box);
+    }
+
+    function processFlow() {
+        var svg = d3.select("body").append("svg")
+            .attr("width", width)
+            .attr("height", height);
+
+        flow.steps.forEach(createNode);
+        flow.requirements.forEach(createLink);
+
+        force
+            .nodes(nodes)
+            .links(links)
+            .start();
+
+        var link = svg.selectAll(".link")
+            .data(links)
+          .enter().append("line")
+            .attr("class", "link")
+            .style("stroke-width", function(d) { return d.isSoft ? 1 : 2; });
+
+        var node = svg.selectAll(".node")
+            .data(nodes)
+          .enter().append("g")
+            .attr("class", "node")
+            .call(force.drag);
+
+        node.append("rect")
+            .attr({ rx: 5, ry:5, width:180, height:18 })
+            .style("fill", function(d) { return color(d.group); });
+
+        node.append("text").text( function(d) { return d.name; })
+            .attr({ dy:"1.1em", width:100, height:16, x:4, y:2 });
+
+        node.append("title")
+            .text(function(d) { return d.name; });
+
+        force.on("tick", function() {
+            link.attr("x1", function(d) { return d.source.x; })
+                .attr("y1", function(d) { return d.source.y; })
+                .attr("x2", function(d) { return d.target.x; })
+                .attr("y2", function(d) { return d.target.y; });
+
+            node.attr("transform", function(d) { return "translate(" + (d.x - 180/2) + "," + (d.y - 18/2) + ")"; });
+        });
+    }
+
+
+    // Web socket callbacks
+
+    function handleOpen() {
+        console.log('WebSocket open');
+    }
+
+    // Handles the specified (incoming) message using handler bindings.
+    function handleMessage(msg) {
+        console.log('rx: ', msg);
+        evt = JSON.parse(msg.data);
+        if (evt.event === 'progress') {
+
+        } else if (evt.event === 'log') {
+
+        } else if (evt.event === 'flow') {
+            flow = evt.payload;
+            processFlow();
+        }
+    }
+
+    function handleClose() {
+        console.log('WebSocket closed');
+    }
+
+    if (false) {
+        d3.json("data.json", function (error, data) {
+            flow = data;
+            processFlow();
+        });
+        return;
+    }
+
+    // Open the web-socket
+    ws = new WebSocket(document.location.href.replace('http:', 'ws:'));
+    if (ws) {
+        ws.onopen = handleOpen;
+        ws.onmessage = handleMessage;
+        ws.onclose = handleClose;
+    }
+
 })();
\ No newline at end of file
diff --git a/utils/stc/src/test/java/org/onlab/stc/CompilerTest.java b/utils/stc/src/test/java/org/onlab/stc/CompilerTest.java
index 4604cf7..d70eff0 100644
--- a/utils/stc/src/test/java/org/onlab/stc/CompilerTest.java
+++ b/utils/stc/src/test/java/org/onlab/stc/CompilerTest.java
@@ -52,11 +52,11 @@
         System.setProperty("test.dir", TEST_DIR.getAbsolutePath());
     }
 
-    public static FileInputStream getStream(String name) throws FileNotFoundException {
+    static FileInputStream getStream(String name) throws FileNotFoundException {
         return new FileInputStream(new File(TEST_DIR, name));
     }
 
-    private static void stageTestResource(String name) throws IOException {
+    static void stageTestResource(String name) throws IOException {
         byte[] bytes = toByteArray(CompilerTest.class.getResourceAsStream(name));
         write(bytes, new File(TEST_DIR, name));
     }
diff --git a/utils/stc/src/test/java/org/onlab/stc/CoordinatorTest.java b/utils/stc/src/test/java/org/onlab/stc/CoordinatorTest.java
index c6566ca..c6f057e 100644
--- a/utils/stc/src/test/java/org/onlab/stc/CoordinatorTest.java
+++ b/utils/stc/src/test/java/org/onlab/stc/CoordinatorTest.java
@@ -66,8 +66,8 @@
 
     private class Listener implements StepProcessListener {
         @Override
-        public void onStart(Step step) {
-            print("> %s: started", step.name());
+        public void onStart(Step step, String command) {
+            print("> %s: started; %s", step.name(), command);
         }
 
         @Override
diff --git a/utils/stc/src/test/java/org/onlab/stc/MonitorLayoutTest.java b/utils/stc/src/test/java/org/onlab/stc/MonitorLayoutTest.java
new file mode 100644
index 0000000..4b7f561
--- /dev/null
+++ b/utils/stc/src/test/java/org/onlab/stc/MonitorLayoutTest.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onlab.stc;
+
+import org.junit.Test;
+import org.onlab.stc.MonitorLayout.Box;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+import static org.onlab.stc.CompilerTest.getStream;
+import static org.onlab.stc.CompilerTest.stageTestResource;
+import static org.onlab.stc.MonitorLayout.SLOT_WIDTH;
+import static org.onlab.stc.Scenario.loadScenario;
+
+/**
+ * Tests of the monitor layout functionality.
+ */
+public class MonitorLayoutTest {
+
+    private MonitorLayout layout;
+
+    private Compiler getCompiler(String name) throws IOException {
+        stageTestResource(name);
+        Scenario scenario = loadScenario(getStream(name));
+        Compiler compiler = new Compiler(scenario);
+        compiler.compile();
+        return compiler;
+    }
+
+    @Test
+    public void basic() throws IOException {
+        layout = new MonitorLayout(getCompiler("layout-basic.xml"));
+        validate(layout, null, 0, 1, 5, 2);
+        validate(layout, "a", 1, 1, 1, 1, 1, -SLOT_WIDTH / 2);
+        validate(layout, "b", 2, 2, 1, 1, 0, 0);
+        validate(layout, "f", 3, 3, 1);
+
+        validate(layout, "g", 1, 1, 4, 1, 1, SLOT_WIDTH / 2);
+        validate(layout, "c", 2, 1, 1);
+        validate(layout, "d", 3, 2, 1);
+        validate(layout, "e", 4, 3, 1);
+    }
+
+    @Test
+    public void basicNest() throws IOException {
+        layout = new MonitorLayout(getCompiler("layout-basic-nest.xml"));
+        validate(layout, null, 0, 1, 6, 2);
+        validate(layout, "a", 1, 1, 1, 1, 1, -SLOT_WIDTH / 2);
+        validate(layout, "b", 2, 2, 1);
+        validate(layout, "f", 3, 3, 1);
+
+        validate(layout, "g", 1, 1, 5, 1);
+        validate(layout, "c", 2, 1, 1);
+
+        validate(layout, "gg", 3, 2, 3, 1);
+        validate(layout, "d", 4, 1, 1);
+        validate(layout, "e", 5, 2, 1);
+    }
+
+    @Test
+    public void staggeredDependencies() throws IOException {
+        layout = new MonitorLayout(getCompiler("layout-staggered-dependencies.xml"));
+        validate(layout, null, 0, 1, 7, 4);
+        validate(layout, "a", 1, 1, 1, 1, 1, -SLOT_WIDTH - SLOT_WIDTH / 2);
+        validate(layout, "aa", 1, 1, 1, 1, 1, -SLOT_WIDTH / 2);
+        validate(layout, "b", 2, 2, 1);
+        validate(layout, "f", 3, 3, 1);
+
+        validate(layout, "g", 1, 1, 5, 2, 1, +SLOT_WIDTH / 2);
+        validate(layout, "c", 2, 1, 1);
+
+        validate(layout, "gg", 3, 2, 3, 2);
+        validate(layout, "d", 4, 1, 1);
+        validate(layout, "dd", 4, 1, 1);
+        validate(layout, "e", 5, 2, 1);
+
+        validate(layout, "i", 6, 6, 1);
+    }
+
+    @Test
+    public void deepNext() throws IOException {
+        layout = new MonitorLayout(getCompiler("layout-deep-nest.xml"));
+        validate(layout, null, 0, 1, 7, 6);
+        validate(layout, "a", 1, 1, 1);
+        validate(layout, "aa", 1, 1, 1);
+        validate(layout, "b", 2, 2, 1);
+        validate(layout, "f", 3, 3, 1);
+
+        validate(layout, "g", 1, 1, 5, 2);
+        validate(layout, "c", 2, 1, 1);
+
+        validate(layout, "gg", 3, 2, 3, 2);
+        validate(layout, "d", 4, 1, 1);
+        validate(layout, "dd", 4, 1, 1);
+        validate(layout, "e", 5, 2, 1);
+
+        validate(layout, "i", 6, 6, 1);
+
+        validate(layout, "g1", 1, 1, 6, 2);
+        validate(layout, "g2", 2, 1, 5, 2);
+        validate(layout, "g3", 3, 1, 4, 2);
+        validate(layout, "u", 4, 1, 1);
+        validate(layout, "v", 4, 1, 1);
+        validate(layout, "w", 5, 2, 1);
+        validate(layout, "z", 6, 3, 1);
+    }
+
+
+    private void validate(MonitorLayout layout, String name,
+                          int absoluteTier, int tier, int depth, int breadth) {
+        Box b = layout.get(name);
+        assertEquals("incorrect absolute tier", absoluteTier, b.absoluteTier());
+        assertEquals("incorrect tier", tier, b.tier());
+        assertEquals("incorrect depth", depth, b.depth());
+        assertEquals("incorrect breadth", breadth, b.breadth());
+    }
+
+    private void validate(MonitorLayout layout, String name,
+                          int absoluteTier, int tier, int depth, int breadth,
+                          int top, int center) {
+        validate(layout, name, absoluteTier, tier, depth, breadth);
+        Box b = layout.get(name);
+        assertEquals("incorrect top", top, b.top());
+        assertEquals("incorrect center", center, b.center());
+    }
+
+    private void validate(MonitorLayout layout, String name,
+                          int absoluteTier, int tier, int depth) {
+        validate(layout, name, absoluteTier, tier, depth, 1);
+    }
+
+}
\ No newline at end of file
diff --git a/utils/stc/src/test/java/org/onlab/stc/StepProcessorTest.java b/utils/stc/src/test/java/org/onlab/stc/StepProcessorTest.java
index 2f04a5d..570c96d 100644
--- a/utils/stc/src/test/java/org/onlab/stc/StepProcessorTest.java
+++ b/utils/stc/src/test/java/org/onlab/stc/StepProcessorTest.java
@@ -51,7 +51,7 @@
     @Test
     public void basics() {
         Step step = new Step("foo", "ls " + DIR.getAbsolutePath(), null, null, null);
-        StepProcessor processor = new StepProcessor(step, DIR, delegate);
+        StepProcessor processor = new StepProcessor(step, DIR, delegate, null);
         processor.run();
         assertTrue("should be started", delegate.started);
         assertTrue("should be stopped", delegate.stopped);
@@ -65,7 +65,7 @@
         private boolean started, stopped, output;
 
         @Override
-        public void onStart(Step step) {
+        public void onStart(Step step, String command) {
             started = true;
         }
 
diff --git a/utils/stc/src/test/resources/org/onlab/stc/layout-basic-nest.xml b/utils/stc/src/test/resources/org/onlab/stc/layout-basic-nest.xml
new file mode 100644
index 0000000..19c48db
--- /dev/null
+++ b/utils/stc/src/test/resources/org/onlab/stc/layout-basic-nest.xml
@@ -0,0 +1,27 @@
+<!--
+  ~ Copyright 2015 Open Networking Laboratory
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<scenario name="basic-nest">
+    <step name="a"/>
+    <step name="b" requires="a"/>
+    <step name="f" requires="b"/>
+    <group name="g">
+        <step name="c"/>
+        <group name="gg" requires="c">
+            <step name="d"/>
+            <step name="e" requires="d"/>
+        </group>
+    </group>
+</scenario>
\ No newline at end of file
diff --git a/utils/stc/src/test/resources/org/onlab/stc/layout-basic.xml b/utils/stc/src/test/resources/org/onlab/stc/layout-basic.xml
new file mode 100644
index 0000000..d7dc138
--- /dev/null
+++ b/utils/stc/src/test/resources/org/onlab/stc/layout-basic.xml
@@ -0,0 +1,25 @@
+<!--
+  ~ Copyright 2015 Open Networking Laboratory
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<scenario name="basic">
+    <step name="a"/>
+    <step name="b" requires="a"/>
+    <step name="f" requires="b"/>
+    <group name="g">
+        <step name="c"/>
+        <step name="d" requires="c"/>
+        <step name="e" requires="d"/>
+    </group>
+</scenario>
\ No newline at end of file
diff --git a/utils/stc/src/test/resources/org/onlab/stc/layout-deep-nest.xml b/utils/stc/src/test/resources/org/onlab/stc/layout-deep-nest.xml
new file mode 100644
index 0000000..bbe1ac1
--- /dev/null
+++ b/utils/stc/src/test/resources/org/onlab/stc/layout-deep-nest.xml
@@ -0,0 +1,41 @@
+<!--
+  ~ Copyright 2015 Open Networking Laboratory
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<scenario name="basic-nest">
+    <step name="a"/>
+    <step name="aa"/>
+    <step name="b" requires="a"/>
+    <step name="f" requires="b,aa"/>
+    <group name="g">
+        <step name="c"/>
+        <group name="gg" requires="c">
+            <step name="d"/>
+            <step name="dd" requires="c"/>
+            <step name="e" requires="d"/>
+        </group>
+    </group>
+    <step name="i" requires="f,g"/>
+
+    <group name="g1">
+        <group name="g2">
+            <group name="g3">
+                <step name="u"/>
+                <step name="v"/>
+                <step name="w" requires="u,v"/>
+                <step name="z" requires="u,w"/>
+            </group>
+        </group>
+    </group>
+</scenario>
\ No newline at end of file
diff --git a/utils/stc/src/test/resources/org/onlab/stc/layout-staggered-dependencies.xml b/utils/stc/src/test/resources/org/onlab/stc/layout-staggered-dependencies.xml
new file mode 100644
index 0000000..318b4ba
--- /dev/null
+++ b/utils/stc/src/test/resources/org/onlab/stc/layout-staggered-dependencies.xml
@@ -0,0 +1,30 @@
+<!--
+  ~ Copyright 2015 Open Networking Laboratory
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<scenario name="basic-nest">
+    <step name="a"/>
+    <step name="aa"/>
+    <step name="b" requires="a"/>
+    <step name="f" requires="b,aa"/>
+    <group name="g">
+        <step name="c"/>
+        <group name="gg" requires="c">
+            <step name="d"/>
+            <step name="dd" requires="c"/>
+            <step name="e" requires="d"/>
+        </group>
+    </group>
+    <step name="i" requires="f,g"/>
+</scenario>
\ No newline at end of file