flow-next-deps · git:20261106.d27e372 · 2026-11-06 · sha256 8b2be898c9e9d7b7
flow-next-deps git:20261106.d27e372A
Immutable. This exact content never changes and is served at /api/v1/blob/8b2be898c9e9d7b7.
---
name: flow-next-deps
description: "Show spec dependency graph and execution order. Use when asking 'what's blocking what', 'execution order', 'dependency graph', 'what order should specs run', 'critical path', 'which specs can run in parallel'."
---
# Flow-Next Dependency Graph
Visualize spec dependencies, blocking chains, and execution phases.
## Preamble
flowctl is bundled with the plugin (not on PATH). Define once; subsequent blocks use `$FLOWCTL`:
```bash
FLOWCTL="${CODEX_HOME:-$HOME/.codex}/scripts/flowctl"
[ -x "$FLOWCTL" ] || FLOWCTL="<plugin-root>/scripts/flowctl" # <plugin-root> = the directory two levels above this skill's SKILL.md file (the harness gave you that file's absolute path when the skill loaded); substitute it literally
[ -x "$FLOWCTL" ] || FLOWCTL=".flow/bin/flowctl"
```
## Setup
```bash
$FLOWCTL detect --json | jq -e '.exists' >/dev/null && echo "OK: .flow/ exists" || echo "ERROR: run $FLOWCTL init"
command -v jq >/dev/null 2>&1 && echo "OK: jq installed" || echo "ERROR: brew install jq"
```
## Step 1: Gather Spec Data
Build a consolidated view of all specs with their dependencies — a single per-spec loop for the whole skill, over open specs only (a done spec needs only its status, which the specs list carries). Steps 2 and 3 reuse the cached file (bash vars do not survive across prompt turns, so the cache is a file at a literal agent-composed path — compose `<suffix>` once, e.g. 4 random chars, and reuse the same literal path in every later block):
```bash
# ONE gather — Steps 2 and 3 read this file; never re-run the per-spec loop
SPECS_FILE="${TMPDIR:-/tmp}/flow-deps-specs-<suffix>.json"
SPECS_LIST="$($FLOWCTL specs --json)"
{
printf '%s' "$SPECS_LIST" | jq -c '.specs[] | select(.status == "done") | {id, title, status}'
printf '%s' "$SPECS_LIST" | jq -r '.specs[] | select(.status != "done") | .id' | while read -r id; do
$FLOWCTL show "$id" --json | jq -c '{
id: .id,
title: .title,
status: .status,
plan_review: .plan_review_status,
deps: (.depends_on_epics // [])
}'
done
} | jq -s '.' > "$SPECS_FILE"
cat "$SPECS_FILE"
```
### Done when
- `$SPECS_FILE` exists at the composed literal path and parses as a JSON array with one object per spec returned by `flowctl specs --json`.
- The per-spec `flowctl show` loop has run exactly once for this invocation, over the open specs. **Steps 2 and 3 read that file.** A second run of the gather loop has broken this.
## Step 2: Identify Blocking Chains
Determine which specs are ready vs blocked (pure jq, works on any shell):
```bash
# Reuse the Step 1 gather — same literal path, NO re-fetch (one heavy loop total)
SPECS_FILE="${TMPDIR:-/tmp}/flow-deps-specs-<suffix>.json"
# Compute blocking status
jq -r '
# Build status lookup
(map({(.id): .status}) | add // {}) as $status |
# Check each non-done spec
.[] | select(.status != "done") |
.id as $id | .title as $title |
# Find deps that are not done
([.deps[] | select($status[.] != "done")] | join(", ")) as $blocked_by |
if ($blocked_by | length) == 0 then
"READY: \($id) - \($title)"
else
"BLOCKED: \($id) - \($title) (by: \($blocked_by))"
end
' "$SPECS_FILE"
```
### Done when
- Every non-`done` spec in `$SPECS_FILE` emitted exactly one `READY:` or `BLOCKED:` line, and each `BLOCKED:` line names the specific deps holding it.
- The jq ran against the cached file, not a fresh `flowctl show` sweep.
## Step 3: Compute Execution Phases
Group specs into parallel execution phases:
```bash
# Reuse the Step 1 gather — same literal path, NO re-fetch (one heavy loop total)
SPECS_FILE="${TMPDIR:-/tmp}/flow-deps-specs-<suffix>.json"
# fence:deps-phases
# Phase assignment algorithm (run in jq for reliability)
jq '
# Build status lookup
(map({(.id): .status}) | add // {}) as $status |
# Filter to non-done specs
[.[] | select(.status != "done")] as $open |
# Assign phases until a round places nothing, so a chain of any depth is phased
{assigned: [], result: [], open: $open, stalled: false} |
until(.stalled;
.assigned as $assigned |
# Find specs not yet assigned whose deps are all done or in earlier phases
([.open[] | select(
([.id] | inside($assigned) | not) and
((.deps // []) | all(. as $d | $status[$d] == "done" or ($assigned | index($d))))
)] | map(.id)) as $ready |
if ($ready | length) > 0 then
.result += [{phase: ((.result | length) + 1), specs: [.open[] | select(.id | IN($ready[]))]}] |
.assigned += $ready
else .stalled = true end
) |
# Emit the phases AND the residue: any open spec never assigned can never start.
# Dropping it silently is the one way /deps gives a WRONG answer (the graph it exists to
# expose hides the deadlock). Surface it with its unresolved deps and its cause: on a
# dependency cycle (with the path), a dep that is not among the specs, or blocked only
# by another deadlocked spec.
.assigned as $asg |
([ $open[] | select(.id as $i | ($asg | index($i)) | not)
| {key: .id, value: [ (.deps // [])[] | select(. as $d | ($asg | index($d)) or ($status[$d] == "done") | not) ]} ]
| from_entries) as $unres |
# Breadth-first walk over unresolved deps back to $id; the path, or null
def cycle_of($id):
{queue: [$id], parent: {}, last: null} |
until((.queue | length) == 0 or .last != null;
.queue[0] as $n | .queue |= .[1:] |
reduce $unres[$n][] as $d (.;
if .last != null then .
elif $d == $id then .last = $n
elif .parent[$d] != null or $unres[$d] == null then .
else .parent[$d] = $n | .queue += [$d] end)) |
.parent as $p |
if .last == null then null
else [ .last | recurse(if . == $id then empty else $p[.] end) ] | reverse + [$id] end;
{ phases: .result,
deadlocked: [ $open[] | select($unres[.id] != null) | .id as $i | cycle_of($i) as $cycle
| { id, status, unresolved_deps: $unres[$i],
cause: (if $cycle != null then "cycle"
elif any($unres[$i][]; $status[.] == null) then "missing"
else "blocked" end) }
+ (if $cycle != null then {cycle: $cycle} else {} end) ] }
' "$SPECS_FILE"
```
### Done when
- The jq result carries both `.phases` and `.deadlocked`, every open spec appears in exactly one of them, and every `.deadlocked` entry carries a `cause`.
- **The report is rendered from `.phases` and `.deadlocked`.** Phases narrated from a reading of the spec titles have broken this.
## Output Format
Present results as:
```markdown
## Spec Dependency Graph
### Status Overview
| Spec | Title | Status | Dependencies | Blocked By |
|------|-------|--------|--------------|------------|
| **fn-1-add-auth** | Add Authentication | **READY** | - | - |
| fn-2-add-oauth | Add OAuth Login | blocked | fn-1-add-auth | fn-1-add-auth |
| fn-3-user-profile | User Profile Page | blocked | fn-1-add-auth, fn-2-add-oauth | fn-2-add-oauth |
### Execution Phases
Render from the jq result's `.phases`:
| Phase | Specs | Can Start |
|-------|-------|-----------|
| **1** | fn-1-add-auth | **NOW** |
| 2 | fn-2-add-oauth | After Phase 1 |
| 3 | fn-3-user-profile | After Phase 2 |
### ⚠️ Deadlocked / Unresolvable
**This section renders exactly when `.deadlocked` is non-empty** — and when it does, it is the
most important part of the report. Each entry is an open spec that could not be placed in
any phase. These are invisible to `ready`/`flow --auto` (they just never become ready) — this is the
one place the graph surfaces them.
| Spec | Status | Unresolved deps | Cause |
|------|--------|-----------------|-------|
| fn-7-x | open | fn-9-y | cycle: fn-7-x → fn-9-y → fn-7-x |
| fn-8-z | open | fn-99-gone | missing dependency |
| fn-10-w | open | fn-7-x | blocked by a deadlocked spec |
The Cause column renders each entry's `cause` (and `cycle` path); it is never guessed. A cycle is
broken with `flowctl spec rm-dep` on one of its edges; a missing dependency was deleted or never
existed. **Every entry of `.deadlocked` reaches the report.** A report that lists phases while silently dropping an open spec has broken this.
### Critical Path
fn-1-add-auth → fn-2-add-oauth → fn-3-user-profile (3 phases)
```
**This skill is read-only inspection.** Edge changes are reported as commands for the
operator to run — `flowctl spec add-dep <spec> <dep>` / `rm-dep <spec> <dep>`. A run that
executes either command itself, or edits a spec file, has broken this.
## Quick One-Liner
For a fast dependency check:
```bash
$FLOWCTL specs --json | jq -r '.specs[] | select(.status != "done") | "\(.id): \(.title) [\(.status)]"'
```
## When to Use
- "What's the execution order for specs?"
- "What's blocking progress?"
- "Show me the dependency graph"
- "What's the critical path?"
- "Which specs can run in parallel?"
- "What should I work on next?"