Tasks: the-loop as a graph of nodes with entry/exit hooks
Phase 3 of 3. Derived from the locked
requirements.mdanddesign.md(both approved by @MadaraUchiha-314 on PR #110).Delivery shape. This is epic-sized — a runtime, a hook registry, nine shipped hooks, three integration transports, a breaking config migration and an escape hatch. It is therefore sequenced as five vertical slices, each independently mergeable, each leaving the repository working. Slice A alone is useful (it is the drift report this whole work item started from); nothing later is wasted if priorities change.
Delivery slices
| Slice | Delivers | Useful on its own? |
|---|---|---|
| A | hook contract + registry + validating hooks + the-loop check | yes — the drift report over all 34 spec folders |
| B | graph model, graph state, runtime, edges | yes — the-loop graph status |
| C | integrations (transport config, GitHub/Slack/Jira) + the breaking migration | yes — removes triplicated config |
| D | the shipped PDLC graph, side-effecting hooks, human gate, the-loop run | yes — the loop actually runs |
| E | escape hatch, harness stop-hook wrappers, CI gate | yes — enforcement + override |
Task list
TDD invariant (tdd.mode: standard): no production code without a failing test that motivates it. Security-relevant tasks name the negative test proving the boundary.
Slice A — the contract and the checker
- [x] 1.
HookContext/HookResultdataclasses and theMessagetypecli/the_loop/graph/contract.py;statusispass|block|wait|skip;messagesordered;datafree-form;retriabledefaulting true. Secret handles only (R2.7).- Depends on: none
- Requirements: R2.1, R2.2, R2.7
- Test:
pytest cli/tests/test_graph_contract.py(red→green)
- [x] 2. Hook registry —
@hook("name"),_REGISTRY,get_hook(),iter_hooks()- Mirrors
commands/base.py'sCommand/@registerpattern exactly (R6b.2). Duplicate name is aValueErrorat import, as the command registry already does. - Depends on: 1
- Requirements: R6b.2
- Test:
pytest cli/tests/test_graph_registry.py— registration, lookup, duplicate refusal
- Mirrors
- [x] 3. Chain executor — run hooks in order, short-circuit on first non-
pass- A raising or timing-out hook becomes
blockwithretriable=False— neverpass. - Depends on: 1, 2
- Requirements: R2.6, R3.1, R3.2, R3.4
- Test:
pytest cli/tests/test_graph_chain.py; negative: a hook that raises yieldsblock, notpass(abuse case 6)
- A raising or timing-out hook becomes
- [x] 4.
validate-artifactshook — existence, front-matter lock, required sections- Aggregates: every unmet requirement in one result, not one per round (R3.5).
- Depends on: 2, 3
- Requirements: R5.2, R3.5
- Test:
pytest cli/tests/test_hook_validate_artifacts.py— asserts a doc missing two sections yields one result with two messages
- [x] 5.
lint-artifactshook — markdownlint +diagramsRender- Mermaid blocks extracted and parsed; the incident that motivated this is in
design.md. - Depends on: 2, 3
- Requirements: R5.4
- Test:
pytest cli/tests/test_hook_lint_artifacts.py— a fixture with a backticked mermaid label blocks
- Mermaid blocks extracted and parsed; the incident that motivated this is in
- [x] 6.
the-loop checkcommand —--format table|json,--all,--recompute- Read-only: no network, no subprocess, no mutation (R8.8).
- Depends on: 3, 4, 5
- Requirements: R8.8, R8.4
- Test:
pytest cli/tests/test_check_integration.py; Scenario: check reports the specific unmet predicate for a design node missing its Security design section
- [x] 7. Run
the-loop check --allover this repository and record the drift report- This is the evidence the work item promised: the 34 existing spec folders, baselined.
- Depends on: 6
- Requirements: R8.4
- Test: output attached to the PR as evidence
Slice B — the graph and the runtime
- [x] 8. Graph model + loader — parse, validate, resolve, index, freeze
- Every structural failure is a startup failure naming the offending element (R6b.1). Cycles accepted (R1.6).
- Depends on: 2
- Requirements: R1.1, R1.2, R1.3, R1.5, R1.6, R6b.1
- Test:
pytest cli/tests/test_graph_model.py; negative: an edge naming an undeclared node fails at load with the id (abuse case 5)
- [x] 9. Repo-supplied graph is ignored with a warning
- Depends on: 8
- Requirements: R1.4
- Test: negative —
test_repo_graph_ignored; Scenario: A repository declaring workflow.graph is ignored with a warning
- [x] 10.
GraphState— load/save (atomic),reconstruct()from artifacts- Persist before the dependent side effect (R8.2). Unparseable → reconstruct, warn, keep the file (R8.3).
- Depends on: 1
- Requirements: R8.1, R8.2, R8.3
- Test:
pytest cli/tests/test_graph_state.py; Scenario: A work item with a deleted graph-state file resumes at the node its artifacts imply
- [x] 11. Edge resolution —
on: <outcome>, first-declared wins, no-match parks + escalates- Depends on: 8, 10
- Requirements: R1.5
- Test:
pytest cli/tests/test_graph_edges.py
- [x] 12. Runtime
advance()+ attempt accounting + escalation- Same predicate twice consecutively, or
maxAttempts, escalates and stops (R8.5). - Depends on: 3, 10, 11
- Requirements: R8.5, R8.6
- Test:
pytest cli/tests/test_graph_runtime.py; Scenario: A node failing the same predicate twice escalates instead of retrying
- Same predicate twice consecutively, or
- [x] 13. Event-log records for every transition, hook non-
pass, and edge taken- Depends on: 12
- Requirements: R8.7
- Test:
pytest cli/tests/test_graph_eventlog.py
Slice C — integrations and the breaking migration
- [x] 14.
Integrationprotocol + capability declaration + load-time capability check- A graph needing an unimplemented op fails at startup naming op, target and both fixes (R6.9).
- Depends on: 8
- Requirements: R6.8, R6.9, R6.10
- Test:
pytest cli/tests/test_integration_capabilities.py
- [x] 15.
integrationsconfig block +autoresolution + fail-closedauto= token → binary → fail naming both remedies; explicit transport never silently degrades (R6.3, R6.4).- Depends on: 14
- Requirements: R6.2, R6.3, R6.4
- Test:
pytest cli/tests/test_integration_config.py
- [x] 16. GitHub
clitransport — wrap the existingghpaths as a provider- Reuses
announce/comments/control/reactions/pollercode rather than replacing it (R6.14). - Depends on: 15
- Requirements: R6.6, R6.14
- Test: shared contract suite (task 19)
- Reuses
- [x] 17. GitHub
apitransport — stdlib HTTP + token,gh auth tokenas credential source- Depends on: 15
- Requirements: R6.6
- Test: shared contract suite (task 19)
- [x] 18. Slack
sdktransport (officialslack-sdk) + dependency-freewebhooktransport- Adds the work item's only new runtime dependency; zero transitive.
- Depends on: 15
- Requirements: R6.5
- Test:
pytest cli/tests/test_integration_slack.py; Scenario: A Slack webhook failure records and continues without wedging the graph
- [x] 19. Shared integration contract test suite — every provider, every operation
- Proves
apiandclibehave identically rather than assuming it. - Depends on: 16, 17, 18
- Requirements: R6.10
- Test:
pytest cli/tests/test_integration_contract.pyparametrized over providers
- Proves
- [x] 20. Breaking config migration — remove
ghBinary, bumpversion, refuse old configs- Runtime refuses to start naming key, replacement and
/the-loop:upgrade-the-loop(R6a.6). - Depends on: 15
- Requirements: R6a.1–R6a.6
- Test: negative —
test_runtime_refuses_unmigrated_config(R6a.8)
- Runtime refuses to start naming key, replacement and
- [x] 21. Teach
/the-loop:upgrade-the-loopthe migration; update both config templates- Deterministic key move, idempotent,
--dry-run, reports what it changed (R6a.7). - Depends on: 20
- Requirements: R6a.7, R6a.8
- Test: old-config fixture → expected new config; run twice, assert idempotent
- Deterministic key move, idempotent,
- [x] 22.
mcp-callhook — delegate to the harness with schema-constrained output- Depends on: 14
- Requirements: R6.11
- Test:
pytest cli/tests/test_hook_mcp_call.py
Slice D — the PDLC graph, side effects and the human gate
- [x] 23. Author the shipped graph
cli/the_loop/graph/pdlc.yaml+ its schema; validate in CI- Splits the six nodes currently hiding inside
needs-review. - Depends on: 8
- Requirements: R1.1, R1.2, R6b.6
- Test: CI validates the shipped graph;
pytest cli/tests/test_shipped_graph.py
- Splits the six nodes currently hiding inside
- [x] 24.
set-phase-label,log-entry,notify,request-reviewhooks- Comments carry the self-authored marker (R5.6); recipients only from
collaborators.yaml(R5.7). - Depends on: 14, 16
- Requirements: R5.1, R5.5, R5.6, R5.7, R9.2
- Test: negative — a recipient not in
collaborators.yamlis refused (abuse case 8)
- Comments carry the self-authored marker (R5.6); recipients only from
- [x] 25.
classify-feedbackhook — schema-constrained, authorized authors only- Claude Code
--json-schema; Cursor embeds schema + validates + bounded retry. Invalid after retries →wait, never an assumed outcome. - Depends on: 3
- Requirements: R4.8, R4.9
- Test: negative —
test_unauthorized_comment_not_read(abuse cases 1–3); Scenario: A comment from an unauthorized user is not read and the gate stays waiting
- Claude Code
- [x] 26.
record-feedbackhook — append to the artifact's## Review comments- Append-only, attributed, dated; never rewrites earlier entries.
- Depends on: 3
- Requirements: R4.5, R5.3
- Test:
pytest cli/tests/test_hook_record_feedback.py
- [x] 27. Add
## Review commentsto the artifact templates- Depends on: 26
- Requirements: R5.2
- Test:
validate-artifactsrequires the section on a gated artifact
- [x] 28. Human-gate node behaviour —
waiton indecisive, three decisive outcomes- Depends on: 12, 25, 26
- Requirements: R4.1–R4.6
- Test: Scenario: A partial review comment leaves the gate waiting rather than advancing
- [x] 29.
session: inherit+ fallback to fresh seeded with the artifacts- Depends on: 28
- Requirements: R4.7, R7.3, R7.4
- Test: Scenario: A changes-requested outcome returns to the producing node in the same harness session
- [x] 30.
the-loop run— drive nodes;--dry-run; respectControlStorepause/stop- Depends on: 12, 23
- Requirements: R7.1, R7.5
- Test:
pytest cli/tests/test_run_integration.py
- [x] 31.
verify-testshook- Depends on: 3
- Requirements: R5.1
- Test:
pytest cli/tests/test_hook_verify_tests.py
Slice E — enforcement and the escape hatch
- [x] 32.
the-loop graph force— the escape hatch- Moves the pointer, never forges a verdict: records
forced, leaves the gate's real verdict intact so--recomputestill reports it unmet.--reasonrequired. Unknown node refused. Undeclared transition warned.required-gate bypass warned explicitly. - Depends on: 12
- Requirements: R10.1–R10.9
- Test:
pytest cli/tests/test_graph_force.py; negative:test_force_does_not_mark_gate_satisfied— after forcing,check --recomputestill reports the bypassed gate as unmet (R10.4)
- Moves the pointer, never forges a verdict: records
- [x] 33. Force audit trail — graph state, execution log, event log, marked ticket comment
- Depends on: 32, 24
- Requirements: R10.5
- Test: asserts all four records exist
- [x] 34. Harness stop-hook wrappers —
hooks/the-loop-gate.py(ClaudeStop) and.cursor/hooks.json(Cursorstop)- Claude blocks the stop; Cursor returns
followup_message. Attempt cap enforced on the Claude path (Cursor caps natively). - Depends on: 6, 12
- Requirements: R3.2
- Test:
pytest cli/tests/test_harness_hook_wrappers.py
- Claude blocks the stop; Cursor returns
- [x] 35. CI + pre-push gate running
the-loop check --recompute; baseline the existing 34- Depends on: 6, 7
- Requirements: R8.4
- Test: Scenario: CI fails a work item whose graph-state claims a node complete that the artifacts contradict
- [x] 36. Capability docs +
reference/updates in the same PR as the behaviourdocs/capabilities/spec-workflow.md,cli.md, and the workflow reference's "open design question" now answered bydecision-041.- Depends on: 30, 32
- Requirements: —
- Test: ready-to-ship gate item
Dependency graph (DAG)
flowchart LR
subgraph A["Slice A — contract + checker"]
T1[1 contract] --> T2[2 registry] --> T3[3 chain]
T3 --> T4[4 validate] & T5[5 lint]
T4 & T5 --> T6[6 check] --> T7[7 drift report]
end
subgraph B["Slice B — graph + runtime"]
T2 --> T8[8 model] --> T9[9 ignore repo graph]
T1 --> T10[10 state]
T8 & T10 --> T11[11 edges] --> T12[12 runtime] --> T13[13 events]
T3 --> T12
end
subgraph C["Slice C — integrations + migration"]
T8 --> T14[14 capabilities] --> T15[15 transport config]
T15 --> T16[16 gh cli] & T17[17 gh api] & T18[18 slack]
T16 & T17 & T18 --> T19[19 contract suite]
T15 --> T20[20 breaking migration] --> T21[21 upgrade command]
T14 --> T22[22 mcp-call]
end
subgraph D["Slice D — PDLC graph + gate"]
T8 --> T23[23 shipped graph]
T14 & T16 --> T24[24 side-effect hooks]
T3 --> T25[25 classify] & T26[26 record-feedback] & T31[31 verify-tests]
T26 --> T27[27 templates]
T12 & T25 & T26 --> T28[28 gate] --> T29[29 session inherit]
T12 & T23 --> T30[30 run]
end
subgraph E["Slice E — enforcement + override"]
T12 --> T32[32 force] --> T33[33 audit]
T24 --> T33
T6 & T12 --> T34[34 stop-hook wrappers]
T6 & T7 --> T35[35 CI gate]
T30 & T32 --> T36[36 capability docs]
endDelivery status (2026-07-29)
36 of 36 tasks complete. make check green: ruff, ruff-format, pyright, markdownlint, config validation, 684 tests (1 skipped — the Slack sdk transport, an optional extra).
| Slice | State |
|---|---|
A — contract, registry, chain, validators, check | complete, drift report produced |
| B — model, state, runtime, edges, events | complete |
C — integrations, breaking config migration, mcp-call | complete |
D — shipped graph, hooks, human gate, graph run | complete |
| E — escape hatch, stop-hook wrappers, CI gate, capability docs | complete |
Deliberately out of scope: paying down the 35-folder backlog the drift report surfaced. The CI gate binds work that touches a spec folder, so new work is covered from here; the backlog is a separate work item with its own review.
Checkpoints
Tests run and the execution log is updated at every slice boundary, and at each task inside a slice per contextManagement.taskBoundary: compact. Each task records its test command and its red→green transition as evidence.
| Checkpoint | Gate |
|---|---|
| End of A | make check green; drift report attached to the PR |
| End of B | make check green; graph status walks a fixture work item |
| End of C | contract suite green across providers; migration idempotent; runtime refuses old config |
| End of D | the loop runs a work item end to end in --dry-run |
| End of E | force leaves --recompute honest; CI gate active; capability docs updated |
After the last task: self-review ×3, critic-review ×3, then the security review gate (security.review) recorded in the execution log — with a named human sign-off required at risk tier 4 before the work item can be marked ready.