Skip to content

Commit 8517941

Browse files
committed
feat: add task-graph validation hook and plan audit completeness gate
New hook: validate-task-graph.sh (SubagentStart) - Validates task-graph.json schema before teammate spawn - Checks: valid JSON, required fields, dependency references, cycle detection - Blocks (exit 2) on invalid schema or circular dependencies - 12 test assertions, all passing Plan audit completeness: - Mandatory Plan Audit Result table in progress.md (7 checks) - Hard gate: DO NOT present plan until all 7 rows filled - Template added to workspace-templates.md
1 parent fab1cbd commit 8517941

5 files changed

Lines changed: 337 additions & 0 deletions

File tree

docs/workspace-templates.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,20 @@ Append-only log of significant decisions.
5757

5858
- [{timestamp}] {decision and reasoning}
5959

60+
## Plan Audit Result
61+
62+
Filled by the lead during Phase 1a Step 3 (plan audit). All 7 rows must be completed before presenting the plan to the user.
63+
64+
| # | Check | Status | Notes |
65+
|---|-------|--------|-------|
66+
| 1 | Task completeness | {PASS/FAIL} | {notes} |
67+
| 2 | Dependency coherence | {PASS/FAIL} | {notes} |
68+
| 3 | File reference validity | {PASS/FAIL} | {notes} |
69+
| 4 | Scope coverage | {PASS/FAIL} | {notes} |
70+
| 5 | Reference freshness | {PASS/FAIL} | {notes} |
71+
| 6 | Feasibility | {PASS/FAIL} | {notes} |
72+
| 7 | Parallelizability | {PASS/FAIL} | {notes} |
73+
6074
## Plan Proposals
6175

6276
| Teammate | Task | Proposal | Status | Revisions |

