Design: lock artifacts at the approval gate, not before it
Architecture
One new hook and two edited graphs. The producing nodes stop demanding status: approved; the approval nodes gain a lock-artifacts step that flips the status as part of classifying the human's one approval. The skills stop re-implementing approvals in prose.
flowchart TB
subgraph before [before — two approvals per phase]
P1[producing node] -->|"validate-artifacts locked: true<br/>(approval #1, out-of-band)"| G1{approval node}
G1 -->|"classify-feedback (approval #2)"| N1[next node]
end
subgraph after [after — one approval, owned by the gate]
P2[producing node] -->|"validate-artifacts sections only"| G2{approval node}
G2 -->|"classify-feedback → record-feedback<br/>→ lock-artifacts writes status: approved + approvedBy"| N2[next node]
endThe lock-artifacts hook (cli/the_loop/graph/hooks/feedback.py)
- Declared as
{hook: lock-artifacts, with: {artifacts: [...]}}; entries use the same alternation vocabulary asproduces(requirements.md|bugfix.md) and resolve through the sameresolve_produces, so presence and ambiguity policy cannot drift. - Reads the most recent
classify-feedbackresult from the same chain run (ctx.results, therecord-feedbackpattern). Outcomeapproved/approved-with-comments→ lock; anything else →skipped(the gate stays a pure classifier on thechanges-requestedpath, whose edge routes backward). - Locking = splice the artifact's front-matter block in place:
status:value becomesapproved,approvedBy:merges the approving authors (inline comments and every other line preserved — the yamlpatch principle applied to front matter). A missingapprovedBykey is inserted afterstatus; a missing front-matter block is created. - After writing, the hook re-reads the front matter and verifies the lock landed; a splice that cannot prove itself blocks (fail closed), as does an ambiguous slot (two names present) or an unreadable file.
- An absent artifact is skipped: with
designdeclared away,design-approvalstill has onlytesting-plan.mdto lock. The hook never declaresdata["outcome"], soclassify-feedback's classification keeps routing the edge.
Graph changes
pdlc-work-item-loop.yaml:
| Node | Change |
|---|---|
brainstorming, requirements-definition, design, test-planning, tasks-breakdown | drop locked: true (sections/lint checks stay) |
requirements-approval | exit gains {hook: lock-artifacts, with: {artifacts: ["requirements.md|bugfix.md"]}} |
design-approval | exit gains {hook: lock-artifacts, with: {artifacts: [design.md, testing-plan.md]}} — one gate, two artifacts, exactly as its comment declares |
pdlc-contribution-loop.yaml: scoped-plan drops locked: true; plan-approval gains {hook: lock-artifacts, with: {artifacts: [contribution.md]}}.
Hook order on a gate: classify-feedback → record-feedback → lock-artifacts — the feedback is recorded into the document first, then the status flip seals it; both read the same classification, and only the lock is conditional on it being an approval.
Skill / command changes
skills/the-loop/SKILL.md+reference/workflow.md: "iterate until locked" becomes "iterate with feedback at the gate; the approval node locks". Sessions never setstatus: approvedand never post approval requests of their own —request-reviewon the gate is the one ask.commands/create-tasks-plan.mdstep 4 and the matching lines innew-requirement.md,create-design.md,create-testing-plan.md,work-on.md,brainstorm.md: the "request human review / do not proceed until approved / record the approver" steps are removed or rewritten to defer to the graph's gates; gate-less artifacts (brainstorm.md,tasks.md) proceed on shape alone.docs/capabilities/process-graph.mdandspec-workflow.mdupdated to match.
Interfaces
- New hook name
lock-artifactsin the shipped registry; params:artifacts: [<name-or-alternation>, ...](required — no artifacts, no lock). - No CLI, API or state-file changes.
HookResultvocabulary unchanged.
Data models
Front matter gains no new keys: status and approvedBy already exist in every bundled template; the hook now maintains them instead of the session.
Error handling
| Failure | Behaviour |
|---|---|
No prior classify-feedback in the chain, or outcome not an approval | skipped — the gate's own classification routes |
| Declared artifact absent | that slot skipped (planned absence); lock proceeds for the present ones |
| Two names of one slot present | block (the validate-artifacts ambiguity rule) |
| Unreadable/unwritable file, or the splice does not re-parse to a locked state | block, fail closed |
Security design
The trust boundary is unchanged: only classify-feedback reads comments, only authorized non-self-authored authors are counted, and lock-artifacts consumes that verdict in-process. The abuse case "attacker comment flips an artifact to approved" still requires an authorized author, exactly as before. Recording approvedBy from the classified comments improves the paper trail the skills previously kept by hand.
Testing strategy
See testing-plan.md: unit tests for the new hook (approve/lock, changes-requested skip, absent-artifact skip, ambiguity block, splice verification), graph-shape assertions, and the e2e suite reworked so fixtures are emitted unlocked — the happy path now proves the gate locks them (the regression test of AC 1.5), and gate-rejection pivots to a missing-section block.