hooks/hooks.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,15 @@
7575
}
7676
],
7777
"SubagentStart": [
78+
{
79+
"hooks": [
80+
{
81+
"type": "command",
82+
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/validate-task-graph.sh",
83+
"timeout": 15
84+
}
85+
]
86+
},
7887
{
7988
"hooks": [
8089
{

scripts/validate-task-graph.sh

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
#!/bin/bash
2+
# Hook: SubagentStart
3+
# Validates task-graph.json schema and checks for circular dependencies
4+
# BEFORE teammates are spawned. Blocks if invalid or cyclic.
5+
#
6+
# Exit 0 = valid (allow spawn)
7+
# Exit 2 = invalid (block spawn with feedback)
8+
9+
# Graceful jq fallback — can't validate without jq
10+
if ! command -v jq &>/dev/null; then
11+
exit 0
12+
fi
13+
14+
INPUT=$(cat)
15+
CWD=$(echo "$INPUT" | jq -r '.cwd // empty')
16+
TEAM=$(echo "$INPUT" | jq -r '.team_name // empty')
17+
18+
# Need both to locate task-graph.json
19+
if [ -z "$CWD" ] || [ -z "$TEAM" ]; then
20+
exit 0
21+
fi
22+
23+
# Resolve workspace path (with -fix suffix fallback for remediation teams)
24+
GRAPH_FILE="$CWD/.agent-team/$TEAM/task-graph.json"
25+
if [ ! -f "$GRAPH_FILE" ]; then
26+
BASE_NAME="${TEAM%-fix}"
27+
if [ "$BASE_NAME" != "$TEAM" ] && [ -f "$CWD/.agent-team/$BASE_NAME/task-graph.json" ]; then
28+
GRAPH_FILE="$CWD/.agent-team/$BASE_NAME/task-graph.json"
29+
else
30+
# No task-graph.json — allow spawn (workspace may not be initialized yet)
31+
exit 0
32+
fi
33+
fi
34+
35+
# --- Check 1: Valid JSON ---
36+
GRAPH=$(jq '.' "$GRAPH_FILE" 2>/dev/null)
37+
if [ -z "$GRAPH" ]; then
38+
echo "BLOCKED: task-graph.json exists but is not valid JSON. Fix the file before spawning teammates." >&2
39+
echo "File: $GRAPH_FILE" >&2
40+
exit 2
41+
fi
42+
43+
# --- Check 2: Has nodes object with at least 1 entry ---
44+
NODE_COUNT=$(echo "$GRAPH" | jq '.nodes | length // 0' 2>/dev/null)
45+
if [ -z "$NODE_COUNT" ] || [ "$NODE_COUNT" -eq 0 ]; then
46+
# Empty nodes is OK — plan stage may not have populated yet
47+
exit 0
48+
fi
49+
50+
# --- Check 3: Required fields on each node ---
51+
MISSING_FIELDS=$(echo "$GRAPH" | jq -r '
52+
[.nodes | to_entries[] |
53+
select(
54+
(.value.subject == null) or
55+
(.value.status == null) or
56+
(.value.depends_on == null)
57+
) | .key
58+
] | join(", ")
59+
' 2>/dev/null)
60+
61+
if [ -n "$MISSING_FIELDS" ] && [ "$MISSING_FIELDS" != "" ]; then
62+
echo "BLOCKED: task-graph.json nodes missing required fields (subject, status, depends_on)." >&2
63+
echo "Affected nodes: $MISSING_FIELDS" >&2
64+
echo "Fix the task-graph.json schema before spawning teammates." >&2
65+
exit 2
66+
fi
67+
68+
# --- Check 4: depends_on references point to existing node IDs ---
69+
DANGLING=$(echo "$GRAPH" | jq -r '
70+
.nodes as $nodes |
71+
[.nodes | to_entries[] | .value.depends_on[]? |
72+
select($nodes[.] == null)
73+
] | unique | join(", ")
74+
' 2>/dev/null)
75+
76+
if [ -n "$DANGLING" ] && [ "$DANGLING" != "" ]; then
77+
echo "BLOCKED: task-graph.json has dangling dependency references." >&2
78+
echo "Missing node IDs: $DANGLING" >&2
79+
echo "All depends_on entries must reference existing node IDs." >&2
80+
exit 2
81+
fi
82+
83+
# --- Check 5: Cycle detection (DFS) ---
84+
HAS_CYCLE=$(echo "$GRAPH" | jq '
85+
def has_cycle(id; visited):
86+
if (visited | index(id)) then true
87+
elif (.nodes[id] == null) then false
88+
else
89+
. as $g |
90+
any(.nodes[id].depends_on[]; . as $dep | $g | has_cycle($dep; visited + [id]))
91+
end;
92+
. as $root |
93+
any(.nodes | keys[]; . as $k | $root | has_cycle($k; []))
94+
' 2>/dev/null)
95+
96+
if [ "$HAS_CYCLE" = "true" ]; then
97+
# Find the cycle for error message
98+
CYCLE_NODES=$(echo "$GRAPH" | jq -r '
99+
def find_cycle(id; visited):
100+
if (visited | index(id)) then [id]
101+
elif (.nodes[id] == null) then []
102+
else
103+
. as $g |
104+
[.nodes[id].depends_on[] | . as $dep | $g | find_cycle($dep; visited + [id])] |
105+
add // []
106+
end;
107+
. as $root |
108+
[.nodes | keys[] | . as $k | $root | find_cycle($k; [])] | add | unique | join(" -> ")
109+
' 2>/dev/null)
110+
echo "BLOCKED: Circular dependency detected in task-graph.json." >&2
111+
echo "Cycle involves: $CYCLE_NODES" >&2
112+
echo "Fix the dependency graph before spawning teammates. Remove or reorder depends_on entries to break the cycle." >&2
113+
exit 2
114+
fi
115+
116+
# All checks passed
117+
exit 0

skills/plan/SKILL.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,23 @@ Both paths converge here. Every plan is audited before the user sees it.
176176

177177
The Team Lead performs the audit inline -- reading the plan file, cross-referencing codebase state, and evaluating each check. No separate skill invocation needed.
178178

179+
**MANDATORY: Record audit results in progress.md.** Before proceeding to Step 4 or Phase 1b, you MUST fill in this table in the workspace `progress.md`:
180+
181+
```markdown
182+
## Plan Audit Result
183+
| # | Check | Status | Notes |
184+
|---|-------|--------|-------|
185+
| 1 | Task completeness | {PASS/FAIL} | {notes} |
186+
| 2 | Dependency coherence | {PASS/FAIL} | {notes} |
187+
| 3 | File reference validity | {PASS/FAIL} | {notes} |
188+
| 4 | Scope coverage | {PASS/FAIL} | {notes} |
189+
| 5 | Reference freshness | {PASS/FAIL} | {notes} |
190+
| 6 | Feasibility | {PASS/FAIL} | {notes} |
191+
| 7 | Parallelizability | {PASS/FAIL} | {notes} |
192+
```
193+
194+
**DO NOT present the plan to the user (Phase 2) until all 7 rows are filled.** This is a hard gate — incomplete audits produce incomplete plans.
195+
179196
#### Step 4 -- User Decision Gate
180197

181198
Present to user regardless of audit status:
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
#!/bin/bash
2+
# Tests for scripts/validate-task-graph.sh (SubagentStart hook)
3+
# Validates task-graph.json schema and cycle detection before teammate spawn
4+
5+
source "$(dirname "$0")/../lib/test-helpers.sh"
6+
7+
HOOK="$PROJECT_ROOT/scripts/validate-task-graph.sh"
8+
9+
echo "ValidateTaskGraph hook tests"
10+
echo "============================"
11+
12+
if ! command -v jq &>/dev/null; then
13+
printf " ${YELLOW}SKIP${RESET} all — jq not installed (hooks degrade gracefully without it)\n"
14+
exit 0
15+
fi
16+
17+
# --- Test 1: No task-graph.json — allow (exit 0) ---
18+
setup_temp_dir
19+
setup_mock_workspace "test-team"
20+
cd "$TEST_TEMP_DIR"
21+
run_hook "$HOOK" '{"cwd":"'"$TEST_TEMP_DIR"'","team_name":"test-team","hook_event_name":"SubagentStart"}'
22+
assert_exit_code 0 "$HOOK_EXIT" "1: Allow when no task-graph.json"
23+
cleanup_temp_dir
24+
25+
# --- Test 2: Valid task-graph.json — allow (exit 0) ---
26+
setup_temp_dir
27+
setup_mock_workspace "test-team"
28+
setup_mock_task_graph "test-team"
29+
cd "$TEST_TEMP_DIR"
30+
run_hook "$HOOK" '{"cwd":"'"$TEST_TEMP_DIR"'","team_name":"test-team","hook_event_name":"SubagentStart"}'
31+
assert_exit_code 0 "$HOOK_EXIT" "2: Allow valid task-graph.json"
32+
cleanup_temp_dir
33+
34+
# --- Test 3: Malformed JSON — block (exit 2) ---
35+
setup_temp_dir
36+
setup_mock_workspace "test-team"
37+
echo "not valid json {{{" > "$TEST_TEMP_DIR/.agent-team/test-team/task-graph.json"
38+
cd "$TEST_TEMP_DIR"
39+
run_hook "$HOOK" '{"cwd":"'"$TEST_TEMP_DIR"'","team_name":"test-team","hook_event_name":"SubagentStart"}'
40+
assert_exit_code 2 "$HOOK_EXIT" "3: Block malformed JSON"
41+
assert_stderr_contains "not valid JSON" "$HOOK_STDERR" "3: Error mentions invalid JSON"
42+
cleanup_temp_dir
43+
44+
# --- Test 4: Missing required fields — block (exit 2) ---
45+
setup_temp_dir
46+
setup_mock_workspace "test-team"
47+
cat > "$TEST_TEMP_DIR/.agent-team/test-team/task-graph.json" <<'GRAPH'
48+
{
49+
"team": "test",
50+
"nodes": {
51+
"#1": {
52+
"subject": "Task 1"
53+
}
54+
}
55+
}
56+
GRAPH
57+
cd "$TEST_TEMP_DIR"
58+
run_hook "$HOOK" '{"cwd":"'"$TEST_TEMP_DIR"'","team_name":"test-team","hook_event_name":"SubagentStart"}'
59+
assert_exit_code 2 "$HOOK_EXIT" "4: Block missing required fields"
60+
assert_stderr_contains "missing required fields" "$HOOK_STDERR" "4: Error mentions missing fields"
61+
cleanup_temp_dir
62+
63+
# --- Test 5: Dangling dependency reference — block (exit 2) ---
64+
setup_temp_dir
65+
setup_mock_workspace "test-team"
66+
cat > "$TEST_TEMP_DIR/.agent-team/test-team/task-graph.json" <<'GRAPH'
67+
{
68+
"team": "test",
69+
"nodes": {
70+
"#1": {
71+
"subject": "Task 1",
72+
"owner": "impl-1",
73+
"status": "pending",
74+
"depends_on": ["#99"],
75+
"completed_at": null,
76+
"output_files": [],
77+
"critical_path": false,
78+
"convergence_point": false
79+
}
80+
}
81+
}
82+
GRAPH
83+
cd "$TEST_TEMP_DIR"
84+
run_hook "$HOOK" '{"cwd":"'"$TEST_TEMP_DIR"'","team_name":"test-team","hook_event_name":"SubagentStart"}'
85+
assert_exit_code 2 "$HOOK_EXIT" "5: Block dangling dependency"
86+
assert_stderr_contains "dangling dependency" "$HOOK_STDERR" "5: Error mentions dangling ref"
87+
cleanup_temp_dir
88+
89+
# --- Test 6: Circular dependency — block (exit 2) ---
90+
setup_temp_dir
91+
setup_mock_workspace "test-team"
92+
cat > "$TEST_TEMP_DIR/.agent-team/test-team/task-graph.json" <<'GRAPH'
93+
{
94+
"team": "test",
95+
"nodes": {
96+
"#1": {
97+
"subject": "Task 1",
98+
"owner": "impl-1",
99+
"status": "pending",
100+
"depends_on": ["#2"],
101+
"completed_at": null,
102+
"output_files": [],
103+
"critical_path": false,
104+
"convergence_point": false
105+
},
106+
"#2": {
107+
"subject": "Task 2",
108+
"owner": "impl-2",
109+
"status": "pending",
110+
"depends_on": ["#1"],
111+
"completed_at": null,
112+
"output_files": [],
113+
"critical_path": false,
114+
"convergence_point": false
115+
}
116+
}
117+
}
118+
GRAPH
119+
cd "$TEST_TEMP_DIR"
120+
run_hook "$HOOK" '{"cwd":"'"$TEST_TEMP_DIR"'","team_name":"test-team","hook_event_name":"SubagentStart"}'
121+
assert_exit_code 2 "$HOOK_EXIT" "6: Block circular dependency"
122+
assert_stderr_contains "Circular dependency" "$HOOK_STDERR" "6: Error mentions cycle"
123+
cleanup_temp_dir
124+
125+
# --- Test 7: Empty nodes — allow (exit 0) ---
126+
setup_temp_dir
127+
setup_mock_workspace "test-team"
128+
echo '{"team":"test","nodes":{}}' > "$TEST_TEMP_DIR/.agent-team/test-team/task-graph.json"
129+
cd "$TEST_TEMP_DIR"
130+
run_hook "$HOOK" '{"cwd":"'"$TEST_TEMP_DIR"'","team_name":"test-team","hook_event_name":"SubagentStart"}'
131+
assert_exit_code 0 "$HOOK_EXIT" "7: Allow empty nodes"
132+
cleanup_temp_dir
133+
134+
# --- Test 8: Valid graph with dependencies — allow (exit 0) ---
135+
setup_temp_dir
136+
setup_mock_workspace "test-team"
137+
cat > "$TEST_TEMP_DIR/.agent-team/test-team/task-graph.json" <<'GRAPH'
138+
{
139+
"team": "test",
140+
"nodes": {
141+
"#1": {
142+
"subject": "Task 1",
143+
"owner": "impl-1",
144+
"status": "pending",
145+
"depends_on": [],
146+
"completed_at": null,
147+
"output_files": ["src/a.ts"],
148+
"critical_path": true,
149+
"convergence_point": false
150+
},
151+
"#2": {
152+
"subject": "Task 2",
153+
"owner": "impl-2",
154+
"status": "pending",
155+
"depends_on": ["#1"],
156+
"completed_at": null,
157+
"output_files": ["src/b.ts"],
158+
"critical_path": false,
159+
"convergence_point": false
160+
},
161+
"#3": {
162+
"subject": "Review",
163+
"owner": "reviewer",
164+
"status": "pending",
165+
"depends_on": ["#1", "#2"],
166+
"completed_at": null,
167+
"output_files": [],
168+
"critical_path": false,
169+
"convergence_point": true
170+
}
171+
}
172+
}
173+
GRAPH
174+
cd "$TEST_TEMP_DIR"
175+
run_hook "$HOOK" '{"cwd":"'"$TEST_TEMP_DIR"'","team_name":"test-team","hook_event_name":"SubagentStart"}'
176+
assert_exit_code 0 "$HOOK_EXIT" "8: Allow valid graph with dependencies"
177+
cleanup_temp_dir
178+
179+
print_summary
180+
exit "$TESTS_FAILED"

0 commit comments

Comments
 (0)