diff --git a/.do-work/archive/REQ-283-markdown-port-path.md b/.do-work/archive/REQ-283-markdown-port-path.md new file mode 100644 index 0000000..421eb52 --- /dev/null +++ b/.do-work/archive/REQ-283-markdown-port-path.md @@ -0,0 +1,40 @@ +# REQ-283: Markdown-default tracker port path + + +**UR:** UR-045 +**Status:** done +**Created:** 2026-07-31 +**Layer:** none +**Entry point:** /do-work phase agents with tracker.backend unset or markdown +**Terminal state:** All work-item ops resolve through agents/tracker/port.md + markdown.md; existing lib tests and conformance pass without Linear +**Parent:** +**Closure proof:** checkpoint:.do-work/runs/RUN-060.yml#REQ-283 commit:84aa0f0 tests:passed +**Criteria approved:** agent-drafted +**Priority:** 3 +**Size:** M +**Files:** agents/tracker/port.md agents/tracker/markdown.md agents/config.md SKILL.md +**Depends on:** + +## Task + +Define the reachable path for markdown-default multi-tracker: config resolves backend to markdown, agents load port + markdown backend, and product behavior matches today. + +## Context + +Design §2 goals 1 and Done-when #1; §5 load path; clarification: full map in one UR ordered by §16 phasing. + +## Acceptance Criteria + +- [x] Path-unit documents entry (default/markdown backend) and terminal (regression green, no Linear required) +- [x] Child REQs under this path implement config, port catalog, markdown mapping, and agent load-path wiring +- [x] No dual-write or Linear requirement on this path +- [x] When tracker.backend is unset or empty, resolution treats backend as markdown (no hard-stop, no Linear tools required) + +## Verification Steps + +1. **runtime** `test -f agents/tracker/port.md && test -f agents/tracker/markdown.md` + - Expected: both backend docs exist after children complete +2. **test** `bash lib/tests/*.test.sh 2>/dev/null | tail -5; bash lib/conformance-scan.sh . || true` + - Expected: markdown regression surface still runnable + +## Outputs diff --git a/.do-work/archive/REQ-284-tracker-config-schema.md b/.do-work/archive/REQ-284-tracker-config-schema.md new file mode 100644 index 0000000..bc7e8c8 --- /dev/null +++ b/.do-work/archive/REQ-284-tracker-config-schema.md @@ -0,0 +1,48 @@ +# REQ-284: Add tracker.* config schema and validation + + +**UR:** UR-045 +**Status:** done +**Created:** 2026-07-31 +**Layer:** agents +**Entry point:** +**Terminal state:** +**Parent:** REQ-283 +**Closure proof:** checkpoint:.do-work/runs#REQ-284 commit:8949762 tests:passed +**Criteria approved:** agent-drafted +**Priority:** 3 +**Size:** M +**Files:** agents/config.md SKILL.md +**Depends on:** REQ-283 + +## Task + +Add tracker.backend and tracker.linear.* schema (design §7) to agents/config.md canonical template and schema reference. Document defaults (backend markdown). When backend is linear, document hard-fail rules: team unresolved, MCP tools undiscoverable, status_map state missing on team. + +## Context + +Design §7 Config schema; clarification: defaults + hard fail on missing workflow state. Interaction with ledger/parallel remains valid. + +## Acceptance Criteria + +- [x] config template includes tracker.backend default markdown and tracker.linear keys from design §7 +- [x] Schema reference documents validation: backend=linear requires resolvable team and discoverable Linear MCP tools +- [x] status_map defaults match design; missing team workflow state is hard-fail with rename instructions +- [x] Load Config section describes resolving tracker.backend (default markdown if missing/empty) + +## Verification Steps + +1. **runtime** `grep -n 'tracker:' agents/config.md | head` + - Expected: tracker section present in canonical template +2. **runtime** `grep -nE 'backend: markdown|status_map|team_id' agents/config.md` + - Expected: key fields documented + +## Integration + +**Reachability:** agents/config.md Load Config — every phase agent already loads this first (agents/start.md Step 0, etc.) + +**Data dependencies:** .do-work/config.yml project config values + +**Service dependencies:** agents/config.md Load Config migration of missing keys + +## Outputs diff --git a/.do-work/archive/REQ-285-tracker-port-catalog.md b/.do-work/archive/REQ-285-tracker-port-catalog.md new file mode 100644 index 0000000..73ce4c6 --- /dev/null +++ b/.do-work/archive/REQ-285-tracker-port-catalog.md @@ -0,0 +1,49 @@ +# REQ-285: Write agents/tracker/port.md op catalog + + +**UR:** UR-045 +**Status:** done +**Created:** 2026-07-31 +**Layer:** agents +**Entry point:** +**Terminal state:** +**Parent:** REQ-283 +**Closure proof:** checkpoint:.do-work/runs#REQ-285 commit:3b66f13 tests:passed +**Criteria approved:** agent-drafted +**Priority:** 3 +**Size:** M +**Files:** agents/tracker/port.md +**Depends on:** REQ-283 REQ-284 + +## Task + +Create agents/tracker/port.md defining the shared op catalog (~create_ur, read_ur, list_urs, append_ideate, create_req, claim_req, list_claimable_reqs, archive_req, etc. per design §5.4), preconditions, claim/deps/footprint semantic rules, mid-flight MCP failure rule (leave claimed; resume/unblock repairs), and deps authority (Linear relations authoritative; body is mirror). + +## Context + +Design §5.4 Operation catalog; §8 claim; clarifications on mid-flight failure and relations-authoritative deps. Shared rules only — no backend-specific tool calls. + +## Acceptance Criteria + +- [x] Every op name from design §5.4 appears with intent and preconditions +- [x] Documents: Linear unusable ⇒ hard stop never silent markdown fallback +- [x] Documents: mid-flight MCP failure leaves claim active; resume/unblock repair +- [x] Documents: deps eligibility uses native blocks relations as authority; body **Depends on:** is mirror +- [x] Documents work-item vs runtime split (design §5.5) + +## Verification Steps + +1. **runtime** `test -f agents/tracker/port.md && grep -cE '^[|] `?[a-z_]+`?' agents/tracker/port.md || grep -c create_ur agents/tracker/port.md` + - Expected: op catalog present with create_ur and peers +2. **runtime** `grep -nE 'hard stop|relations authoritative|leave claimed' agents/tracker/port.md` + - Expected: clarification rules present + +## Integration + +**Reachability:** Phase agents read agents/tracker/port.md after config load (design §5.2) + +**Data dependencies:** Op names are the only work-item storage API for phase agents + +**Service dependencies:** agents/config.md tracker.backend resolution + +## Outputs diff --git a/.do-work/archive/REQ-286-markdown-backend-doc.md b/.do-work/archive/REQ-286-markdown-backend-doc.md new file mode 100644 index 0000000..440d306 --- /dev/null +++ b/.do-work/archive/REQ-286-markdown-backend-doc.md @@ -0,0 +1,48 @@ +# REQ-286: Write agents/tracker/markdown.md mapping + + +**UR:** UR-045 +**Status:** done +**Created:** 2026-07-31 +**Layer:** agents +**Entry point:** +**Terminal state:** +**Parent:** REQ-283 +**Closure proof:** checkpoint:.do-work/runs#REQ-286 commit:09e3d1e tests:passed +**Criteria approved:** agent-drafted +**Priority:** 3 +**Size:** M +**Files:** agents/tracker/markdown.md lib/claim-req.sh lib/pick-req.sh lib/check-deps.sh lib/check-footprint.sh lib/heartbeat.sh +**Depends on:** REQ-285 + +## Task + +Document how each port op maps to existing file paths and lib/*.sh (claim-req.sh, pick-req.sh, check-deps.sh, check-footprint.sh, heartbeat.sh, etc.). Markdown remains the default backend implementation — no behavior change required beyond documentation that freezes the mapping. + +## Context + +Design §5.1–5.3; Connector: reuse parallel coordination semantics, not reimplement. lib/*.sh stay markdown-backend only. + +## Acceptance Criteria + +- [x] Every op in port.md has a markdown implementation note (script path and/or file glob) +- [x] Explicitly states no Linear-aware bash required for markdown backend +- [x] Claim atomicity documented as mv/git mv race (exit 2) matching claim-req.sh +- [x] Mapping never invents a lib/*.sh path that does not exist in the repo; missing scripts are called out as gaps, not invented + +## Verification Steps + +1. **runtime** `grep -n claim_req agents/tracker/markdown.md && grep -n claim-req.sh agents/tracker/markdown.md` + - Expected: claim_req maps to claim-req.sh +2. **runtime** `grep -nE 'pick-req|check-deps|check-footprint|heartbeat' agents/tracker/markdown.md` + - Expected: coordination libs mapped + +## Integration + +**Reachability:** Loaded when tracker.backend is markdown or unset + +**Data dependencies:** .do-work/REQ-*.md, working/, archive/, user-requests/ + +**Service dependencies:** lib/claim-req.sh lib/pick-req.sh lib/check-deps.sh lib/check-footprint.sh lib/heartbeat.sh + +## Outputs diff --git a/.do-work/archive/REQ-287-wire-agents-port-load.md b/.do-work/archive/REQ-287-wire-agents-port-load.md new file mode 100644 index 0000000..0ccff42 --- /dev/null +++ b/.do-work/archive/REQ-287-wire-agents-port-load.md @@ -0,0 +1,49 @@ +# REQ-287: Wire phase agents to tracker load path + + +**UR:** UR-045 +**Status:** done +**Created:** 2026-07-31 +**Layer:** agents +**Entry point:** +**Terminal state:** +**Parent:** REQ-283 +**Closure proof:** checkpoint:.do-work/runs#REQ-287 commit:73569b0 tests:passed +**Criteria approved:** agent-drafted +**Priority:** 3 +**Size:** L +**Files:** agents/intake.md agents/capture.md agents/ideate.md agents/question.md agents/verify.md agents/start.md agents/go.md agents/run.md agents/run-worker.md agents/review.md agents/status.md agents/close.md agents/unblock.md agents/resume.md agents/upgrade.md agents/retro.md agents/log.md agents/help.md agents/audit.md agents/config.md agents/tracker/port.md agents/tracker/markdown.md SKILL.md +**Depends on:** REQ-286 + +## Task + +Update every phase agent listed in design §13 so work-item storage goes only through named port ops after loading config → port.md → agents/tracker/.md. Markdown behavior must remain byte-compatible. Do not invent Linear calls here — load path + markdown op discipline only. + +## Context + +Design §5.2 and §13; ideate risk: missing one agent causes split brain. Clarification: agents layer only. + +## Acceptance Criteria + +- [x] Each §13 agent file instructs: load config, resolve tracker.backend, read port.md, read backend md, call only named port ops for work-item storage +- [x] No agent documents silent fallback from linear to markdown +- [x] Markdown path still references existing lib/file flows via markdown.md ops (no mass rewrite of lib/*.sh required in this REQ) +- [x] SKILL.md or config.md points at the load-path contract once +- [x] If backend resolves to linear but agents/tracker/linear.md is missing/unreadable, agents hard-stop with setup instructions — never fall through to markdown paths + +## Verification Steps + +1. **runtime** `for f in agents/intake.md agents/capture.md agents/run.md agents/run-worker.md agents/status.md; do grep -q 'tracker' "$f" || echo MISSING:$f; done` + - Expected: key agents mention tracker load path +2. **runtime** `grep -L 'port.md\|tracker.backend\|tracker/' agents/intake.md agents/capture.md agents/run.md agents/close.md agents/upgrade.md || true` + - Expected: spot-check load path references + +## Integration + +**Reachability:** Invoked at start of every phase agent (design §5.2 step list) + +**Data dependencies:** agents/tracker/port.md agents/tracker/markdown.md .do-work/config.yml + +**Service dependencies:** agents/config.md Load Config + +## Outputs diff --git a/.do-work/archive/REQ-288-linear-spike-path.md b/.do-work/archive/REQ-288-linear-spike-path.md new file mode 100644 index 0000000..8766acc --- /dev/null +++ b/.do-work/archive/REQ-288-linear-spike-path.md @@ -0,0 +1,42 @@ +# REQ-288: Linear MCP capability spike path + + +**UR:** UR-045 +**Status:** done +**Created:** 2026-07-31 +**Layer:** none +**Entry point:** Operator sets sandbox Linear team; agent runs spike against live MCP tools +**Terminal state:** Capability matrix committed (which tools exist for Initiatives, Projects, InitiativeToProject, issue relations, Team Docs) and hard-stop copy validated; CRUD REQs unblocked +**Parent:** +**Closure proof:** checkpoint:.do-work/runs#REQ-288 commit:d6d31a1 tests:passed +**Criteria approved:** agent-drafted +**Priority:** 3 +**Size:** M +**Files:** agents/tracker/linear.md docs/superpowers/specs/2026-07-31-do-work-multi-tracker-design.md +**Depends on:** REQ-285 + +## Task + +Prove on a sandbox Linear team that the hierarchy and non-ticket homes are implementable via live MCP tools before wiring full Linear CRUD into the loop. + +## Context + +Clarification: Spike first, then implement. Design §17 risk #1 MCP thin tools. Linear skill requires search_tool rediscovery. + +## Acceptance Criteria + +- [x] Spike produces a written matrix of available tools vs required ops (Initiatives, Projects, link, relations/blocks, Docs, comments, workflow states) +- [x] Hard-stop message when MCP missing is verified (setup instructions, no invented data) +- [x] status_map validation against real team states documented (defaults + hard fail if missing) +- [x] No production work-item migration in this path + +## Verification Steps + +1. **runtime** `grep -nE 'Initiative|blocks|Doc|hard.stop|capability' agents/tracker/linear.md | head` + - Expected: spike findings land in linear.md + +## Manual checks (advisory) + +- [x] Connect Linear MCP (OAuth) to a sandbox team and confirm tools via search_tool — Observable: linear tools listed, not handshake failure + +## Outputs diff --git a/.do-work/archive/REQ-289-linear-spike-matrix.md b/.do-work/archive/REQ-289-linear-spike-matrix.md new file mode 100644 index 0000000..517b4dc --- /dev/null +++ b/.do-work/archive/REQ-289-linear-spike-matrix.md @@ -0,0 +1,52 @@ +# REQ-289: Run Linear MCP spike and draft linear.md skeleton + + +**UR:** UR-045 +**Status:** done +**Created:** 2026-07-31 +**Layer:** agents +**Entry point:** +**Terminal state:** +**Parent:** REQ-288 +**Closure proof:** checkpoint:.do-work/runs#REQ-289 commit:a675df1 tests:passed +**Criteria approved:** agent-drafted +**Priority:** 3 +**Size:** L +**Files:** agents/tracker/linear.md +**Depends on:** REQ-288 + +## Task + +Using Linear skill protocol (search_tool → use_tool live), discover tools on sandbox team; write agents/tracker/linear.md skeleton with capability matrix, hard-stop setup instructions, and notes on gaps (GraphQL fallbacks). Do not implement full op sequences yet beyond discovery probes. + +## Context + +Clarification spike-first; ~/.grok/skills/linear/SKILL.md MCP-first rediscovery. + +## Acceptance Criteria + +- [x] agents/tracker/linear.md exists with capability matrix table +- [x] Documents hard-stop when MCP unauthenticated/missing with Linear skill setup steps +- [x] Records whether Initiatives, InitiativeToProject, issue relations, Team Docs are available +- [x] No secrets committed + +## Verification Steps + +1. **runtime** `test -f agents/tracker/linear.md && grep -n 'capability\|tool' agents/tracker/linear.md | head` + - Expected: skeleton with matrix present +2. **runtime** `grep -nE 'hard stop|OAuth|mcp.linear' agents/tracker/linear.md` + - Expected: setup hard-stop copy present + +## Manual checks (advisory) + +- [x] Execute discovery against sandbox team in a session with Linear MCP connected — Observable: matrix rows filled from live tools not guesses + +## Integration + +**Reachability:** Read when tracker.backend=linear after port.md + +**Data dependencies:** tracker.linear.team_id / team_key from config + +**Service dependencies:** Linear MCP via search_tool/use_tool; linear skill SKILL.md + +## Outputs diff --git a/.do-work/archive/REQ-290-linear-crud-path.md b/.do-work/archive/REQ-290-linear-crud-path.md new file mode 100644 index 0000000..72a9374 --- /dev/null +++ b/.do-work/archive/REQ-290-linear-crud-path.md @@ -0,0 +1,39 @@ +# REQ-290: Linear UR/REQ CRUD path + + +**UR:** UR-045 +**Status:** done +**Created:** 2026-07-31 +**Layer:** none +**Entry point:** /do-work intake or start with tracker.backend: linear and valid team config +**Terminal state:** Initiative + Project do-work/{UR-id} + Issues/sub-issues exist with §9 templates; create/read/list/update ops work +**Parent:** +**Closure proof:** checkpoint:.do-work/runs#REQ-290 commit:530ecad tests:passed +**Criteria approved:** agent-drafted +**Priority:** 2 +**Size:** M +**Files:** agents/tracker/linear.md +**Depends on:** REQ-289 + +## Task + +Implement Linear work-item create/read/update/list for URs and REQs per hierarchy §6 and templates §9, without claim/run coordination yet. + +## Context + +Design §6, §9; phasing step 2 after spike. + +## Acceptance Criteria + +- [x] create_ur: Initiative + Project do-work/{UR-id} + link +- [x] create_req/update_req/read_req/list_reqs_for_ur against Project +- [x] Issue body uses §9.2 template; path-units use parentId sub-issues +- [x] Linear issue ids only (e.g. ENG-123) — no parallel REQ-NNN allocation in Linear mode +- [x] If Linear MCP tools are undiscoverable or team_id unresolved at create_ur/create_req time, hard-stop with setup instructions — no partial Initiative without Project, no markdown dual-write + +## Verification Steps + +1. **runtime** `grep -nE 'create_ur|create_req|do-work/\{ur' agents/tracker/linear.md` + - Expected: CRUD sequences documented + +## Outputs diff --git a/.do-work/archive/REQ-291-linear-templates-crud.md b/.do-work/archive/REQ-291-linear-templates-crud.md new file mode 100644 index 0000000..ec908b9 --- /dev/null +++ b/.do-work/archive/REQ-291-linear-templates-crud.md @@ -0,0 +1,48 @@ +# REQ-291: Linear templates and CRUD op sequences + + +**UR:** UR-045 +**Status:** done +**Created:** 2026-07-31 +**Layer:** agents +**Entry point:** +**Terminal state:** +**Parent:** REQ-290 +**Closure proof:** checkpoint:.do-work/runs#REQ-291 commit:e3269ba tests:passed +**Criteria approved:** agent-drafted +**Priority:** 2 +**Size:** L +**Files:** agents/tracker/linear.md +**Depends on:** REQ-290 + +## Task + +Flesh linear.md with Initiative/Issue templates (§9) and full MCP sequences for create_ur, read_ur, list_urs, append_ideate, append_clarifications, create_req, update_req, read_req, list_reqs_for_ur, set_files, set_blocked_by (relations + body mirror). + +## Context + +Design §9 templates; clarification relations authoritative with body mirror on write. + +## Acceptance Criteria + +- [x] Templates match design §9.1 and §9.2 including machine markers and +- [x] set_blocked_by creates blocks relations and mirrors **Depends on:** +- [x] Labels Layer/*, Size/*, path-unit documented +- [x] status_map used for workflow states; validation hard-fails missing states + +## Verification Steps + +1. **runtime** `grep -n 'do-work-ur\|do-work-req' agents/tracker/linear.md` + - Expected: template markers present +2. **runtime** `grep -nE 'blocks|Depends on|set_blocked_by' agents/tracker/linear.md` + - Expected: deps dual-write documented + +## Integration + +**Reachability:** intake/capture/ideate/question call these ops when backend=linear + +**Data dependencies:** Linear Initiative/Project/Issue; tracker.linear config + +**Service dependencies:** port.md op names; Linear MCP + +## Outputs diff --git a/.do-work/archive/REQ-292-linear-claim-path.md b/.do-work/archive/REQ-292-linear-claim-path.md new file mode 100644 index 0000000..25fecf0 --- /dev/null +++ b/.do-work/archive/REQ-292-linear-claim-path.md @@ -0,0 +1,38 @@ +# REQ-292: Linear claim and status path + + +**UR:** UR-045 +**Status:** done +**Created:** 2026-07-31 +**Layer:** none +**Entry point:** /do-work run|status|unblock|resume with backend linear +**Terminal state:** Optimistic claim comment protocol works; status reports claimers/heartbeats; unblock/resume match markdown semantics; mid-flight failure leaves claimed +**Parent:** +**Closure proof:** checkpoint:.do-work/runs#REQ-292 commit:a911cb5 tests:passed +**Criteria approved:** agent-drafted +**Priority:** 2 +**Size:** M +**Files:** agents/tracker/linear.md +**Depends on:** REQ-291 + +## Task + +Ship claim/heartbeat/unblock/resume/status/list_claimable_reqs on Linear per §8 with human assignee preserved. + +## Context + +Design §8; clarification leave claimed on MCP death. + +## Acceptance Criteria + +- [x] Claim uses agent_claim_marker comment + workflow in_progress; assignee not stolen +- [x] Heartbeat updates; stale uses heartbeat_max_age_seconds or parallel.stale_threshold_seconds +- [x] Unblock → backlog + claim status released +- [x] Resume refreshes heartbeat; concurrent-conflict stopper when claim race lost + +## Verification Steps + +1. **runtime** `grep -nE 'do-work-claim|heartbeat|unblock|list_claimable' agents/tracker/linear.md` + - Expected: claim protocol sections present + +## Outputs diff --git a/.do-work/archive/REQ-293-linear-claim-ops.md b/.do-work/archive/REQ-293-linear-claim-ops.md new file mode 100644 index 0000000..aa44db7 --- /dev/null +++ b/.do-work/archive/REQ-293-linear-claim-ops.md @@ -0,0 +1,48 @@ +# REQ-293: Implement Linear claim heartbeat status ops + + +**UR:** UR-045 +**Status:** done +**Created:** 2026-07-31 +**Layer:** agents +**Entry point:** +**Terminal state:** +**Parent:** REQ-292 +**Closure proof:** checkpoint:.do-work/runs#REQ-293 commit:0422998 tests:passed +**Criteria approved:** agent-drafted +**Priority:** 2 +**Size:** L +**Files:** agents/tracker/linear.md agents/status.md agents/unblock.md agents/resume.md agents/run.md +**Depends on:** REQ-292 + +## Task + +Document and wire claim_req, heartbeat_req, set_req_status, unblock_req, list_claimable_reqs, and status reporting for Linear backend. Update status/unblock/resume/run agent text to use port ops (Linear sequences in linear.md). + +## Context + +Design §8 example claim comment; multi-agent safety without FS rename. + +## Acceptance Criteria + +- [x] Example claim comment block matches design §8 +- [x] list_claimable_reqs: project filter + backlog state + deps via relations + footprint from Files + unclaimed +- [x] status agent can render Linear claimers/heartbeats for a UR Project +- [x] Mid-flight failure policy stated: leave claimed; resume/unblock + +## Verification Steps + +1. **runtime** `grep -n 'agent_claim_marker\|do-work-claim' agents/tracker/linear.md` + - Expected: marker documented +2. **runtime** `grep -n tracker agents/status.md agents/unblock.md agents/resume.md` + - Expected: agents reference tracker/port + +## Integration + +**Reachability:** run orchestrator claim step; /do-work status|unblock|resume + +**Data dependencies:** Issue comments + workflow state; human assignee + +**Service dependencies:** port claim_req/heartbeat_req; Linear MCP comments API + +## Outputs diff --git a/.do-work/archive/REQ-294-linear-run-path.md b/.do-work/archive/REQ-294-linear-run-path.md new file mode 100644 index 0000000..7a32a18 --- /dev/null +++ b/.do-work/archive/REQ-294-linear-run-path.md @@ -0,0 +1,40 @@ +# REQ-294: Linear run coordination path + + +**UR:** UR-045 +**Status:** done +**Created:** 2026-07-31 +**Layer:** none +**Entry point:** /do-work run with backend linear +**Terminal state:** Worker can pick claim deps footprint archive a REQ using Linear as sole work-item store; worktrees/git remain local; commit messages use Linear issue ids +**Parent:** +**Closure proof:** checkpoint:.do-work/runs#REQ-294 commit:3b158d8 tests:passed +**Criteria approved:** agent-drafted +**Priority:** 2 +**Size:** M +**Files:** agents/tracker/linear.md agents/run.md agents/run-worker.md +**Depends on:** REQ-293 + +## Task + +Close the run loop on Linear: pick/claim/deps/footprint/archive_req + append_run_note; local runtime unchanged. + +## Context + +Design §5.5 runtime stays local; §6.5 commit convention; phasing step 5. + +## Acceptance Criteria + +- [x] archive_req sets done + closure proof + outputs on Issue +- [x] Footprint overlap uses **Files:** from issue bodies of in-progress claims +- [x] Deps satisfaction uses blocks relations (authoritative) +- [x] Commit/PR message format uses Linear issue id per §6.5 +- [x] Optional local ledger telemetry when ledger.enabled without becoming second work-item store +- [x] Mid-flight Linear MCP failure after claim leaves the issue claimed (active claim comment + in_progress); worker stops for resume/unblock — never silent-releases and never falls back to markdown store + +## Verification Steps + +1. **runtime** `grep -nE 'archive_req|append_run_note|feat\(ENG' agents/tracker/linear.md agents/run.md agents/run-worker.md | head` + - Expected: archive and commit convention present + +## Outputs diff --git a/.do-work/archive/REQ-295-linear-run-archive-ops.md b/.do-work/archive/REQ-295-linear-run-archive-ops.md new file mode 100644 index 0000000..d5179a0 --- /dev/null +++ b/.do-work/archive/REQ-295-linear-run-archive-ops.md @@ -0,0 +1,50 @@ +# REQ-295: Linear run pick deps footprint archive + + +**UR:** UR-045 +**Status:** done +**Created:** 2026-07-31 +**Layer:** agents +**Entry point:** +**Terminal state:** +**Parent:** REQ-294 +**Closure proof:** checkpoint:.do-work/runs#REQ-295 commit:4b789aa tests:passed +**Criteria approved:** agent-drafted +**Priority:** 2 +**Size:** L +**Files:** agents/tracker/linear.md agents/run.md agents/run-worker.md agents/review.md +**Depends on:** REQ-294 + +## Task + +Implement linear.md sequences for list_claimable_reqs ordering, footprint checks, archive_req, append_run_note; update run.md and run-worker.md to use port ops and Linear id branch naming (req/ENG-123). Review gate still applies before archive. + +## Context + +Design §15 testing; worktree isolation unchanged. Connector: semantics from parallel coordination design. + +## Acceptance Criteria + +- [x] Worktree branch may use req/ sanitized for git refs +- [x] Review gate still required before archive when review.required +- [x] append_run_note posts YAML-fenced ledger fields as Issue comment +- [x] No Linear-aware bash required in lib/ for v1 +- [x] Failed review or failed acceptance-evidence gate does not call archive_req; issue stays in_progress/stopped with claim protocol intact +- [x] Concurrent claim loss surfaces concurrent-conflict stopper with resume allowed (same semantics as markdown multi-agent mode) + +## Verification Steps + +1. **runtime** `grep -nE 'list_claimable|archive_req|append_run_note' agents/tracker/linear.md` + - Expected: ops present +2. **runtime** `grep -nE 'tracker|port.md|linear' agents/run.md agents/run-worker.md | head` + - Expected: run agents load tracker + +## Integration + +**Reachability:** /do-work run claim loop Step 1 + +**Data dependencies:** Linear issues + local .worktrees + state locks + +**Service dependencies:** port ops; lib only for local runtime (provision-worktree, etc.) + +## Outputs diff --git a/.do-work/archive/REQ-296-linear-artifacts-path.md b/.do-work/archive/REQ-296-linear-artifacts-path.md new file mode 100644 index 0000000..9361fc3 --- /dev/null +++ b/.do-work/archive/REQ-296-linear-artifacts-path.md @@ -0,0 +1,38 @@ +# REQ-296: Linear non-ticket artifacts and close path + + +**UR:** UR-045 +**Status:** done +**Created:** 2026-07-31 +**Layer:** none +**Entry point:** capture append_decision; verify/close write reports; retro calibration; run notes +**Terminal state:** Artifacts live only in fixed Linear homes (§10); agents never invent ad-hoc locations +**Parent:** +**Closure proof:** checkpoint:.do-work/runs#REQ-296 commit:60e2d17 tests:passed +**Criteria approved:** agent-drafted +**Priority:** 2 +**Size:** M +**Files:** agents/tracker/linear.md agents/close.md agents/verify.md agents/retro.md agents/capture.md +**Depends on:** REQ-291 + +## Task + +Map decisions, calibration, verify, close, run notes to Linear homes and implement write/read ops. + +## Context + +Design §10; Done-when non-ticket homes fixed. + +## Acceptance Criteria + +- [x] Decisions + calibration = Team Docs create-if-missing with configured titles +- [x] Verify/close = Initiative sections + comments +- [x] Run notes = Issue comments (+ optional Project update) +- [x] Gate locks remain local state/* + +## Verification Steps + +1. **runtime** `grep -nE 'decisions_doc|calibration|write_verify|write_close|append_decision' agents/tracker/linear.md` + - Expected: artifact ops present + +## Outputs diff --git a/.do-work/archive/REQ-297-linear-artifact-homes.md b/.do-work/archive/REQ-297-linear-artifact-homes.md new file mode 100644 index 0000000..f8652b4 --- /dev/null +++ b/.do-work/archive/REQ-297-linear-artifact-homes.md @@ -0,0 +1,49 @@ +# REQ-297: Implement Linear artifact home ops + + +**UR:** UR-045 +**Status:** done +**Created:** 2026-07-31 +**Layer:** agents +**Entry point:** +**Terminal state:** +**Parent:** REQ-296 +**Closure proof:** checkpoint:.do-work/runs#REQ-297 commit:2fbecbc tests:passed +**Criteria approved:** agent-drafted +**Priority:** 2 +**Size:** L +**Files:** agents/tracker/linear.md agents/close.md agents/verify.md agents/retro.md agents/capture.md agents/ideate.md agents/question.md agents/run-worker.md +**Depends on:** REQ-296 + +## Task + +Implement append_decision, write_verify_report, write_close_report, calibration doc read/write, append_run_note consumers in linear.md; point capture/ideate/question/verify/close/retro at port ops. + +## Context + +Design §10 table; decisions one-line format preserved. + +## Acceptance Criteria + +- [x] Doc titles from config decisions_doc_title / calibration_doc_title +- [x] Same one-line decisions grammar as .do-work/decisions.md +- [x] Close agent walks path-units using Linear issue ids +- [x] Retro prefers Linear run notes when backend=linear +- [x] If Team Doc create/update or Initiative description append fails (permission or size), hard-stop — agents must not invent ad-hoc issue comments or alternate doc titles outside §10 homes + +## Verification Steps + +1. **runtime** `grep -nE 'append_decision|write_close_report|do-work/decisions' agents/tracker/linear.md` + - Expected: homes implemented in doc +2. **runtime** `grep -n tracker agents/close.md agents/verify.md agents/retro.md | head` + - Expected: agents reference tracker + +## Integration + +**Reachability:** capture decision write; verify/close/retro phases + +**Data dependencies:** Team Docs; Initiative description sections + +**Service dependencies:** Linear Docs MCP; port artifact ops + +## Outputs diff --git a/.do-work/archive/REQ-298-linear-milestone-path.md b/.do-work/archive/REQ-298-linear-milestone-path.md new file mode 100644 index 0000000..aacf228 --- /dev/null +++ b/.do-work/archive/REQ-298-linear-milestone-path.md @@ -0,0 +1,38 @@ +# REQ-298: Linear milestone mode path + + +**UR:** UR-045 +**Status:** done +**Created:** 2026-07-31 +**Layer:** none +**Entry point:** Milestone-shaped UR (saas-thesis handoff + ### Milestones) with backend linear +**Terminal state:** Active milestone cursor on Project description; list_milestone_reqs filters; deploy gate via local gate-owner.md with human y/n +**Parent:** +**Closure proof:** checkpoint:.do-work/runs#REQ-298 commit:70f18a9 tests:passed +**Criteria approved:** agent-drafted +**Priority:** 1 +**Size:** M +**Files:** agents/tracker/linear.md agents/capture.md agents/run.md +**Depends on:** REQ-295 REQ-297 + +## Task + +Support milestone mode under Linear without changing trigger shape or local deploy-gate ownership. + +## Context + +Design §11; runtime gate locks stay local. + +## Acceptance Criteria + +- [x] Trigger unchanged (source + ### Milestones) +- [x] Cursor in Project description +- [x] Deploy gate first orchestrator owns local state/gate-owner.md +- [x] list_milestone_reqs / set_active_milestone / read_active_milestone ops work + +## Verification Steps + +1. **runtime** `grep -nE 'do-work-milestone|list_milestone|gate-owner' agents/tracker/linear.md agents/run.md | head` + - Expected: milestone markers present + +## Outputs diff --git a/.do-work/archive/REQ-299-linear-milestone-ops.md b/.do-work/archive/REQ-299-linear-milestone-ops.md new file mode 100644 index 0000000..eb3f74b --- /dev/null +++ b/.do-work/archive/REQ-299-linear-milestone-ops.md @@ -0,0 +1,52 @@ +# REQ-299: Implement Linear milestone cursor ops + + +**UR:** UR-045 +**Status:** done +**Created:** 2026-07-31 +**Layer:** agents +**Entry point:** +**Terminal state:** +**Parent:** REQ-298 +**Closure proof:** checkpoint_log:passed commit:9e1638a +**Criteria approved:** agent-drafted +**Priority:** 1 +**Size:** M +**Files:** agents/tracker/linear.md agents/capture.md agents/run.md +**Depends on:** REQ-298 + +## Task + +Implement read/set active milestone and list_milestone_reqs in linear.md; ensure capture/run milestone branches call port ops under Linear backend. + +## Context + +Design §11; label M1 or Project milestone entity when MCP supports it. + +## Acceptance Criteria + +- [x] Project description marker format documented and parsed +- [x] Siblings idle on deploy gate same as markdown mode +- [x] write_gate_state remains local-allowed +- [x] When Project description has no milestone marker, `read_active_milestone` returns empty/not-in-milestone (does not invent a milestone id) +- [x] Concurrent gate ownership still serializes via local `state/gate-owner.md` even when milestone content is remote + +## Verification Steps + +1. **runtime** `grep -nE 'read_active_milestone|set_active_milestone|list_milestone_reqs' agents/tracker/linear.md` + - Expected: ops present + +## Integration + +**Reachability:** capture milestone decompose; run milestone drain/gate + +**Data dependencies:** Project description milestone marker; local gate-owner.md + +**Service dependencies:** port milestone ops; run.md gate flow + +## Outputs + +- agents/tracker/linear.md — REQ-299 path unit + complete read/set/list milestone ops, parse algorithm, empty→null, concurrent local write_gate_state +- agents/capture.md — Linear milestone branches call port ops; null cursor does not invent id on read +- agents/run.md — Linear filter/idle-wait/gate drain call port ops; local gate-owner concurrent serialize + diff --git a/.do-work/archive/REQ-300-migrate-linear-path.md b/.do-work/archive/REQ-300-migrate-linear-path.md new file mode 100644 index 0000000..3d2a767 --- /dev/null +++ b/.do-work/archive/REQ-300-migrate-linear-path.md @@ -0,0 +1,46 @@ +# REQ-300: Idle markdown→Linear migration path + + +**UR:** UR-045 +**Status:** done +**Created:** 2026-07-31 +**Layer:** none +**Entry point:** /do-work upgrade migrate (or conformance migrate step) when working/ empty +**Terminal state:** All URs/REQs in Linear; tracker.backend linear; local user-requests/archive historical read-only; no dual-write +**Parent:** +**Closure proof:** checkpoint_log:passed commit:1c0de08 +**Criteria approved:** agent-drafted +**Priority:** 1 +**Size:** M +**Files:** agents/upgrade.md agents/tracker/linear.md agents/tracker/port.md +**Depends on:** REQ-297 REQ-291 + +## Task + +One-shot idle migration per design §12, surfaced via upgrade/conformance (clarification + UR-039). + +## Context + +Design §12; clarification migration under upgrade. + +## Acceptance Criteria + +- [x] Preflight: working empty, no active claims, operator confirms +- [x] Creates Initiatives/Projects/Issues for backlog+archive; maps status/relations/parents +- [x] Sets tracker.backend linear + team ids in config +- [x] Leaves markdown trees read-only historical; ops stop reading them +- [x] Supports dry-run reporting planned creates without write +- [x] If working/ is non-empty or active claims exist, migration refuses entirely (no partial cutover, config backend left unchanged) +- [x] If Linear MCP is unusable during migration, hard-stops with setup instructions and leaves markdown trees + config unchanged (no partial cutover) + +## Verification Steps + +1. **runtime** `grep -nE 'migrat|backend: linear|working/' agents/upgrade.md agents/tracker/linear.md | head` + - Expected: migration UX documented + +## Outputs + +- agents/tracker/linear.md — migrate_markdown_to_linear M0–M7 sequence +- agents/tracker/port.md — catalog + contract for migrate_markdown_to_linear +- agents/upgrade.md — Step 9 /do-work upgrade migrate UX + diff --git a/.do-work/archive/REQ-301-migrate-upgrade-wiring.md b/.do-work/archive/REQ-301-migrate-upgrade-wiring.md new file mode 100644 index 0000000..e3de27f --- /dev/null +++ b/.do-work/archive/REQ-301-migrate-upgrade-wiring.md @@ -0,0 +1,55 @@ +# REQ-301: Wire migration into upgrade/conformance + + +**UR:** UR-045 +**Status:** done +**Created:** 2026-07-31 +**Layer:** agents +**Entry point:** +**Terminal state:** +**Parent:** REQ-300 +**Closure proof:** checkpoint_log:passed commit:abe6087 +**Criteria approved:** agent-drafted +**Priority:** 1 +**Size:** L +**Files:** agents/upgrade.md agents/tracker/linear.md lib/conformance-scan.sh lib/tests/conformance-scan.test.sh +**Depends on:** REQ-300 + +## Task + +Add upgrade/conformance path for idle migration with dry-run flag; implement migration sequences in linear.md; never migrate when working/ non-empty. + +## Context + +UR-039 upgrade centralization; design §12 step 7 no dual-write after cutover. + +## Acceptance Criteria + +- [x] Destructive/confirm gate for migration (operator confirmation) +- [x] Dry-run lists planned Linear creates without writing +- [x] Post-cutover work-item ops ignore historical markdown trees +- [x] Idempotent enough to re-run safely or clearly refuse if already linear +- [x] When `tracker.backend` is already `linear`, upgrade migrate refuses or reports already-migrated without rewriting Issues + +## Verification Steps + +1. **runtime** `grep -nE 'migrat|dry-run|tracker.backend' agents/upgrade.md` + - Expected: upgrade mentions migration +2. **runtime** `grep -nE 'preflight|working/' agents/tracker/linear.md agents/upgrade.md | head` + - Expected: idle preflight present + +## Integration + +**Reachability:** /do-work upgrade migrate step + +**Data dependencies:** .do-work/user-requests archive backlog; Linear team + +**Service dependencies:** conformance-scan/upgrade agent; linear create_* ops + +## Outputs + +- agents/upgrade.md — Step 9 destructive confirm, dry-run, already-linear refuse +- agents/tracker/linear.md — REQ-301 hard rules, M1/M7, failure matrix +- lib/conformance-scan.sh — migrate-linear not a scanner drift row +- lib/tests/conformance-scan.test.sh — regression coverage + diff --git a/.do-work/archive/REQ-302-multi-tracker-docs.md b/.do-work/archive/REQ-302-multi-tracker-docs.md new file mode 100644 index 0000000..17d7fbf --- /dev/null +++ b/.do-work/archive/REQ-302-multi-tracker-docs.md @@ -0,0 +1,48 @@ +# REQ-302: Document multi-tracker in SKILL and guides + + +**UR:** UR-045 +**Status:** done +**Created:** 2026-07-31 +**Layer:** none +**Entry point:** Operator reads SKILL.md / getting-started / troubleshooting for Linear backend +**Terminal state:** Docs describe tracker.backend, load path, hard-stops, commit convention, migration, and human-assignee warning +**Parent:** +**Closure proof:** checkpoint_log:passed commit:bc37cbb +**Criteria approved:** agent-drafted +**Priority:** 1 +**Size:** M +**Files:** SKILL.md docs/getting-started.md docs/HOW-IT-WORKS.md docs/troubleshooting.md +**Depends on:** REQ-287 REQ-301 + +## Task + +Update operator-facing docs for multi-tracker: config, Linear setup, hard-stop behavior, claim protocol warning (do not clear claim comments while run live), migration, markdown default. + +## Context + +Design §16 step 9; open risk #5 human UI. + +## Acceptance Criteria + +- [x] SKILL.md documents tracker.* and load path +- [x] getting-started or troubleshooting covers Linear MCP connect + team_id +- [x] Documents no dual-write and hard-stop rules +- [x] Documents Linear commit message convention +- [x] Documents human-assignee warning: do not clear agent claim comments while a run is live +- [x] If a listed guide file is missing, create it or document the pointer in an existing guide (do not leave broken cross-links) + +## Verification Steps + +1. **runtime** `grep -nE 'tracker.backend|agents/tracker' SKILL.md | head` + - Expected: SKILL mentions tracker +2. **runtime** `grep -rnE 'tracker.backend|Linear' docs/getting-started.md docs/HOW-IT-WORKS.md 2>/dev/null | head` + - Expected: guides mention tracker/Linear + +## Outputs + +- SKILL.md — multi-tracker operator docs +- docs/getting-started.md — Linear optional setup +- docs/HOW-IT-WORKS.md — multi-tracker section +- docs/troubleshooting.md — Linear MCP/team_id/claim/migrate + diff --git a/.do-work/decisions.md b/.do-work/decisions.md new file mode 100644 index 0000000..6a79e9d --- /dev/null +++ b/.do-work/decisions.md @@ -0,0 +1,26 @@ +2026-06-12 | UR-036 | human/device validation never blocks merge — automated gates green means delivery proceeds; human checks live post-merge | stranded worktrees were the failure mode +2026-06-12 | UR-036 | pending-validation REQs live in .do-work/pending/, not working/ | working/ implies a live claim + heartbeat; pending has neither +2026-06-12 | UR-036 | human-wait is a first-class Status (pending-validation), not a stopper reason | a stopper strands work; a status parks it after delivery +2026-06-12 | REQ-239/REQ-240 | pending-validation dependency-satisfaction must be honored by BOTH lib/check-deps.sh AND lib/pick-req.sh's inline dep filter (each globs archive/ ∪ pending/) | pick-req reimplements dep-checking inline and is the real claim-arbitration consumer; fixing only check-deps left dependents of parked REQs unclaimable +2026-07-09 | UR-039 | human/device checks never gate closure — REQs archive as done with a `## Manual checks (advisory)` block; supersedes all four 2026-06-12 UR-036/REQ-239-240 entries | user validation is outside the system +2026-07-09 | UR-039 | pending-validation Status, .do-work/pending/, and approve/reject are removed; dependency satisfaction is archive/-only | pending/ implied in-system user validation +2026-07-09 | UR-039 | project maintenance centralizes in /do-work upgrade via a state-probing conformance manifest (no version stamp in config.yml) | the filesystem is the version; detectors are idempotent +2026-07-09 | UR-039 | run.md + run-worker.md rewired in one REQ (REQ-245) rather than split per file | the deferred_checks report-field rename spans both sides of the contract; a split leaves a broken mid-state +2026-07-10 | UR-041 | layer "templates" out of scope | user answered "No" at layer-coverage prompt +2026-07-10 | UR-041 | un-run test/build suite derives unproven via orchestrator-stamped `**Suite:** not-run` header; human/device advisories still never affect proven-ness — refines (does not supersede) the 2026-07-09 advisory-model entries | proven must mean automated verification ran +2026-07-10 | UR-041 | retired config keys are removed only via a curated tombstone list under explicit /do-work upgrade (destructive row); user-added keys are never flagged | additive config loader never deletes; safety by construction +2026-07-10 | UR-042 | deps-format fix kept in one REQ (both parsers + SKILL.md doc + tests) rather than split | pick-req.sh and check-deps.sh must agree on tokenization; a split leaves an inconsistent mid-state + +2026-07-23 | UR-043 | layer "commands" out of scope | user answered "No" at layer-coverage prompt +2026-07-23 | UR-043 | layer "templates" out of scope | user answered "No" at layer-coverage prompt +2026-07-23 | UR-044 | layer "commands" out of scope | user declined layer-coverage prompt; default No for lib/agents/docs-only brief +2026-07-23 | UR-044 | layer "templates" out of scope | user declined layer-coverage prompt; default No for lib/agents/docs-only brief + +2026-07-31 | UR-045 | layer "commands" out of scope | user answered "No" at layer-coverage prompt +2026-07-31 | UR-045 | layer "templates" out of scope | user answered "No" at layer-coverage prompt +2026-07-31 | UR-045 | Linear MCP capability spike before full Linear CRUD | user clarification spike-first +2026-07-31 | UR-045 | mid-flight Linear MCP failure leaves claim active; resume/unblock repairs | user clarification +2026-07-31 | UR-045 | status_map defaults hard-fail if team workflow state missing | user clarification +2026-07-31 | UR-045 | deps eligibility: native Linear blocks relations authoritative; body Depends on is mirror | user clarification +2026-07-31 | UR-045 | migration surfaced via /do-work upgrade + conformance, not a separate forever command | inferred+confirmed + UR-039 +2026-07-31 | hierarchy | Linear UR home is Project Milestone on shared product_project, not Initiative | Linear MCP lacks Initiative create/list tools diff --git a/.do-work/user-requests/UR-045/ideate.md b/.do-work/user-requests/UR-045/ideate.md new file mode 100644 index 0000000..45927e8 --- /dev/null +++ b/.do-work/user-requests/UR-045/ideate.md @@ -0,0 +1,34 @@ +# Ideate — UR-045 + +**Reviewed:** 2026-07-31 + +## Explorer — Assumptions & Perspectives + +- **Assumes Linear Initiatives + InitiativeToProject exist in the operator's MCP surface.** The hierarchy is the whole model (UR = Initiative, Project per UR). Scenario: a workspace plan without Initiatives (or MCP tools that only expose Issues/Projects) would leave `create_ur` / `read_ur` / verify-close homes unimplemented. Triggers §6 hierarchy and §6.4 intake sequence. +- **Assumes every phase agent will actually load the port (not just "should").** ~18 agents touch work items; missing one keeps raw `.do-work/REQ-*` paths forever. Scenario: `status` or `unblock` still globs markdown while `run` uses Linear → split brain and false claim/idle reports. Triggers §5.2 load path and §13 agent list. +- **Non-ticket homes (Docs, Initiative sections) need capacity and permission checks.** Decisions, calibration, verify, and close are parked in Team Docs and Initiative description sections. Scenario: doc create is blocked for the bot, or Initiative description hits a size limit mid-capture/verify → agent invents ad-hoc comments (forbidden) or hard-stops mid-UR. Triggers §10 non-ticket park and §9.1 "prefer description appends". +- **Human Linear UI editors are unmodeled stakeholders.** Claim is comment protocol + workflow state while assignee stays human. Scenario: a human clears claim comments, renames `do-work/UR-007`, or moves status outside the map while a run is live → concurrent-conflict thrash or unclaimable backlog. Triggers §8 claim protocol and open risk #5. +- **Migration leaves historical trees readable but not authoritative — offline tooling may not know.** Retro, coverage rollups, and humans grepping archive will still see markdown. Scenario: after cutover someone runs a script against `.do-work/archive/` and treats it as live backlog. Triggers §12 step 6 and §7 ledger "telemetry only" distinction. + +## Challenger — Risks & Edge Cases + +- **Optimistic claim is weaker than `claim-req.sh`'s rename race.** Markdown claim uses atomic `mv`/`git mv` (exit 2 on race). Linear is re-read + comment. Scenario: two orchestrators claim the same issue in the same second; both pass re-read → dual workers, dual worktrees, merge chaos. Triggers §8 atomicity story and open risk #3. +- **"Linear unusable ⇒ hard stop" vs partial outages mid-REQ.** Clear at start; foggy mid-flight. Scenario: MCP dies after claim but before heartbeat/archive — worker cannot release cleanly; issue stuck in Progress with stale claim comment. Triggers §4 "Linear unusable" and §14 error table (no mid-flight recovery path). +- **`status_map` and label prefixes are team-specific landmines.** Defaults map stopped → "Canceled". Scenario: team has no "Canceled", or "Done" is a different workflow state name → every `set_req_status` fails after capture has already created issues. Triggers §7 config schema and validation rules. +- **Native `blocks` relations + mirrored `**Depends on:**` can diverge.** Two sources of truth for deps. Scenario: human edits relation graph in UI but not the body (or vice versa); `list_claimable_reqs` follows relations while humans read the body. Triggers §4 Deps decision and §14 "Relation tool missing" fallback. +- **Milestone cursor on Project description is another optimistic shared write.** Deploy gate still uses local `gate-owner.md`, but active milestone content is remote. Scenario: two terminals advance milestone after gate without serializing Project description updates → lost checklist updates. Triggers §11 milestone mode. +- **Phasing (9 steps) under-orders test/regression for markdown.** Port + markdown.md first is right, but every agent touch is a regression surface. Scenario: half-migrated agents (capture on port, run still filesystem) ship and pass markdown tests while Linear path is unusable. Triggers §16 phasing and §15 testing. + +## Connector — Links & Reuse + +- **Reuse parallel coordination design, not its FS primitives.** `docs/superpowers/specs/2026-05-21-do-work-parallel-coordination-design.md` and `lib/{pick,claim,check-deps,check-footprint,heartbeat,scan-stale,deadlock-check}.sh` define the semantics port.md must restate; Linear must reimplement, not call bash. +- **Header schema and path-units are already the body template.** SKILL.md REQ header fields + path-unit/`Parent:` model map 1:1 to §9.2 Issue template and sub-issues — capture output shape stays stable across backends. +- **Upgrade/conformance is the natural migration door.** UR-039 decision: maintenance centralizes in `/do-work upgrade` via conformance manifest. Migration (§12) should be a conformance/upgrade row or explicit upgrade step, not a one-off agent-only path. +- **Linear skill already mandates live tool rediscovery.** `~/.grok/skills/linear/SKILL.md` (MCP-first, `search_tool` then `use_tool`) matches §17 risk #1 and §14 MCP missing — `linear.md` should import that protocol, not invent tool names. +- **Standing decisions memory format is portable.** `.do-work/decisions.md` one-line format becomes Team Doc `do-work/decisions` with the same line grammar; capture/ideate/question/worker read paths already treat the file as optional. +- **Straggler backlog item is unrelated.** `REQ-270` (depends-on tokenizer) sits at backlog root from UR-042 — not part of multi-tracker; do not fold into this UR's decomposition. +- **No existing `agents/tracker/`** — greenfield under `agents/`; layers for this project remain `[agents, commands, templates]` with likely commands/templates out of scope (docs + agents + config only), consistent with UR-043/044 layer answers. + +## Summary + +The design is implementation-ready on product decisions, but the hard work is **mechanical port adoption across every phase agent** and **honest Linear claim/deps semantics** without silent markdown fallback. Decompose so markdown regression stays green at every phase boundary, freeze the op catalog early, and treat Linear MCP capability discovery (Initiatives, relations, Docs) as a first-class spike path-unit before wiring the full run loop. diff --git a/.do-work/user-requests/UR-045/input.md b/.do-work/user-requests/UR-045/input.md new file mode 100644 index 0000000..d9c0907 --- /dev/null +++ b/.do-work/user-requests/UR-045/input.md @@ -0,0 +1,487 @@ +--- +ur: UR-045 +received: 2026-07-31 +status: captured +source: docs/superpowers/specs/2026-07-31-do-work-multi-tracker-design.md +classification: feature +layers_in_scope: [agents, commands, templates] +layer_decisions: + commands: no + templates: no +reqs: + - { id: REQ-283, layer: none, integration_confidence: n/a } + - { id: REQ-284, layer: agents, integration_confidence: high } + - { id: REQ-285, layer: agents, integration_confidence: high } + - { id: REQ-286, layer: agents, integration_confidence: high } + - { id: REQ-287, layer: agents, integration_confidence: high } + - { id: REQ-288, layer: none, integration_confidence: n/a } + - { id: REQ-289, layer: agents, integration_confidence: high } + - { id: REQ-290, layer: none, integration_confidence: n/a } + - { id: REQ-291, layer: agents, integration_confidence: high } + - { id: REQ-292, layer: none, integration_confidence: n/a } + - { id: REQ-293, layer: agents, integration_confidence: high } + - { id: REQ-294, layer: none, integration_confidence: n/a } + - { id: REQ-295, layer: agents, integration_confidence: high } + - { id: REQ-296, layer: none, integration_confidence: n/a } + - { id: REQ-297, layer: agents, integration_confidence: high } + - { id: REQ-298, layer: none, integration_confidence: n/a } + - { id: REQ-299, layer: agents, integration_confidence: high } + - { id: REQ-300, layer: none, integration_confidence: n/a } + - { id: REQ-301, layer: agents, integration_confidence: high } + - { id: REQ-302, layer: none, integration_confidence: n/a } +acknowledged_partials: [] +--- + + +## Capture summary (2026-07-31) + +| Item | Value | +|---|---| +| Classification | feature | +| Layers in scope | agents, commands, templates | +| Layer decisions | commands: no, templates: no | +| REQs generated | 20 | + +| REQ | Layer | Integration confidence | +|---|---|---| +| REQ-283 | none | n/a | +| REQ-284 | agents | high | +| REQ-285 | agents | high | +| REQ-286 | agents | high | +| REQ-287 | agents | high | +| REQ-288 | none | n/a | +| REQ-289 | agents | high | +| REQ-290 | none | n/a | +| REQ-291 | agents | high | +| REQ-292 | none | n/a | +| REQ-293 | agents | high | +| REQ-294 | none | n/a | +| REQ-295 | agents | high | +| REQ-296 | none | n/a | +| REQ-297 | agents | high | +| REQ-298 | none | n/a | +| REQ-299 | agents | high | +| REQ-300 | none | n/a | +| REQ-301 | agents | high | +| REQ-302 | none | n/a | + + +# UR-045: User Request + +## Request + +# Design: Do-work multi-tracker (markdown + Linear) + +**Date:** 2026-07-31 +**Status:** approved for implementation planning +**Source:** `.scratch/do-work-multi-tracker/` wayfinder + brainstorming session + +## 1. Problem + +do-work stores work items (URs, REQs, decisions, verify/close reports) only as local markdown under `.do-work/`. Operators who want Linear as the system of record cannot run the full do-work loop without dual-maintaining tickets. The skill needs a second backend without rewriting product philosophy (TDD-per-REQ, worktrees, review gate, multi-agent claim/deps/footprint). + +## 2. Goals + +1. **Markdown remains the default backend** — current UR/REQ files and `lib/*.sh` behavior stay the happy path when `tracker.backend` is unset or `markdown`. +2. **Linear is a full second backend** — with `tracker.backend: linear`, work items live **only** in Linear (no dual-write, no local UR/REQ markdown as source of truth). +3. **Tracker port** — one conceptual op catalog; backends plug in. GitHub Issues / Jira can follow later as new backend files (not built in this effort). +4. **v1 ship surface = full map destination** — core loop (intake → ideate → capture → verify → run claim/deps/footprint → status/close), **milestone mode**, Linear homes for ledger notes / decisions / verify / close / calibration, and **idle one-shot markdown→Linear migration**. + +### Done when + +- Default/markdown: behavior matches today (regression). +- Linear configured: an agent can complete intake → ideate → capture → verify → run (claim / deps / footprint) → status / close against Linear via the Linear skill/MCP, preserving multi-agent safety semantics. +- Milestone deploy gates work under Linear mode. +- Non-ticket artifacts have fixed Linear homes; agents do not invent ad-hoc locations. +- Idle migration moves a markdown project to Linear without dual-write. + +## 3. Non-goals + +- Implementing GitHub Issues or Jira backends (pattern only). +- Dual-write or markdown mirror while on Linear. +- Changing TDD-per-REQ, worktree isolation, or post-build review philosophy — only the **store** for work items changes. +- Requiring Linear for all users. +- True distributed locks on Linear (optimistic claim only). + +## 4. Decisions (locked) + +| Decision | Choice | +|----------|--------| +| Architecture | Tracker port docs: `agents/tracker/{port,markdown,linear}.md` | +| Hierarchy | UR = Initiative; one Project per UR named `do-work/{UR-id}`; REQs = Issues in that Project; Project linked to Initiative via `InitiativeToProject` | +| Product container | Team + config — **not** one long-lived product Project for all URs | +| Linear IDs | Linear mode uses Linear issue identifiers only (e.g. `ENG-123`). No parallel `REQ-NNN` allocation | +| UR naming slug | Sequential `UR-NNN` still used as Project name / Initiative metadata slug only | +| Path-units | Parent Issue + layer children as sub-issues (`parentId`) | +| Deps | Native Linear relation type `blocks` (+ mirrored `**Depends on:**` line in body) | +| Footprint | Structured `**Files:**` (and related header fields) in Issue description — no custom fields | +| Claim | Human operator remains Linear **assignee**; agents claim via workflow status + heartbeat **comment** protocol | +| Claim atomicity | Optimistic re-read before write; loser → concurrent-conflict / stop; resume allowed | +| Linear unusable | Hard stop — never silent fallback to markdown | +| Migration | One-shot when idle (`working/` empty); then Linear-only | +| Non-ticket park | Decisions + calibration = team Docs; verify/close = Initiative; run notes = Issue comments (+ optional Project update) | +| Runtime/git | Stay local: worktrees, merges, `state/*` locks, events, config.yml | + +## 5. Architecture + +### 5.1 File layout + +``` +agents/tracker/port.md # shared contract: op names, preconditions, agent-callable surface +agents/tracker/markdown.md # file + lib/*.sh implementation of those ops +agents/tracker/linear.md # Linear skill/MCP sequences for the same ops +# later: agents/tracker/github.md, jira.md +``` + +### 5.2 Load path + +Every phase agent that touches work items: + +1. Load config (`agents/config.md`) +2. Resolve `tracker.backend` (default `markdown` if missing/empty) +3. Read `agents/tracker/port.md` +4. Read `agents/tracker/.md` +5. For work-item storage, call **only** named port ops (never raw `.do-work/REQ-*` paths or raw Linear tools outside the backend file) + +Phase agents keep product logic (TDD, review, decomposition). They do not re-implement store details. + +### 5.3 Bash vs agent steps + +| Backend | Work-item ops | Runtime | +|---------|---------------|---------| +| **markdown** | Existing `lib/*.sh` + file paths, documented in `markdown.md` | worktrees, git, events, state locks — unchanged | +| **linear** | Agent steps invoking Linear skill/MCP, documented in `linear.md`. No Linear-aware bash required for v1 | same local runtime/git | + +Shared **rules** (when to claim, what “deps satisfied” means, footprint overlap) live in `port.md`. Shared **shell** only for the file store. + +### 5.4 Operation catalog + +Coarse lifecycle (~12–25 ops). Names freeze intent; exact set may grow slightly when templates land: + +| Op | Intent | +|----|--------| +| `ensure_product_container` | Team/product labeling ready; no single product Project required | +| `create_ur` | Record intake brief | +| `read_ur` | Load brief (+ ideate if present) | +| `list_urs` | Enumerate URs for prompts/status | +| `append_ideate` | Write ideate onto UR | +| `append_clarifications` | Question phase Q&A | +| `create_req` | Create one REQ in backlog | +| `update_req` | Edit REQ body/fields | +| `read_req` | Load full REQ | +| `list_reqs_for_ur` | All REQs for a UR (any status) | +| `list_claimable_reqs` | Backlog, deps ok, footprint ok, unclaimed — pick order | +| `claim_req` | Optimistic claim + in-progress | +| `heartbeat_req` | Refresh liveness | +| `set_req_status` | stopped / in-progress / etc. | +| `set_blocked_by` | Deps graph | +| `set_files` | Footprint list | +| `archive_req` | Done + closure proof / outputs | +| `unblock_req` | Return to backlog, clear claim | +| `append_decision` | Standing decisions memory | +| `write_verify_report` | Verify output for a UR | +| `write_close_report` | Close output for a UR | +| `append_run_note` | Ledger-ish / cost note for a REQ or run | +| `read_active_milestone` | Milestone cursor | +| `set_active_milestone` | Advance / set milestone | +| `list_milestone_reqs` | REQs for active milestone | +| `write_gate_state` | Deploy-gate coordination (local lock still allowed) | + +Markdown may implement several ops by composing existing scripts. Linear maps each to skill/MCP sequences. + +### 5.5 Work-item vs runtime split + +From storage inventory (~88 ops): **work-item** data moves to Linear in Linear mode; **runtime/git/config** stay local. + +**Must map to Linear:** UR create/read/update; REQ create/edit; status transitions; deps/footprint fields; archive (done + proof + outputs); decisions; close/verify reports; ideate/clarifications; run cost notes; calibration; milestone cursor content. + +**Stay local:** claim stamp equivalent is comments (not files) but **local** still includes worktrees, branches, merges, PRs, `state/events.jsonl`, gate-owner, final-suite locks, feedback.lock, context-pack, retry-counters, config.yml, conformance/install. + +## 6. Linear hierarchy and identifiers + +### 6.1 Hierarchy + +``` +Team (config) +└── Initiative (UR) — brief, ideate, verify, close + └── Project do-work/{UR-id} — linked via InitiativeToProject + └── Issue (path-unit parent) + └── Sub-issue (layer child) +``` + +### 6.2 Naming + +| Entity | Naming | +|--------|--------| +| Project (machine-stable) | `do-work/{UR-id}` e.g. `do-work/UR-007` — agents resolve by name/id; humans must not rename without updating ids | +| Initiative (human-facing) | Free title; may include UR id for scanability (`UR-007: Add SSO`); not the sole lookup key | +| Issue | Linear identifier only (`ENG-123`). Titles short and actionable; body holds do-work schema | + +### 6.3 List / scope + +| Need | How | +|------|-----| +| `list_reqs_for_ur` | `list_issues` filtered by that UR’s **Project** id | +| `list_claimable_reqs` | Same project filter + status + deps + footprint + unclaimed | +| `status` for a UR | Issues in that Project + claim comments | +| `read_ur` | Initiative description (and comments if needed) | +| Product-wide backlog | Optional: Projects matching `do-work/UR-*` for the team | + +### 6.4 Intake create sequence (Linear) + +1. Allocate next `UR-NNN` slug (scan existing Initiatives/Projects / id cache). +2. Create **Initiative** (title human; description = template with verbatim brief). +3. Create **Project** named `do-work/UR-NNN` on configured team. +4. Link Project → Initiative. +5. Capture creates Issues (and sub-issues) only in that Project. + +### 6.5 Commits and PRs (Linear mode) + +Commit / PR messages reference the Linear issue id: + +``` +feat(ENG-123): short title + +Issue: ENG-123 +UR: UR-007 +Output: path/to/primary/output +``` + +No `.do-work/archive/REQ-…` path required. Worktree branch naming may use `req/ENG-123` (sanitize for git ref rules). + +## 7. Config schema + +```yaml +tracker: + backend: markdown # markdown | linear + linear: + team_id: "" # required when backend=linear (or resolve via team_key) + team_key: "" # optional alternate resolve + default_assignee_id: "" # human operator; set on issue create when configured + project_name_pattern: "do-work/{ur_id}" + initiative_title_pattern: "{ur_id}: {title}" + status_map: + backlog: "Todo" + in_progress: "In Progress" + stopped: "Canceled" # override if team has a dedicated Stopped state + done: "Done" + labels: + layer_prefix: "Layer/" + path_unit: "path-unit" + size_prefix: "Size/" + agent_claim_marker: "" + heartbeat_max_age_seconds: null # null → use parallel.stale_threshold_seconds + decisions_doc_title: "do-work/decisions" + calibration_doc_title: "do-work/calibration" +``` + +**Validation when `backend: linear`:** hard fail if team cannot be resolved or Linear MCP tools are undiscoverable. Message must tell the operator how to connect Linear (skill setup), not invent data. + +**Interaction with existing keys:** `ledger`, `parallel`, `delivery`, `review`, `layers` remain valid. In Linear mode, **authoritative** run/cost notes are Linear Issue comments via `append_run_note`. If `ledger.enabled: true`, the orchestrator may **also** append local `.do-work/runs/RUN-NNN.yml` for offline retro tooling — that local file is telemetry only, not a second work-item store. Retro prefers Linear run notes when `backend: linear`, falling back to local runs if comments are unavailable. + +## 8. Claim protocol (Linear) + +Human always owns **assignee** (config `default_assignee_id` on create; agents do not steal assignee for claim). + +| Concept | Rule | +|---------|------| +| Unclaimed | Workflow state maps to backlog **and** no active claim comment (or last claim is `released` / unblocked) | +| Claim | Re-read issue; if another agent has active claim and fresh heartbeat → fail; else set state → in_progress; post comment with `agent_claim_marker`, `agent_id`, `claimed_at`, `heartbeat`, optional `session`, `status: active` | +| Heartbeat | New claim-protocol comment (or append) with updated `heartbeat` ISO timestamp; consumers take the latest active claim block | +| Stale | Latest active heartbeat older than `heartbeat_max_age_seconds` or `parallel.stale_threshold_seconds` | +| Unblock | State → backlog; claim comment `status: released` | +| Resume | stopped → in_progress; refresh heartbeat; assignee unchanged | +| Concurrent conflict | Same stopper semantics as markdown multi-agent mode | + +**Atomicity story:** MCP has no filesystem atomic rename. Good enough = re-read + comment protocol + timestamp. Document as intentional. + +### Example claim comment + +```markdown + +agent_id: hostname.pid +claimed_at: 2026-07-31T12:00:00Z +heartbeat: 2026-07-31T12:05:00Z +session: optional-uuid +status: active +``` + +## 9. Templates + +### 9.1 Initiative (UR) + +Machine-stable sections in Initiative description: + +```markdown + +**UR-id:** UR-007 +**Class:** feature +**Created:** YYYY-MM-DD +**Project:** do-work/UR-007 +**Project-id:** {linear-project-uuid} + +## Brief +{verbatim intake} + +## Clarifications + +## Ideate + +## Open gaps + +## Capture summary + +## Verify + +## Closure +``` + +Prefer description appends; fall back to Initiative comments if size limits require it. + +### 9.2 Issue (REQ) + +```markdown + +**UR:** UR-007 +**Layer:** agents | none | … +**Parent:** ENG-100 | none +**Entry point:** … # path-unit parents only +**Terminal state:** … # path-unit parents only +**Files:** path1 path2 +**Depends on:** ENG-101 ENG-102 +**Size:** S|M|L +**Priority:** 1-3 +**Criteria approved:** agent-drafted +**Closure proof:** +**Suite:** + +## Task + +## Acceptance Criteria +- [ ] … + +## Verification Steps +1. … + +## Integration + +## Manual checks (advisory) +- [ ] … + +## Outputs +``` + +**Labels:** `Layer/{name}`, `Size/{S|M|L}`, `path-unit` on parents. +**Estimate:** map Size to team T-shirt when enabled. +**States:** via `status_map`. +**Deps:** create `blocks` relations and mirror ids in `**Depends on:**`. +**Path-units:** parent Issue + sub-issues; children set Linear `parentId` and `**Parent:**`. + +## 10. Non-ticket artifact homes + +| Artifact | Linear home | Format | Writers / readers | +|----------|-------------|--------|-------------------| +| Decisions | Team Doc `do-work/decisions` (create-if-missing) | One line per decision (same as today) | capture write; capture/ideate/question/worker read | +| Run / cost notes | Comment on Issue after attempt; optional Project update for run rollup | YAML fenced block (ledger fields) | run | +| Verify report | Initiative `## Verify` + Initiative comment | Full report markdown | verify, go | +| Close report | Initiative `## Closure` + comment | Per path-unit results | close | +| Calibration | Team Doc `do-work/calibration` | Full calibration body | retro write; capture read | +| Milestone cursor | Project description `` | active M + checklist | capture, run | +| Gate locks | **Local** `state/gate-owner.md`, `final-suite-*.md` | unchanged | run | + +## 11. Milestone mode (Linear) + +- Trigger unchanged (UR shape with `source: /saas-thesis handoff` + `### Milestones`). +- REQs for a milestone are Issues in the UR Project, filterable by milestone marker (Project milestone entity when MCP supports it, else label `M1` / section metadata). +- Active milestone cursor on Project description marker. +- Deploy gate: first orchestrator owns gate via **local** `state/gate-owner.md`; human y/n advances cursor; siblings idle as today. + +## 12. Migration (markdown → Linear) + +One-shot, idle-only: + +1. **Preflight:** no files in `working/`; no active claims; operator confirms. +2. Create/update Team Docs for decisions (and empty calibration if missing). +3. For each UR: create Initiative + Project `do-work/UR-NNN` + link; body from `input.md` / ideate / closure. +4. For each REQ in backlog + archive: create Issue in that Project; map status; relations; parent/sub-issues; preserve checkboxes. In-flight forbidden by preflight. +5. Set `tracker.backend: linear` and resolved team ids in config. +6. Leave `.do-work/user-requests/` and `archive/` as **read-only historical** trees (do not delete); work-item ops stop reading them. +7. No dual-write after cutover. + +Surface via `/do-work upgrade` conformance/migrate path or an explicit migrate step documented in upgrade agent — implementation plan chooses the exact command UX without changing these rules. + +## 13. Agents and libraries in scope + +**Must load port and branch on backend:** +intake, capture, ideate, question, audit, verify, run, run-worker, review, status, close, unblock, resume, start, go, upgrade, retro, log, help (docs pointers). + +**lib/*.sh:** remain markdown-backend implementations. Linear reimplements pick/claim/deps/footprint/heartbeat/archive-integrity **semantics** in `linear.md` via MCP. No requirement for Linear-aware bash in v1. + +**SKILL.md + config.md:** document `tracker.*`, load path, hard-stop rules, commit convention for Linear ids. + +## 14. Error handling + +| Failure | Behavior | +|---------|----------| +| Linear MCP missing / unauthenticated | Hard stop with setup instructions from Linear skill | +| Team id unresolved | Hard stop; do not guess | +| Claim race lost | Stop with concurrent-conflict; `/do-work resume` allowed | +| Relation tool missing | Prefer GraphQL/fallback documented in `linear.md`; if unavailable, description-only deps + one-time warning | +| Template parse failure | Stop REQ; do not invent fields | +| Budget reached | Same boundary as today; costs from Linear run notes | + +## 15. Testing and proof + +1. **Markdown regression:** existing `lib/tests` + conformance pass with `backend: markdown` (default). +2. **Port contract:** checklist that both backend docs implement every op name in `port.md`. +3. **Linear integration:** sandbox team manual/agent harness; no secrets in repo. +4. **Migrate dry-run:** report planned creates without writing when flag set. + +## 16. Implementation phasing (for writing-plans) + +Suggested dependency order (single plan, multi-PR REQs): + +1. Config schema + load path + `port.md` stub ops + `markdown.md` mapping existing behavior +2. Initiative/Issue templates + `linear.md` CRUD for UR/REQ +3. Claim/heartbeat/unblock/resume + status +4. Capture/ideate/question/verify against port +5. Run loop pick/claim/deps/footprint/archive on Linear +6. Close, decisions doc, run notes, calibration +7. Milestone mode on Linear +8. Migration one-shot + upgrade wiring +9. Docs (SKILL.md, getting-started, troubleshooting) + +## 17. Open risks + +1. **Linear MCP offline / thin tools** — initiative link, issue relations may need GraphQL; agents must rediscover tools live. +2. **No custom fields** — all structure is markdown conventions; parse discipline is mandatory. +3. **Optimistic claim** — weaker than FS rename; acceptable with documented conflict/resume. +4. **Linear IDs only** — breaks continuity with markdown `REQ-NNN` history after migrate (by design). +5. **Human assignee + agent claim comments** — humans can still edit Linear UI and break protocol; status/docs should warn “do not clear agent claim comments while run is live.” + +## 18. References + +- `.scratch/do-work-multi-tracker/map.md` and issues 01–10 +- `docs/superpowers/specs/2026-05-21-do-work-parallel-coordination-design.md` +- `agents/config.md`, `SKILL.md` +- Linear skill: MCP-first, rediscover tools live + +## Clarifications + +**Q:** Full map vs phased URs; migration home; dual-write; REQ-270 scope +**A:** One UR implements the full §2/§16 map with REQs ordered by the 9-step phasing. Migration/command UX lands under `/do-work upgrade` + conformance (UR-039). No dual-write; Linear hard-stop never falls back to markdown. REQ-270 backlog leftover is out of scope for UR-045. *(inferred, confirmed)* + +**Q:** Design §6 makes UR = Linear Initiative + Project do-work/{UR-id}, linked via InitiativeToProject. How ready is the target Linear workspace for that hierarchy? +**A:** Spike first, then implement — first REQs must rediscover live MCP tools and prove Initiatives/relations/Docs work on a sandbox team before wiring the full agent load path. + +**Q:** If Linear MCP dies mid-REQ after claim (In Progress + active claim comment) but before archive — what should hard-stop leave behind? +**A:** Leave claimed; resume repairs — issue stays in Progress with last claim/heartbeat; operator uses `/do-work resume` or `unblock` after MCP recovers (same multi-agent recovery story as markdown concurrent-conflict). + +**Q:** Config §7 status_map defaults (backlog→Todo, in_progress→In Progress, stopped→Canceled, done→Done) vs team-specific workflows +**A:** Defaults + hard fail on missing state — ship design defaults; when backend=linear, validate every mapped state exists on the team and hard-stop with rename instructions if not. + +**Q:** If native `blocks` relations and mirrored `**Depends on:**` body diverge, which wins for list_claimable_reqs / check-deps? +**A:** Relations authoritative — native blocks relations drive claim eligibility; body is display/mirror; set_blocked_by always updates both when relation tools exist. + +**Q:** Project layers [agents, commands, templates] — which are in scope for multi-tracker? +**A:** agents only — agents/tracker/*, phase agents, config.md, SKILL.md docs pointers. commands and templates out of scope (same pattern as UR-043/044). diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..4fc7937 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,21 @@ +name: test + +on: + push: + branches: [main] + pull_request: + +jobs: + suite: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # The lib/ coordination primitives are plain-bash with no external + # dependencies — the aggregate runner is the single source of truth. + - name: Run lib test suite + run: bash lib/tests/run-all.sh + + # Doc-drift lint: normative docs must not contradict the code. + - name: Doc-drift lint + run: bash lib/doc-lint.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index a2e919a..9cf749f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,63 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## Unreleased +**Operator status / help UX (scannable situation room)** + +**Changed** +- `/do-work status` (unscoped): archive rows in the situation-room table are capped to the **15 newest** REQ ids (lexicographic / zero-padded `REQ-NNN` order). Totals still count every archived REQ. When more exist, output includes `_… and N more archived_` and points operators at `status UR-NNN` or `.do-work/archive/`. **Consumer impact:** scripts that scrape unscoped status for a full archive inventory will miss older rows — scope with `UR-NNN` or read `archive/` directly. +- Archive rows always render Status **`done`** (bucket location is authoritative; stale `**Status:**` headers in archive files are ignored in the table). **Consumer impact:** parsers that expected archive-row status to mirror the file header will now always see `done`. +- Idle unscoped projects (backlog=0, working=0, archive>0) print a short “no live work” scan cue above the table; empty projects suggest `/do-work start "…"`. +- `/do-work` help next steps: empty backlog no longer suggests `capture` for URs whose REQs already live in `archive/` (drained project); capture is only suggested for open URs with zero REQs anywhere. +- Agent/docs copy for status and primary-loop commands updated (`agents/status.md`, `agents/help.md`, `docs/commands.md`, `docs/getting-started.md`, `SKILL.md`). + +**Hub-only skill install (UR-044 / REQ-278)** + +**Changed** +- `install.sh` installs only into the shared skills hub (`AGENTS_SKILLS_HUB` / `~/.agents/skills/do-work`). Legacy `--env claude` / `--env codex` is ignored with a note; dual targets under `~/.claude/skills` and `~/.codex/skills` are no longer supported as install destinations. +- `lib/install-target.sh` resolves the hub path only (`skill_dir|backup_dir|skills hub`); dual env resolution is removed. +- README Installation uses the `agent-native/do-work` remote and hub clone path, matching `install.sh`. + +**Removed** +- Primary dual-install documentation and tests that treated Claude/Codex skill directories as current install targets. + +**UI verification requires Playwright screenshots (UR-043)** + +**Changed** +- `agents/run-worker.md` Step 6 `ui` type: every `ui` verification step must capture a Playwright PNG under `.do-work/user-requests/UR-NNN/ui-evidence/`, vision-read the image, and assert the expected UI outcome from the image. Accessibility/DOM snapshot alone is insufficient. Missing Playwright/browser is not an `environment` deferral — hard-fail as `verification-failing` after retries. Acceptance evidence for `type: ui` must cite the screenshot path. +- `agents/capture.md` / `agents/audit.md`: `ui` steps are written and auto-fixed as navigate + screenshot + vision-assert; automated screenshot steps are not moved to advisory manual checks. +- `lib/check-acceptance-evidence.sh`: `type: ui` requires a non-empty image `ref` under `ui-evidence/` that exists on disk (soft text-only ui evidence fails the gate). +- `agents/review.md`: missing/invalid UI screenshot evidence is a **blocker**. + +**Session telemetry emitter + hooks (REQ-037)** + +**Added** +- `lib/emit-event.sh [data-json]`: appends one well-formed JSON line (`{ts, session, type, data?}`) to `{project}/.do-work/state/events.jsonl`, creating `state/` defensively with a single atomic append. Matches the consumer extension's event-stream parser contract. +- `lib/session-hook.sh `: Claude Code `SessionStart` / `Stop` hook entry point. Reads `session_id` (and `cwd`) from the hook stdin JSON, emits `session.start` (with `data.marker` from `$DO_WORK_UI_MARKER` when set) or `session.end`. A silent no-op (exit 0, no writes) in any project without `.do-work/`; always exits 0 so a hook never breaks the session. +- `lib/install-hooks.sh [--check] `: idempotently merges the two hooks into `{project}/.claude/settings.json` (python3-backed, dedups by command string, preserves unrelated settings). Degrades gracefully to `skipped` when python3 is absent. +- `/do-work install` now wires the session hooks; `/do-work upgrade` gains a `session-hooks` conformance row that adds them idempotently to existing projects. + +**Advisory manual-checks model replaces approve/reject** + +**Removed** +- `/do-work approve` and `/do-work reject` commands. +- The `pending-validation` REQ status and the `.do-work/pending/` directory. +- The `notifications.on_pending_validation` config key. + +**Added** +- `/do-work upgrade`: explicit conformance command for project maintenance. `lib/conformance-scan.sh` is a state-probing conformance manifest — it runs a read-only scan at startup; destructive fixes (e.g. removing a retired config key) run only inside `upgrade`, after confirmation. +- `**Suite:** not-run` header marker: written on an archived REQ when its own test/build suite could not be provisioned in the worker's worktree. `lib/derive-status.sh` derives such a REQ `unproven`. + +**Changed** +- `## Post-merge validation` renamed to `## Manual checks (advisory)`. Human/device checks are advisory only, never block archive, and are not surfaced by any command automatically. +- `proven` semantics: a REQ whose own test/build suite could not be provisioned now derives `unproven` (via the `**Suite:** not-run` marker) even though it still archives as `done`. Human/device advisory items still never affect proven-ness. + +**Migration** +- Nothing replaces `approve`/`reject`: REQs archive as `done` once automated gates (tests, verification, review, policy) pass; human follow-up now lives in the archived REQ's `## Manual checks (advisory)` checklist instead of a blocking pending state. +- Nothing replaces the `notifications.on_pending_validation` hook. Remove the key from `.do-work/config.yml` — `/do-work upgrade` detects and removes retired keys automatically. +- Scripted consumers of `pending-validation` status or `.do-work/pending/` must drop those code paths; both are gone. + **Added** -- Archive-integrity guardrail: `lib/check-archive-integrity.sh` runs at the persistence boundary (`agents/run.md` Step 4b / 4-pr.4, `agents/approve.md` 3b) and rejects archiving a `done` REQ unless its on-disk state is internally consistent — `**Status:** done`, a non-empty `**Closure proof:**`, and zero unchecked `- [ ]` items inside `## Acceptance Criteria`. Replaces trust in worker/orchestrator prose (the worker is *instructed* to tick each `- [x]` and set status, but that is an LLM step it can silently skip). A failure stops the REQ with `**Reason:** archive-integrity` instead of archiving. Covered by `lib/tests/check-archive-integrity.test.sh`. Root-caused from a data-quality audit that found 37 archived REQs with stale (non-`done`) status and 50 archived `done` REQs with unchecked acceptance criteria. +- Archive-integrity guardrail: `lib/check-archive-integrity.sh` runs at the persistence boundary (`agents/run.md` Step 4b / 4-pr.4) and rejects archiving a `done` REQ unless its on-disk state is internally consistent — `**Status:** done`, a non-empty `**Closure proof:**`, and zero unchecked `- [ ]` items inside `## Acceptance Criteria`. Replaces trust in worker/orchestrator prose (the worker is *instructed* to tick each `- [x]` and set status, but that is an LLM step it can silently skip). A failure stops the REQ with `**Reason:** archive-integrity` instead of archiving. Covered by `lib/tests/check-archive-integrity.test.sh`. Root-caused from a data-quality audit that found 37 archived REQs with stale (non-`done`) status and 50 archived `done` REQs with unchecked acceptance criteria. **Changed** - `**Criteria approved:** agent-drafted` no longer stops dispatch. Criteria provenance remains visible, but existing backlog REQs run unless dependencies, footprint, policy, tests, verification, review, or genuinely ambiguous criteria stop them. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fec8a62..d3eddf9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,6 +42,22 @@ git clone https://github.com/rawphp/do-work.git ~/do-work-dev ln -sf ~/do-work-dev ~/.claude/skills/do-work ``` +## Running the tests + +The coordination primitives in `lib/*.sh` are covered by a suite under +`lib/tests/`. Every test is a self-contained, plain-bash `*.test.sh` script — +no `bats` or other external dependency, compatible with macOS bash 3.2. Run the +whole suite with the aggregate runner: + +```bash +bash lib/tests/run-all.sh # runs every lib/tests/*.test.sh; exits non-zero on any failure +bash lib/tests/.test.sh # run one suite directly +``` + +`lib/tests/` is the single home for these tests — don't add `*.test.sh` files at +the `lib/` top level. The runner is also what CI executes (`.github/workflows/test.yml`), +alongside the doc-drift lint (see below), so a green local run matches a green CI run. + ## Submitting Changes 1. Fork the repo and create a feature branch. @@ -279,7 +295,9 @@ bash lib/tests/doc-lint.test.sh # run its tests It scans the live, normative docs only — `SKILL.md`, `README.md`, `docs/`, `agents/` — and **never** scans `.do-work/`, `CHANGELOG.md`, `.git/`, or `docs/superpowers/` (the dated spec/plan tree). Those locations legitimately preserve retired terminology; scanning them would reproduce the UR-029 over-broad find-and-replace failure. -**The rule: when you fix a doc conflict, add its pattern to the lint in the same commit.** A fix that doesn't also guard against regression is half a fix — the contradiction can drift back the next time someone edits an adjacent file. Add the new check to `scan_file` (or `scan_judgment_markers`) in `lib/doc-lint.sh` and a planted-violation case to `lib/tests/doc-lint.test.sh`, then confirm `bash lib/doc-lint.sh` is still clean against the repo. If a stale term is legitimately used inside an explicit retirement note, exclude that case narrowly (as the `same-branch` check skips lines that also say `retired`) rather than dropping the pattern. +**Path-scoped pattern — `bare-runtime-bash-lib` (ORI-242 / ORI-246):** under `SKILL.md`, `agents/`, and `references/`, bare runtime invocations of the form `bash lib/...` fail the scan. Runtime docs (including the skill entry) must use `bash {skill-root}/lib/...` after skill-root resolve / Load Config. **Allowlist (skill-dev regression gates only, under `agents/` / `references/`):** `bash lib/tests/...` and `bash lib/conformance-scan.sh` (tracker docs / CONTRIBUTING). **Not allowlisted on `SKILL.md`:** bare `bash lib/conformance-scan.sh` — entry conformance must use the skill-root form (a line that also says `never` is treated as a prohibition note documenting the anti-pattern). Catalog identity `lib/*.sh` without a leading `bash ` is not matched. This pass is separate from the default roots so `references/` is not pulled into unrelated patterns. + +**The rule: when you fix a doc conflict, add its pattern to the lint in the same commit.** A fix that doesn't also guard against regression is half a fix — the contradiction can drift back the next time someone edits an adjacent file. Add the new check to `scan_file` (or `scan_judgment_markers` / a path-scoped helper) in `lib/doc-lint.sh` and a planted-violation case to `lib/tests/doc-lint.test.sh`, then confirm `bash lib/doc-lint.sh` is still clean against the repo. If a stale term is legitimately used inside an explicit retirement note, exclude that case narrowly (as the `same-branch` check skips lines that also say `retired`) rather than dropping the pattern. For skill-dev-only bare `bash lib/` lines under `agents/` or `references/`, extend the `bare-runtime-bash-lib` allowlist rather than dropping the pattern — do **not** allowlist bare entry form on `SKILL.md`. ## Questions? diff --git a/README.md b/README.md index 280a99c..d52cda4 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,64 @@ # Do Work -A Claude Code and Codex skill that turns natural-language briefs into discrete, traceable tasks and executes them autonomously — with TDD, evidence gates, review, and a git commit per task. +An agent-harness skill that turns natural-language briefs into discrete, traceable tasks and executes them autonomously — with TDD, evidence gates, review, and a git commit per task. Works with any agent that loads skills from a shared hub (Claude Code, Codex, Cursor, and others). Two commands: `/do-work start` to define the work, `/do-work go` to execute it. +## User documentation + +Task-based guides for installing and running `/do-work` (not contributor internals): + +| Guide | Contents | +|-------|----------| +| [docs/README.md](docs/README.md) | Docs index | +| [docs/getting-started.md](docs/getting-started.md) | Install → first start → first go | +| [docs/concepts.md](docs/concepts.md) | UR, REQ, gates, evidence | +| [docs/commands.md](docs/commands.md) | Command and flag reference | +| [docs/troubleshooting.md](docs/troubleshooting.md) | Common failure symptoms | +| [docs/HOW-IT-WORKS.md](docs/HOW-IT-WORKS.md) | Phase-by-phase deep dive | + +

+ License + Tests +

+ +

+ Any agent harness
+ + Claude Code + + + Codex CLI + + Any skills hub agent +

+ --- ## Installation -### One-liner +Installs into the shared skills hub (`~/.agents/skills/do-work` by default). All agents wired to that hub share one install. (`--env` is accepted for compatibility and ignored.) -Choose the target assistant environment explicitly: +### One-liner ```bash -# Claude Code -curl -fsSL https://raw.githubusercontent.com/rawphp/do-work/main/install.sh | bash -s -- --env claude +curl -fsSL https://raw.githubusercontent.com/agent-native/do-work/main/install.sh | bash +``` -# Codex -curl -fsSL https://raw.githubusercontent.com/rawphp/do-work/main/install.sh | bash -s -- --env codex +Optional live symlink from a checkout (dev): + +```bash +curl -fsSL https://raw.githubusercontent.com/agent-native/do-work/main/install.sh | bash -s -- --from-cwd +# or: bash install.sh --source /path/to/do-work ``` ### Or clone manually ```bash -# Claude Code -git clone https://github.com/rawphp/do-work.git ~/.claude/skills/do-work - -# Codex -git clone https://github.com/rawphp/do-work.git ~/.codex/skills/do-work +git clone https://github.com/agent-native/do-work.git ~/.agents/skills/do-work ``` -Claude Code or Codex picks up the `/do-work` slash command from the environment you installed into. +Override the hub directory with `AGENTS_SKILLS_HUB` (same as `install.sh`). Wire any agent harness to load skills from that hub. --- @@ -74,6 +102,7 @@ Flags: | `/do-work go [UR-NNN] --auto-fix` | Verifies, auto-fixes gaps, then runs. | | `/do-work go [UR-NNN] --no-layers` | Verifies + runs, but skips layer-coverage checks for this UR. | | `/do-work install` | Creates `.do-work/` folder structure in current project. | +| `/do-work upgrade` | Brings `.do-work/` state into conformance with the current skill. | | `/do-work intake [brief]` | Records brief verbatim as next UR file. | | `/do-work capture [UR-NNN]` | Decomposes a UR into REQ files. | | `/do-work question [UR-NNN]` | Grills you about your brief — extracts assumptions, gaps, constraints. | @@ -81,7 +110,14 @@ Flags: | `/do-work ideate [UR-NNN]` | Surfaces assumptions, risks, and connections. | | `/do-work verify [UR-NNN]` | Scores REQ coverage (0-100%), lists gaps. | | `/do-work verify [UR-NNN] --auto-fix` | Verify + auto-create missing REQs. | -| `/do-work run` | Executes backlog: TDD loop, acceptance evidence, policy checks, review, archive/ledger. | +| `/do-work run [UR-NNN]` | Executes backlog: TDD loop, acceptance evidence, policy checks, review, archive/ledger. Optional UR-NNN scopes the run. | +| `/do-work run --parallel N` | Single-session parallel mode: dispatches up to N concurrent workers from one terminal. | +| `/do-work run --budget ` | Caps estimated model spend for the run; stops at the next REQ boundary when reached. | +| `/do-work status [UR-NNN]` | Live situation room: REQs, claimers, heartbeats, deadlock warnings, coverage rollup. | +| `/do-work close UR-NNN` | Validates the integrated result of a UR against its verbatim brief; writes a closure report. | +| `/do-work unblock REQ-NNN` | Forces a stuck REQ out of `working/` back to the backlog. | +| `/do-work resume REQ-NNN` | Re-dispatches a fresh worker for a stopped REQ. | +| `/do-work retro` | Mines the run ledger into a learning report + capture calibration guidance. | | `/do-work log` | Generates build-in-public draft posts for configured platforms. | | `/do-work` | Show help. | @@ -109,21 +145,33 @@ Normal completion is proof-backed. Capture may mark generated criteria as `agent This skill is multi-file. `SKILL.md` is the entrypoint and routes commands to agent files: ``` -.do-work/ +do-work/ ├── SKILL.md ← entrypoint and command router ├── agents/ │ ├── start.md ← orchestrator: intake + ideate + capture │ ├── go.md ← orchestrator: verify + run │ ├── intake.md ← records brief verbatim +│ ├── upgrade.md ← conformance manifest and fixes │ ├── question.md ← interactive brief questioning (opt-in) │ ├── audit.md ← autonomous REQ quality audit (always-on) │ ├── ideate.md ← surfaces assumptions & risks │ ├── capture.md ← decomposes into REQ files │ ├── verify.md ← scores coverage -│ ├── run.md ← TDD execution loop with evidence/review gates +│ ├── run.md ← orchestrator: dispatches a worker per REQ +│ ├── run-worker.md ← worker: TDD-and-commits a single REQ │ ├── review.md ← post-build scope, evidence, policy, and regression review +│ ├── status.md ← read-only situation room +│ ├── close.md ← validates a UR's integrated result against its brief +│ ├── unblock.md ← forces a stuck REQ back to the backlog +│ ├── resume.md ← re-dispatches a worker for a stopped REQ +│ ├── retro.md ← mines the run ledger into learning reports │ ├── log.md ← build-in-public draft posts -│ └── config.md ← reusable config loading +│ ├── help.md ← command help +│ └── config.md ← reusable config loading + canonical config template +├── lib/ ← deterministic bash primitives (claiming, policy, +│ │ evidence, ledger, conformance scan, …) +│ └── tests/ ← plain-bash test suites (run-all.sh) +├── docs/ ├── install.sh └── README.md ``` @@ -146,6 +194,8 @@ your-project/ ├── working/ ← current REQ in flight ├── archive/ ← completed REQs ├── logs/ ← build-in-public log drafts + ├── state/ ← coordination state (milestones, calibration, …) + ├── runs/ ← run ledger records (RUN-NNN.yml) └── REQ-001-slug.md ← backlog tasks REQ-002-slug.md ... @@ -195,9 +245,8 @@ verify: | `model.escalation` | string | `opus` | Escalation model for high-risk or failed work | | `verify.threshold` | integer | `90` | Minimum confidence score (0-100) for `go` to auto-run without `--force`. | | `ledger.enabled` | boolean | `true` | Write structured run records under `.do-work/runs/` | -| `notifications.on_pending_validation` | string | `""` | Shell command run once when a REQ parks as `pending-validation`. Supports `{req}`, `{title}`, `{checks}` placeholders. Empty = disabled. Failure produces a one-line warning and never blocks the loop. | -For the full key reference including `feedback`, `parallel`, `next_steps`, `review`, `acceptance`, `risk`, `security`, `cost`, `ledger`, and `notifications`, see [`agents/config.md`](agents/config.md). +For the full key reference including `feedback`, `parallel`, `next_steps`, `review`, `acceptance`, `risk`, `security`, `cost`, `ledger`, `delivery`, `routing`, and `worktree`, see [`agents/config.md`](agents/config.md). --- @@ -220,7 +269,10 @@ Capture inspects the codebase to draft the answers, verifies cited files actuall ## Parallel Execution -do-work supports parallel execution across multiple terminals. Open two or three terminals, run `/do-work run` in each, and the orchestrators pick disjoint REQs from the backlog and work in parallel. No flag is needed — parallel mode is implicit when a second terminal joins. +do-work supports parallel execution in two forms: + +- **Multi-terminal.** Open two or three terminals, run `/do-work run` in each, and the orchestrators pick disjoint REQs from the backlog and work in parallel. No flag is needed — parallel mode is implicit when a second terminal joins. +- **Single-session.** Run `/do-work run --parallel N` and one orchestrator dispatches up to N concurrent workers (capped at 10), serializing merge/archive through a queue. Defaults from `parallel.max_workers` in config. Three guarantees keep the parallel terminals from stepping on each other: @@ -230,7 +282,7 @@ Three guarantees keep the parallel terminals from stepping on each other: **When parallel mode shines.** Backlogs of 5+ independent REQs — the work-sharing payoff grows with the backlog size. For single-REQ work, tightly-coupled REQs, or milestone deploy gates (which stay single-agent by design), the simplicity of one terminal is often the better trade-off. -**Isolation per REQ.** Every REQ runs in a dedicated `git worktree` on a `req/REQ-NNN` branch. The orchestrator creates the worktree before dispatch and tears it down after merging — no REQ ever touches the base branch directly. +**Isolation per REQ.** Every REQ runs in a dedicated `git worktree` on a `req/REQ-NNN` branch. The orchestrator creates the worktree before dispatch and tears it down after merging — no REQ ever touches the base branch directly. Dependency directories (`vendor`, `node_modules`, `.venv`) are symlinked from the main checkout into each worktree automatically; use `worktree.link_paths` and `worktree.setup_command` in config for monorepo layouts the auto-detection misses. See `SKILL.md` `## Parallel Execution` for the full behavioural reference, including state files (`gate-owner.md`, `final-suite-running.md`). diff --git a/SKILL.md b/SKILL.md index 6ebc36e..b5b70e6 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,16 +1,35 @@ --- name: do-work description: > - Project management skill for the do-work system — a file-based autonomous loop - that turns natural-language briefs into discrete, traceable tasks (REQ files) - and executes them one at a time with TDD and a git commit per task. + Autonomous project-management loop: natural-language briefs → traceable work + items (URs/REQs) → isolated TDD workers with one git commit per task. Default + store is local markdown under .do-work/; optional Linear as sole backend via + tracker.backend (no dual-write, hard-stop if Linear unusable). Differentiator: + file-coordinated multi-agent runs with footprint-aware claims, worktree + isolation, and verify/review/archive gates — not a generic todo list. Triggers on: "do-work", "intake", "capture", "verify", "run the loop", - "backlog", "user request", "REQ-", "UR-", "question", "audit". + "backlog", "user request", "REQ-", "UR-", "question", "audit", + "linear backlog", "tracker.backend", "migrate to Linear". --- # do-work -File-based project management: Start → Go. (Or granular: Intake → Capture → Verify → Run.) +Start → Go. (Or granular: Intake → Capture → Verify → Run.) + +Work-item storage is pluggable (`tracker.backend`: **markdown** default, or **linear**). Runtime/git (worktrees, merges, state locks, `config.yml`) always stay local. + +## Primary loop + +Most days you only need these: + +| Command | What it does | +|---------|-------------| +| `/do-work start [brief]` | Record a brief and build the REQ backlog (ideate on by default; auto-installs). | +| `/do-work go [UR-NNN]` | Verify coverage, then audit + run when confidence ≥ threshold (default 90%). | +| `/do-work status [UR-NNN]` | Live situation room: in-flight, backlog, recent done, coverage. | +| `/do-work` | Help + suggested next steps for this project. | + +Flags for start/go (`--no-ideate`, `--force`, `--auto-fix`, …) are in the full table below. ## Quick Reference @@ -24,6 +43,7 @@ File-based project management: Start → Go. (Or granular: Intake → Capture | `/do-work go [UR-NNN] --auto-fix` | Verifies, auto-fixes gaps, then runs if >= 90%. | | `/do-work go [UR-NNN] --no-layers` | Verify + run, skipping layer-coverage checks for this UR. | | `/do-work install` | Creates `.do-work/` structure in current project. | +| `/do-work upgrade` | Brings the project's .do-work/ state into conformance with the current skill — runs the manifest's detectors and applies fixes (interactive confirmation on destructive rows). Idempotent. | | `/do-work intake [brief]` | Records brief verbatim as next UR file. | | `/do-work capture [UR-NNN]` | Decomposes a UR brief into REQ files in the backlog. | | `/do-work question [UR-NNN]` | Grills you about your brief — extracts assumptions, gaps, constraints. | @@ -40,11 +60,11 @@ File-based project management: Start → Go. (Or granular: Intake → Capture | `/do-work retro` | Mines the run ledger and feedback fingerprints to produce a human report and regenerate `.do-work/state/calibration.md` — advisory capture guidance derived from historical patterns. | | `/do-work unblock REQ-NNN` | Forces a stuck REQ out of working/ back to the backlog — strips claim stamp, resets status. | | `/do-work resume REQ-NNN` | Re-dispatches a fresh worker for a stopped REQ — preserves claim, refreshes heartbeat. | -| `/do-work approve REQ-NNN` | Confirms a pending-validation REQ's human checks and archives it with human closure proof. | -| `/do-work reject REQ-NNN [note]` | Returns a pending-validation REQ to the backlog with the rejection note as rework context. | | `/do-work log` | Generates build-in-public draft posts for configured platforms. | | `/do-work` | Show this help. | +Deep per-subcommand stubs (install bootstrap template, flag wiring, pre-flight notes): [references/commands.md](references/commands.md). + --- ## Agent files @@ -54,675 +74,147 @@ Detailed instructions for each phase live in separate files. Read the referenced - [agents/start.md](agents/start.md) — Orchestrator: intake + ideate + capture - [agents/go.md](agents/go.md) — Orchestrator: verify + conditional run - [agents/intake.md](agents/intake.md) — Records brief verbatim as next UR file +- [agents/upgrade.md](agents/upgrade.md) — Brings project state into conformance with the current skill - [agents/question.md](agents/question.md) — Interactive brief questioning - [agents/audit.md](agents/audit.md) — Autonomous REQ quality audit - [agents/ideate.md](agents/ideate.md) — Surfaces assumptions, risks, and connections - [agents/capture.md](agents/capture.md) — Decomposes brief into REQ files - [agents/verify.md](agents/verify.md) — Scores REQ coverage against brief -- [agents/run.md](agents/run.md) — Orchestrator: dispatches a worker subagent per REQ +- [agents/run.md](agents/run.md) — Orchestrator: dispatches a worker subagent per REQ; deep sequences: [references/run-loop.md](references/run-loop.md), [references/run-parallel.md](references/run-parallel.md) - [agents/run-worker.md](agents/run-worker.md) — Worker: TDD-and-commits a single REQ in a fresh subagent session - [agents/review.md](agents/review.md) — Post-build gate: reviews scope, acceptance evidence, tests, secrets, docs, and regression risk before archive - [agents/status.md](agents/status.md) — Read-only situation room: REQs, claimers, heartbeats, deadlock warnings, coverage rollup - [agents/close.md](agents/close.md) — Validates the integrated result of a UR against its verbatim brief; walks path-unit entry points in the merged app; writes `UR-NNN/closure.md` - [agents/unblock.md](agents/unblock.md) — Force a stuck in-flight REQ back to the backlog - [agents/resume.md](agents/resume.md) — Re-dispatch a fresh worker for a stopped REQ -- [agents/approve.md](agents/approve.md) — Approve or reject a pending-validation REQ: approve archives it with human closure proof; reject returns it to the backlog with a rejection note - [agents/log.md](agents/log.md) — Generates build-in-public draft posts - [agents/retro.md](agents/retro.md) — Mines the run ledger to produce a learning report and regenerate `calibration.md` -- [agents/config.md](agents/config.md) — Reusable config loading instructions +- [agents/config.md](agents/config.md) — Reusable config loading instructions (includes `tracker.backend` resolution) +- [agents/tracker/port.md](agents/tracker/port.md) — Tracker port: shared work-item op catalog and load path +- [agents/tracker/markdown.md](agents/tracker/markdown.md) — Default markdown backend (`.do-work/` + `lib/*.sh`) +- [agents/tracker/linear.md](agents/tracker/linear.md) — Optional Linear backend (when `tracker.backend: linear`); sequences: [references/linear-ops.md](references/linear-ops.md) +- [agents/help.md](agents/help.md) — Contextual help when invoked with no subcommand -Run ledger: when `ledger.enabled: true`, `/do-work run` writes append-only `.do-work/runs/RUN-NNN.yml` records with model, cost, commands, tests, changed files, review outcome, result, and proof status. Set `ledger.enabled: false` to disable ledger writes. +Run ledger: when `ledger.enabled: true`, `/do-work run` writes append-only `.do-work/runs/RUN-NNN.yml` records. Set `ledger.enabled: false` to disable. --- -## Project Root Detection - -At the start of every subcommand: - -```bash -git rev-parse --show-toplevel -``` - -If this fails (not a git repo), use the current working directory. -All references below use `{project}` to mean this resolved root. - -### Migration check - -Immediately after resolving `{project}` and before executing any subcommand-specific instructions, check whether this project's data folder needs migrating from the legacy `do-work/` location to `.do-work/`. Apply these four detection branches: - -| State at `{project}` | Action | -|---|---| -| `.do-work/` exists AND `do-work/` does not exist | Already migrated. Continue silently. | -| `do-work/` exists AND `.do-work/` does not exist | Migrate (see below), then continue. | -| Both `do-work/` and `.do-work/` exist | **Halt.** Output the conflict message below and stop the subcommand. | -| Neither exists | No migration needed. Continue (the `install` flow handles fresh projects). | - -**Migration procedure** (legacy `do-work/` → `.do-work/`): - -```bash -# Prefer `git mv` so history follows the rename. Fall back to plain `mv` if the path is -# gitignored or this is not a git repo (both make `git mv` fail). -git mv {project}/do-work {project}/.do-work 2>/dev/null \ - || mv {project}/do-work {project}/.do-work -``` - -Then rewrite `.gitignore` if it contains a line matching `^do-work/?$`: - -```bash -if [ -f {project}/.gitignore ] && grep -Eq '^do-work/?$' {project}/.gitignore; then - sed -i.bak -E 's|^do-work/?$|.do-work/|' {project}/.gitignore && rm {project}/.gitignore.bak -fi -``` - -After the directory rename and `.gitignore` rewrite succeed, scan the consumer project for hardcoded `do-work/` references and print a warning if any are found. - -**Consumer-ref scan targets:** - -- All `*.md` files in `{project}` (recursive), excluding: `.git/`, `node_modules/`, `vendor/`, `.worktrees/`, `dist/`, `build/` -- `{project}/.gitignore` itself (in case it contains other `do-work/` patterns beyond the one already rewritten) - -**Pattern:** regex `(^|[^.])do-work/` — matches the literal legacy form; the leading `[^.]` guard excludes post-migration `.do-work/` references so only genuine stale references surface. - -If matches are found, print: +## Hard-stops (tracker summary) -``` -Migration warning: consumer files still reference the legacy do-work/ path. -Review and update these manually — migration does NOT auto-rewrite consumer docs: - - CLAUDE.md:14: Run intake: read systems/do-work/agents/intake.md - CLAUDE.md:18: Identify which project the work is for in {project}/do-work/ - README.md:42: See do-work/ for backlog state - ... - -(Total: N references across M files.) -``` +Full multi-backend deep dive: [references/tracker.md](references/tracker.md). -If zero matches, print nothing — silent success. +| `tracker.backend` | Behavior | +|-------------------|----------| +| **unset / empty / missing** | Treat as **`markdown`** — no hard-stop, no Linear tools | +| **`markdown`** | Default: local `.do-work/` files + `lib/*.sh` | +| **`linear`** | Linear is the sole work-item store | -**Advisory only.** This warning is informational. Never auto-rewrite consumer files — that is a separate explicit user action. +**Load path** (every phase that touches work items): (1) [agents/config.md](agents/config.md), (2) resolve `tracker.backend` (default **markdown**), (3) [agents/tracker/port.md](agents/tracker/port.md), (4) `agents/tracker/.md`, (5) call only named port ops for storage. -**Failure handling.** If the scan command itself fails (permission denied, regex error on a particular platform, or any other non-zero exit from the underlying grep), skip the warning step silently. Migration already succeeded; the consumer-ref scan is best-effort and must never block the user from completing migration. +**Hard-stop (no silent fallback):** when effective backend is `linear` and Linear is unusable (MCP missing/unauthenticated, team unresolved, missing `status_map` state) **or** `agents/tracker/linear.md` is missing/unreadable, agents **hard-stop** with setup instructions — they never fall through to markdown work-item paths. Also hard-stop when skill-root cannot be resolved at entry (Project Root Detection) or later (Load Config step 8). Canonical contract: `agents/tracker/port.md` + Load Config steps 6–8 in `agents/config.md`. -**Skill source exclusion.** When this skill is invoked against its own source clone (`~/.claude/skills/do-work/`), that clone intentionally gitignores `.do-work/` and is excluded from this scan — no self-referential warnings will fire. +**No dual-write.** With `tracker.backend: linear`, Linear is the **only** work-item store. Agents must not mirror URs/REQs into local markdown as a second source of truth, and must not fall back to markdown when Linear fails (hard-stop instead). After idle migration (`/do-work upgrade migrate`), historical `.do-work/user-requests/` and `archive/` trees remain on disk as **read-only history** — work-item ops ignore them. -Output `Migrated do-work/ → .do-work/` and continue with the subcommand. +**Linear hierarchy:** **UR = Project Milestone** on a **shared product Project** per local product (`tracker.linear.product_project` — name or UUID; **default empty**). Resolve: explicit `product_project` → `project.name` → git-root basename; `ensure_product_container` create-if-missing + **always persist UUID**. Never invent skill name `do-work` for empty config (example name for this skill repo only). REQs = Issues with that milestone. Not Initiatives (MCP has no Initiative create tools). -**Both-exist conflict.** When both `{project}/do-work/` and `{project}/.do-work/` are present, do not migrate. Halt the subcommand with this exact text and exit: +**Linear commit / branch** (when `backend: linear`): subject uses Linear issue id only (`feat(ENG-123): …`); footer `Issue:` / `UR:` / `Output:`; branch/worktree `req/` (dir hard-defaults lowercase). Markdown backend still uses `feat(REQ-NNN): …` with `REQ:` / `UR:` archive paths — see [references/concepts.md](references/concepts.md#commit-convention). -``` -Migration conflict: both do-work/ and .do-work/ exist at {project}. Resolve manually before re-running. -``` +**Operator warning (Linear claims):** human remains Issue assignee; agents claim via workflow state + claim-protocol comment (``). Do not clear/edit/delete agent claim comments in the Linear UI while a run is live. Recover with `/do-work status`, then `resume` or `unblock`. -**No mid-flight protection.** This check does not inspect `working/` for in-flight REQs before migrating. Migration is rare in practice; the assumption is that the user runs it on an idle project. A migration that runs while a parallel `/do-work run` is mid-REQ will cause that worker to fail on the next file-system access — accept that risk rather than introducing a coordination layer for a once-per-project event. - -**Critical: skill directory is read-only at runtime.** The skill is loaded from `~/.claude/skills/do-work/` — this is a separate git clone. NEVER edit files, stage changes, or commit inside the skills directory. All edits and commits MUST happen in `{project}`. If a REQ targets agent files (e.g. `agents/log.md`), edit them at `{project}/agents/log.md`, not at the skill clone path. +Markdown remains the default. Operator setup: [docs/troubleshooting.md](docs/troubleshooting.md) § Linear tracker backend; [docs/HOW-IT-WORKS.md](docs/HOW-IT-WORKS.md) § Multi-tracker; [docs/getting-started.md](docs/getting-started.md). --- -## File Naming - -- User requests: `UR-001`, `UR-002`, ... (zero-padded to 3 digits) -- Feature requests: `REQ-001-short-slug.md`, `REQ-002-short-slug.md`, ... -- Slugs are lowercase kebab-case, max 5 words - -## Milestone Mode - -When a UR file contains both: - -1. The marker `source: /saas-thesis handoff` (in frontmatter or body) -2. A `### Milestones` heading with `#### M1` (or higher) subheadings - -`/do-work` enters **milestone mode**. The differences from normal flow: - -- Capture decomposes ONE milestone at a time, not the whole UR. -- REQ files are prefixed: `REQ-M1-001-.md`, `REQ-M2-001-.md`. -- Run loop halts at the end of each milestone's REQs and prompts for the deploy gate. -- Deploy-gate sign-off is non-delegable human confirmation. -- State files in `{project}/.do-work/state/`: - - `active-milestone.md` — single line, current milestone identifier (e.g. `M1`). - - `milestones.md` — checklist of all milestones with status: `pending` / `captured` / `running` / `deployed`. - -Milestone mode is **implicit** — triggered by UR shape, not a flag. URs that do not match the trigger continue to behave as before. The `/saas-thesis` skill produces UR files with the correct shape for handoff. - -## Parallel Execution - -do-work offers parallelism two complementary ways. **Multi-terminal mode** (below) needs no flag — launch `/do-work run` from several terminals and the coordination layer keeps them from colliding. **Single-session parallel mode** (`--parallel N`) lets one terminal fan out N concurrent workers itself. The two compose: one `--parallel 3` orchestrator and two plain `/do-work run` terminals are just three agent-ids in the same claim arbitration. - -`/do-work run [UR-NNN]` is safe to launch from multiple terminals simultaneously. Up to 10 orchestrators can run in parallel — each claims a different REQ from the backlog. Coordination is handled by a dedicated library layer; no flag, no daemon, no in-memory state is required for multi-terminal mode. - -### Single-session parallel mode (`--parallel N`) - -`/do-work run [UR-NNN] --parallel N` makes **one** orchestrator dispatch up to `N` concurrent workers from a single terminal, integrating their results through a serialized merge queue. It does not replace multi-terminal mode — it adds one-terminal parallelism on top of the same coordination primitives. - -- **Window width `N`.** The maximum number of concurrently dispatched workers. Effective `N = min(flag-or-config, 10)`. -- **Default `N = 1`** (absent flag, `--parallel 1`, or `parallel.max_workers: 1`) ⇒ the serial loop runs byte-for-byte unchanged; the parallel code path is entered only when `N > 1`. -- **Config default.** `parallel.max_workers` in `.do-work/config.yml` (default `1`, under the existing `parallel:` section alongside `parallel.stale_threshold_seconds`) sets the project default. The `--parallel` flag overrides it per-run. -- **Cap `10`.** A request above 10 is clamped to 10 with a one-line notice, matching the 10-orchestrator design bound and protecting the shared main working tree, git object store, and the single `feedback.lock`. - -**How it works** (full spec: `agents/run.md` `## Parallel Run Mode`; design: `docs/design/single-session-parallel.md`): - -- **Fan-out** is N concurrent `Agent`-tool dispatches in one turn — the same dispatch surface serial mode uses, not a separate scheduler. -- **Claim-as-slot-frees.** The orchestrator claims one REQ immediately before each dispatch (never a batch up front) so `pick-req.sh`'s footprint exclusion sees each claim before the next pick. The window refills one REQ each time a slot frees. -- **Serialized merge queue.** Workers return on `req/REQ-NNN` branches in any order. Integration runs in two stages: **Stage A** (acceptance-evidence → policy → independent review) is read-only and may run N-wide; **Stage B** (ledger → merge → archive → teardown → metadata commit) is serial, single-writer — at most one merge/archive touches the main working tree and `.do-work/` at any instant, exactly as serial mode. -- **Failure isolation.** One stopped worker (or a failed gate / a 5-retry merge exhaustion → `concurrent-conflict`) frees its slot and surfaces per-REQ in arrival order; the other workers and queued reports proceed. No new stopper reasons. -- **Deploy gates stay single-flow.** Milestone deploy gates are not parallelised — the existing first-to-detect drain check and single y/n prompt are unchanged; fan-out pauses new claims while a gate is open. -- **Coordination lib untouched.** `pick-req.sh`, `claim-req.sh`, `check-footprint.sh`, `scan-stale.sh` and the rest keep their contracts; this mode is a run-loop shape change, not a primitive change. - -### Coordination Layer - -**Footprint-aware claiming.** Before claiming a REQ, each orchestrator calls `lib/pick-req.sh` to identify the next safe candidate. `lib/pick-req.sh` uses `lib/check-footprint.sh` to detect file-level overlap between the candidate and every REQ currently in `working/`. If overlap exists, the candidate is skipped and the next backlog entry is evaluated. This eliminates the primary source of cross-agent file conflicts. - -**Dependency-aware ordering.** `lib/pick-req.sh` also calls `lib/check-deps.sh` to verify that all `Depends on:` REQs listed in the candidate's header have been archived (status: done) before the candidate is eligible. Circular dependency chains are gated at capture time by `lib/cycle-check.sh` — a dependency graph with a cycle will be rejected during capture, not at run time. - -**Atomic claim.** Once a safe, dep-satisfied candidate is selected, `lib/claim-req.sh` writes the ownership stamp atomically: - -```markdown - -**Claimed by:** hostname.pid -**Claimed at:** 2026-05-21T11:42:08Z -**Heartbeat:** 2026-05-21T11:42:08Z - -``` - -**Checkpoint-based liveness.** Each worker stamps the `**Heartbeat:**` timestamp in its REQ file via `lib/heartbeat.sh` at natural progress checkpoints — after reading the REQ, after each TDD cycle, after each verification step, and before commit — rather than from a background timer (a backgrounded loop cannot survive a fresh-shell-per-call harness). `lib/scan-stale.sh` (called during pre-flight and by `/do-work status`) flags REQs whose heartbeat is older than `parallel.stale_threshold_seconds` (default 900 s / 15 minutes — sized to span the gap between checkpoints) as potentially dead. Stale REQs surface in the status report for human triage — they are not automatically unblocked. - -**Deadlock detection.** `lib/deadlock-check.sh` checks for circular wait chains across the `working/` set: does REQ-A depend on REQ-B which depends on REQ-A (both in-flight)? Any cycle found is reported immediately by `/do-work status` under a `DEADLOCK DETECTED` banner. Recovery is manual: use `/do-work unblock REQ-NNN` to break the cycle. - -**Visible differences from single-agent mode:** - -- The per-REQ announce line is prefixed with `[]` (where `agent-id` is `hostname.pid`) so you can attribute output across terminals. -- Multiple REQs appear in `working/` simultaneously, each carrying the ownership stamp above. -- The final cross-REQ test suite runs once, from whichever orchestrator drains last (gated by `.do-work/state/final-suite-running.md` lockfile). -- On a commit or merge conflict, the losing worker waits up to ~110 seconds (5 retries: 5s / 15s / 30s / 60s backoff) before exiting with `status: stopped`, `reason: concurrent-conflict`. Use `/do-work resume REQ-NNN` to re-dispatch. - -### Isolation per REQ - -Workers always run in isolated git worktrees at `{project}/.worktrees/req-NNN` on a `req/REQ-NNN` branch. The orchestrator merges the branch back into the base branch and tears down the worktree after integration. See [agents/run-worker.md](agents/run-worker.md) `## Isolation Mode` and `## Worktree Workflow` for the canonical procedure. - -### Recovery Commands - -| Situation | Command | -|---|---| -| REQ is stuck / worker died / heartbeat stale | `/do-work unblock REQ-NNN` — strips claim, returns REQ to backlog | -| REQ stopped (concurrent-conflict / transient error) | `/do-work resume REQ-NNN` — refreshes heartbeat, re-dispatches worker | -| Deadlock or unclear state | `/do-work status [UR-NNN]` — renders live situation room, deadlock banner | -| REQ merged, awaiting human validation | `/do-work approve REQ-NNN` / `/do-work reject REQ-NNN` | - -See `agents/status.md`, `agents/unblock.md`, `agents/resume.md` for agent-level instructions. - -### Constraints That Stay Single-Agent - -- Milestone deploy gates remain non-delegable — the first orchestrator to detect milestone-complete owns the gate; siblings idle (logging `Idle — waiting on milestone M deploy gate`) and resume when `.do-work/state/active-milestone.md` advances. `.do-work/state/gate-owner.md` records the gate owner. -- The stale-slot prompt in pre-flight runs in whichever orchestrator finds the stale slot first. - -### State Files - -All coordination state lives under `.do-work/state/`: - -- `gate-owner.md` — agent-id currently handling a milestone deploy gate (deleted on resolve). -- `final-suite-running.md` (or `final-suite-M-running.md` in milestone mode) — lockfile for the final cross-REQ test suite. - -### Implementation Reference - -- Claim: `lib/pick-req.sh`, `lib/check-footprint.sh`, `lib/claim-req.sh` -- Dependencies: `lib/check-deps.sh`, `lib/cycle-check.sh` -- Liveness: `lib/heartbeat.sh`, `lib/scan-stale.sh` -- Deadlock: `lib/deadlock-check.sh` -- Archive integrity: `lib/check-archive-integrity.sh` — pre-archive gate enforcing Status `done` + non-empty Closure proof + zero unchecked acceptance criteria (`agents/run.md` Step 4b/4-pr.4, `agents/approve.md` 3b) -- Orchestrator: `agents/run.md` §§ Agent Identity, Pre-flight Check, Step 1: Claim the next REQ, When the Backlog is Empty, Step 7b -- Worker: `agents/run-worker.md` §§ Isolation Mode, Worktree Workflow, Concurrent-Conflict Retry - -## Layers - -do-work uses project-declared layers to gap-check feature briefs. Declare your project's layers once in `.do-work/config.yml`: - -```yaml -layers: [frontend, backend] # web app -# layers: [commands, core, output] # CLI tool -# layers: [public_api, internal] # library / SDK -# layers: [agents, commands, templates] # do-work itself -``` - -Capture and verify use this list to enforce that REQs cover every declared layer for `feature`-class briefs (or surface explicit "no" decisions). Empty `layers:` opts out — feature briefs will halt until layers are declared or `--no-layers` is passed. - -Every REQ written by capture carries a `**Layer:**` field naming one of the declared layers, or `none` for bug-fix / pure-refactor / test-only REQs. - -Feature REQs that add new surface (anything callable or visible from outside their own code) include an `## Integration` section answering three sub-questions: - -- **Reachability** — How does the user (or caller) reach this? -- **Data dependencies** — What existing data does this read or write? -- **Service dependencies** — What existing services or modules does this extend? - -Capture inspects the codebase to draft answers and verifies each cited file/symbol exists before claiming high confidence. Verify enforces the Integration block on every non-`none` feature REQ. - -## Path Units - -For feature-class briefs, capture decomposes by reachable path first. A path-unit is a top-level REQ that names: - -- `**Entry point:**` — how a user, caller, command, or system reaches the path. -- `**Terminal state:**` — the observable end state that proves the path closed. - -Layer-specific work is captured as child REQs underneath the path-unit. Child REQs carry the normal `**Layer:**` value and point back to the path-unit with `**Parent:** REQ-NNN`. Layers therefore operate inside path-units: they still prevent frontend/backend/command/template gaps, but the closure unit is the reachable path. - -Migration is additive. Legacy REQs without `**Entry point:**`, `**Terminal state:**`, or `**Parent:**` remain valid. New path-units must have both entry point and terminal state before they can verify or archive as complete. - -## Decisions Memory - -`.do-work/decisions.md` is an append-only, cross-UR record of standing decisions (ADR-lite). It gives capture, ideate, and workers a shared institutional memory so a call made in one UR ("validation lives server-side") is not re-litigated or contradicted in the next. +## Project Root Detection -**Format** — one line per decision, no paragraphs (this is a memory, not documentation; anything needing prose belongs in a design doc): +At the start of every subcommand: +```bash +git rev-parse --show-toplevel ``` -YYYY-MM-DD | UR/REQ ref | decision | rationale -``` - -- `YYYY-MM-DD` — the date the decision was recorded. -- `UR/REQ ref` — the UR or REQ the decision was made under (e.g. `UR-035` or `REQ-224`). -- `decision` — the standing choice, stated as a constraint. -- `rationale` — one phrase explaining why. - -**Discipline:** - -- **Append-only.** Never rewrite or delete an existing line. -- **Supersede with a new line.** To reverse or change a decision, append a fresh line that references the superseded one (e.g. `... | supersedes 2026-06-01 entry | ...`). The old line stays as history. -- **The file is optional.** No agent creates it; it comes into being when the first decision is appended. An absent file is silently fine everywhere it is read. - -**Writers:** capture appends a line at judgment points where a choice shapes the decomposition (a layer opt-out, a split-vs-merge call, a layer-coverage user answer). **Readers:** capture (Step 1), ideate (Step 2 project context), run-worker (Step 2 — treats standing decisions as constraints), and question all read it when present. - -## REQ Header Schema - -Every REQ file carries a structured header immediately below the title. The canonical field list is: - -| Field | Required | Description | -|---|---|---| -| `**UR:**` | yes | Parent UR identifier (e.g. `UR-030`) | -| `**Status:**` | yes | `backlog` / `in-progress` / `stopped` / `done` / `pending-validation` | -| `**Created:**` | yes | ISO date (YYYY-MM-DD) | -| `**Layer:**` | yes | Declared project layer, or `none` for bug-fix/refactor/test-only REQs | -| `**Entry point:**` | optional | How a user, caller, command, or system reaches this path-unit. Required to be non-empty for top-level path-unit REQs. | -| `**Terminal state:**` | optional | The observable end state that proves this path-unit is complete. Required to be non-empty for top-level path-unit REQs. | -| `**Parent:**` | optional | Parent path-unit REQ id for child layer-tasks. Empty or absent on top-level path-units and legacy REQs. | -| `**Closure proof:**` | optional | Evidence reference proving verification passed, such as `checkpoint:.do-work/runs/RUN-001.yml#REQ-123` or `commit:abc123 tests:passed`; empty until proven. | -| `**Criteria approved:**` | optional | Acceptance-criteria provenance: `agent-drafted` when capture generated it, or `human ` when a human previously reviewed it. This field does not block run. | -| `**Priority:**` | optional | Backlog urgency `1`–`3` (3 = most urgent), derived by capture from dependency-graph depth. Read by `lib/pick-req.sh` to order claimable candidates (Priority desc, then REQ number asc). Absent or out-of-range sorts as `2`, so legacy REQs are unaffected. | -| `**Size:**` | optional | Effort estimate `S` / `M` / `L`, derived by capture from file count, layer span, and criteria count. `Size: L` is a primary opus-escalation signal in `agents/run.md` Model Selection. Absent falls back to the lexical heuristics. | -| `**Files:**` | yes | Space-separated list of primary output files — used by `lib/check-footprint.sh` for overlap detection | -| `**Depends on:**` | optional | Space-separated REQ ids this REQ must not start before (e.g. `REQ-144 REQ-145`) — checked by `lib/check-deps.sh` | -A **path-unit** is a REQ whose `**Entry point:**` and `**Terminal state:**` are both non-empty. Path-units describe a vertical, reachable slice of intent. Child layer-tasks point back to a path-unit with `**Parent:**`; legacy REQs without these fields remain valid because the migration is additive. - -`**Status:**` remains writable and authoritative for coordination (`backlog`, `working/`, dependency gating, stale checks, and archive flow). `**Closure proof:**` is a separate evidence signal used to derive whether a done REQ is proven; it does not replace the coordination status field. - -`pending-validation` is a terminal **delivered-but-unclosed** state. When a worker's automated gates all pass but a human or device sign-off still remains, `/do-work run` merges the code and tears down the worktree anyway (never stranding work on a branch), then parks the REQ in `.do-work/pending/` with `**Status:** pending-validation` and an empty `**Closure proof:**` — the outstanding checklist lives in the REQ's `## Post-merge validation` section. `/do-work approve REQ-NNN` later completes closure (writing the proof and archiving). A REQ parked in `.do-work/pending/` counts as a **satisfied dependency** — its code is already merged, so dependents stay claimable (`lib/check-deps.sh` globs `.do-work/pending/` alongside `.do-work/archive/`). The `.do-work/pending/` directory is created on demand by `agents/run.md` on the first park, so `/do-work install` does not pre-create it. - -### `## Post-merge validation` section - -An optional REQ body section that holds human, device, or environment checks that cannot be executed by a worker in an isolated worktree. - -**Written by:** `agents/capture.md` on path-unit REQs (or the single REQ for legacy-style decompositions) when the brief includes checks that require a human, a physical device, or an environment the worker cannot provision. Capture writes this section — and its executability self-correction scan (Step 4b) moves any mis-classified `## Verification Steps` entries here automatically before committing REQ files. - -**Ignored by workers:** Workers never execute `## Post-merge validation` items. The section is explicitly outside the checkpoint loop. Workers do not mark these items passed, failed, or deferred — they are not part of the worker's checkpoint log. - -**Consumed post-merge:** After `/do-work run` parks a pending-validation REQ in `.do-work/pending/`, the `## Post-merge validation` checklist is the canonical list of outstanding human/device checks. It is consumed by: +If this fails (not a git repo), use the current working directory. +All references below use `{project}` to mean this resolved root. -- `/do-work approve REQ-NNN` — the approver walks each item, records evidence, and closes the REQ. -- `/do-work close UR-NNN` — includes pending-validation REQs in its walk, surfacing the `## Post-merge validation` checklist for each. +### Skill-root resolve (before conformance) -**Format (each item):** a checklist line stating what a person should do and what observable outcome confirms it: +Immediately after resolving `{project}` and **before** the conformance check, resolve `$SKILL_ROOT` / `{skill-root}` — the absolute path of the do-work skill install root (directory containing `lib/` **and** at least one skill marker: `SKILL.md` **or** `agents/`). -```markdown -## Post-merge validation +**Recipe:** same walk-up as Load Config step 8 in [agents/config.md](agents/config.md) — **not** a second folklore one-level `dirname/..`. Start from the absolute path of **this** file (`SKILL.md`); walk parents until markers match; hard-stop at filesystem root if none found. **No** env / hub / CWD fallback. -- [ ] [Action: what a person should do] — Observable outcome: [what they should see or confirm] +```bash +# Start at the directory of this SKILL.md; walk parents until +# markers match (lib/ AND (SKILL.md OR agents/)). Hard-stop at filesystem +# root if none found. No env/hub/CWD fallback. +d="$(cd "$(dirname "")" && pwd)" +SKILL_ROOT="" +while true; do + if [ -d "$d/lib" ] && { [ -f "$d/SKILL.md" ] || [ -d "$d/agents" ]; }; then + SKILL_ROOT="$d" + break + fi + [ "$d" = "/" ] && break + d="$(dirname "$d")" +done +# non-empty $SKILL_ROOT required — else hard-stop (see below) ``` -`**Criteria approved:** agent-drafted` means capture generated the acceptance criteria. It is informational provenance, not a run gate. Existing backlog REQs should run unless dependencies, footprint, policy, tests, verification, review, or genuinely ambiguous criteria stop them. +**Inherit for phase agents:** keep this resolved `$SKILL_ROOT` in context for the whole agent turn. Phase agents (and Load Config step 8) **inherit** it when it is still an absolute directory that satisfies the markers — they re-resolve only if missing, empty, non-absolute, or invalid. Do not invent a second resolve recipe in phase docs. -When a REQ is claimed by a worker, a claim block is inserted between the title and the first header field: +**Hard-stop when skill-root is unknown at entry.** If the harness did not provide an absolute path for this `SKILL.md` **and** walk-up cannot find a valid skill install root, **stop immediately** — do not run conformance, do not dispatch: -```markdown - -**Claimed by:** hostname.pid -**Claimed at:** 2026-05-21T11:42:08Z -**Heartbeat:** 2026-05-21T11:42:08Z - -``` +`skill-root unknown: cannot resolve $SKILL_ROOT (walk-up from loaded file; no env/hub/CWD fallback). Provide an absolute path to the loaded instruction file under the skill install root, or a valid pre-resolved $SKILL_ROOT that contains lib/ and (SKILL.md or agents/).` -The heartbeat timestamp is refreshed in-place by `lib/heartbeat.sh` — this is a filesystem-only operation, never a git commit. Canonical documentation: `.do-work/archive/REQ-144-extend-req-template-schema.md`. +### Conformance check -## Commit Convention +Immediately after `$SKILL_ROOT` is resolved and before executing any subcommand-specific instructions, run the conformance detectors via the skill install root (**never** bare `bash lib/conformance-scan.sh` — that assumes CWD is the skill root and fails in consumer projects): +```bash +bash {skill-root}/lib/conformance-scan.sh "{project}" ``` -feat(REQ-NNN): short title - -REQ: .do-work/archive/REQ-NNN-slug.md -UR: .do-work/user-requests/UR-NNN/input.md -Output: path/to/primary/output -``` - -Commits are created per-REQ on completion. The claim/heartbeat update path (`lib/heartbeat.sh`) is filesystem-only — it writes directly to the REQ file and does **not** produce a git commit. Unblock operations use `chore(REQ-NNN): unblock — return to backlog` as the commit message. - -## Checkpointed Verification - -REQ `## Verification Steps` are ordered checkpoints. Workers execute them in sequence and record a checkpoint log that localizes both success and failure: - -```yaml -req: REQ-NNN -status: passed | failed -checkpoints: - - step: 1 - total: 3 - type: test - command: "npm test -- --filter settings" - status: passed - - step: 2 - total: 3 - type: runtime - command: "curl http://localhost:3000/settings" - status: failed - handoff: "route -> render" -last_good_step: 1 -failed_step: 2 -``` - -On failure, the log must answer: which step failed, at which handoff, and what the last good step was. On success, the full passed checkpoint log becomes the natural target for `**Closure proof:**`. - ---- - -## Subcommand Instructions - -### No subcommand - -Print the Quick Reference table, then read and follow [agents/help.md](agents/help.md) to display contextual suggestions. - ---- - -### install - -Create the do-work folder structure. Idempotent — safe to run multiple times. - -1. Detect `{project}`. -2. Create directories if they do not already exist: - - `{project}/.do-work/user-requests/` - - `{project}/.do-work/working/` - - `{project}/.do-work/archive/` - - `{project}/.do-work/logs/` - - `{project}/.do-work/state/` -3. Create `{project}/.do-work/config.yml` if it does not already exist, using the bootstrap template below. This template contains the keys most commonly customised at install time. The full default template — including all sections, defaults, and inline documentation — is the canonical template in `agents/config.md`. On first agent run, the config loader in `agents/config.md` migrates any missing sections and keys from that canonical template automatically, so new installs receive all defaults without needing the full template written to disk by install. -```yaml -# do-work configuration -# Edit this file to customize agent behavior. -# Full schema and defaults: agents/config.md (canonical template) +The scanner is read-only and may exit `1` when drift is detected. Interpret each output line as ` `: -project: - name: "" - -# Declare your project's layers, e.g. [frontend, backend] for a web app, -# [commands, core, output] for a CLI, [agents, commands, templates] for do-work. -# Capture and verify use this list to gap-check briefs. Leave empty to -# opt out of layer-coverage checks. -layers: [] - -test: - suite_command: "" # e.g. "./vendor/bin/pest", "npx vitest run", "npm test" -``` - -4. Report what was created vs already existed. Example: +- `legacy-dir safe-blocking ...` — auto-apply the `legacy-dir` fix from [agents/upgrade.md](agents/upgrade.md)'s conformance manifest inline, preserving the existing legacy `do-work/` → `.do-work/` behaviour and advisory consumer-ref scan. Output `Migrated do-work/ → .do-work/` and continue. +- `dir-conflict blocking ...` — halt with the existing verbatim conflict message: ``` -do-work installed at /path/to/project/.do-work/ - -Created: - .do-work/user-requests/ - .do-work/working/ - .do-work/archive/ - .do-work/logs/ - .do-work/state/ - .do-work/config.yml - -Ready. Run `/do-work start` to record your first brief. -Feature work first needs layers declared in .do-work/config.yml -(e.g. layers: [frontend, backend]), or run start with --no-layers. -See the comment already in config.yml. +Migration conflict: both do-work/ and .do-work/ exist at {project}. Resolve manually before re-running. ``` -If already installed, report "Already installed." and stop. - ---- - -### start [brief] [--no-ideate] [--no-layers] - -Record a brief and decompose it into REQ files in one shot. Ideate runs by default and ends with an interactive gate (Grill / Continue / Stop). - -1. Detect `{project}`. -2. Check if `{project}/.do-work/` exists. If not, run install automatically first, then continue. -3. Determine the brief: - - If text was provided after `start`, use it as the brief. - - If not, ask the user to paste their brief and wait. -4. Note whether `--no-ideate` or `--no-layers` are present in the arguments. -5. Read [agents/start.md](agents/start.md) in full. -6. Follow the start agent instructions exactly. Ideate runs by default unless `--no-ideate` was present. Pass `--no-layers` through to capture if present. - ---- - -### go [UR-NNN] [--force] [--auto-fix] - -Verify REQ coverage and conditionally execute the backlog. - -1. Detect `{project}`. -2. Determine the UR: - - If `UR-NNN` was provided, use it. - - If not, list `{project}/.do-work/user-requests/` and ask which UR to verify against. -3. Note whether `--force` or `--auto-fix` are present in the arguments. -4. Read [agents/go.md](agents/go.md) in full. -5. Follow the go agent instructions exactly. Pass through any flags. - ---- - -### intake [brief] - -Record a natural-language brief as the next UR file. Never skip to planning or implementation. - -1. Detect `{project}`. -2. Check if `{project}/.do-work/` exists. If not, run install automatically first, then continue. -3. Determine the brief: - - If text was provided after `intake`, use it as the brief. - - If not, ask the user to paste their brief and wait. -4. Read [agents/intake.md](agents/intake.md) in full. -5. Follow the intake agent instructions exactly. - ---- - -### capture [UR-NNN] - -Decompose a UR brief into discrete REQ files in the backlog. - -1. Detect `{project}`. -2. Determine the UR: - - If `UR-NNN` was provided, use it. - - If not, list `{project}/.do-work/user-requests/` and ask which UR to capture. -3. Confirm `{project}/.do-work/user-requests/{UR-NNN}/input.md` exists. If not, report error and stop. -4. Read [agents/capture.md](agents/capture.md) in full. -5. Follow the capture agent instructions exactly. - ---- +- `pending-dir destructive ...` — print `pending/ detected — run /do-work upgrade to archive & remove it` and continue. +- `stale-config-key destructive ...` — print `stale config key(s) detected — run /do-work upgrade to remove retired config keys` and continue. +- Unknown row ids — print the scanner line verbatim and continue for forward compatibility. -### ideate [UR-NNN] +Startup never applies destructive fixes and never prompts. Destructive rows are handled only by explicit `/do-work upgrade`. -Surface assumptions, risks, and connections in a brief before decomposition. +**No mid-flight protection.** The `legacy-dir` safe-blocking fix does not inspect `working/` for in-flight REQs before migrating. Migration is rare in practice; the assumption is that the user runs it on an idle project. A migration that runs while a parallel `/do-work run` is mid-REQ will cause that worker to fail on the next file-system access — accept that risk rather than introducing a coordination layer for a once-per-project event. -1. Detect `{project}`. -2. Determine the UR: - - If `UR-NNN` was provided, use it. - - If not, list `{project}/.do-work/user-requests/` and ask which UR to review. -3. Confirm `{project}/.do-work/user-requests/{UR-NNN}/input.md` exists. If not, report error and stop. -4. Read [agents/ideate.md](agents/ideate.md) in full. -5. Follow the ideate agent instructions exactly. +**Critical: skill directory is read-only at runtime.** The skill is loaded from a skill install path (e.g. `~/.claude/skills/do-work/` or `~/.grok/skills/do-work/`) — a separate clone. NEVER edit files, stage changes, or commit inside the skills directory. All edits and commits MUST happen in `{project}`. If a REQ targets agent files (e.g. `agents/log.md`), edit them at `{project}/agents/log.md`, not at the skill clone path. --- -### question [UR-NNN] - -Grill the user about their brief — extract assumptions, gaps, and constraints through one-at-a-time questioning. - -1. Detect `{project}`. -2. Determine the UR: - - If `UR-NNN` was provided, use it. - - If not, list `{project}/.do-work/user-requests/` and ask which UR to question. -3. Confirm `{project}/.do-work/user-requests/{UR-NNN}/input.md` exists. If not, report error and stop. -4. Read [agents/question.md](agents/question.md) in full. -5. Follow the question agent instructions exactly. - ---- - -### audit [UR-NNN] - -Interrogate REQ quality for a given UR — auto-fix soft spots and report changes. +## Dispatch -1. Detect `{project}`. -2. Determine the UR: - - If `UR-NNN` was provided, use it. - - If not, list `{project}/.do-work/user-requests/` and ask which UR to audit. -3. Confirm `{project}/.do-work/user-requests/{UR-NNN}/input.md` exists. If not, report error and stop. -4. Read [agents/audit.md](agents/audit.md) in full. -5. Follow the audit agent instructions exactly. +After project-root detection and conformance: ---- - -### verify [UR-NNN] [--auto-fix] +1. Match the subcommand (Quick Reference). +2. For deep stubs (install template, flag/pre-flight detail): read [references/commands.md](references/commands.md) for that subcommand. +3. Read the matching agent file from the index above and follow it exactly. +4. **Before any work-item store I/O:** Load Config → resolve `tracker.backend` → port → backend. Do not treat local `.do-work/user-requests/` or backlog trees as live truth when `backend: linear`. -Score REQ coverage against the original brief. List gaps and issues. - -1. Detect `{project}`. -2. Determine the UR: - - If `UR-NNN` was provided, use it. - - If not, list `{project}/.do-work/user-requests/` and ask which UR to verify against. -3. Note whether `--auto-fix` is present in the arguments. -4. Read [agents/verify.md](agents/verify.md) in full. -5. Follow the verify agent instructions. If `--auto-fix` was present, follow the auto-fix section. +No subcommand → print Quick Reference, then follow [agents/help.md](agents/help.md). --- -### run [UR-NNN] [--parallel N] [--budget ] - -Execute the backlog autonomously — until empty or a stopper is hit. The optional `UR-NNN` argument scopes execution to that UR's REQs only, ignoring all other backlog entries. The orchestrator dispatches a fresh worker subagent per REQ (see [agents/run-worker.md](agents/run-worker.md)) and reads its structured return report. - -By default the orchestrator runs **serially** — one REQ at a time. The optional `--parallel N` flag enables **single-session parallel mode**: one orchestrator dispatches up to `N` concurrent workers from a single terminal, then serializes their integration through a merge queue. See `## Parallel Execution → Single-session parallel mode`. - -- `N` is the maximum number of concurrently dispatched workers (the window width). Effective `N = min(flag-or-config, 10)`. -- **Default `N = 1`** (absent flag, `--parallel 1`, or `parallel.max_workers: 1`) ⇒ the serial loop runs byte-for-byte unchanged. -- When `--parallel` is absent, the default comes from **`parallel.max_workers`** in `.do-work/config.yml` (default `1`). The flag overrides config per-run; config sets the project default. -- A request above the cap (e.g. `--parallel 20`) is clamped to `10` with a one-line notice. - -**Budget (`--budget `).** `--budget` caps the run's cumulative **estimated** model spend (a tier-weighted dollar estimate per worker attempt, recorded in the ledger's `cost_estimate_num` field — not a metered bill). It overrides `cost.budget` from `.do-work/config.yml` for this invocation only. - -- **Stop semantics.** After each worker attempt's ledger write, the orchestrator sums estimated spend (`lib/run-ledger.sh --sum-run`) and compares it to the budget. When estimated spend reaches the budget, it **finishes the in-flight REQ's integration first** (never abandons a mid-merge), then stops gracefully at the next REQ boundary with a **budget-stop report** (estimated spend vs budget, REQs completed vs remaining). It never silently exceeds an explicit budget. -- **Empty / unset budget ⇒ unlimited** (default behaviour, unchanged). The gate is inert and adds no per-run cost. -- Under `--parallel N`, the same gate rides the merge queue: once the budget is reached the orchestrator stops refilling the window and lets live workers drain, then emits the budget-stop report. - -1. Detect `{project}`. -2. Determine UR scope: - - If `UR-NNN` was provided, record it — the run agent will filter the backlog to that UR. - - If not provided, the full backlog is in scope. -3. Resolve the parallel window width: `--parallel N` if given, else `parallel.max_workers` (default 1), clamped to 10. `N == 1` runs serial; `N > 1` runs `agents/run.md`'s `## Parallel Run Mode`. Resolve the effective budget: `--budget ` if given, else `cost.budget` (empty = unlimited). -4. Pre-flight checks: - - Working/ files are classified by agents/run.md's pre-flight (mine/sibling/stale buckets); stale slots are surfaced only when the backlog has no claimable REQ — do not prompt merely because working/ is non-empty. - - If no `REQ-NNN-*.md` files exist in `{project}/.do-work/` (backlog root) within scope, report "Backlog is empty." and stop. -5. Read [agents/run.md](agents/run.md) in full. -6. Follow the run agent instructions exactly, passing through the UR scope, the resolved parallel window width, and the effective budget. - ---- - -### status [UR-NNN] - -Render a read-only live situation room: all in-flight REQs, their claimers, heartbeat ages, any deadlock warnings, and a Coverage section showing intended/proven/unproven REQs. - -1. Detect `{project}`. -2. Determine UR scope: - - If `UR-NNN` was provided, pass it through to scope the report to that UR's REQs. - - If not provided, all in-flight REQs are reported. -3. Confirm `{project}/.do-work/` exists. If not, report "do-work not installed." and stop. -4. Read [agents/status.md](agents/status.md) in full. -5. Follow the status agent instructions exactly. No state changes, no commits, no prompts. - ---- - -### close UR-NNN - -Validate the integrated result of a UR against its verbatim brief — walking every path-unit's entry point to its terminal state in the merged app — and write a per-path-unit closure report. - -1. Detect `{project}`. -2. Confirm `UR-NNN` was provided. If not, report "close requires a UR id (e.g. /do-work close UR-042)." and stop. -3. Confirm `{project}/.do-work/user-requests/UR-NNN/input.md` exists. If not, report "UR-NNN not found at {project}/.do-work/user-requests/UR-NNN/. Check the UR number and try again." and stop. -4. Read [agents/close.md](agents/close.md) in full. -5. Follow the close agent instructions exactly. The close agent is dispatched as a fresh subagent — pass only the project do-work path, the UR reference, and the merged branch. - ---- - -### unblock REQ-NNN - -Force a stuck in-flight REQ out of `working/` and back into the backlog. Use when a worker died, a concurrent-conflict won't resolve, or scope creep needs human triage. - -1. Detect `{project}`. -2. Confirm `REQ-NNN` was provided. If not, report "unblock requires a REQ id (e.g. /do-work unblock REQ-042)." and stop. -3. Confirm `{project}/.do-work/working/REQ-NNN-*.md` exists. If not, report "REQ-NNN is not in working/ — nothing to unblock." and stop. -4. Read [agents/unblock.md](agents/unblock.md) in full. -5. Follow the unblock agent instructions exactly, including the judgment gate on partial commits (Step 3 in the agent file). - ---- - -### resume REQ-NNN - -Re-dispatch a fresh worker for a stopped REQ without sending it back through the backlog. Preserves the existing claim stamp; only the heartbeat is refreshed. - -1. Detect `{project}`. -2. Confirm `REQ-NNN` was provided. If not, report "resume requires a REQ id (e.g. /do-work resume REQ-042)." and stop. -3. Confirm `{project}/.do-work/working/REQ-NNN-*.md` exists and its `**Status:**` is `stopped`. If not in `working/`, report "REQ-NNN is not in working/ — nothing to resume." If status is not `stopped`, report the actual status and stop. -4. Read [agents/resume.md](agents/resume.md) in full. -5. Follow the resume agent instructions exactly. Resume is a one-shot — do not loop back to the backlog after dispatch. - ---- - -### approve REQ-NNN - -Complete closure on a REQ parked in `.do-work/pending/` — confirm the Post-merge validation checklist and archive the REQ with human closure proof. - -1. Detect `{project}`. -2. Confirm `REQ-NNN` was provided. If not, report "approve requires a REQ id (e.g. /do-work approve REQ-042)." and stop. -3. Confirm `{project}/.do-work/pending/REQ-NNN-*.md` exists. If not, report "REQ-NNN is not in pending/ — nothing to approve." and stop. -4. Read [agents/approve.md](agents/approve.md) in full. -5. Follow the approve agent instructions exactly, passing verb `approve`. - ---- - -### reject REQ-NNN [note] - -Return a pending-validation REQ to the backlog with a rejection note as rework context for the next worker. Merged code is NOT reverted. - -1. Detect `{project}`. -2. Confirm `REQ-NNN` was provided. If not, report "reject requires a REQ id (e.g. /do-work reject REQ-042 )." and stop. -3. Confirm `{project}/.do-work/pending/REQ-NNN-*.md` exists. If not, report "REQ-NNN is not in pending/ — nothing to reject." and stop. -4. Read [agents/approve.md](agents/approve.md) in full. -5. Follow the approve agent instructions exactly, passing verb `reject` and any note supplied after the REQ id. - ---- - -### log - -Generate build-in-public draft posts for configured social media platforms. - -1. Detect `{project}`. -2. Read [agents/log.md](agents/log.md) in full. -3. Follow the log agent instructions exactly. - ---- +## On-demand references -### retro +Load only when the active task needs them (one hop from this file): -Mine the run ledger and feedback fingerprints; produce a human report; regenerate `.do-work/state/calibration.md` as advisory capture guidance. +| Reference | When | +|-----------|------| +| [references/commands.md](references/commands.md) | Executing a subcommand; install bootstrap YAML; full step stubs | +| [references/tracker.md](references/tracker.md) | Configuring or debugging multi-tracker / Linear keys, claims, commits | +| [references/concepts.md](references/concepts.md) | Naming, milestone mode, parallel coordination, layers, path-units, decisions, REQ header schema, commit convention, checkpointed verification | -1. Detect `{project}`. -2. Confirm `{project}/.do-work/` exists. If not, report "do-work not installed." and stop. -3. Read [agents/retro.md](agents/retro.md) in full. -4. Follow the retro agent instructions exactly. Pass the resolved `{project}/.do-work/` path as the project do-work path. +Recovery (stuck / stopped / deadlock): `/do-work unblock`, `/do-work resume`, `/do-work status` — see agent files and [references/concepts.md](references/concepts.md#recovery-commands). diff --git a/agents/approve.md b/agents/approve.md deleted file mode 100644 index fd8ff2f..0000000 --- a/agents/approve.md +++ /dev/null @@ -1,254 +0,0 @@ -# Approve / Reject Agent - -You are the Approve/Reject agent in the Do Work system. Your job is to close the human-sign-off loop on REQs that are parked in `.do-work/pending/` — either archiving them as done (approve) or returning them to the backlog for rework (reject). - -This agent never runs automated tests, never re-runs the worker, and never touches source code. It is the bookkeeping half of the merge-first, approve-later contract: the code landed when the REQ entered `pending/`; you are completing closure. - ---- - -## Judgment Points - -The following steps require model judgment that cannot be reduced to a rule. Each is marked inline with a `> **JUDGMENT:**` block at the relevant step. - -| # | Step | Decision | -|---|------|----------| -| J1 | Step 3a (Approve — checklist confirmation) | Whether the user's confirm answer is an unambiguous yes (proceed) or a hesitation that needs clarifying before archiving. A bare "yes" / "y" / "looks good" is unambiguous. Anything with a qualifier ("mostly", "except…", "I think so") must be treated as not-confirmed — ask the follow-up. | - ---- - -## When Invoked - -You will be given: - -1. A project do-work path: `{project}/.do-work/` -2. A verb: `approve` or `reject` -3. A REQ id: `REQ-NNN` -4. (For `reject` only, optional) A rejection note - -Invoked via: -- `/do-work approve REQ-NNN` -- `/do-work reject REQ-NNN [note]` - ---- - -## Steps - -### 0. Load Config - -Read and follow the **Load Config** section of [config.md](config.md). - -Keep `ledger.enabled` in context — both flows conditionally append a ledger note. - -### 1. Locate the REQ - -Glob `{project}/.do-work/pending/REQ-NNN-*.md`. - -- If **no match**: the REQ is not pending. Search these locations in order and report the first match: - - `{project}/.do-work/archive/REQ-NNN-*.md` → report `"REQ-NNN is already archived (done). No action needed."` - - `{project}/.do-work/working/REQ-NNN-*.md` → report `"REQ-NNN is in working/ (in-progress or stopped) — run /do-work run or /do-work resume to advance it."` - - `{project}/.do-work/REQ-NNN-*.md` (backlog root) → report `"REQ-NNN is in the backlog — run /do-work run to claim and execute it."` - - Not found anywhere → report `"REQ-NNN not found in pending/, archive/, working/, or the backlog root."` - - Stop in all cases. Do not proceed to approval or rejection. -- If **multiple matches**: report the ambiguity and stop. Do not guess. -- If **exactly one match**: record the absolute path as `REQ_PATH` and the slug filename as `REQ_FILE`. Continue. - -Confirm `**Status:**` reads `pending-validation`. If it does not, report the actual status and stop — this guard prevents double-archiving an already-done REQ or touching a REQ in a state this agent does not own. - -### 2. Read the REQ - -Read `REQ_PATH` in full. Extract: - -- `REQ_ID` — from the filename (e.g. `REQ-236`) -- `TITLE` — the `# REQ-NNN: ...` heading text -- `POST_MERGE_CHECKLIST` — the full text of the `## Post-merge validation` section, if present. If the section is absent or empty, treat the checklist as empty (approve/reject still proceed — an empty checklist means nothing was deferred, which is fine). - -Print the checklist to the user before proceeding: - -``` -REQ-NNN — pending validation -Post-merge validation checklist: - -``` - ---- - -## Approve flow (verb = `approve`) - -### 3a. Confirm with the user - -> **JUDGMENT:** _(J1)_ A confirmation is unambiguous when the user expresses clear assent. Qualifications ("mostly ok", "except item 2") must be followed up — do not archive a REQ the user is not fully satisfied with. - -Use the `AskUserQuestion` tool with the question: - -``` -Have all Post-merge validation items above been performed and behaved as specified? -(This sign-off is non-delegable — answer Yes only if you personally performed every check.) -``` - -Options: -1. **"Yes — all items confirmed"** → proceed to Step 3b -2. **"Abort — not all items confirmed"** → stop without changing any state; report `"Approval aborted. REQ-NNN remains in pending/."` - -If the checklist was empty, adjust the question to: `"No Post-merge validation items were recorded. Confirm this REQ is ready to archive as done?"` with the same two options. - -### 3b. Archive the REQ - -Perform all edits to `REQ_PATH` in a single pass, then move it: - -1. Strip the ownership stamp block (``, inclusive) if present — pending REQs may carry a stale stamp from their original worker. -2. Set `**Status:** done`. -3. Resolve the approver name: - ```bash - APPROVER="$(git config user.name 2>/dev/null || echo "unknown")" - APPROVE_DATE="$(date -u +%Y-%m-%d)" - ``` -4. Write `**Closure proof:** human-approved `. -5. Check off every item in `## Post-merge validation` — replace each `- [ ]` with `- [x]`. -5b. **Archive-integrity gate.** With `REQ_PATH` fully rewritten (Status done, Closure proof written, post-merge items checked), run the deterministic guardrail before the move: - ```bash - bash {skill-root}/lib/check-archive-integrity.sh "$REQ_PATH" - ``` - It asserts `**Status:** done`, a non-empty `**Closure proof:**`, and zero unchecked `- [ ]` inside `## Acceptance Criteria`. **Exit non-zero ⇒ do not archive:** report the script's stderr diagnostics and stop, leaving the REQ in `pending/`. This is the same persistence-boundary gate run.md Step 4b uses, applied to the human-approval archive path so neither route can archive a malformed `done` REQ. -6. Move the file to `archive/`: - ```bash - mv {project}/.do-work/pending/REQ-FILE {project}/.do-work/archive/REQ-FILE - ``` - -### 3c. Commit the archive move - -```bash -git add {project}/.do-work/archive/REQ-NNN-slug.md -git commit -m "chore(REQ-NNN): approve — human validation passed - -REQ: {project}/.do-work/archive/REQ-NNN-slug.md" -``` - -If `.do-work/` is gitignored in this project (`git add` silently adds nothing and `git commit` would produce an empty commit), skip the commit silently — the filesystem move is the authoritative record. - -### 3d. Ledger note (conditional) - -When `ledger.enabled` is true, append a ledger entry recording the approval: - -```bash -bash lib/run-ledger.sh \ - --project {project} \ - --req {project}/.do-work/archive/REQ-NNN-slug.md \ - --result "approved" \ - --review "human-approved" -``` - -### 3e. Report - -``` -Approved. - -REQ-NNN → archive/ -Status: done -Closure proof: human-approved -Checklist: -Commit: -``` - -Stop. Do not invoke run, verify, or any next-step prompt. - ---- - -## Reject flow (verb = `reject`) - -### 4a. Require a rejection note - -If no note was supplied at invocation, use the `AskUserQuestion` tool: - -``` -A rejection note is required — a bare reject with no reason is not actionable for the next worker. -What is the reason for rejecting REQ-NNN? -``` - -Present a text-entry option. If the user does not provide a note (empty answer or cancellation), stop without changing any state: `"Rejection aborted — no note provided. REQ-NNN remains in pending/."` - -Record the note as `REJECTION_NOTE`. - -### 4b. Update the REQ file - -Perform all edits to `REQ_PATH` in a single pass: - -1. Strip the ownership stamp block (``, inclusive) if present. -2. Set `**Status:** backlog`. -3. Resolve the rejecting user: - ```bash - REJECTOR="$(git config user.name 2>/dev/null || echo "unknown")" - REJECT_DATE="$(date -u +%Y-%m-%d)" - ``` -4. Append a `## Rejection` section at the end of the file: - ```markdown - ## Rejection - - - **Rejected by:** - - **Date:** - - **Note:** - - The merged code is not reverted. The next worker should treat `## Rejection` as context for rework and address the note as a forward fix. - ``` - -Leave `## Post-merge validation` intact — the next worker may need to re-execute the same checks after the rework. - -### 4c. Move the REQ to the backlog root - -```bash -mv {project}/.do-work/pending/REQ-FILE {project}/.do-work/REQ-FILE -``` - -The merged code is NOT reverted. Rework is a forward fix by the next worker, which reads `## Rejection` as context. - -### 4d. Commit the backlog move - -```bash -git add {project}/.do-work/REQ-NNN-slug.md -git commit -m "chore(REQ-NNN): reject — returned to backlog - -REQ: {project}/.do-work/REQ-NNN-slug.md" -``` - -If `.do-work/` is gitignored, skip the commit silently. - -### 4e. Ledger note (conditional) - -When `ledger.enabled` is true, append a ledger entry recording the rejection: - -```bash -bash lib/run-ledger.sh \ - --project {project} \ - --req {project}/.do-work/REQ-NNN-slug.md \ - --result "rejected" \ - --review "human-rejected" -``` - -### 4f. Report - -``` -Rejected. - -REQ-NNN → backlog -Status: backlog -Reason: -Rejected: on -Commit: - -Note: merged code is preserved. The next worker will read ## Rejection as rework context. -``` - -Stop. Do not invoke run, verify, or any next-step prompt. - ---- - -## Rules - -- **Only operates on `pending/`.** Refuse to approve or reject a REQ that is not in `.do-work/pending/`. Report where the REQ actually is and the correct next action. -- **Approve sign-off is non-delegable.** The agent never marks Post-merge validation items as checked on its own — confirmation is always explicit via `AskUserQuestion`. -- **Reject note is mandatory.** A bare rejection with no reason is not actionable; always require and record the note. -- **Merged code is never reverted on reject.** Rework is a forward fix. The `## Rejection` section carries the note for the next worker. -- **One REQ per invocation.** No batching. -- **Strip claim stamps atomically.** Pending REQs may carry stale stamps; strip the entire `` block. A half-removed stamp is worse than leaving it. -- **Commit message follows `chore(REQ-NNN): ...` convention.** Never use `feat:` or `fix:` — approve/reject is bookkeeping, not implementation. -- **No `AskUserQuestion` next-step prompt after the report.** Both flows are terminal actions. -- **Respect `ledger.enabled`.** Only write a ledger entry when the config flag is true; never hard-fail on a missing ledger. diff --git a/agents/audit.md b/agents/audit.md index 9843eb7..4756bfb 100644 --- a/agents/audit.md +++ b/agents/audit.md @@ -33,6 +33,20 @@ You are invoked automatically by the Go agent after Verify passes, or standalone Read and follow the **Load Config** section of [config.md](config.md). +### 0a. Tracker load path + +Work-item storage (URs, REQs, decisions, verify/close reports, run notes) goes **only** through named tracker port ops after config is loaded: + +1. Resolve effective `tracker.backend` (missing/empty/whitespace → `markdown`). +2. Read `agents/tracker/port.md` (shared op catalog + rules). +3. Read `agents/tracker/.md` (e.g. `markdown.md` or `linear.md`). +4. For work-item storage, call **only** named port ops from that backend file — never raw `.do-work/REQ-*` paths or raw Linear tools outside the backend doc. + +**Hard rules:** +- **No silent fallback** from `linear` to `markdown`. If backend is `linear`, do not substitute UR/REQ markdown as the store. +- If backend resolves to **`linear`** but `agents/tracker/linear.md` is **missing or unreadable**, **hard-stop** with setup instructions (restore the Linear backend doc / connect Linear skill). Never fall through to markdown paths. +- Markdown backend: ops map — **invoke** coordination scripts as `bash {skill-root}/lib/...` after Load Config step 8 resolves `$SKILL_ROOT`; **catalog identity** remains `lib/*.sh` in `markdown.md` — use those ops; do not re-implement store details here. + ### 1. Read ground truth Read `{project}/.do-work/user-requests/UR-NNN/input.md` in full — including the `## Clarifications` section if it exists. Clarifications are user-verified answers from the Question agent and carry the highest authority for interpreting intent. @@ -102,7 +116,7 @@ Then check the REQ's `## Verification Steps` block for any step of type `ui`. **Auto-fix vs flag decision:** -- **Auto-fix** when the missing `ui` step can be inferred unambiguously from a specific acceptance criterion — translate the criterion into a concrete navigate + assert step. Example: criterion `user sees a success toast after form submit` → add `ui` step `Navigate to /form, submit valid data, assert toast with text "Success" is visible`. The inferred step must include: target URL or route, the action taken, and a specific element/text to assert. +- **Auto-fix** when the missing `ui` step can be inferred unambiguously from a specific acceptance criterion — translate the criterion into a concrete navigate + **Playwright screenshot** + vision-assert step. Example: criterion `user sees a success toast after form submit` → add `ui` step `Navigate to /form, submit valid data, screenshot to .do-work/user-requests/UR-NNN/ui-evidence/REQ-NNN-step-N.png, vision-assert toast with text "Success" is visible in the image`. The inferred step must include: target URL or route, the action taken, screenshot under `ui-evidence/`, and a specific element/text to assert from the image. - **Flag** when the criteria describe user-visible behaviour but the target route, action, or assertion cannot be inferred without guessing — do not fabricate a step. Report `[FLAG] REQ-NNN has user-visible acceptance criteria but no ui verification step; target route/action unclear — add manually.` #### Dimension 7: Footprint Plausibility Check @@ -132,7 +146,7 @@ Does the REQ's `## Verification Steps` contain steps that violate the worker-exe | Category | Example indicator phrases | |---|---| -| **Human judgment** | "user confirms", "manually check", "looks correct", "[HUMAN]", "verify visually", "confirm the badge" | +| **Human judgment** | "user confirms", "manually check", "looks correct", "[HUMAN]", "confirm the badge looks right to you" — do **not** relocate automated Playwright screenshot `ui` steps (navigate + `ui-evidence` PNG + vision assert) | | **Physical device** | "on-device", "on the phone", "on iOS", "on Android", "on the watch" | | **Unprovisionable environment** | "in production", "requires login", "against the live API", "on-device build" | | **Explicit human-action phrasing** | "Ask the user to...", "Have someone...", "Check with the team..." | @@ -146,11 +160,11 @@ Does the REQ's `## Verification Steps` contain steps that violate the worker-exe **Auto-fix:** 1. Move the offending step out of `## Verification Steps` entirely. -2. Append it to `## Post-merge validation` as a checklist item: `- [ ] [original step text] — Observable outcome: [infer from step context or leave blank for manual fill]`. -3. Create `## Post-merge validation` if absent, using the section header and comment block from `agents/capture.md`'s REQ template. +2. Append it to `## Manual checks (advisory)` as a checklist item: `- [ ] [original step text] — Observable outcome: [infer from step context or leave blank for manual fill]`. +3. Create `## Manual checks (advisory)` if absent, using the section header and comment block from `agents/capture.md`'s REQ template. 4. Renumber any remaining `## Verification Steps` entries so numbering stays contiguous. -Report each fix in the audit change report as: `[FIXED] REQ-NNN step N — non-executable step (category: , indicator: "") moved to ## Post-merge validation`. +Report each fix in the audit change report as: `[FIXED] REQ-NNN step N — non-executable step (category: , indicator: "") moved to ## Manual checks (advisory)`. ### 4. Apply fixes @@ -161,7 +175,7 @@ For each REQ, apply auto-fixes inline: - Add dependency annotations (Dimension 4) - Add missing `ui` verification step when unambiguously inferrable (Dimension 6) - Append missing footprint paths to `**Files:**` (Dimension 7) -- Move non-executable verification steps to `## Post-merge validation` (Dimension 8) +- Move non-executable verification steps to `## Manual checks (advisory)` (Dimension 8) - Apply blanket find-and-replace guard augmentations when triggered (see below) #### Blanket find-and-replace guard (mirror of capture.md Step 4d) @@ -207,7 +221,7 @@ Audit Report — UR-NNN - N error paths added - N dependency annotations added - N footprint paths appended to `**Files:**` -- N non-executable verification steps moved to `## Post-merge validation` +- N non-executable verification steps moved to `## Manual checks (advisory)` - N flags requiring user judgment - Overall: [clean / minor fixes applied / needs attention] ``` diff --git a/agents/capture.md b/agents/capture.md index b7c4443..e85385c 100644 --- a/agents/capture.md +++ b/agents/capture.md @@ -17,11 +17,12 @@ The following steps require model judgment that cannot be reduced to a rule. Eac ## When Invoked -You will be given a path to a user-request folder, e.g.: +You will be given a UR reference: -``` -{project}/.do-work/user-requests/UR-001/ -``` +| Backend | Invocation | +|---------|------------| +| **markdown** | Path to a user-request folder, e.g. `{project}/.do-work/user-requests/UR-001/` | +| **linear** | UR slug (e.g. `UR-001`) and/or UR Project Milestone id — **no** local folder required | --- @@ -31,29 +32,87 @@ You will be given a path to a user-request folder, e.g.: Read and follow the **Load Config** section of [config.md](config.md). +### 0a. Tracker load path + +Work-item storage (URs, REQs, decisions, verify/close reports, run notes) goes **only** through named tracker port ops after config is loaded: + +1. Resolve effective `tracker.backend` (missing/empty/whitespace → `markdown`). +2. Read `agents/tracker/port.md` (shared op catalog + rules). +3. Read `agents/tracker/.md` (e.g. `markdown.md` or `linear.md`). +4. For work-item storage, call **only** named port ops from that backend file — never raw `.do-work/REQ-*` paths or raw Linear tools outside the backend doc. + +**Hard rules:** +- **No silent fallback** from `linear` to `markdown`. If backend is `linear`, do not substitute UR/REQ markdown as the store. +- If backend resolves to **`linear`** but `agents/tracker/linear.md` is **missing or unreadable**, **hard-stop** with setup instructions (restore the Linear backend doc / connect Linear skill). Never fall through to markdown paths. +- Markdown backend: ops map — **invoke** coordination scripts as `bash {skill-root}/lib/...` after Load Config step 8 resolves `$SKILL_ROOT`; **catalog identity** remains `lib/*.sh` in `markdown.md` — use those ops; do not re-implement store details here. + +### Capture REQ store — backend branch (ORI-9) + +| Concern | Markdown | Linear | +|---------|----------|--------| +| Brief / ideate / clarifications | `input.md`, optional `ideate.md` | **`read_ur`** (UR Project Milestone §9.1: Brief, Ideate, Clarifications) | +| Create REQs | Write `{project}/.do-work/REQ-NNN-*.md` | Port op **`create_req`** only — Issues on **product Project** + **UR milestone**; Linear issue ids only (e.g. `ENG-123`). **No** local `REQ-*.md` as store. | +| List REQs for this UR | Glob backlog/working/archive | Port op **`list_reqs_for_ur`** (product Project + UR milestone filter) — same op verify uses later | +| Capture summary / status | `input.md` body + frontmatter | Update UR milestone description sections/status fields (never overwrite `## Brief` verbatim intake); no local `input.md` dual-write | +| Hard-stop | n/a | MCP / create-issue tools missing → hard-stop; never write local backlog REQs as substitute | + +**When effective backend is `linear`:** use **`create_req`** exclusively for REQ persistence; after create, optionally **`list_reqs_for_ur`** to verify Issues landed. Do **not** dual-write under `.do-work/REQ-*` or `user-requests/`. + +### Decisions / calibration — backend branch (REQ-296 / REQ-297) + +Standing decisions and capture calibration are **work-item memory**, not runtime locks. Homes are fixed by design §10 / the active backend file — never invent alternate paths or Doc titles. + +| Concern | Markdown (`markdown.md`) | Linear (`linear.md`) | +|---------|--------------------------|----------------------| +| Read decisions | `{project}/.do-work/decisions.md` if present | **Read decisions** helper — Team Doc `tracker.linear.decisions_doc_title` (default `do-work/decisions`); missing Doc → empty | +| Append decision | Append one line to `.do-work/decisions.md` (create if absent) | **`append_decision`** — append-only line on that Team Doc (create-if-missing). Same grammar: `YYYY-MM-DD \| UR/REQ ref \| decision \| rationale` | +| Read calibration | `{project}/.do-work/state/calibration.md` if present | **Read calibration Doc** — Team Doc `tracker.linear.calibration_doc_title` (default `do-work/calibration`); missing → continue without | + +**When effective backend is `linear`:** do **not** read or write local `.do-work/decisions.md` or `state/calibration.md` as the store. Use the sequences in `agents/tracker/linear.md` only. **When `markdown`:** keep the file paths in the steps below. + +**Hard-stop (Linear writes):** if `append_decision` Doc create/update fails (permission, size, MCP), hard-stop — do **not** invent Issue comments, alternate Doc titles, or a local `decisions.md` dual-write. + ### 1. Read the brief -Read `UR-NNN/input.md` in full. +**Brief / assets / ideate — backend branch (ORI-9):** -Read every file in `UR-NNN/assets/` if it exists. +| Backend | How to load | +|---------|-------------| +| **markdown** | Read `UR-NNN/input.md` in full. Read every file in `UR-NNN/assets/` if it exists. Read `UR-NNN/ideate.md` if it exists. | +| **linear** | Call **`read_ur`** for `UR-NNN`. Use `## Brief` (+ `## Clarifications` if present) as the brief. Use `## Ideate` if present as advisory ideate observations. Optional local assets only if the operator keeps them on disk — not a dual store. **Do not** require `user-requests/UR-NNN/input.md` or local `ideate.md`. | -Read `UR-NNN/ideate.md` if it exists. Keep ideate observations in context as advisory input for decomposition — they inform your work but are not requirements to blindly follow. If the file does not exist (e.g. the user ran `--no-ideate` or capture is running standalone), continue without it. +Keep ideate observations in context as advisory input for decomposition — they inform your work but are not requirements to blindly follow. If ideate is absent (e.g. `--no-ideate` or standalone capture), continue without it. -Read `{project}/.do-work/state/calibration.md` if it exists. Keep its guidance bullets in context as advisory calibration — they inform how you size REQs, scope `**Files:**`, and split acceptance criteria, but they never block decomposition and are not hard requirements. This parallel mirrors the ideate.md pattern above: both are advisory; the brief always wins; absence is silently ignored. If the file is absent (no `/do-work retro` has run yet, or the project is new), continue without it. +**Calibration (advisory):** +- **Markdown:** Read `{project}/.do-work/state/calibration.md` if it exists. +- **Linear:** Read the calibration Team Doc via linear.md **Read calibration Doc** (title `calibration_doc_title` / default `do-work/calibration`). -Read `{project}/.do-work/decisions.md` if it exists — the append-only cross-UR decisions memory (format and discipline in SKILL.md § Decisions Memory). Each line records a standing decision (`YYYY-MM-DD | UR/REQ ref | decision | rationale`). Hold these in context while decomposing: they are prior calls that should shape how you split and scope REQs so this UR does not contradict them (e.g. a recorded "validation lives server-side" decision tells you which layer a validation REQ belongs to). If the file is absent (no decision has been recorded yet), continue without it — never create it just to read it. +Keep guidance bullets in context as advisory calibration — they inform how you size REQs, scope `**Files:**`, and split acceptance criteria, but they never block decomposition and are not hard requirements. This parallel mirrors the ideate pattern above: both are advisory; the brief always wins; absence is silently ignored. If calibration is absent (no `/do-work retro` has run yet, or the project is new), continue without it — never create the store just to read it. + +**Decisions (constraints):** +- **Markdown:** Read `{project}/.do-work/decisions.md` if it exists. +- **Linear:** Load via linear.md **Read decisions** (Team Doc `decisions_doc_title` / default `do-work/decisions`). + +Each line records a standing decision (`YYYY-MM-DD | UR/REQ ref | decision | rationale`). Hold these in context while decomposing: they are prior calls that should shape how you split and scope REQs so this UR does not contradict them (e.g. a recorded "validation lives server-side" decision tells you which layer a validation REQ belongs to). If the store is empty/absent (no decision has been recorded yet), continue without it — never create it just to read it. ### 1b. Detect milestone mode -Inspect the brief (`UR-NNN/input.md`) for the milestone-mode trigger. Milestone mode is active if BOTH: +Inspect the brief for the milestone-mode trigger (**unchanged** across backends). Brief source: + +- **Markdown:** `UR-NNN/input.md` +- **Linear:** `read_ur` (Initiative `## Brief` / machine sections); trigger strings must appear in that brief text + +Milestone mode is active if BOTH: 1. The frontmatter or body contains the marker `source: /saas-thesis handoff`. 2. The body contains a `### Milestones` heading with at least one `#### M1` (or higher) subheading. -If both conditions are met, you are in **milestone mode**. Set a flag and continue. Otherwise behave exactly as the existing capture flow (skip to Step 2 unchanged). +If both conditions are met, you are in **milestone mode**. Set a flag and continue. Otherwise behave exactly as the existing capture flow (skip to Step 2 unchanged). Do **not** invent a Linear-only trigger. When in milestone mode: +#### Markdown backend + - **Ensure `.do-work/state/` exists.** Run `mkdir -p {project}/.do-work/state` defensively before any state write. Installs from before REQ-170 may not have created the directory. - Identify the **active milestone**. Read `{project}/.do-work/state/active-milestone.md` if it exists. If it does not exist, the active milestone is `M1`. - Decompose ONLY the active milestone, not the whole brief. @@ -72,17 +131,27 @@ When in milestone mode: Mark the active milestone as `captured` once REQ files are written. Other statuses: `pending` (not yet captured), `captured` (REQs written), `running` (run loop active), `deployed` (deploy gate passed). +#### Linear backend (REQ-298 path; REQ-299 ops) + +- **Trigger** is the same two bullets above — no Linear-only activation. +- Identify the **active milestone** via port op **`read_active_milestone`** (`agents/tracker/linear.md`) on Project `do-work/{UR-id}` (``). That op returns `active: null` when the Project description has **no milestone marker** — it **does not invent a milestone id**. If `active` is null (no cursor yet) **and** the brief trigger is true, capture policy uses `M1` as the first-decompose target only — then persists via **`set_active_milestone`**. +- Decompose ONLY the active milestone, not the whole brief. R-mapping is built against ONLY that milestone's user-value, deploy gate, and high-level REQs. +- Create Issues with **`create_req`** only in Project `do-work/{UR-id}`. Linear issue ids are the REQ identifiers (no `REQ-M1-NNN` filenames). +- On each Issue: set body `**Milestone:** M` and, when labels exist, label `M` (see linear.md Issue milestone markers). Prefer Project milestone entity when MCP supports it. +- After writing REQs for this milestone, call **`set_active_milestone`** with `active: M` and checklist status `captured` for that M (full checklist from the brief on first write). Do **not** write local `state/active-milestone.md` / `milestones.md` as the work-item store. +- Deploy-gate ownership remains local (`write_gate_state` / `gate-owner.md` — local-only even when cursor is remote) — capture does not claim the gate. + ### 2. Determine the next REQ number If **milestone mode** (from Step 1b): -- Scan for existing `REQ-M--*.md` files matching the active milestone in both backlog root and `archive/`. -- Find the highest number for this milestone. New REQ = highest + 1, zero-padded to 3 digits. -- If no REQs for this milestone exist yet, start at `REQ-M-001`. + +- **Markdown:** Scan for existing `REQ-M--*.md` files matching the active milestone in both backlog root and `archive/`. Find the highest number for this milestone. New REQ = highest + 1, zero-padded to 3 digits. If no REQs for this milestone exist yet, start at `REQ-M-001`. +- **Linear (REQ-299):** Call port op **`list_milestone_reqs`** for active `M` (status `any`). Linear allocates issue ids — do not invent `REQ-NNN` / `REQ-M1-NNN` names. Use the list only for sequence metadata / capture summary counts if needed. If **not in milestone mode**: -- Scan the backlog root and `archive/` for existing `REQ-NNN-*.md` files (no milestone prefix). -- Find the highest existing REQ number. Start from the next one. (Existing behavior — unchanged.) -- If no REQs exist yet, start at `REQ-001`. + +- **Markdown:** Scan the backlog root and `archive/` for existing `REQ-NNN-*.md` files (no milestone prefix). Find the highest existing REQ number. Start from the next one. (Existing behavior — unchanged.) If no REQs exist yet, start at `REQ-001`. +- **Linear:** Issues are created via **`create_req`**; Linear issue ids are allocated by Linear (no local number allocation). ### 2b. Classify the brief @@ -142,12 +211,15 @@ when they don't apply (e.g. internal CLI scripts). Hold `layers_in_scope` (the per-UR list) in context for downstream steps. -If `--no-layers` produced a deliberate per-UR opt-out (a `feature` brief proceeding with `layers_in_scope: []`), append one line to `{project}/.do-work/decisions.md` in the documented format (SKILL.md § Decisions Memory), creating the file if absent: +If `--no-layers` produced a deliberate per-UR opt-out (a `feature` brief proceeding with `layers_in_scope: []`), append one standing decision line (SKILL.md § Decisions Memory format): ``` YYYY-MM-DD | UR-NNN | layer-coverage checks skipped for this UR | --no-layers opt-out ``` +- **Markdown:** append to `{project}/.do-work/decisions.md` (create if absent). +- **Linear:** call port op **`append_decision`** (`agents/tracker/linear.md`) — Team Doc create-if-missing; do not also write local `decisions.md`. + This is a judgment-point choice that shapes the whole decomposition. Append-only; do not write this line when layers are in scope normally. ### 3. Decompose the brief @@ -178,7 +250,7 @@ If `ideate.md` was loaded in Step 1, use its observations as advisory context wh - Do not bundle unrelated concerns into a single REQ - If a task has a clear dependency chain, order the REQ numbers to reflect it (lower numbers first) - Each child REQ must address exactly one layer-specific behavior change or one internal component. If a REQ description contains the word "and" joining two unrelated outcomes, split it into two REQs. When in doubt, split. -- If you make a non-obvious split-vs-merge call that shapes the decomposition (e.g. deliberately keeping two related concerns in one REQ, or splitting where the brief implied one unit), append a one-line record to `{project}/.do-work/decisions.md` in the documented format (SKILL.md § Decisions Memory), creating the file if absent: `YYYY-MM-DD | UR-NNN | | `. Routine, obvious splits do not need a line — only choices a future capture might otherwise re-litigate. +- If you make a non-obvious split-vs-merge call that shapes the decomposition (e.g. deliberately keeping two related concerns in one REQ, or splitting where the brief implied one unit), append one decision line: `YYYY-MM-DD | UR-NNN | | `. **Markdown:** append to `.do-work/decisions.md` (create if absent). **Linear:** **`append_decision`** on the decisions Team Doc (no local dual-write). Routine, obvious splits do not need a line — only choices a future capture might otherwise re-litigate. - A path-unit REQ may be documentation/state only: it defines the path, owns closure semantics, and depends on its child layer REQs. ### 3b. Verify full coverage before writing @@ -252,11 +324,14 @@ If you discover a requirement that was missed, add a REQ for it before proceedin ### 4. Write REQ files -For each task, write a file to the backlog root: +**Persist REQs via backend branch (ORI-9):** -``` -{project}/.do-work/REQ-NNN-short-slug.md -``` +| Backend | How to create each REQ | +|---------|------------------------| +| **markdown** | Write a file to the backlog root: `{project}/.do-work/REQ-NNN-short-slug.md` | +| **linear** | Call port op **`create_req`** (`agents/tracker/linear.md`) for each planned task — Issue on **product Project**, **milestone** = parent UR Project Milestone, body §9.2, labels as available. Resulting id is the **Linear issue id only** (e.g. `ENG-123`). Path-unit parents first, then children with `parentId`. **Do not** write local `.do-work/REQ-*.md` as the store. After all creates (or on verify), **`list_reqs_for_ur`** may be used to confirm Issues for this UR. | + +Decomposition content (Task / Context / AC / Verification / Integration fields) is the same for both backends; only the store differs. Under Linear, `**UR:**` / `**Parent:**` / `**Depends on:**` use Linear issue ids and the UR slug; never invent parallel `REQ-NNN` allocation. **Every REQ must carry a `**Layer:**` field.** Set it from the R-number's tag (Step 3b). If multiple R-numbers map to the same REQ, they must all share the same tag — otherwise split the REQ. Bug-fix briefs (classification from Step 2b) write `**Layer:** none` on every REQ. @@ -309,9 +384,9 @@ Use this format exactly: 1. **[test|build|runtime|ui]** [exact command or action] - Expected: [what success looks like — be specific] -## Post-merge validation +## Manual checks (advisory) -> Optional. Human, device, or environment checks that cannot run in a worker's isolated worktree. Workers never execute this section; it is consumed after merge by `/do-work approve` and `/do-work close`. Each item states what to do and what observable outcome confirms it. +> Optional. Human, device, or environment checks that cannot run in a worker's isolated worktree. Workers never execute this section; it never blocks archive. The checklist is preserved in the archived REQ as an advisory record for humans, outside the validation gate, and is not surfaced by any command automatically. Each item states what to do and what observable outcome confirms it. > > Write this section on path-unit REQs (or the single REQ for legacy-style decompositions) only when the brief includes checks that require human judgment, a physical device, or an environment the worker cannot provision. @@ -363,29 +438,30 @@ Use the right type for the task: | `test` | Automated test coverage | `./vendor/bin/pest --filter=LeadStatusTest` | | `build` | App must compile cleanly | `npm run build` | | `runtime` | Call an endpoint or CLI and check output | `curl http://localhost:8000/api/leads` → expect 200 with `status: discarded` | -| `ui` | Visual check in a running browser | Navigate to `/leads`, take snapshot, confirm "Discarded" tab is visible | +| `ui` | Playwright **screenshot** visual check (mandatory for user-visible work) | Navigate to `/leads`, save screenshot to `.do-work/user-requests/UR-NNN/ui-evidence/REQ-NNN-step-1.png`, vision-assert "Discarded" tab is visible in the image | **Executability rule (HARD RULE — never write non-executable steps into `## Verification Steps`):** -Every verification step in `## Verification Steps` must be executable by a worker inside its isolated git worktree using only tools and runtimes the worker can start itself. A step is **non-executable** — and must therefore be placed in `## Post-merge validation` instead — if it falls into any of these four categories: +Every verification step in `## Verification Steps` must be executable by a worker inside its isolated git worktree using only tools and runtimes the worker can start itself. A step is **non-executable** — and must therefore be placed in `## Manual checks (advisory)` instead — if it falls into any of these four categories: | Category | Description | Example phrases to flag | |---|---|---| -| **Human judgment** | Requires a human to make a visual or contextual call | "user confirms", "manually check", "looks correct", "[HUMAN]", "verify visually", "confirm the badge" | +| **Human judgment** | Requires a human to make a taste/contextual call that no automated tool can settle | "user confirms", "manually check", "looks correct", "[HUMAN]", "confirm the badge looks right to you" — **not** an automated Playwright screenshot `ui` step (those stay in Verification Steps) | | **Physical device** | Requires a mobile phone, watch, hardware, IoT sensor, or other physical device | "on-device", "on the phone", "on iOS", "on Android", "on the watch" | | **Unprovisionable environment** | Requires external credentials, a live third-party sandbox, or a runtime the worker genuinely cannot start in the worktree (e.g. a native mobile app build, a production database, an external OAuth callback) | "in production", "requires login", "against the live API", "on-device build" | | **Explicit human-action phrasing** | The step wording is imperative toward a human, not a command | "Ask the user to...", "Have someone...", "Check with the team..." | -If a brief describes a check that falls into one of these categories, **do not write it into `## Verification Steps`**. Write it into `## Post-merge validation` instead. +If a brief describes a check that falls into one of these categories, **do not write it into `## Verification Steps`**. Write it into `## Manual checks (advisory)` instead. **Rules for writing verification steps:** - **Bug fixes:** Step 1 must reproduce the original bug path and confirm it no longer occurs. Do not skip this. - **User-visible acceptance criteria → `ui` step required.** If any acceptance criterion in the REQ describes user-visible behaviour, the REQ must include at least one `ui` verification step. Trigger on any of these concrete phrases in the criteria (checklist, not judgement call): `user sees`, `page shows`, `page renders`, `button is clickable`, `form displays`, `element is visible`, `message appears`, `toast appears`, `error appears`, `navigates to`, or any other phrase describing what a person sees or does on screen. If none of these phrases appear in the acceptance criteria, no `ui` step is required — this is the explicit "no phantom UI" escape for purely backend REQs (config keys, internal APIs with no caller, CLI-only changes). -- **UI changes:** Always include at least one `ui` step (navigate + snapshot + assert element present). This is the same rule as above, restated for REQs whose title/task is explicitly a UI change — both rules must hold. +- **UI changes:** Always include at least one `ui` step: **navigate + Playwright screenshot to `ui-evidence/` + vision-assert element/text visible in the image**. Accessibility/DOM snapshot alone is not enough. This is the same rule as above, restated for REQs whose title/task is explicitly a UI change — both rules must hold. - **API/backend changes:** Include a `runtime` step hitting the actual endpoint and checking the response. - **Pure refactors:** `test` steps only are sufficient if behaviour is unchanged. -- **New pages/components:** Include `build` + `ui` steps minimum. +- **New pages/components:** Include `build` + `ui` steps minimum (screenshot-backed `ui`). +- **Screenshot-backed `ui` steps are worker-executable** — write them in `## Verification Steps`, never in `## Manual checks (advisory)`. Do not use vague "verify visually" phrasing for automated checks; write a concrete navigate target, screenshot path under `.do-work/user-requests/UR-NNN/ui-evidence/`, and Expected outcome a vision pass can falsify (specific text/element/state). - Steps must be specific enough that a pass/fail verdict is unambiguous — "looks good" is not a valid expected outcome. - Steps must be ordered so a worker can record `step N of M`, `last_good_step`, and `failed_step` in the checkpoint log. - When a step crosses a boundary (for example API -> render, command -> file, input -> persistence), name that handoff in the Expected outcome so failures localize cleanly. @@ -415,7 +491,7 @@ After writing all REQ files, review each REQ's acceptance criteria for specifici After the criteria quality pass, scan each REQ's `## Verification Steps` for non-executable entries (see the executability rule in `### Writing effective Verification Steps` above). For each step that matches any of the four non-executable categories (human judgment, physical device, unprovisionable environment, explicit human-action phrasing): 1. Move the step out of `## Verification Steps` entirely. -2. Add it to the REQ's `## Post-merge validation` section as a checklist item (create the section if absent, following the template format). +2. Add it to the REQ's `## Manual checks (advisory)` section as a checklist item (create the section if absent, following the template format). 3. Renumber any remaining `## Verification Steps` entries so numbering stays contiguous. This corrects capture errors before they reach a worker. It is non-blocking and requires no user interaction. @@ -450,13 +526,13 @@ Options: **No path:** record the decision in working state. The actual frontmatter write happens later in Step 6b. For now, hold `layer_decisions[] = no` in context. -Also append the decision to the cross-UR decisions memory (this is a judgment-point choice that shapes the decomposition — the layer is being deliberately left out of this UR). Append one line to `{project}/.do-work/decisions.md` in the documented format (SKILL.md § Decisions Memory), creating the file if it does not yet exist: +Also append the decision to the cross-UR decisions memory (this is a judgment-point choice that shapes the decomposition — the layer is being deliberately left out of this UR). One line: ``` YYYY-MM-DD | UR-NNN | layer "" out of scope | user answered "No" at layer-coverage prompt ``` -Use today's date and the actual UR id and layer name. This is the only place capture creates the file — append-only, one line per "No" answer, never rewrite existing lines. +Use today's date and the actual UR id and layer name. **Markdown:** append to `{project}/.do-work/decisions.md` (create if absent) — append-only, one line per "No" answer, never rewrite existing lines. **Linear:** **`append_decision`** (Team Doc create-if-missing); never rewrite prior lines; no local `decisions.md` dual-write. **Loop:** after each layer is resolved (yes or no), continue to the next uncovered layer until none remain. @@ -530,7 +606,7 @@ Background about the rename... After all REQ files are written (Steps 4, 4b, 4c, 4d complete), validate that the `**Depends on:**` graph is acyclic. ```bash -bash lib/cycle-check.sh UR-NNN +bash {skill-root}/lib/cycle-check.sh UR-NNN ``` Replace `UR-NNN` with the actual UR identifier. The script scans all REQs matching that UR across backlog, working, and archive, builds the dep graph, and runs DFS cycle detection. @@ -543,7 +619,7 @@ Replace `UR-NNN` with the actual UR identifier. The script scans all REQs matchi 2. Build a fingerprint: `cap-cycle-UR-NNN` (replace UR-NNN with the actual id). 3. Call file-feedback to log the event: ```bash - bash lib/file-feedback.sh cap-cycle "cap-cycle-UR-NNN" \ + bash {skill-root}/lib/file-feedback.sh cap-cycle "cap-cycle-UR-NNN" \ '{"ur":"UR-NNN","cycle":"'"$cycle_path"'"}' \ "cap-cycle: circular dependency in UR-NNN" \ "Cycle detected during capture of UR-NNN: $cycle_path" @@ -609,7 +685,11 @@ For each qualifying REQ in scope, fill the `## Integration` block by answering t ### 6. Write capture summary to UR body -Prepend (or replace, on re-run — see Step 7 idempotency rules) a summary block to `input.md`'s body, immediately after the YAML frontmatter close (`---`) and before the `## Request` heading. +**Backend branch:** +- **Markdown:** Prepend (or replace, on re-run — see Step 7 idempotency rules) a summary block to `input.md`'s body, immediately after the YAML frontmatter close (`---`) and before the `## Request` heading. +- **Linear:** Write the same summary content onto the **UR Project Milestone** description (e.g. under `## Capture summary` or fenced `` … ``) via rediscovered milestone update tools — same surface as `append_ideate`. **Never** overwrite `## Brief`. **Never** dual-write local `input.md`. REQ rows use **Linear issue ids**. + +**Markdown path (detail):** Prepend (or replace, on re-run — see Step 7 idempotency rules) a summary block to `input.md`'s body, immediately after the YAML frontmatter close (`---`) and before the `## Request` heading. Format: @@ -653,7 +733,11 @@ On re-run: if both fence comments are present, replace everything from `" + heartbeat_max_age_seconds: null # null → use parallel.stale_threshold_seconds + decisions_doc_title: "do-work/decisions" + calibration_doc_title: "do-work/calibration" + verify: threshold: 90 # minimum confidence score (0-100) for go to auto-run without --force -notifications: - on_pending_validation: "" # shell command to run when a REQ parks as pending-validation. - # Placeholders: {req} = REQ id (e.g. REQ-234), {title} = first line - # of the REQ's Task section, {checks} = newline-joined outstanding - # Post-merge validation items. Empty = disabled (no execution, no overhead). - # Example (Telegram via curl): - # on_pending_validation: > - # curl -s -X POST https://api.telegram.org/bot$TELEGRAM_TOKEN/sendMessage - # -d chat_id=$TELEGRAM_CHAT_ID - # -d text="⏳ {req} pending validation: {checks}" - # Example (macOS notification): - # on_pending_validation: "osascript -e 'display notification \"{checks}\" with title \"do-work: {req} pending\"'" - # Subagent routing for the run orchestrator. Ordered list of {match, agent} # rules: the classifier scans each REQ top-to-bottom and dispatches the first # rule whose `match` fits; if none match it falls back to `general-purpose` @@ -147,17 +176,123 @@ routing: [] # agent: llm-app-engineer ``` -4. **Migrate missing keys to disk.** Compare the existing config.yml against the default template above. For each top-level section (`project`, `log`, `next_steps`, `feedback`, `parallel`, `review`, `acceptance`, `risk`, `security`, `model`, `cost`, `ledger`, `delivery`, `verify`, `notifications`, `routing`) and each key within those sections: +4. **Migrate missing keys to disk.** Compare the existing config.yml against the default template above. For each top-level section (`project`, `log`, `next_steps`, `feedback`, `parallel`, `review`, `acceptance`, `risk`, `security`, `model`, `cost`, `ledger`, `delivery`, `worktree`, `tracker`, `verify`, `routing`) and each key within those sections: - If a **top-level section is entirely missing** from the file (e.g. `next_steps:` does not appear), append the full section block — including all keys, default values, and inline comments — to the end of the file. - If a **top-level section exists but is missing individual keys** (e.g. `log:` exists but `batch_size` is absent), append the missing keys with their default values to that section. This applies to nested-map keys too — e.g. if `log:` exists but `log.max_chars` is absent, append it with its default map (`{x: 280, linkedin: 1300}`) and inline comment. - - **Never overwrite existing values.** If a key exists in the file, keep the user's value regardless of what the default says. For nested maps, treat presence of the parent key as "existing" — if `log.max_chars:` is present, do not overwrite any of its entries or add missing platform entries, even if the default template has more. + - **Never overwrite existing values.** If a key exists in the file, keep the user's value regardless of what the default says. For nested maps, treat presence of the parent key as "existing" — if `log.max_chars:` is present, do not overwrite any of its entries or add missing platform entries, even if the default template has more. In particular, never replace a non-empty `tracker.linear.product_project` (name or UUID) with the template empty default or with the skill name `do-work`. - If **no keys are missing**, do not write to the file. Skip this step silently. - If keys were added, report: `Config updated: added [list of added keys/sections]` -5. Keep the final merged values (file values + defaults for anything still missing) in context for subsequent steps. +4b. **Seed `project.name` from directory basename when empty (install / first load).** After create (step 3) or migrate (step 4): + + - Let `name` = current `project.name` (treat missing, null, empty, or whitespace-only as empty). + - If `name` is **non-empty** → leave it alone; do **not** overwrite. + - If `name` is **empty** → set `project.name` to the **git-root directory basename** (the basename of the detected project root from startup) and **write** that value to `{project}/.do-work/config.yml`. + - This runs on create and on every load where `project.name` is still blank (e.g. operator cleared it, or an older template left it empty). It never replaces a deliberate non-empty name. + +5. Keep the final merged values (file values + defaults for anything still missing, plus any seed from step 4b) in context for subsequent steps. + +6. **Resolve tracker backend (markdown-default).** After the merged config is in context, set the effective work-item backend: + + - If `tracker.backend` is **missing**, **null**, **empty**, or **whitespace-only** → effective backend = **`markdown`**. + - If `tracker.backend` is **`markdown`** (case-sensitive value as stored) → effective backend = **`markdown`**. + - If `tracker.backend` is **`linear`** → effective backend = **`linear`**. + - Otherwise → hard-stop with a config error naming the unknown backend; do not guess. + + When the effective backend is **`markdown`**: load `agents/tracker/port.md` then `agents/tracker/markdown.md` for work-item ops; **do not** require Linear MCP, credentials, or dual-write. Existing `lib/*.sh` + `.do-work/` behavior remains the implementation. `tracker.linear.*` keys may still be migrated onto disk as defaults, but they are **inert** while backend resolves to markdown (no Linear validation). + +7. **Validate Linear config when effective backend is `linear`.** Run these checks **before** any work-item op. On failure, **hard-stop** — never silent-fallback to markdown, never invent team ids or workflow states. + + | Check | Hard-fail when | Operator message must include | + |-------|----------------|-------------------------------| + | Team resolve | Neither `tracker.linear.team_id` nor `tracker.linear.team_key` resolves to a live team on Linear | How to set `team_id` / `team_key` in `.do-work/config.yml`; do not guess a team | + | Linear MCP tools | Linear skill/MCP tools are missing, unauthenticated, or undiscoverable | How to connect Linear (Linear skill setup / MCP auth) — not fabricated data | + | `status_map` states | Any mapped workflow state name is missing on the resolved team's workflow | List the missing do-work status → expected state name; instruct rename of the team state **or** override `tracker.linear.status_map.` to an existing state name | + + **status_map defaults** (design §7): `backlog → "Todo"`, `in_progress → "In Progress"`, `stopped → "Canceled"`, `done → "Done"`. Override per-team when the team's workflow uses different labels; missing states are never invented. + + **Heartbeat age:** when `tracker.linear.heartbeat_max_age_seconds` is `null` or missing, use `parallel.stale_threshold_seconds` (default `900`). + + **Interaction with other keys:** `ledger`, `parallel`, `delivery`, `review`, `layers` remain valid under Linear. Authoritative run/cost notes are Linear Issue comments via port op `append_run_note`. If `ledger.enabled: true`, the orchestrator may **also** append local `.do-work/runs/RUN-NNN.yml` for offline retro tooling — local runs are telemetry only, not a second work-item store. Retro prefers Linear run notes when `backend: linear`, falling back to local runs if comments are unavailable. + + When the effective backend is **`linear`**: load `agents/tracker/port.md` then `agents/tracker/linear.md` for work-item ops after the validations above pass. If `agents/tracker/linear.md` is **missing or unreadable**, **hard-stop** with setup instructions (restore the backend doc from the skill install / Linear skill setup) — **never** fall through to `markdown.md` or invent Linear tool sequences. + +8. **Resolve skill-root (`$SKILL_ROOT` / `{skill-root}`).** Once per agent turn, resolve the absolute path of the do-work skill install root — a directory that contains `lib/` **and** at least one skill marker (`SKILL.md` **or** `agents/`). **Token definition:** `{skill-root}` means this resolved absolute path for **all later steps in the same agent turn**. Keep `$SKILL_ROOT` in context; substitute it wherever docs or bash lines write `{skill-root}` (especially `{skill-root}/lib/...`). + + **Single home:** this step is the only place that defines the resolve recipe. `references/run-loop.md` §2a and the run-worker **Skill root** input are thin consumers — they must not invent a second folklore recipe. + + **Markers (valid skill install root):** a candidate directory is valid iff **both**: + 1. it contains a `lib/` directory, **and** + 2. it contains `SKILL.md` **or** an `agents/` directory (or both). + + Requiring a co-marker with `lib/` avoids monorepo false roots that happen to have a bare `lib/` higher in the tree. + + **Inherit rule:** If `$SKILL_ROOT` is already set in this agent turn's context (orchestrator-passed **Skill root**, or resolved earlier in the same turn) **and** that path is an absolute directory that satisfies the markers above, **keep it — do not re-resolve**. If missing, empty, non-absolute, or invalid (fails markers), clear it and run the walk-up recipe below. Nested agents inherit the entry-resolved root this way. + + **Recipe (walk-up from loaded instruction file — no env / hub / CWD fallback):** + + One-level `dirname/..` is **not** the algorithm — nested paths (e.g. `agents/tracker/linear.md`) and `SKILL.md` at the skill root need more than a single parent hop. + + ```bash + # Start at the directory of the loaded instruction file; walk parents until + # markers match (lib/ AND (SKILL.md OR agents/)). Hard-stop at filesystem + # root if none found. No env/hub/CWD fallback. + d="$(cd "$(dirname "")" && pwd)" + SKILL_ROOT="" + while true; do + if [ -d "$d/lib" ] && { [ -f "$d/SKILL.md" ] || [ -d "$d/agents" ]; }; then + SKILL_ROOT="$d" + break + fi + [ "$d" = "/" ] && break + d="$(dirname "$d")" + done + # non-empty $SKILL_ROOT required — else hard-stop (see below) + ``` + + **Examples** (all resolve to the skill install root): + + | Loaded file | Walk starts at | Resolves to | + |-------------|----------------|-------------| + | `{skill}/agents/run.md` | `…/agents` | `{skill}` (parent has `lib/` + markers) | + | `{skill}/agents/tracker/linear.md` | `…/agents/tracker` | `{skill}` (walk past `tracker` → `agents` → skill root; one-level `..` would wrongly stop at `agents/`) | + | `{skill}/references/run-loop.md` | `…/references` | `{skill}` | + | `{skill}/SKILL.md` | `{skill}` itself | `{skill}` (start directory already matches markers) | + + - Prefer the absolute path of the instruction file the agent is currently executing when walk-up is needed (e.g. `agents/run.md`, `agents/config.md`, `agents/run-worker.md`, `agents/tracker/linear.md`, a loaded `references/*.md`, or `SKILL.md`). + - Use the walk-up result as `$SKILL_ROOT` only when it satisfies the markers. + - When this project **is** the do-work skill itself, `$SKILL_ROOT` equals the project root and lib calls work directly. When the project is any other consumer repo, `$SKILL_ROOT` points at the skill clone where `lib/` actually lives — not at the consumer project root. + - Orchestrators that dispatch workers pass the same absolute `$SKILL_ROOT` as the worker **Skill root** input and substitute it into pasted `run-worker.md` instructions (see `agents/run-worker.md` When Invoked #5 and `references/run-loop.md` Step 2 dispatch). Workers and nested agents **inherit** that value when it still satisfies markers. + + **Hard-stop when the path cannot be determined.** If any of the following is true, **stop immediately** with a clear operator message — do not guess: + + - Inherit failed (missing/invalid `$SKILL_ROOT`) **and** the harness did not provide an absolute path for the loaded instruction file + - Walk-up from the loaded file reaches the filesystem root without finding a directory that satisfies the markers (`lib/` **and** (`SKILL.md` **or** `agents/`)) + - The path is empty or otherwise unknown after the recipe + + Operator message (example): + + `skill-root unknown: cannot resolve $SKILL_ROOT (walk-up from loaded file; no env/hub/CWD fallback). Provide an absolute path to the loaded instruction file under the skill install root, or a valid pre-resolved $SKILL_ROOT that contains lib/ and (SKILL.md or agents/).` + + **Do not** fall back to process CWD, hub paths (`~/.agents/skills/do-work`, `~/.claude/skills/do-work`), or invent a path from `DO_WORK_SKILL_ROOT` / other env vars when inherit markers fail. **Do** inherit a **valid** `$SKILL_ROOT` already set in this turn's context (see inherit rule above). + +9. **Resolve and bind `tracker.linear.product_project` when effective backend is `linear`.** Run after step 7 validations pass and the Linear backend docs are loaded — **before** any work-item CRUD that needs the product Project. When effective backend is **`markdown`**, skip this step entirely (`product_project` is inert). + + **Resolve order (name/lookup key only — does not rewrite an already-set value):** + + 1. Let `pp` = current `tracker.linear.product_project` (missing, null, or whitespace-only → treat as empty). + 2. If `pp` is **non-empty** (name **or** UUID) → **lookup key = `pp`**. Explicit config wins. Do **not** replace it with `project.name`, the git-root basename, or the skill name `do-work`. Existing product UUIDs (and explicit names) are left untouched by this fallback chain. + 3. If `pp` is **empty** → lookup key = `project.name` when that is non-empty; else the **git-root directory basename** (same basename as step 4b). Never fall back to a hard-coded skill name. + 4. Call port op **`ensure_product_container`** with that lookup key: resolve the Linear Project by name or UUID; **create-if-missing** when the key is a name and no Project matches. + 5. On success, **always persist** the resolved Project **UUID** back to `tracker.linear.product_project` in `{project}/.do-work/config.yml` and in the in-memory config. If the file already stores that same UUID, skip the write (idempotent). + 6. On failure (unresolved after create attempt, MCP missing, permission error) → **hard-stop** with operator instructions; never invent a product Project and never silent-fallback to markdown. + + **Rewrite rules:** The empty → `project.name` → basename chain runs **only** when `product_project` is truly empty. It must never overwrite an explicit existing value. The only write after a non-empty start is ensure's **UUID bind** (e.g. name → UUID once resolved). After a true empty state, ensure binds and step 5 persists the UUID so subsequent loads take the explicit-UUID path. + +**Phase-agent contract:** every phase agent that touches work items follows the **Tracker load path** (config → resolve `tracker.backend` → `port.md` → `agents/tracker/.md` → only named port ops). The shared load path is defined once here and in `agents/tracker/port.md`; each phase agent restates a short copy so a missing wire cannot cause split-brain storage. Every phase agent that invokes skill `lib/` scripts also depends on step 8 (`$SKILL_ROOT` / `{skill-root}`) from this same Load Config block. -**Never fail or stop because of a missing or incomplete config.** If config creation or migration fails for any reason, proceed with in-memory defaults. +**Never fail or stop because of a missing or incomplete config file** (steps 1–5, including 4b seed). If config creation or migration fails for any reason, proceed with in-memory defaults (including `tracker.backend: markdown`). **Exceptions (deliberate hard-stops, not config-file completeness problems):** step 7 Linear validation (and missing `linear.md`) and step 9 product_project ensure/bind when the operator has opted into `backend: linear`; step 8 skill-root resolve when walk-up (or inherit) cannot determine an absolute skill install root. --- @@ -165,7 +300,7 @@ routing: [] | Key | Type | Default | Description | |-----|------|---------|-------------| -| `project.name` | string | `""` | Project display name | +| `project.name` | string | `""` (seeded from git-root directory basename when empty on create/first load — Load Config step 4b; never overwrites a non-empty name) | Project display name. Also the preferred empty-`product_project` fallback when `backend: linear` (Load Config step 9). | | `layers` | list of strings | `[]` | Project's declared layers for gap-aware capture. Capture and verify check that REQs cover each declared layer. Empty = opt out (feature briefs will halt until declared or `--no-layers` is passed). | | `log.enabled` | boolean | `true` | Whether the log step runs after Go | | `log.platforms` | list | `[]` | Platforms to generate draft posts for (e.g. `[x, linkedin]`) | @@ -194,5 +329,25 @@ routing: [] | `delivery.mode` | string | `merge` | How `agents/run.md` Step 4 delivers a passing REQ. `merge` (default) reproduces today's behaviour byte-for-byte: merge `req/REQ-NNN` into the base branch locally, archive, tear down the worktree, delete the branch. `pr` replaces the local merge with a GitHub PR: push the branch, open a PR via `gh pr create`, record the PR URL in the archived REQ's `## Outputs` and the ledger entry, archive, tear down the worktree — but leave the branch alive (the PR owns it). `pr` mode requires a configured git remote and the `gh` CLI; if either is missing the run stops with a `missing-creds` stopper and the REQ stays in `working/` — it **never** silently falls back to `merge`. Consumers: `agents/run.md`. | | `delivery.pr.granularity` | string | `req` | Only consulted when `delivery.mode` is `pr`. `req` (default) opens one PR per REQ directly off `req/REQ-NNN`. `ur` accumulates each completed REQ branch onto a shared `ur/UR-NNN` integration branch and opens a single PR when that UR's backlog drains, so a whole UR ships as one reviewable PR. Consumers: `agents/run.md`. | | `verify.threshold` | integer | `90` | Minimum confidence score (0-100) that `agents/go.md` requires before auto-running without `--force`. Consumers: `agents/verify.md`, `agents/go.md`. | -| `notifications.on_pending_validation` | string | `""` | Shell command template executed once when a REQ parks as `pending-validation`. Supports three placeholders: `{req}` (REQ id, e.g. `REQ-234`), `{title}` (first line of the REQ's Task section), `{checks}` (newline-joined outstanding `## Post-merge validation` items). Placeholders are substituted before execution. Empty or absent = disabled — no command runs and no warning prints. A non-zero exit or missing binary produces at most a one-line warning and never stops the loop. Typical uses: Telegram ping via `curl`, macOS `osascript` alert, webhook `curl`. Consumers: `agents/run.md` Step 4 park sequence. | | `routing` | list of `{match, agent}` maps | `[]` | Ordered subagent-routing rules for the run orchestrator's REQ classification. Each entry is `{match: , agent: }`. The classifier scans rules top-to-bottom (first match wins) and dispatches the matching `agent`; if no rule matches — or the list is empty — it falls back to `general-purpose` silently. Ships empty so the stock skill is portable (no machine-specific agents). A commented example block in the template above reproduces the original specialist table for users who want to restore it. Consumers: `agents/run.md`, `agents/resume.md`. | +| `worktree.link_paths` | list of strings | `[]` | Extra dependency directories to symlink from the main checkout into each worker worktree (e.g. `[server/vendor, web/node_modules]`). Additive to auto-detected dirs: `composer.json` → `vendor`, `package.json` → `node_modules`, `pyproject.toml` / `requirements.txt` → `.venv`. Use this for monorepo or subdir layouts where the auto-detection misses a directory. Consumers: `lib/provision-worktree.sh`. | +| `worktree.setup_command` | string | `""` | Optional fallback command run inside the worktree when a dependency directory is absent from the main checkout and cannot be symlinked (e.g. `"composer install --no-interaction"`). The provisioner tries symlinking first (symlink-first semantics); this command runs only when a required dir is missing and symlinking fails. Empty = no fallback (the worktree is used as-is). Consumers: `lib/provision-worktree.sh`, `agents/run-worker.md`. | +| `tracker.backend` | string | `"markdown"` | Work-item store backend: `markdown` (default — local `.do-work/` + `lib/*.sh`) or `linear` (Linear as sole work-item store). **Unset, empty, or missing key resolves to `markdown`** — no hard-stop, no Linear tools required. No dual-write between backends. When `linear`, Load Config step 7 hard-fails if team cannot be resolved, Linear MCP tools are undiscoverable, or any `status_map` state is missing on the team. Consumers: all phase agents that touch URs/REQs via `agents/tracker/port.md` + `agents/tracker/.md`. | +| `tracker.linear.team_id` | string | `""` | Linear team UUID. **Required when `backend: linear`** unless `team_key` alone resolves the team. Empty + unresolvable team_key → hard-fail (do not guess). Consumers: `agents/tracker/linear.md`, Load Config step 7. | +| `tracker.linear.team_key` | string | `""` | Optional alternate team resolve (Linear team key string). Used when `team_id` is empty. Consumers: `agents/tracker/linear.md`, Load Config step 7. | +| `tracker.linear.default_assignee_id` | string | `""` | Human operator Linear user id set as issue **assignee** on create when non-empty. Agents claim via workflow status + claim comments — they do not steal assignee. Consumers: `agents/tracker/linear.md` create/claim ops. | +| `tracker.linear.product_project` | string | `""` | Shared Linear Project (**name or UUID**) that holds all UR Project Milestones + Issues — **not** one Project per UR. **Default is empty**, not the skill name `do-work`. When `backend: linear`, Load Config step 9 resolve order: (1) explicit non-empty `product_project` (name\|UUID) wins and is never replaced by the empty-fallback chain; (2) if empty/missing → `project.name`; (3) if that empty → git-root directory basename; (4) `ensure_product_container` create-if-missing; (5) **always persist** the resolved Project **UUID** back to this key. Explicit existing values (including a bound UUID) are left alone by the fallback chain; only ensure's UUID bind may update the field after a true empty (or name→UUID bind). Consumers: `agents/tracker/linear.md`, `ensure_product_container`, intake/`create_ur`. | +| `tracker.linear.ur_milestone_name_pattern` | string | `"{ur_id}: {title}"` | Human-facing Project Milestone name pattern for each UR. `{ur_id}` is the sequential UR slug; `{title}` is the brief title. Consumers: `agents/tracker/linear.md` create_ur / list_urs. | +| `tracker.linear.project_name_pattern` | string | `"do-work/{ur_id}"` | **Deprecated** pattern for per-UR Linear Project name (ignored for UR home; URs are Project Milestones on `product_project`). Kept for migrate compatibility. Consumers: legacy notes only. | +| `tracker.linear.initiative_title_pattern` | string | `"{ur_id}: {title}"` | Pattern for Initiative title. `{title}` is the human-facing brief title. Consumers: `agents/tracker/linear.md` intake. | +| `tracker.linear.status_map.backlog` | string | `"Todo"` | Team workflow state name for unclaimed/backlog REQs. **Hard-fail** if this state is missing on the team when `backend: linear` — rename the team state or override this key. Consumers: claim/list/status ops in `agents/tracker/linear.md`. | +| `tracker.linear.status_map.in_progress` | string | `"In Progress"` | Team workflow state for claimed/in-progress REQs. Same missing-state hard-fail as other status_map keys. Consumers: claim/heartbeat/resume. | +| `tracker.linear.status_map.stopped` | string | `"Canceled"` | Team workflow state for stopped REQs. Override if the team has a dedicated Stopped state. Same missing-state hard-fail. Consumers: set_req_status, resume. | +| `tracker.linear.status_map.done` | string | `"Done"` | Team workflow state for archived/done REQs. Same missing-state hard-fail. Consumers: archive_req. | +| `tracker.linear.labels.layer_prefix` | string | `"Layer/"` | Prefix for layer labels (e.g. `Layer/agents`). Consumers: create_req / update_req on Linear. | +| `tracker.linear.labels.path_unit` | string | `"path-unit"` | Label applied to path-unit parent Issues. Consumers: create_req. | +| `tracker.linear.labels.size_prefix` | string | `"Size/"` | Prefix for size labels (e.g. `Size/M`). Consumers: create_req. | +| `tracker.linear.agent_claim_marker` | string | `""` | HTML comment marker at the start of agent claim-protocol comments. Consumers: claim_req, heartbeat_req, unblock_req, list_claimable_reqs. | +| `tracker.linear.heartbeat_max_age_seconds` | integer or null | `null` | Max age of latest active claim heartbeat before the claim is stale. **`null` → use `parallel.stale_threshold_seconds`** (default `900`). Consumers: claim eligibility, scan-stale equivalent on Linear. | +| `tracker.linear.decisions_doc_title` | string | `"do-work/decisions"` | Team Doc title for standing decisions (create-if-missing). Consumers: append_decision, capture/ideate readers. | +| `tracker.linear.calibration_doc_title` | string | `"do-work/calibration"` | Team Doc title for calibration body. Consumers: retro write; capture read. | diff --git a/agents/go.md b/agents/go.md index a6999a4..ec42e0d 100644 --- a/agents/go.md +++ b/agents/go.md @@ -25,6 +25,20 @@ You will be given: Read and follow the **Load Config** section of [config.md](config.md). Keep the loaded config in context — sub-agents will load config independently but the orchestrator needs it for the conditional log step. +### 0a. Tracker load path + +Work-item storage (URs, REQs, decisions, verify/close reports, run notes) goes **only** through named tracker port ops after config is loaded: + +1. Resolve effective `tracker.backend` (missing/empty/whitespace → `markdown`). +2. Read `agents/tracker/port.md` (shared op catalog + rules). +3. Read `agents/tracker/.md` (e.g. `markdown.md` or `linear.md`). +4. For work-item storage, call **only** named port ops from that backend file — never raw `.do-work/REQ-*` paths or raw Linear tools outside the backend doc. + +**Hard rules:** +- **No silent fallback** from `linear` to `markdown`. If backend is `linear`, do not substitute UR/REQ markdown as the store. +- If backend resolves to **`linear`** but `agents/tracker/linear.md` is **missing or unreadable**, **hard-stop** with setup instructions (restore the Linear backend doc / connect Linear skill). Never fall through to markdown paths. +- Markdown backend: ops map — **invoke** coordination scripts as `bash {skill-root}/lib/...` after Load Config step 8 resolves `$SKILL_ROOT`; **catalog identity** remains `lib/*.sh` in `markdown.md` — use those ops; do not re-implement store details here. + ### 0b. Validate UR exists Before delegating to any sub-agent, confirm the UR directory exists: @@ -147,7 +161,7 @@ If `config.next_steps.enabled` is `true`: The go agent is a top-level orchestrator — it is never a delegate, so no suppression logic is needed. Sub-agents (verify, run, log) must suppress their own AskUserQuestion prompts when running inside go. -If `config.next_steps.enabled` is `false` or missing: skip the AskUserQuestion and stop. +If `config.next_steps.enabled` is `false` or missing: output `Next step: /do-work status` (or `/do-work start "…" for new work) and stop. --- diff --git a/agents/help.md b/agents/help.md index ead3a0b..b4d5cd2 100644 --- a/agents/help.md +++ b/agents/help.md @@ -16,6 +16,20 @@ You are called after the Quick Reference table has already been printed. Your jo Read and follow the **Load Config** section of [config.md](config.md). +### 0a. Tracker load path + +Work-item storage (URs, REQs, decisions, verify/close reports, run notes) goes **only** through named tracker port ops after config is loaded: + +1. Resolve effective `tracker.backend` (missing/empty/whitespace → `markdown`). +2. Read `agents/tracker/port.md` (shared op catalog + rules). +3. Read `agents/tracker/.md` (e.g. `markdown.md` or `linear.md`). +4. For work-item storage, call **only** named port ops from that backend file — never raw `.do-work/REQ-*` paths or raw Linear tools outside the backend doc. + +**Hard rules:** +- **No silent fallback** from `linear` to `markdown`. If backend is `linear`, do not substitute UR/REQ markdown as the store. +- If backend resolves to **`linear`** but `agents/tracker/linear.md` is **missing or unreadable**, **hard-stop** with setup instructions (restore the Linear backend doc / connect Linear skill). Never fall through to markdown paths. +- Markdown backend: ops map — **invoke** coordination scripts as `bash {skill-root}/lib/...` after Load Config step 8 resolves `$SKILL_ROOT`; **catalog identity** remains `lib/*.sh` in `markdown.md` — use those ops; do not re-implement store details here. + ### 1. Detect project state Check the following conditions in order: @@ -27,7 +41,6 @@ Check the following conditions in order: 5. Are there REQ files in `{project}/.do-work/archive/`? 6. Are there `RUN-NNN.yml` files in `{project}/.do-work/runs/`? (Retro heuristic: runs exist but no `calibration.md` → suggest retro.) 7. Do any archived REQs have a non-empty `**Entry point:**` field (path-unit REQs) for a given UR, and does that UR lack a `closure.md` in `{project}/.do-work/user-requests/UR-NNN/`? (Close heuristic: run has drained for a UR with path-units but no closure report yet.) -8. Are there `REQ-NNN-*.md` files in `{project}/.do-work/pending/`? (Approve heuristic: REQs merged and awaiting human validation.) ### 2. Print contextual suggestions @@ -75,14 +88,34 @@ path-unit backlog detected: top-level path REQs define reachable flows; child RE **If URs exist but backlog is empty:** +Do **not** treat every empty-backlog project the same. Distinguish URs that still need decomposition from a drained project whose REQs already live in `archive/` (or Linear done): + +1. Find the **most recent** `UR-NNN` under `user-requests/` (highest N). +2. Check whether **any** REQ for that UR exists in backlog, `working/`, or `archive/` (markdown: `**UR:** UR-NNN` on REQ files; Linear: `list_reqs_for_ur`). +3. Also scan older open URs for any with **zero** REQs anywhere — those still need capture. + +**A — Latest UR has no REQs yet (or any open UR has zero REQs):** + +``` +Suggested next steps: + /do-work capture UR-NNN — Decompose the request into tasks + /do-work go UR-NNN — Verify and run after capture + /do-work start "describe your feature or task" — Record a new brief instead +``` + +Prefer the **oldest** zero-REQ open UR for the capture line when more than one exists; otherwise use the latest UR. Replace `UR-NNN` with that real number. + +**B — Backlog empty and all open URs already have REQs (drained / archive-only):** + ``` Suggested next steps: - /do-work capture UR-NNN — Decompose the latest request into tasks - /do-work go UR-NNN — Verify and run for a specific request /do-work start "describe your feature or task" — Record a new brief + /do-work status — Review the situation room ``` -Replace `UR-NNN` with the most recent UR number. +Do **not** suggest `capture` for a UR that already has REQs in archive — that re-decomposes finished work and confuses operators. + +Then (when the 4-suggestion cap allows) still add retro / close from the heuristics below. **If `runs/` has entries but `.do-work/state/calibration.md` does not exist:** @@ -94,20 +127,12 @@ Suggest retro alongside other applicable suggestions (do not replace them — ad **If archived path-unit REQs exist for a UR but that UR has no `closure.md`:** -Suggest close alongside other applicable suggestions (add it when this condition is true and the 4-suggestion cap allows). Use the most recently completed UR: +Suggest close alongside other applicable suggestions (add it when this condition is true and the 4-suggestion cap allows). Use the most recently completed UR that still needs closure: ``` /do-work close UR-NNN — Walk path-unit entry points end-to-end and write the UR closure report ``` -**If REQs exist in `pending/` (awaiting human validation):** - -Suggest approve alongside other applicable suggestions (add it when this condition is true and the 4-suggestion cap allows). Replace `N` with the actual count: - -``` - /do-work approve REQ-NNN — N REQ(s) awaiting human validation — confirm and archive -``` - **If do-work exists but is empty (no URs, no REQs):** ``` diff --git a/agents/ideate.md b/agents/ideate.md index 5e0ca62..e14f3e8 100644 --- a/agents/ideate.md +++ b/agents/ideate.md @@ -8,11 +8,12 @@ You are powered by the Creativity Engine's three most relevant modes: Explorer, ## When Invoked -You will be given a path to a user-request folder, e.g.: +You will be given a UR reference: -``` -{project}/.do-work/user-requests/UR-001/ -``` +| Backend | Invocation | +|---------|------------| +| **markdown** | Path to a user-request folder, e.g. `{project}/.do-work/user-requests/UR-001/` | +| **linear** | UR slug (e.g. `UR-001`) and/or Linear **UR Project Milestone** id — **not** a required local folder | You may also be invoked by the Start agent as part of the default pipeline (ideate runs unless `--no-ideate` is passed). @@ -24,17 +25,49 @@ You may also be invoked by the Start agent as part of the default pipeline (idea Read and follow the **Load Config** section of [config.md](config.md). +### 0a. Tracker load path + +Work-item storage (URs, REQs, decisions, verify/close reports, run notes) goes **only** through named tracker port ops after config is loaded: + +1. Resolve effective `tracker.backend` (missing/empty/whitespace → `markdown`). +2. Read `agents/tracker/port.md` (shared op catalog + rules). +3. Read `agents/tracker/.md` (e.g. `markdown.md` or `linear.md`). +4. For work-item storage, call **only** named port ops from that backend file — never raw `.do-work/REQ-*` paths or raw Linear tools outside the backend doc. + +**Hard rules:** +- **No silent fallback** from `linear` to `markdown`. If backend is `linear`, do not substitute UR/REQ markdown as the store. +- If backend resolves to **`linear`** but `agents/tracker/linear.md` is **missing or unreadable**, **hard-stop** with setup instructions (restore the Linear backend doc / connect Linear skill). Never fall through to markdown paths. +- Markdown backend: ops map — **invoke** coordination scripts as `bash {skill-root}/lib/...` after Load Config step 8 resolves `$SKILL_ROOT`; **catalog identity** remains `lib/*.sh` in `markdown.md` — use those ops; do not re-implement store details here. + +### Ideate store — backend branch (ORI-9) + +| Concern | Markdown | Linear (`linear.md`) | +|---------|----------|----------------------| +| Load brief | `UR-NNN/input.md` (+ optional `assets/`) | Port op **`read_ur`** — UR **Project Milestone** §9.1 (`## Brief` + clarifications if any). No local `input.md` required. | +| Persist observations | Write `{project}/.do-work/user-requests/UR-NNN/ideate.md` | Port op **`append_ideate`** — write/append under `## Ideate` on the **UR milestone** description. **No** local `ideate.md` as the work-item store. | +| Open gaps (Continue gate) | `open_gaps:` in `input.md` frontmatter (or `## Notes — Open Gaps`) | Append / replace machine-readable open-gaps on the **UR milestone** body (prefer a fenced block or `open_gaps:` under a `## Notes` / frontmatter-equivalent section that does **not** overwrite `## Brief`). Use `read_ur` then milestone update sequence from linear.md (`save_milestone` / discovered update via the same surface as `append_ideate`). | +| Hard-stop | n/a for local files | If Linear MCP / milestone tools unusable → **hard-stop**. Never write local `user-requests/.../ideate.md` as substitute. | + +**When effective backend is `linear`:** Linear is the sole store. Do **not** dual-write `user-requests/UR-NNN/ideate.md` or require that path to exist. **When `markdown`:** keep the local paths in the steps below. + ### 1. Read the brief -Read `UR-NNN/input.md` in full. +**Markdown:** Read `UR-NNN/input.md` in full. Read every file in `UR-NNN/assets/` if it exists. -Read every file in `UR-NNN/assets/` if it exists. +**Linear:** Call port op **`read_ur`** for `UR-NNN` (resolve product Project + UR Project Milestone per `agents/tracker/linear.md`). Use `## Brief` (and `## Clarifications` if present) as the brief. Optional local assets only if the operator keeps them on disk — not a dual work-item store. If the UR milestone is missing → error to caller (do not invent a local UR folder). ### 2. Read relevant project context Scan the project folder for existing code, REQs in the archive, and any documentation that gives you context on what already exists. -Read `{project}/.do-work/decisions.md` if it exists — the append-only cross-UR decisions memory (format in SKILL.md § Decisions Memory). Each line is a standing decision from a prior UR. Use these to ground Connector observations (reuse, overlap) and to flag when the brief contradicts a recorded decision. If the file is absent (no decision recorded yet), continue without it — never create it. +**Decisions (constraints — backend branch, REQ-297):** + +| Backend | How to load standing decisions | +|---------|--------------------------------| +| **markdown** | Read `{project}/.do-work/decisions.md` if it exists | +| **linear** | **Read decisions** helper in `agents/tracker/linear.md` — Team Doc `tracker.linear.decisions_doc_title` (default `do-work/decisions`); missing Doc → empty. Do **not** read local `decisions.md` as the store | + +Each line uses the same one-line grammar as SKILL.md § Decisions Memory: `YYYY-MM-DD | UR/REQ ref | decision | rationale`. Lines are standing decisions from prior work. Use them to ground Connector observations (reuse, overlap) and to flag when the brief contradicts a recorded decision. If the store is empty/absent (no decision recorded yet), continue without it — never create it on the read path. Read at most 10 files (excluding node_modules, vendor, and build artifacts). Stop scanning after you have enough context to ground your observations — do not audit the whole codebase. @@ -68,13 +101,7 @@ Find links to existing work and patterns. ### 4. Write the review -Write observations to: - -``` -{project}/.do-work/user-requests/UR-NNN/ideate.md -``` - -Use this format exactly: +Use this format exactly for the ideate body (Explorer / Challenger / Connector / Summary): ```markdown # Ideate — UR-NNN @@ -101,6 +128,13 @@ Use this format exactly: [2-3 sentences: the most important things to consider before decomposing this brief into tasks.] ``` +**Persist via backend branch (ORI-9) — Linear first when `backend: linear`:** + +| Backend | How to store | +|---------|--------------| +| **linear** | Call port op **`append_ideate`** (`agents/tracker/linear.md`) with the body above. Target is the **UR Project Milestone** `## Ideate` section. Prefer section append; never overwrite `## Brief`. **Do not** write `{project}/.do-work/user-requests/UR-NNN/ideate.md`. If MCP/update tools fail → hard-stop. | +| **markdown** | Write observations to `{project}/.do-work/user-requests/UR-NNN/ideate.md` (create/overwrite that file as today). | + ### 5. Report and prompt — interactive gate Output the completion report: @@ -108,7 +142,7 @@ Output the completion report: ``` Ideate complete for UR-NNN. -Written: {project}/.do-work/user-requests/UR-NNN/ideate.md +Written: Gaps surfaced: - [gap 1, one line] @@ -116,7 +150,7 @@ Gaps surfaced: - ... ``` -Compile the gaps from the Explorer "Assumptions & Perspectives" and Challenger "Risks & Edge Cases" sections of the just-written ideate.md — pick the top 3-5, one line each. +Compile the gaps from the Explorer "Assumptions & Perspectives" and Challenger "Risks & Edge Cases" sections of the just-written ideate body — pick the top 3-5, one line each. **Then, regardless of `config.next_steps.enabled` setting, present the gate via `AskUserQuestion`:** @@ -125,7 +159,7 @@ Question: `How would you like to proceed?` Options: 1. **"Grill me"** — Run interactive Q&A on the surfaced gaps before capture 2. **"Continue"** — Proceed to capture as-is, gaps recorded -3. **"Stop"** — Halt — let me revise input.md, then re-run +3. **"Stop"** — Halt — let me revise the brief, then re-run (`AskUserQuestion` exposes 3 options; this fits within the 4-option limit.) @@ -134,8 +168,14 @@ Options: **Behaviour per option:** - **(1) Grill me:** Read and follow [question.md](question.md) in full, scoped to the gaps just listed. After question.md returns, control flows back here — automatically continue to capture (do not re-show this gate). -- **(2) Continue:** Write the surfaced gaps to the UR's `input.md` YAML frontmatter under an `open_gaps:` list (one item per gap, verbatim). If `open_gaps:` already exists in the frontmatter (rare — the user re-ran ideate), overwrite it. Then return control to the caller (start.md) so it proceeds to capture. If `input.md` has no frontmatter (legacy UR — should not happen for new URs but guard anyway), append the gap list to the body under a `## Notes — Open Gaps` heading instead. -- **(3) Stop:** Output `Halted by user — revise {project}/.do-work/user-requests/UR-NNN/input.md and re-run`. Return control to the caller; the caller must NOT proceed to capture. +- **(2) Continue:** Record the surfaced gaps under `open_gaps:` (one item per gap, verbatim). If `open_gaps:` already exists (rare — the user re-ran ideate), overwrite it. + - **Markdown:** write to the UR's `input.md` YAML frontmatter. If `input.md` has no frontmatter (legacy UR), append under `## Notes — Open Gaps` instead. + - **Linear:** update the **UR Project Milestone** description (same rediscovery surface as `append_ideate` / `read_ur`) — do **not** require or write local `input.md`. Never overwrite `## Brief`. + - Then return control to the caller (start.md) so it proceeds to capture. +- **(3) Stop:** + - **Markdown:** Output `Halted by user — revise {project}/.do-work/user-requests/UR-NNN/input.md and re-run`. + - **Linear:** Output `Halted by user — revise UR-NNN brief on Linear (UR milestone) and re-run`. + - Return control to the caller; the caller must NOT proceed to capture. Users pick Grill at this gate when they want interactive Q&A, after seeing the actual gaps. The gate runs whether or not `next_steps.enabled` is true — this is a workflow gate, not a next-step suggestion. @@ -143,9 +183,11 @@ Users pick Grill at this gate when they want interactive Q&A, after seeing the a ## Rules -- Never modify `input.md` — your output goes to a separate file +- **Markdown:** never modify the brief text in `input.md` as ideate output — observations go to `ideate.md` (open_gaps frontmatter is the Continue-gate exception). +- **Linear:** never write local `user-requests/.../ideate.md` as the store; use **`append_ideate`** only. Never overwrite `## Brief`. - Every observation must include: (1) what the risk or assumption is, (2) a concrete scenario where it would cause a problem, and (3) which part of the brief triggers the concern. Observations missing any of these three elements must be rewritten before saving. - Only surface high-confidence observations. Queue uncertain ones under a "Lower confidence" subheading if you include them at all. - Be concise. One insight per bullet. Don't bundle. - Do not decompose the brief into tasks — that is Capture's job - Do not block the pipeline. You are advisory. +- Hard-stop if backend is `linear` and Linear MCP is unusable — never silent markdown fallback. diff --git a/agents/intake.md b/agents/intake.md index 4cfb511..86fa109 100644 --- a/agents/intake.md +++ b/agents/intake.md @@ -18,6 +18,96 @@ You will be given: Read and follow the **Load Config** section of [config.md](config.md). +### 0a. Tracker load path + +Work-item storage (URs, REQs, decisions, verify/close reports, run notes) goes **only** through named tracker port ops after config is loaded: + +1. Resolve effective `tracker.backend` (missing/empty/whitespace → `markdown`). +2. Read `agents/tracker/port.md` (shared op catalog + rules). +3. Read `agents/tracker/.md` (e.g. `markdown.md` or `linear.md`). +4. For work-item storage, call **only** named port ops from that backend file — never raw `.do-work/REQ-*` paths or raw Linear tools outside the backend doc. + +**Hard rules:** +- **No silent fallback** from `linear` to `markdown`. If backend is `linear`, do not substitute UR/REQ markdown as the store. +- If backend resolves to **`linear`** but `agents/tracker/linear.md` is **missing or unreadable**, **hard-stop** with setup instructions (restore the Linear backend doc / connect Linear skill). Never fall through to markdown paths. +- Markdown backend: ops map — **invoke** coordination scripts as `bash {skill-root}/lib/...` after Load Config step 8 resolves `$SKILL_ROOT`; **catalog identity** remains `lib/*.sh` in `markdown.md` — use those ops; do not re-implement store details here. + +### Intake store — backend branch (ORI-9) + +| Backend | How intake records a UR | +|---------|-------------------------| +| **linear** | Port op **`create_ur`** only (`agents/tracker/linear.md`) — product Project + **UR Project Milestone** with §9.1 body and verbatim brief. **Do not** create `{project}/.do-work/user-requests/UR-NNN/` or write local `input.md` as the store. Report **UR slug + product project id/name + milestone id/name**. | +| **markdown** | Local folder + `input.md` under `{project}/.do-work/user-requests/UR-NNN/` (steps 1–5 below). | + +**When effective backend is `linear`:** run **Linear path** (steps L1–L4) and skip markdown folder steps. If Linear MCP / milestone tools unusable → **hard-stop** — never silent markdown fallback. + +--- + +### Linear path (`tracker.backend: linear`) + +#### L1. Existing UR reference + +If the brief explicitly references an existing UR (e.g. "update UR-003"): + +1. Call **`read_ur`** for that slug (or **`list_urs`** then match). +2. If not found → report "UR-NNN does not exist. Creating a new UR instead." and continue to L2. +3. If found and still intake-equivalent (no REQs / status still intake in §9.1 machine fields): + - Ask whether to overwrite the brief on the UR milestone. + - If yes: update milestone description via linear.md rediscovery (`save_milestone` / same surface as `append_ideate`) — replace only `## Brief` content with the new verbatim message; preserve markers and other sections. Go to L3. + - If no: treat as new UR → L2. +4. If the UR already has capture-level state (Issues / `status: captured` equivalent), treat as a new UR → L2. + +#### L2. Create UR via port + +Call port op **`create_ur`** with the user's message **verbatim** as the brief body (§9.1 `## Brief` / Request content). Sequences live in `agents/tracker/linear.md`: + +1. Preflight + **`ensure_product_container`** +2. Allocate next `UR-NNN` from product-project milestones +3. Create **Project Milestone** (name from `ur_milestone_name_pattern`) with `` + §9.1 template +4. Return UR slug, product project id/name, milestone id/name + +**Never** allocate only a local folder. **Never** dual-write under `.do-work/user-requests/`. + +#### L3. Verify (Linear) + +1. **`read_ur`** for the new/updated slug. +2. Confirm machine marker `` and `**UR-id:**` match. +3. Confirm `## Brief` (or Request section) contains the user's original message verbatim. +4. Confirm product project + milestone ids from create are present / resolvable. + +If any check fails, fix via Linear update tools before proceeding — or hard-stop if tools fail. Do not invent a local markdown UR as repair. + +#### L4. Report and prompt (Linear) + +``` +Intake complete. + +Recorded: Linear UR milestone + UR: UR-NNN + Product project: + Milestone: +``` + +**Then**, same next-steps rules as markdown Step 6, but options refer to the Linear UR (not a local path): + +1. **"Run Capture"** — Proceed to capture for UR-NNN +2. **"Edit the brief"** — Review/edit the UR milestone brief in Linear before capturing +3. **"Skip"** — End the interaction + +If next_steps disabled or running as start delegate: + +``` +Next steps: +- Review the recorded brief on Linear (UR milestone UR-NNN) if anything needs clarifying +- Run Capture for UR-NNN (Linear backend — no local user-requests path required) +``` + +**Do not run Capture. Do not plan. Do not execute anything beyond the report and prompt.** + +--- + +### Markdown path (`tracker.backend: markdown` or unset) + ### 1. Check if the user is referencing an existing UR If the brief explicitly references an existing UR (e.g. "update UR-003", "add to UR-003", "modify UR-003"): @@ -136,5 +226,6 @@ Next steps: - Record the user's message verbatim — never summarise, rephrase, or interpret it - Never create REQ files — that is Capture's job - Never run Capture automatically — always stop after recording and wait for explicit instruction -- Do not add interpretation, plans, or suggestions to input.md -- The assets folder is created but left empty — the user populates it manually +- **Markdown:** do not add interpretation, plans, or suggestions to `input.md`; assets folder is created empty for the user +- **Linear:** do not dual-write local `user-requests/`; sole store is **`create_ur`** / UR milestone; report Linear ids +- Hard-stop if backend is `linear` and Linear MCP is unusable — never silent markdown fallback diff --git a/agents/log.md b/agents/log.md index cb712d0..fd538fc 100644 --- a/agents/log.md +++ b/agents/log.md @@ -19,6 +19,20 @@ You will be given: Read and follow the **Load Config** section of [config.md](config.md). +### 0a. Tracker load path + +Work-item storage (URs, REQs, decisions, verify/close reports, run notes) goes **only** through named tracker port ops after config is loaded: + +1. Resolve effective `tracker.backend` (missing/empty/whitespace → `markdown`). +2. Read `agents/tracker/port.md` (shared op catalog + rules). +3. Read `agents/tracker/.md` (e.g. `markdown.md` or `linear.md`). +4. For work-item storage, call **only** named port ops from that backend file — never raw `.do-work/REQ-*` paths or raw Linear tools outside the backend doc. + +**Hard rules:** +- **No silent fallback** from `linear` to `markdown`. If backend is `linear`, do not substitute UR/REQ markdown as the store. +- If backend resolves to **`linear`** but `agents/tracker/linear.md` is **missing or unreadable**, **hard-stop** with setup instructions (restore the Linear backend doc / connect Linear skill). Never fall through to markdown paths. +- Markdown backend: ops map — **invoke** coordination scripts as `bash {skill-root}/lib/...` after Load Config step 8 resolves `$SKILL_ROOT`; **catalog identity** remains `lib/*.sh` in `markdown.md` — use those ops; do not re-implement store details here. + If `config.log.enabled` is `false`, stop silently — output nothing. If `config.log.platforms` is empty, output: "No platforms configured. Add platforms to `.do-work/config.yml` under `log.platforms` (e.g. `[x, linkedin]`)." and stop. diff --git a/agents/question.md b/agents/question.md index 9e00833..5461792 100644 --- a/agents/question.md +++ b/agents/question.md @@ -8,11 +8,12 @@ You sharpen the brief by asking what the user already knows but didn't say. You ## When Invoked -You will be given a path to a user-request folder, e.g.: +You will be given a UR reference: -``` -{project}/.do-work/user-requests/UR-001/ -``` +| Backend | Invocation | +|---------|------------| +| **markdown** | Path to a user-request folder, e.g. `{project}/.do-work/user-requests/UR-001/` | +| **linear** | UR slug (e.g. `UR-001`) and/or UR Project Milestone id — no local folder required | You may also be invoked from the ideate gate when the user selects "Grill me", or run standalone via the `/do-work question` subcommand. @@ -24,11 +25,32 @@ You may also be invoked from the ideate gate when the user selects "Grill me", o Read and follow the **Load Config** section of [config.md](config.md). +### 0a. Tracker load path + +Work-item storage (URs, REQs, decisions, verify/close reports, run notes) goes **only** through named tracker port ops after config is loaded: + +1. Resolve effective `tracker.backend` (missing/empty/whitespace → `markdown`). +2. Read `agents/tracker/port.md` (shared op catalog + rules). +3. Read `agents/tracker/.md` (e.g. `markdown.md` or `linear.md`). +4. For work-item storage, call **only** named port ops from that backend file — never raw `.do-work/REQ-*` paths or raw Linear tools outside the backend doc. + +**Hard rules:** +- **No silent fallback** from `linear` to `markdown`. If backend is `linear`, do not substitute UR/REQ markdown as the store. +- If backend resolves to **`linear`** but `agents/tracker/linear.md` is **missing or unreadable**, **hard-stop** with setup instructions (restore the Linear backend doc / connect Linear skill). Never fall through to markdown paths. +- Markdown backend: ops map — **invoke** coordination scripts as `bash {skill-root}/lib/...` after Load Config step 8 resolves `$SKILL_ROOT`; **catalog identity** remains `lib/*.sh` in `markdown.md` — use those ops; do not re-implement store details here. + +### Clarifications store — backend branch (ORI-9) + +| Backend | Persist Q&A | +|---------|-------------| +| **markdown** | Append `## Clarifications` to `{project}/.do-work/user-requests/UR-NNN/input.md` | +| **linear** | Port op **`append_clarifications`** — append Q&A under `## Clarifications` on the **UR Project Milestone** description. Never overwrite `## Brief`. **No** local `input.md` dual-write. | + ### 1. Read the brief -Read `UR-NNN/input.md` in full. +**Markdown:** Read `UR-NNN/input.md` in full. Read every file in `UR-NNN/assets/` if it exists. -Read every file in `UR-NNN/assets/` if it exists. +**Linear:** Call **`read_ur`** for `UR-NNN`. Use `## Brief` (+ existing `## Clarifications` / `## Ideate` if present). Optional local assets only if the operator keeps them on disk. ### 2. Analyze for ambiguity @@ -56,9 +78,9 @@ Build a prioritized list of ambiguities, ordered by impact on the downstream dec Before asking the user anything, attempt to resolve each ambiguity from existing artifacts. Check: - The project codebase (source files, configs, existing tests) -- Prior UR `## Clarifications` sections (`user-requests/UR-*/input.md`) -- Archived REQs (`.do-work/archive/REQ-*.md`) -- `.do-work/decisions.md` if present +- Prior UR clarifications — **markdown:** `user-requests/UR-*/input.md` `## Clarifications`; **linear:** UR Project Milestone clarifications via port `read_ur` / `list_urs` (never invent a dual store) +- Prior REQs — **markdown:** `.do-work/archive/REQ-*.md`; **linear:** Issues via port `list_reqs_for_ur` / `read_req` (Linear issue ids) +- **Decisions memory (REQ-297):** **markdown** — `.do-work/decisions.md` if present; **linear** — **Read decisions** helper (`agents/tracker/linear.md`, Team Doc `decisions_doc_title` / default `do-work/decisions`). Same one-line grammar either backend. Do not read local `decisions.md` when backend is linear. For each ambiguity, classify the resolution into one of three buckets: @@ -119,13 +141,7 @@ When stopping, announce: "That covers the key ambiguities. Writing clarification ### 5. Write clarifications -Append a `## Clarifications` section to `{project}/.do-work/user-requests/UR-NNN/input.md`. - -**If `## Clarifications` already exists** (re-run scenario), append new Q&A entries below the existing ones. Never overwrite or modify prior clarifications. - -**If `## Clarifications` does not exist**, append it after the existing content with a blank line separator. - -Use this format exactly for directly-asked answers: +Use this format for directly-asked answers: ```markdown ## Clarifications @@ -146,11 +162,18 @@ For inferences confirmed in the Step 2.5 batch, use this format — the provenan If the user chose "Correct some" for specific inferences, record the corrected values without the `*(inferred, confirmed)*` marker — the correction makes them directly-asserted answers. -**Never modify the original brief text** above the `## Clarifications` section. The brief is the source of truth — clarifications are additive context. +**Persist via backend branch (ORI-9):** + +| Backend | How | +|---------|-----| +| **markdown** | Append a `## Clarifications` section to `{project}/.do-work/user-requests/UR-NNN/input.md`. If the section already exists, append new Q&A below existing entries. Never overwrite prior clarifications. Never modify the original brief text above the section. | +| **linear** | Call port op **`append_clarifications`** (`agents/tracker/linear.md`) with each Q&A pair. Appends under `## Clarifications` on the **UR Project Milestone**; creates the section if missing; never overwrites `## Brief` or prior Q&A. **Do not** write local `input.md`. If MCP fails → hard-stop. | + +The brief is the source of truth — clarifications are additive context. ### 6. Commit -Stage and commit the updated `input.md`: +**Markdown only:** Stage and commit the updated `input.md`: ```bash git add {project}/.do-work/user-requests/UR-NNN/input.md @@ -159,6 +182,8 @@ git commit -m "chore(UR-NNN): record question session clarifications" If the project is not a git repo, skip this step silently. +**Linear:** Skip git for work-item storage (clarifications already on the UR milestone). Do not invent a local dual-write commit. + ### 7. Report and prompt Output the completion report: @@ -166,7 +191,7 @@ Output the completion report: ``` Question session complete for UR-NNN. -Updated: {project}/.do-work/user-requests/UR-NNN/input.md +Updated: Clarifications recorded: N questions answered ``` @@ -188,6 +213,7 @@ If `config.next_steps.enabled` is `false`, missing, or this agent is running as ## Rules - Never modify the original brief text — only append `## Clarifications` below it +- **Linear:** use **`append_clarifications`** only; never dual-write local `input.md`; hard-stop if MCP unusable - Never suggest changes to scope — only extract what the user already knows but didn't write down - Never ask more than one question per message - Never ask compound questions (questions joined by "and" or "also") diff --git a/agents/resume.md b/agents/resume.md index e4ee0e3..d21ce44 100644 --- a/agents/resume.md +++ b/agents/resume.md @@ -25,27 +25,84 @@ Invoked via `/do-work resume REQ-NNN`. Read and follow the **Load Config** section of [config.md](config.md). +### 0a. Tracker load path + +Work-item storage (URs, REQs, decisions, verify/close reports, run notes) goes **only** through named tracker port ops after config is loaded: + +1. Resolve effective `tracker.backend` (missing/empty/whitespace → `markdown`). +2. Read `agents/tracker/port.md` (shared op catalog + rules). +3. Read `agents/tracker/.md` (e.g. `markdown.md` or `linear.md`). +4. For work-item storage, call **only** named port ops from that backend file — never raw `.do-work/REQ-*` paths or raw Linear tools outside the backend doc. + +**Hard rules:** +- **No silent fallback** from `linear` to `markdown`. If backend is `linear`, do not substitute UR/REQ markdown as the store. +- If backend resolves to **`linear`** but `agents/tracker/linear.md` is **missing or unreadable**, **hard-stop** with setup instructions (restore the Linear backend doc / connect Linear skill). Never fall through to markdown paths. +- Markdown backend: ops map — **invoke** coordination scripts as `bash {skill-root}/lib/...` after Load Config step 8 resolves `$SKILL_ROOT`; **catalog identity** remains `lib/*.sh` in `markdown.md` — use those ops; do not re-implement store details here. + +**Branch on effective backend** after load path: + +| Backend | Work-item resume | +|---------|------------------| +| **`markdown`** | Steps **1–6** below (working/ stamp + `heartbeat.sh`) | +| **`linear`** | Steps **L1–L5** — linear.md **Resume** (compose **`set_req_status`** + **`heartbeat_req`**). Id is a **Linear issue id**. Assignee and claim ownership preserved. | + +Invocation under Linear may be `/do-work resume ENG-123`. Worktree/branch isolation stays **local** regardless of backend. + +--- + +## Linear backend (Resume compose) + +Resume is **not** a separate port op name. Follow **Resume (Linear — `agents/resume.md` consumer)** in `agents/tracker/linear.md`. + +### L1. Locate issue and confirm stopped + +1. Get issue by Linear id (rediscover tools; hard-stop if MCP unusable). +2. Workflow must map to `status_map.stopped`. If not stopped → refuse (same as markdown). +3. Latest claim must be `status: active` (**Helper: read active claim**). If `released` or missing → refuse; operator should use run/claim or `/do-work unblock`, not resume. +4. Record prior stopper context from run notes / comments when present for the announce line. + +### L2. Detect worktree mode (local) + +Same as markdown Step 2, using branch `req/` (sanitize for git ref rules) when Linear ids are used: + +- Do **not** delete the existing feature branch. +- Do **not** clear the Linear claim. +- Fresh worker continues on the existing worktree/branch when present. + +### L3. Refresh status and heartbeat (port ops) + +1. **`set_req_status`** → `in_progress` (workflow `status_map.in_progress`). **Do not** change human assignee. **Do not** strip claim comments. +2. **`heartbeat_req`** — refresh `heartbeat` now; preserve `agent_id` and `claimed_at` on the active claim protocol (`agent_claim_marker` / ``). +3. If either op fails (MCP down mid-flight) → **leave claimed** (port mid-flight rule); hard-stop with setup/resume-later instructions. Do not invent markdown working/ cleanup. + +### L4. Dispatch a fresh worker + +Same as markdown Step 4 / [run.md](run.md) classification + model selection + dispatch. Pass the Linear issue id and prior context instead of a `.do-work/working/` path when the worker load path is Linear. Escalation for prior-stopped still applies. + +Announce: + +``` +[] Resuming [type=, model=, prior reason=]: [title] +``` + +### L5. Process the worker report / stop + +Same as markdown Steps 5–6 ([run.md](run.md) report processing). Single-issue operation; do not auto-loop. + +--- + +## Markdown backend + ### 1. Locate the REQ and confirm `stopped` Check whether `{project}/.do-work/working/REQ-NNN-*.md` exists. -- If **no match**: the REQ is not an in-flight working slot — check whether it is instead in `.do-work/pending/`: - - **Found in `pending/`**: refuse with: - ``` - REQ-NNN is in pending-validation state — it cannot be resumed. - The code is already merged; only human sign-off remains. - Outstanding checklist items: - To close: /do-work approve REQ-NNN - To return to backlog: /do-work reject REQ-NNN - ``` - Stop. Do not dispatch a worker, do not modify the REQ, do not stamp a heartbeat. - - **Not in `pending/` either**: report `"REQ-NNN is not in working/ — nothing to resume."` and stop. +- If **no match**: report `"REQ-NNN is not in working/ — nothing to resume."` and stop. - If **multiple matches** in `working/`: report the ambiguity and stop. Do not guess. - If **exactly one match** in `working/`: record the absolute path as `REQ_PATH` and continue. Read `REQ_PATH` and inspect `**Status:**`. -- If `**Status:**` is `pending-validation`: refuse with the same guidance as above (the REQ was moved to `pending/` but a stale `working/` reference should not be resumed). Report the pending state, direct the user to `/do-work approve REQ-NNN` or `/do-work reject REQ-NNN`, and stop. - If `**Status:**` is **not** `stopped`: refuse — report `"REQ-NNN is , not stopped — refusing to resume."` and stop. Resume is exclusively for stopped REQs. Running ones don't need resuming; backlog/archived ones aren't claimed. - If `**Status:**` is `stopped`: read and record `**Reason:**` (e.g. `concurrent-conflict`, `unknown-error`) and any associated context for the announce line. Continue. @@ -80,6 +137,14 @@ bash {skill-root}/lib/heartbeat.sh "$REQ_PATH" If `heartbeat.sh` exits non-zero (missing claim stamp, malformed file), report the failure and stop. Do not dispatch a worker against a REQ with no live heartbeat. +**Refresh the `**Session:**` line.** Resume preserves the original claim ownership, but the *session* now handling the REQ is this one — the extension's REQ→session resume lookup must point at the live session. Use the shared stamp primitive (insert / replace / leave-untouched; never free-hand edit the claim block): + +```bash +bash {skill-root}/lib/stamp-session.sh "$REQ_PATH" +``` + +`stamp-session.sh` re-resolves via `lib/resolve-session.sh` when the session-id argument is omitted, inserts or replaces `**Session:**` inside the claim stamp, and leaves any existing Session line untouched when resolve prints nothing. Filesystem-only — no git commit, same contract as the heartbeat refresh. If it exits non-zero (missing claim stamp, path outside working/), report the failure and stop. + ### 4. Dispatch a fresh worker Re-use the orchestrator dispatch path from [run.md](run.md). Do not duplicate the classification or model-selection rules here. @@ -115,10 +180,10 @@ Resume is a one-shot. Do not claim another REQ, do not invoke run, do not prompt ## Rules -- **Never resume a pending-validation REQ.** A REQ in `.do-work/pending/` (or carrying `**Status:** pending-validation`) has already had its code merged and its worktree torn down — there is no worker to re-dispatch. The correct verb is `/do-work approve REQ-NNN` (to close) or `/do-work reject REQ-NNN` (to return to backlog with a note). Resume must refuse with that guidance and stop without touching claim stamps, heartbeats, or the REQ file. -- Refuse to resume a REQ whose `**Status:**` is not `stopped`. Backlog REQs reclaim through `run.md`; archived REQs are done; `in-progress` REQs are either live or already abandoned (use `/do-work unblock` for those). -- Preserve `**Claimed by:**` and `**Claimed at:**` exactly. Only `**Heartbeat:**` is refreshed. -- For worktree-mode REQs, never delete or reset the `req/REQ-NNN` branch — the fresh worker continues on it. +- Refuse to resume a work item that is not **stopped** with an **active** claim. Backlog reclaim through `run.md`; done/archived are finished; abandoned `in-progress` uses `/do-work unblock`. +- **Markdown:** Preserve `**Claimed by:**` and `**Claimed at:**` exactly. Only `**Heartbeat:**` is refreshed. +- **Linear:** Preserve claim `agent_id` / `claimed_at`; refresh via **`heartbeat_req`**; workflow via **`set_req_status`** → in_progress; never steal assignee. +- For worktree-mode REQs, never delete or reset the feature branch — the fresh worker continues on it. - Do not duplicate classification, model selection, or dispatch logic — refer to [run.md](run.md). When that file changes, resume inherits the change for free. -- Single REQ per invocation. No batching. -- No `AskUserQuestion` next-step prompt unless triggered by the worker-report branch in Step 5. +- Single REQ / issue per invocation. No batching. +- No `AskUserQuestion` next-step prompt unless triggered by the worker-report branch. diff --git a/agents/retro.md b/agents/retro.md index fdbeeaa..5b7ffc0 100644 --- a/agents/retro.md +++ b/agents/retro.md @@ -1,10 +1,10 @@ # Retro Agent -You are the Retro agent in the Do Work system. Your job is to turn the write-only run ledger into a learning signal: run the deterministic rollup, interpret its stats into a human report, and regenerate the project's capture-facing calibration file. +You are the Retro agent in the Do Work system. Your job is to turn the write-only run ledger into a learning signal: run the deterministic rollup, interpret its stats into a human report, and regenerate the project's capture-facing calibration store. -Design contract: `docs/design/retro-learning.md`. The split is fixed — the script (`lib/retro-rollup.sh`) does arithmetic; you do judgment. Do not recompute the script's numbers; interpret them. +Design contract: `docs/design/retro-learning.md`. The split is fixed — the script (`{skill-root}/lib/retro-rollup.sh`) does arithmetic; you do judgment. Do not recompute the script's numbers; interpret them. -You are read-only except for **one** file: `.do-work/state/calibration.md`. You make no commits, run no deploys, and prompt the user for nothing. +You are read-only except for **one** calibration write (backend-selected home). You make no commits, run no deploys, and prompt the user for nothing. --- @@ -20,22 +20,60 @@ You will be given a project do-work path: `{project}/.do-work/`. Read and follow the **Load Config** section of [config.md](config.md). -### 1. Run the rollup +### 0a. Tracker load path + +Work-item storage (URs, REQs, decisions, verify/close reports, run notes) goes **only** through named tracker port ops after config is loaded: + +1. Resolve effective `tracker.backend` (missing/empty/whitespace → `markdown`). +2. Read `agents/tracker/port.md` (shared op catalog + rules). +3. Read `agents/tracker/.md` (e.g. `markdown.md` or `linear.md`). +4. For work-item storage, call **only** named port ops from that backend file — never raw `.do-work/REQ-*` paths or raw Linear tools outside the backend doc. + +**Hard rules:** +- **No silent fallback** from `linear` to `markdown`. If backend is `linear`, do not substitute UR/REQ markdown as the store. +- If backend resolves to **`linear`** but `agents/tracker/linear.md` is **missing or unreadable**, **hard-stop** with setup instructions (restore the Linear backend doc / connect Linear skill). Never fall through to markdown paths. +- Markdown backend: ops map — **invoke** coordination scripts as `bash {skill-root}/lib/...` after Load Config step 8 resolves `$SKILL_ROOT`; **catalog identity** remains `lib/*.sh` in `markdown.md` — use those ops; do not re-implement store details here. + +### Calibration / run-notes home — backend branch (REQ-296 / REQ-297) + +| Concern | Markdown | Linear (`linear.md`) | +|---------|----------|----------------------| +| Calibration write | Truncate-write `{project}/.do-work/state/calibration.md` | **Write calibration Doc** — Team Doc `tracker.linear.calibration_doc_title` (default `do-work/calibration`), create-if-missing, **full replace** body. Create/update failure → hard-stop; never invent alternate titles or local store | +| Run history for rollup | Local `.do-work/runs/RUN-NNN.yml` via `{skill-root}/lib/retro-rollup.sh` | **Prefer Linear first (REQ-297):** **List run notes** helper — Issue comments with `` from `append_run_note`. Fall back to local `RUN-NNN.yml` only if comments unavailable. Local files are telemetry only when `ledger.enabled` | + +**When effective backend is `linear`:** do **not** write local `state/calibration.md` as the store. Use the calibration Team Doc sequence only. Fixed home — never invent alternate Doc titles. + +### 1. Collect run history and run the rollup + +**Prefer Linear run notes when `backend: linear` (REQ-297):** + +1. Call linear.md **List run notes** — rediscover comment tools; collect `` YAML blocks from Issues under `do-work/UR-*` Projects (or the scoped UR). Hold parsed notes in context. +2. Still run the local rollup script when present (it chews optional telemetry): ```bash -bash lib/retro-rollup.sh +bash {skill-root}/lib/retro-rollup.sh ``` -Run it from the project root (the directory containing `.do-work/`). Capture stdout verbatim — these are the facts you will interpret. Warnings on stderr (e.g. `skip malformed ledger row ...`) are informational; note them but do not stop. +Run the script from the project root (the directory containing `.do-work/`). Capture stdout verbatim. Warnings on stderr (e.g. `skip malformed ledger row ...`) are informational; note them but do not stop. + +If `$SKILL_ROOT/lib/retro-rollup.sh` is missing **and** backend is markdown, report `"$SKILL_ROOT/lib/retro-rollup.sh not found — cannot run retro."` and stop. If backend is linear and the script is missing but **List run notes** returned rows, continue interpreting from those notes only. + +**Interpretation priority under Linear:** -If `lib/retro-rollup.sh` is missing, report `"lib/retro-rollup.sh not found — cannot run retro."` and stop. +| Situation | What you interpret | +|-----------|--------------------| +| Linear run notes present | Prefer those notes as authoritative history (design §7); local rollup numbers are secondary if they disagree on coverage | +| Linear notes empty/unavailable, local `runs=N` > 0 | Fall back to local rollup stdout (telemetry) | +| Both empty | Empty-state branch (Step 2) | + +Do not invent spend/stats. Never dual-write a fabricated local ledger from partial Linear data. ### 2. Empty-state branch (§2e) -If the rollup's output is exactly `runs=0`: +If there is **no** history to learn from — markdown: rollup output is exactly `runs=0`; linear: **List run notes** returned zero usable notes **and** rollup is `runs=0` (or script missing with no notes): 1. Render a clean report: `"No run history yet — nothing to learn from. Run some REQs through /do-work run, then retro again."` -2. Write **no** calibration file. Do not create or truncate `.do-work/state/calibration.md`. +2. Write **no** calibration. Do not create or truncate `.do-work/state/calibration.md` (markdown) and do **not** create/replace the Linear calibration Team Doc. 3. Stop. This is the documented degraded output. It is not an error. @@ -63,9 +101,9 @@ From the stats, derive **imperative, capture-facing** rules — one line each, n Rank candidate rules by the recurrence weight (the rollup's `weighted=` value) and by stop/escalation rate. Keep only the **top 8**, highest-signal first. If fewer than 8 exist, write only those. -### 5. Regenerate calibration.md (the one write) +### 5. Regenerate calibration (the one write) -Ensure the directory exists, then **truncate-write** (`>`, never append `>>`) `.do-work/state/calibration.md` in this exact format (`docs/design/retro-learning.md §3c`): +Build the calibration body in this exact format (`docs/design/retro-learning.md §3c`): ```markdown # Calibration — @@ -86,26 +124,32 @@ top_recurrences: , ... --> ``` +**Persist via backend branch (REQ-296):** + +- **Markdown:** ensure `{project}/.do-work/state/` exists, then **truncate-write** (`>`, never append `>>`) `.do-work/state/calibration.md`. +- **Linear:** call linear.md **Write calibration Doc** — Team Doc title `tracker.linear.calibration_doc_title` (default `do-work/calibration`), create-if-missing, **full replace** body. Do **not** also write local `state/calibration.md`. + Hard rules for the write: - **≤8 guidance bullets**, **≤30 lines of guidance** (excluding header and the `retro-meta` footer). If you derived more than 8, drop the rest — do not queue them. -- **Full replace every run.** There is no merge with the prior file. A rule that no longer recurs simply disappears. This is the structural anti-growth guarantee — never append. +- **Full replace every run.** There is no merge with the prior body. A rule that no longer recurs simply disappears. This is the structural anti-growth guarantee — never append. - `runs_analyzed` is the `runs=N` value. `top_recurrences` lists the highest-weighted recurrences with their `weighted=` counts. - Project name: the basename of the project root. ### 6. Stop -Print the report. Confirm the calibration path written. No prompts, no commits, no other writes. +Print the report. Confirm the calibration home written (local path or Linear Doc title). No prompts, no commits, no other writes. --- ## Rules -- **One write only.** The sole file you may create or modify is `.do-work/state/calibration.md`. Never write any other file, never touch the source tree, never write `.do-work/runs/`, REQs, or other `state/` files. -- **Truncate-write, never append.** Use `>`. Appending breaks the size bound and the regeneration guarantee. -- **Bound is yours to enforce.** The script emits all candidates; you select the top 8. Do not write an unbounded file because the rollup printed many lines. +- **One calibration write only.** Markdown: sole file is `.do-work/state/calibration.md`. Linear: sole work-item write is the calibration Team Doc (fixed title). Never write REQs, runs (except as input you already read), or other `state/` files; never touch the source tree. +- **Full replace, never append-merge.** Markdown uses `>`; Linear replaces the Doc body. Appending breaks the size bound and the regeneration guarantee. +- **Bound is yours to enforce.** The script emits all candidates; you select the top 8. Do not write an unbounded body because the rollup printed many lines. - **Interpret, don't recompute.** Treat the rollup's counts/rates/deltas as ground truth. Do not re-derive them or contradict them. -- **Empty state writes nothing.** On `runs=0`, render the "no run history yet" report and write no file. +- **Empty state writes nothing.** On `runs=0` (and no Linear notes to interpret), render the "no run history yet" report and write no calibration. - **Advisory, never blocking.** Calibration informs capture; it is not a requirement. Nothing you produce blocks the pipeline. - **No git commits, no AskUserQuestion prompts, no deploys.** -- If `lib/retro-rollup.sh` is missing, report it and stop. +- **Linear homes are fixed (REQ-296 / REQ-297).** Never invent ad-hoc Doc titles; use `calibration_doc_title` only. Prefer **List run notes** over local telemetry when backend is linear. Doc create/update failure → hard-stop (no local substitute store). +- If `$SKILL_ROOT/lib/retro-rollup.sh` is missing under markdown, report it and stop. Under linear, missing script alone is not fatal when Linear run notes were listed successfully. diff --git a/agents/review.md b/agents/review.md index 04d35f6..a67a3ef 100644 --- a/agents/review.md +++ b/agents/review.md @@ -10,7 +10,9 @@ This gate complements criteria provenance in [run.md](run.md): capture may mark You run as an **independent subagent dispatched by `agents/run.md` Step 3** via the Agent tool — a fresh session with **no run context**. You did not write this code, you do not know the worker's reasoning, and you have no stake in the run finishing. Judge only the artifacts handed to you. Do not assume, request, or reconstruct any run history beyond the named inputs below; their absence is by design, so your verdict is unbiased by the orchestrator's drive to complete. -You will be given exactly these named inputs: +You will be given exactly these named inputs (shape depends on tracker backend): + +**Markdown backend:** 1. The working REQ path: `{project}/.do-work/working/REQ-NNN-slug.md` 2. The matching UR path @@ -18,10 +20,35 @@ You will be given exactly these named inputs: 4. The implementation diff or commit reference 5. The policy-check output (`lib/check-policy.sh` result and exit code) +**Linear backend (`tracker.backend: linear` — REQ-295):** + +1. The **Linear issue id** (e.g. `ENG-123`) — load body via port op **`read_req`** (not a `.do-work/working/` path as source of truth) +2. The matching UR / Project context (UR slug / Initiative id when known) +3. The worker report YAML +4. The implementation diff or commit reference (feature branch may be `req/ENG-123`) +5. The policy-check output (`lib/check-policy.sh` result and exit code) + When the orchestrator runs in **adversarial mode**, you may be one of three reviewers dispatched in parallel, each scoped to a distinct lens (correctness, security, regression). Honour your assigned lens if one is named, but still report any blocker you observe outside it — the orchestrator's 2-of-3 majority gate treats any reviewer's blocker as decisive. --- +## Tracker load path + +Work-item storage (URs, REQs, decisions, verify/close reports, run notes) goes **only** through named tracker port ops: + +1. Load config (`agents/config.md`) and resolve effective `tracker.backend` (missing/empty/whitespace → `markdown`). +2. Read `agents/tracker/port.md` (shared op catalog + rules). +3. Read `agents/tracker/.md` (e.g. `markdown.md` or `linear.md`). +4. For work-item storage, call **only** named port ops from that backend file — never raw `.do-work/REQ-*` paths or raw Linear tools outside the backend doc. + +**Hard rules:** +- **No silent fallback** from `linear` to `markdown`. If backend is `linear`, do not substitute UR/REQ markdown as the store. +- If backend resolves to **`linear`** but `agents/tracker/linear.md` is **missing or unreadable**, **hard-stop** with setup instructions (restore the Linear backend doc / connect Linear skill). Never fall through to markdown paths. +- Markdown backend: ops map — **invoke** coordination scripts as `bash {skill-root}/lib/...` after Load Config step 8 resolves `$SKILL_ROOT`; **catalog identity** remains `lib/*.sh` in `markdown.md` — use those ops; do not re-implement store details here. +- Review is **read-only** for work items: use `read_req` / `read_ur` as needed; **never** call `archive_req`, `claim_req`, `set_req_status`, or `append_run_note`. + +Review is primarily read-only against the REQ (working file or Linear Issue) and worker report; still resolve the load path so any work-item field reads go through port ops for the active backend. + ## Inputs To Inspect - The REQ task, acceptance criteria, verification steps, approved-criteria state, dependencies, and declared file scope @@ -39,11 +66,16 @@ Perform these checks in order: 1. **Scope:** Confirm changed files and behavior match the REQ. Flag unrelated changes, undeclared broad rewrites, or extra features. 2. **Acceptance:** Confirm every acceptance criterion has passing evidence and that the evidence actually supports the criterion. 3. **Verification:** Confirm required verification steps were run or explicitly justified when impossible. -4. **Tests:** Confirm new or changed behavior has appropriate focused tests, plus broader tests when blast radius warrants it. -5. **Secrets:** Inspect changed files and evidence for secrets, credentials, tokens, `.env` content, or sensitive local paths. -6. **Documentation:** Confirm user-facing behavior, install behavior, config, or workflow changes update relevant docs. -7. **Regression risk:** Identify migrations, auth, billing, payments, broad file changes, or other risk triggers that need stronger review. -8. **Policy:** Include deterministic policy-check output from `lib/check-policy.sh`. A blocked path or blocked command from `security.blocked_paths` or `security.blocked_commands` is a blocker. A `risk.require_review` signal is mandatory context: review may pass only after explicitly addressing the signal in findings. +4. **UI screenshot evidence (blocker when applicable):** If the REQ has any `ui` verification step, or any acceptance evidence item with `type: ui`, confirm all of the following. Failure on any item is **severity: blocker** (`status: failed`) — not a warning: + - The worker `checkpoint_log` includes a passed `ui` step whose command/actual cites a screenshot path. + - That path is under `.do-work/user-requests/UR-NNN/ui-evidence/` (or an equivalent documented `ui-evidence` path for the parent UR). + - The PNG exists on disk when the path is resolvable from the project root (or the report embeds an absolute path that exists). + - Evidence is not a11y/DOM narrative alone: a `type: ui` item whose `ref` is free text without a `.png` (or other image) path is insufficient. +5. **Tests:** Confirm new or changed behavior has appropriate focused tests, plus broader tests when blast radius warrants it. +6. **Secrets:** Inspect changed files and evidence for secrets, credentials, tokens, `.env` content, or sensitive local paths. +7. **Documentation:** Confirm user-facing behavior, install behavior, config, or workflow changes update relevant docs. +8. **Regression risk:** Identify migrations, auth, billing, payments, broad file changes, or other risk triggers that need stronger review. +9. **Policy:** Include deterministic policy-check output from `lib/check-policy.sh`. A blocked path or blocked command from `security.blocked_paths` or `security.blocked_commands` is a blocker. A `risk.require_review` signal is mandatory context: review may pass only after explicitly addressing the signal in findings. --- @@ -78,6 +110,15 @@ Use `status: failed` when any blocker exists. Warnings may pass if they do not i ## Stopping Behavior -If review fails, the run orchestrator must leave the REQ in `working/`, set or report a stopped reason, and must not archive it. Worker `status: done` is therefore not sufficient for completion: evidence validation and review must both pass before archive. +If review fails, the run orchestrator must **not archive**: + +| Backend | On `status: failed` | +|---------|---------------------| +| **markdown** | Leave the REQ in `working/`; set/report stopped reason; do not move to `archive/` | +| **linear** | **Do not call `archive_req`**; issue stays `in_progress`/`stopped` with **claim protocol intact** (active claim comment remains); orchestrator may `set_req_status` → stopped + optional `append_run_note` | + +When `review.required: true` (config default), this gate is mandatory before archive on both backends. When `review.required: false`, the orchestrator may skip dispatching this agent entirely. + +Worker `status: done` is therefore not sufficient for completion: evidence validation and review (when required) must both pass before archive. -Do not edit files, merge branches, archive REQs, or write ledger entries. This agent only reviews and reports. +Do not edit files, merge branches, archive REQs, call `archive_req`, or write ledger entries. This agent only reviews and reports. diff --git a/agents/run-worker.md b/agents/run-worker.md index b5cada5..ecd7dab 100644 --- a/agents/run-worker.md +++ b/agents/run-worker.md @@ -24,7 +24,7 @@ The orchestrator dispatches you with these named inputs: 2. **UR input.md path** — absolute path to the originating user request brief 3. **Prior-REQ archived paths** — list of absolute paths to previously archived REQs from the same UR (may be empty) 4. **Context pack path** — absolute path to `.do-work/state/context-pack.md`, a ~200-line orchestrator-generated map of the project (architecture, directory roles, key services, naming & test conventions, how to run the suite). Read it in Step 2. -5. **Skill root** — the resolved absolute path the orchestrator loaded its instructions from (the directory containing `lib/`). Wherever these instructions write `{skill-root}/lib/...`, that means this passed-in value — substitute it. A worker `cd`'d into a consumer project's worktree has no local `lib/`; this is how your heartbeat / feedback calls resolve. +5. **Skill root** — the absolute `$SKILL_ROOT` the orchestrator resolved once in **Load Config step 8** (`agents/config.md`: walk-up from loaded instruction file with `lib/` + (`SKILL.md` or `agents/`) markers, or inherit of a valid pre-set `$SKILL_ROOT`; hard-stop if unknown) and substituted into these instructions. It is the skill install root (the directory containing `lib/` and skill markers). Wherever these instructions write `{skill-root}/lib/...`, that means this passed-in value — substitute it. A worker `cd`'d into a consumer project's worktree has no local `lib/`; this is how your heartbeat / feedback calls resolve. Do **not** invent a second resolve recipe (no env/hub/CWD fallback). If the orchestrator omitted Skill root or left `{skill-root}` unsubstituted and you cannot determine the path, hard-stop per Load Config step 8 (inherit only when the value still satisfies markers; otherwise walk-up from the loaded instruction file). **Context discipline (bounded exploration, not starvation).** Prefer the context pack and the files the REQ and prior REQs cite — they are your primary context. When the implementation genuinely touches a file (a helper you must call, a convention you must match, a test pattern you must follow), you MAY read it to do the work correctly — bounded exploration of files your change actually touches is allowed. You MUST NOT load other REQs or other URs, and you MUST NOT wander into unrelated parts of the repo for general reading. Bounded exploration serves the change in front of you; it is not a license to re-survey the whole project (that is what the context pack is for). @@ -36,8 +36,8 @@ The orchestrator dispatches you with these named inputs: The worker's responsibilities are bounded: -- **Worker = code.** Creates a worktree on a feature branch (`req/REQ-NNN`). Implements + tests + commits to that branch. Never touches `.do-work/`. Never merges back. Never tears down its worktree. -- **Orchestrator = state.** Owns `.do-work/` lifecycle. After the worker returns `status: done`, the orchestrator merges the feature branch into the base branch, moves the REQ from `working/` to `archive/`, commits the metadata change, and tears down the worktree. +- **Worker = code.** Creates a worktree on a feature branch (`req/REQ-NNN` markdown, or `req/` under Linear). Implements + tests + commits to that branch. Never touches `.do-work/` as a work-item store. Never merges back. Never tears down its worktree. +- **Orchestrator = state.** Owns work-item lifecycle (markdown: `.do-work/` move/archive; Linear: `archive_req` / claim release). After the worker returns `status: done` and gates pass, the orchestrator merges the feature branch into the base branch, archives the REQ, commits metadata when applicable, and tears down the worktree. This separation makes parallelism safe by construction: workers cannot interfere with each other's working trees because each one has its own. Merge conflicts surface explicitly at the orchestrator's integration step rather than silently corrupting another worker's in-flight edits. @@ -59,38 +59,83 @@ Record the output as `` (typically `main`). All subsequent merge an ### W2. Create the worktree + feature branch +Resolve names from the tracker backend (load path Step 0). **Linear issue ids are sanitized for git refs** (see `agents/tracker/linear.md` Branch sanitize / design §6.5). + +| Backend | Feature branch | Worktree directory | +|---------|----------------|--------------------| +| **markdown** | `req/REQ-NNN` (e.g. `req/REQ-117`) | `{project}/.worktrees/req-NNN` (e.g. `req-117`) | +| **linear** | `req/` (e.g. `req/ENG-123`) | `{project}/.worktrees/req-` (e.g. `req-eng-123`, hard default) | + +**Sanitize algorithm (Linear — REQ-295):** start from the Linear issue id (e.g. `ENG-123`); keep only `[A-Za-z0-9._-]`; map every other character to `-`; collapse consecutive `-`/`.`; strip leading/trailing `-`/`.`; if empty → hard-stop (do not invent a name). Branch = `req/` (preserve identifier case). Worktree dir **hard-defaults to lowercase** sanitized form (`req-eng-123`) for FS consistency — do not keep mixed-case worktree dirs. + ```bash +# markdown: git worktree add {project}/.worktrees/req-NNN -b req/REQ-NNN +# linear (example ENG-123): +# git worktree add {project}/.worktrees/req-eng-123 -b req/ENG-123 ``` -- Worktree path: `{project}/.worktrees/req-NNN` (where `NNN` is the REQ number, e.g. `req-117`). -- Branch name: `req/REQ-NNN` (e.g. `req/REQ-117`). +Record `` and `` for Step 8 and the Return Report. The orchestrator merges and tears down using these same strings. ### W3. REQ file visibility -The REQ file in `{project}/.do-work/working/REQ-NNN-slug.md` is immediately visible from the worktree because `git worktree` shares the repository's object database and tracked index. No physical copy or move is required. +**Markdown:** The REQ file in `{project}/.do-work/working/REQ-NNN-slug.md` is immediately visible from the worktree because `git worktree` shares the repository's object database and tracked index. No physical copy or move is required. + +**Linear:** There is no local working/ REQ file as source of truth. The orchestrator passes the **Linear issue id** (and any exported body snapshot). Load the Issue via port op **`read_req`** when you need the full description; do not invent a parallel `.do-work/working/` store. + +### W3.5 Provision dependencies + +Before entering the worktree, run the dependency provisioner so that test tooling (Pest, vitest, etc.) can boot: + +```bash +bash {skill-root}/lib/provision-worktree.sh {project} {project}/.worktrees/ +``` + +Capture its stdout summary and interpret each line: + +- `linked: ` — the dependency dir was symlinked from the main checkout into the worktree. Test tooling referencing that path should now boot. +- `ran-setup: ` — the `worktree.setup_command` ran inside the worktree and produced the dependency dir. Test tooling should now boot. +- `unprovisionable: ` — the dir is absent from the main checkout AND no `worktree.setup_command` resolved it. Carry these paths forward; the verification logic in Step 6 uses them to decide whether a failing `test`/`build` step is retryable or genuinely unprovisionable (see **Deferred checkpoint status** in Step 6). + +The provisioner always exits 0 — an `unprovisionable:` line is a reported outcome, not a fatal error. ### W4. Work inside the worktree -`cd` into `{project}/.worktrees/req-NNN` before starting TDD. All edits and commits from `## Steps` Step 3 through Step 8 happen inside this directory. +`cd` into the worktree path from W2 before starting TDD. All edits and commits from `## Steps` Step 3 through Step 8 happen inside this directory. ### W5. Commit on the feature branch -The Step 8 commit (`feat(REQ-NNN): ...`) lands on `req/REQ-NNN` inside the worktree. This is the normal `## Steps` Step 8 commit, executed from within the worktree directory. After the commit succeeds, capture the commit short hash for the Return Report. +The Step 8 commit lands on the feature branch inside the worktree (`req/REQ-NNN` markdown; `req/` Linear). Message format follows tracker backend: `feat(REQ-NNN): …` (markdown) or `feat(ENG-123): …` + `Issue:` footer (Linear §6.5 — see Step 8). This is the normal `## Steps` Step 8 commit, executed from within the worktree directory. After the commit succeeds, capture the commit short hash for the Return Report. -**Worker stops here.** Do NOT merge back. Do NOT tear down the worktree. Do NOT touch `.do-work/`. The orchestrator (see `agents/run.md` post-worker integration steps) is responsible for: +**Worker stops here.** Do NOT merge back. Do NOT tear down the worktree. Do NOT touch `.do-work/` as orchestrator state. Do **not** call `archive_req` (orchestrator + review gate only). The orchestrator (see `agents/run.md` post-worker integration steps) is responsible for: -- Merging `req/REQ-NNN` into `` with conflict-retry handling. -- Moving the REQ file from `.do-work/working/` to `.do-work/archive/`, setting `**Status:** done`, appending the `## Outputs` section based on the YAML report you returned. +- Merging `` into `` with conflict-retry handling. +- Archiving the REQ (markdown: working/ → archive/; Linear: **`archive_req`** only after evidence + review when required). - Tearing down the worktree (`git worktree remove`) and deleting the feature branch (`git branch -d`). -- Committing the `.do-work/` metadata change. +- Committing any local `.do-work/` metadata change when the markdown store is used. -Your `Return Report` must list every output path in the `outputs:` array — the orchestrator uses that list to build the `## Outputs` section it appends to the archived REQ. Returning incomplete `outputs:` means the archive record will be incomplete. +Your `Return Report` must list every output path in the `outputs:` array — the orchestrator uses that list to build the `## Outputs` section (markdown archive or Linear Issue body). Returning incomplete `outputs:` means the archive record will be incomplete. --- ## Steps +### 0. Tracker load path + +Load config and resolve work-item storage before reading/updating REQs: + +1. Read and follow the **Load Config** section of [config.md](config.md) (resolve effective `tracker.backend`; missing/empty/whitespace → `markdown`). +2. Read `agents/tracker/port.md` (shared op catalog + rules). +3. Read `agents/tracker/.md` (e.g. `markdown.md` or `linear.md`). +4. For work-item storage, call **only** named port ops from that backend file — never raw `.do-work/REQ-*` paths or raw Linear tools outside the backend doc. + +**Hard rules:** +- **No silent fallback** from `linear` to `markdown`. If backend is `linear`, do not substitute UR/REQ markdown as the store. +- If backend resolves to **`linear`** but `agents/tracker/linear.md` is **missing or unreadable**, **hard-stop** with setup instructions (restore the Linear backend doc / connect Linear skill). Never fall through to markdown paths. +- Markdown backend: ops map — **invoke** coordination scripts as `bash {skill-root}/lib/...` after Load Config step 8 resolves `$SKILL_ROOT`; **catalog identity** remains `lib/*.sh` in `markdown.md` (including `heartbeat_req` → `lib/heartbeat.sh`) — use those ops; do not re-implement store details here. +- Runtime/git isolation (worktrees, feature branch, commit) stays local regardless of backend. +- **Linear mid-flight (REQ-294):** if Linear MCP fails **after** the orchestrator already claimed this issue (`in_progress` + active claim comment) and before you finish, **leave claimed** — do not release the claim, do not write markdown REQ files as a substitute store, do not invent cleanup. Return `status: stopped` with an appropriate reason (`dependency-missing` / `unknown-error` / etc.); operator uses `/do-work resume` or `unblock` after MCP recovers. Heartbeats under Linear use **`heartbeat_req`** against the Linear issue id (not `lib/heartbeat.sh` on a local working/ file) when the orchestrator passed a Linear-backed REQ. + ### 1. Read the REQ Read the REQ file in full. Understand: @@ -145,7 +190,14 @@ Keep this in mind during implementation so you do not: If the prior-REQ list is empty, skip this substep. -**Read the decisions memory.** Read `{project}/.do-work/decisions.md` if it exists — the append-only cross-UR decisions memory (format in SKILL.md § Decisions Memory). Each line is a **standing decision** and is a constraint on your implementation, not advisory context: do not contradict one. If your REQ's task or acceptance criteria require you to act against a recorded decision line (e.g. the REQ asks you to add client-side validation but a decision line reads `... | validation lives server-side | ...`), do not silently override it — return `status: stopped` with `reason: scope-creep` (if the REQ pushes new behaviour past a standing boundary) or `reason: ambiguous-criteria` (if the REQ and the decision are in direct conflict and you cannot tell which governs), naming the specific decision line verbatim in your report details so the orchestrator can route it for human resolution. If the file is absent (no decision recorded yet), this substep is silently a no-op — never create the file. +**Read the decisions memory (backend branch — REQ-297).** Standing decisions are **constraints** on your implementation, not advisory context — do not contradict one. + +| Backend | How to load | +|---------|-------------| +| **markdown** | Read `{project}/.do-work/decisions.md` if it exists | +| **linear** | **Read decisions** helper in `agents/tracker/linear.md` — Team Doc `tracker.linear.decisions_doc_title` (default `do-work/decisions`); missing → empty. Do **not** use local `decisions.md` as the store | + +Grammar is identical either backend (SKILL.md § Decisions Memory): `YYYY-MM-DD | UR/REQ ref | decision | rationale` (Linear issue ids may appear in the ref slot). If your REQ's task or acceptance criteria require you to act against a recorded decision line (e.g. the REQ asks you to add client-side validation but a decision line reads `... | validation lives server-side | ...`), do not silently override it — return `status: stopped` with `reason: scope-creep` (if the REQ pushes new behaviour past a standing boundary) or `reason: ambiguous-criteria` (if the REQ and the decision are in direct conflict and you cannot tell which governs), naming the specific decision line verbatim in your report details so the orchestrator can route it for human resolution. If the store is absent (no decision recorded yet), this substep is silently a no-op — never create the store just to read it. ### 3. Execute TDD — red first @@ -217,7 +269,7 @@ Review each acceptance criterion in the REQ. Mark each `- [x]` as you verify it. ### 6. Execute verification steps -**Section scope.** Only `## Verification Steps` items are part of the checkpoint loop. The `## Post-merge validation` section (if present in the REQ) is **never executed by the worker** — those items run after the orchestrator merges and are the `/do-work close` or approval flow's responsibility, not yours. +**Section scope.** Only `## Verification Steps` items are part of the checkpoint loop. The `## Manual checks (advisory)` section (if present in the REQ) is **never executed by the worker** — those items are archived as advisory follow-up after automated closure, not worker responsibilities. Read `## Verification Steps` from the REQ. Execute each step in order: @@ -226,19 +278,53 @@ Read `## Verification Steps` from the REQ. Execute each step in order: | `test` | Bash: run the command, check exit code 0 / matching output | | `build` | Bash: run the build command, check exit code 0 and no errors | | `runtime` | Ensure the dev server is running (start in background if not, wait healthy), run the command, compare output to expected | -| `ui` | Playwright: navigate to the URL, take a snapshot, confirm the specified element/text | +| `ui` | **Playwright screenshot + vision assert (mandatory visual check).** See **UI screenshot contract** below. | Record the result of each step in an ordered checkpoint log. Each checkpoint entry must include `step`, `total`, `type`, command/action, expected result, pass/fail status, and a short actual-output summary. If the step crosses a boundary, include the handoff name (for example `input -> persistence`, `API -> render`, or `command -> file`). +**UI screenshot contract (HARD RULE for every `ui` step):** + +A `ui` step is a **visual** check. It is not satisfied by an accessibility tree snapshot, DOM text scrape, or HTML source alone. + +1. **Ensure the app is reachable** — start the dev server in the background if needed; wait until healthy (same as `runtime`). +2. **Navigate with Playwright** to the URL/route in the step (Playwright CLI / project playwright skill — e.g. `playwright-cli open` / `screenshot`, or the project's equivalent). +3. **Capture a PNG screenshot** to a durable path under the parent UR: + ``` + {project}/.do-work/user-requests/UR-NNN/ui-evidence/REQ-NNN-step-.png + ``` + Create `ui-evidence/` if missing. Use the REQ's `**UR:**` field for `UR-NNN` and the verification step number for ``. +4. **Vision-read the image** — open the PNG with the harness Read/vision tool (multimodal). Assert the step's Expected outcome from what is **visible in the image** (element present, text readable, state shown). Do not pass solely because the a11y tree or page text contained a string. +5. **Record evidence** — checkpoint `actual` and acceptance evidence for this step must cite the screenshot path: + - checkpoint: include the path in `command`/`actual` (e.g. `screenshot:.do-work/user-requests/UR-NNN/ui-evidence/REQ-NNN-step-1.png`) + - acceptance evidence: `type: ui` with `ref:` set to that same path (project-relative preferred) + +**Insufficient for pass (do not mark `ui` passed):** + +- Accessibility / ARIA snapshot only +- DOM query or `page.content()` text match without a PNG on disk +- Narrative claim ("looked fine") with no screenshot path +- Empty, zero-byte, or missing PNG file + +**Playwright / browser unavailable — not deferrable:** + +If Playwright (or a usable browser binary) cannot run in this worktree, a `ui` step is **executable but failing infrastructure**, not an inherent `environment` deferral. Attempt the step; on failure follow the normal retry path (up to 3), then return `status: stopped`, `reason: verification-failing` with `failed_step` pointing at the `ui` step. **Do not** mark the step `status: deferred` with `category: environment` solely because Playwright is missing. (True human/device-only checks remain deferred when they appear in the REQ; automated `ui` screenshot steps never convert into advisory manual checks.) + **Heartbeat checkpoint:** after each verification step, stamp the heartbeat — `{skill-root}/lib/heartbeat.sh "$REQ_PATH"` — so a long verification sequence never lets the slot drift stale. **Deferred checkpoint status.** Some verification steps are *inherently* non-executable in a worktree — not because the implementation is wrong, but because running them is structurally impossible regardless of retries: - **`human`** — the step explicitly requires human judgment or confirmation ("Confirm the badge looks correct", "Ask the user to approve"). - **`device`** — the step requires a physical device or external hardware not available in the worktree (mobile device, IoT sensor, etc.). -- **`environment`** — the step requires an environment the worker genuinely cannot provision after a real attempt (a dev server that has no runtime in this worktree, external credentials that are not present, a third-party sandbox that cannot be reached). +- **`environment`** — the step requires an environment the worker genuinely cannot provision after a real attempt (a dev server that has no runtime in this worktree, external credentials that are not present, a third-party sandbox that cannot be reached). **Missing or installable test/build tooling (`vendor/`, `node_modules/`, `.venv/`, etc.) is explicitly NOT a valid `environment` deferral** — step W3.5 runs `{skill-root}/lib/provision-worktree.sh` specifically to supply it; treat a missing dep dir as a provisioning gap to be resolved there, not as grounds for deferral here. + +When you encounter a genuinely non-executable step (`human`, `device`, or `environment` per the above), mark it `status: deferred` in the checkpoint log with a `category` field (`human`, `device`, or `environment`) and a one-sentence `reason` explaining why it cannot be executed here. Add the step to `deferred_checks:` in the Return Report. Then **continue** with the remaining steps. + +**Unprovisionable test/build tooling — loud, human-tracked path.** When a `test` or `build` verification step cannot run because the W3.5 provisioner reported `unprovisionable:` for the required dependency dir AND `worktree.setup_command` did not resolve it, the worker MUST NOT mark the step `deferred`-and-pass as an `environment` deferral, and MUST NOT silently proceed to `done` as if the suite ran. Instead: -When you encounter such a step, mark it `status: deferred` in the checkpoint log with a `category` field (`human`, `device`, or `environment`) and a one-sentence `reason` explaining why it cannot be executed here. Add the step to `pending_validation:` in the Return Report. Then **continue** with the remaining steps. +1. Do NOT classify this as a `human`, `device`, or `environment` deferral. +2. Route the un-run suite to `deferred_checks:` in the Return Report with `category: suite-not-run` (distinct from `human` / `device` / `environment`) and a plain-language `reason` such as: `Run the test suite — dependencies could not be provisioned in the worktree — confirm green`. +3. The orchestrator consolidates that entry into the archived REQ's `## Manual checks (advisory)` section as an unchecked advisory item, and — because the item carries `category: suite-not-run` — additionally stamps a `**Suite:** not-run` header on the archived REQ (see `agents/run.md` Step 4b sub-step 5a). +4. Continue to Step 7 and return `status: done`. The code merges, the REQ archives as done, and the un-run suite becomes explicit advisory follow-up outside the blocking closure path — but the `**Suite:** not-run` marker makes `lib/derive-status.sh` derive the REQ `unproven` until the suite actually runs. The documented stopper-reason enum is unchanged; no new stopper is introduced. **Critical distinction — deferred vs. failing:** - A step that is *executable* but currently failing (test red, endpoint 500s, build broken) is **not** eligible for deferral. It follows the normal retry path and, after 3 retries, returns `verification-failing`. @@ -327,14 +413,31 @@ git status # confirm only REQ-NNN pat git add path/to/changed/implementation/files... # implementation files, listed explicitly git add {project}/.do-work/user-requests/UR-NNN/... # only if this REQ touched UR-owned files +# Markdown backend (default) — REQ-NNN scope: git commit -m "feat(REQ-NNN): short title REQ: {project}/.do-work/working/REQ-NNN-slug.md UR: {project}/.do-work/user-requests/UR-NNN/input.md Output: path/to/primary/output" + +# Linear backend (tracker.backend: linear) — design §6.5; Linear issue id only: +# git commit -m "feat(ENG-123): short title +# +# Issue: ENG-123 +# UR: UR-007 +# Output: path/to/primary/output" ``` -Note the commit message's `REQ:` line points at `working/` (the live slot at commit time), not `archive/`. The orchestrator will rewrite the file system path when it archives the REQ post-merge, but the commit message text is fine as-is — it documents the REQ id, not a stable filesystem path. +**Commit message by tracker backend (REQ-294 / design §6.5):** + +| Backend | Subject | Footer | +|---------|---------|--------| +| **markdown** | `feat(REQ-NNN): short title` | `REQ:` working path; `UR:` input path; `Output:` primary path | +| **linear** | `feat(ENG-123): short title` (Linear issue id) | `Issue: ENG-123`; `UR: UR-NNN` when known; `Output:` primary path — **no** `.do-work/archive/REQ-…` path required | + +**Branch naming (REQ-295):** under Linear, the worktree branch **is** `req/` (W2). Feature-branch isolation is unchanged. Never use `req/REQ-NNN` naming when the active backend is `linear`. + +Note (markdown): the commit message's `REQ:` line points at `working/` (the live slot at commit time), not `archive/`. The orchestrator will rewrite the file system path when it archives the REQ post-merge, but the commit message text is fine as-is — it documents the REQ id, not a stable filesystem path. If `.do-work/` is gitignored in the project, the `.do-work/...` paths above will fail to add — that is expected. Stage and commit only the implementation files. Do not use `--no-verify`. Do not skip hooks. @@ -440,16 +543,19 @@ checkpoint_log: actual: "" status: passed # or "deferred" for inherently non-executable steps handoff: "" -pending_validation: [] # list of deferred verification steps; empty list when nothing deferred - # each entry: { step: "", category: human|device|environment, reason: "" } - # example: [{ step: "Confirm badge renders on user's phone", category: device, - # reason: "Requires physical iOS device not available in worktree" }] +deferred_checks: [] # list of deferred verification steps; empty list when nothing deferred + # each entry: { step: "", category: human|device|environment|suite-not-run, reason: "" } + # human/device/environment are advisory only and never affect proven-ness. + # suite-not-run is reserved for the W3.5-unprovisionable path (Step 6) and is the + # only category that makes the orchestrator stamp `**Suite:** not-run` on archive. + # example: [{ step: "Confirm badge renders on user's phone", category: device, + # reason: "Requires physical iOS device not available in worktree" }] acceptance: AC1: status: passed evidence: - type: test # one of test, command, file, runtime_check, ui - ref: "" + ref: "" # for type: ui, ref MUST be the ui-evidence PNG path (file must exist) milestone_complete: false milestone: "" # active milestone id when milestone_complete is true retry_count: 0 # integer — number of conflict retries consumed (0 = no retries) @@ -462,8 +568,8 @@ Field rules: - `status: done` → `commit` must be set; `reason` empty - `status: done` → `closure_proof` must be non-empty and reference the checkpoint log plus completing commit (for example `checkpoint_log:passed commit:abcdef1`) - `status: done` requires every acceptance criterion to carry `status: passed` with evidence — acceptance criteria can never be deferred. A REQ whose only AC is human-judgment-based has genuinely ambiguous criteria and must return `reason: ambiguous-criteria`, not defer the AC. -- `status: done` with deferred verification steps is valid provided all non-deferred steps passed and all ACs have evidence. The deferred steps are listed in `pending_validation:` for the orchestrator to route. -- `pending_validation:` is always present: empty list (`[]`) when nothing was deferred, populated list when one or more steps were deferred. +- `status: done` with deferred verification steps is valid provided all non-deferred steps passed and all ACs have evidence. The deferred steps are listed in `deferred_checks:` for the orchestrator to consolidate into the archived REQ's advisory section. +- `deferred_checks:` is always present: empty list (`[]`) when nothing was deferred, populated list when one or more steps were deferred. - `status: stopped` → `reason` must match the enum above; `commit` empty - `status: failed` → unrecoverable error (exception thrown, file write failed); `reason: unknown-error` or specific - Always include `milestone_complete` (defaults to `false`) @@ -486,4 +592,4 @@ Field rules: - **Stay in scope.** If the REQ would require changes outside its stated scope, return `status: stopped` with `reason: scope-creep`. - **Stop on ambiguity.** If acceptance criteria are genuinely ambiguous, return `status: stopped` with `reason: ambiguous-criteria`. Do not guess. - **Worktree teardown belongs to the orchestrator.** Workers MUST NOT run `git worktree remove` or `git branch -d`. After you return `status: done`, the orchestrator merges the feature branch, archives the REQ, and tears down the worktree. Running teardown from the worker double-deletes the worktree and can corrupt the orchestrator's post-merge steps. -- **Never invent stopper reasons.** The `reason` field in a stopped/failed report MUST be one of the documented enum values: `tests-failing`, `verification-failing`, `missing-creds`, `ambiguous-criteria`, `scope-creep`, `dependency-missing`, `unknown-error`, `concurrent-conflict`. Do not improvise values outside this list (for example `awaiting-human-verification` is not a valid reason — if the worker hits an inherently non-executable verification step, use `status: deferred` in the checkpoint log and add the step to `pending_validation:` so the orchestrator can route it, then continue toward `status: done`). Inventing reasons outside the enum breaks downstream tooling (status, resume, unblock commands) that pattern-matches on these values. +- **Never invent stopper reasons.** The `reason` field in a stopped/failed report MUST be one of the documented enum values: `tests-failing`, `verification-failing`, `missing-creds`, `ambiguous-criteria`, `scope-creep`, `dependency-missing`, `unknown-error`, `concurrent-conflict`. Do not improvise values outside this list (for example `awaiting-human-verification` is not a valid reason — if the worker hits an inherently non-executable verification step, use `status: deferred` in the checkpoint log and add the step to `deferred_checks:` so the orchestrator can route it into advisory archive data, then continue toward `status: done`). Inventing reasons outside the enum breaks downstream tooling (status, resume, unblock commands) that pattern-matches on these values. diff --git a/agents/run.md b/agents/run.md index 4e3f7d4..02ddcba 100644 --- a/agents/run.md +++ b/agents/run.md @@ -17,9 +17,11 @@ Points where the orchestrator must apply judgment rather than follow a determini | J5 | Step 1 idle-wait (deps/overlap/scope) | Deadlock vs slow-but-live: continue waiting or surface to user? | | J6 | Step 7b drain | Sibling slot not drained after 30 min: continue polling or surface to user? | | J7 | Step D suite failure | Which REQ is responsible for a failing test? | -| J8 | Parallel Run Mode → window fill | Window has free slots but `pick-req.sh` returns empty (overlap/deps): refill now, or wait for a live worker to free a footprint? | +| J8 | Parallel Run Mode → window fill | Window has free slots but pick returns empty (overlap/deps): refill now, or wait for a live worker to free a footprint? | | J9 | Parallel Run Mode → merge queue | Multiple reports ready: which to admit to Stage B next, and is a mid-queue stopper isolated from its siblings? | +Full serial sequences: [references/run-loop.md](../references/run-loop.md). Parallel + drain: [references/run-parallel.md](../references/run-parallel.md). + --- ## When Invoked @@ -28,1302 +30,156 @@ Points where the orchestrator must apply judgment rather than follow a determini /do-work run [UR-NNN] [--parallel N] [--budget ] ``` -You will be given a project do-work path: - -``` -{project}/.do-work/ -``` - -The optional `UR-NNN` argument scopes this orchestrator's claim loop to a single UR. Read it immediately after startup: - -```bash -# $1 is the optional UR-NNN argument passed by the caller -if [ -n "${1:-}" ]; then - SCOPE="$1" # e.g. UR-002 -else - SCOPE="any" # default — consider all REQs in backlog -fi -``` - -Scope is an **in-memory filter, NOT a hard reservation**. Other orchestrators launched without a scope (or with a different scope) can still claim REQs in the same UR. Use scope to focus an orchestrator; do not rely on it to exclude siblings. +Project path: `{project}/.do-work/`. Optional `UR-NNN` scopes the claim loop (in-memory filter, not a hard reservation). ### Parallel window width (`--parallel N`) -`--parallel N` sets how many workers this **single** orchestrator dispatches concurrently from one terminal. It is independent of (and composes with) the existing multi-terminal mode. Resolve the effective window width `N` once at startup: - -1. If `--parallel N` is passed, take that `N`. Otherwise read `parallel.max_workers` from `.do-work/config.yml` (default `1`). The flag overrides config per-run; config sets the project default. -2. Clamp: `N = min(N, 10)`. A request above the cap is clamped to **10** with a one-line notice (`--parallel clamped to 10`). The cap matches the existing 10-orchestrator design bound and protects the shared main working tree, the git object store, and the single `feedback.lock` from contention. -3. **`N == 1` (default — absent flag, `--parallel 1`, or `parallel.max_workers: 1`) ⇒ the existing serial `## The Loop` runs byte-for-byte unchanged.** Do not enter the parallel path. Everything below in `## The Loop`, `## When the Backlog is Empty`, and the gates is exactly as written. -4. **`N > 1` ⇒ follow `## Parallel Run Mode`** instead of the serial `## The Loop`. That section reuses every existing step (claim, dispatch, gates, integrate, recover, drain) and changes only the *shape* of the hot path: window-fill instead of one-at-a-time claim, and a serialized merge queue instead of inline integration. +1. Flag overrides `parallel.max_workers` (default `1`); clamp `N = min(N, 10)`. +2. **`N == 1` ⇒ serial `## The Loop`** (outline below; full body in [run-loop.md](../references/run-loop.md)). +3. **`N > 1` ⇒ [Parallel Run Mode](../references/run-parallel.md)** — window fill + serialized merge queue; reuses the same step semantics. ### Budget (`--budget `) -`--budget ` sets a cumulative spend ceiling for this run. Resolve the effective budget **once at startup**, before the first claim: - -1. If `--budget ` is passed, take that value. Otherwise read `cost.budget` from `.do-work/config.yml`. The flag **overrides config for this invocation only**; it does not write back to disk. -2. **Empty / unset budget ⇒ unlimited** (today's behaviour, no regression). The budget gate below is inert: never compute a sum, never stop for budget. Only a non-empty budget arms the gate. -3. `` is a **bare number** in the budget unit defined below (e.g. `--budget 5.00`). Strip a leading currency symbol if present; reject a non-numeric value with a one-line notice and run unlimited. - -#### Budget unit and per-attempt cost estimation - -The budget unit is **estimated US-dollar model spend**, recorded per worker attempt in the ledger's numeric `cost_estimate_num` field (`lib/run-ledger.sh --cost-estimate`). It is an **estimate, not a metered bill** — the harness does not expose per-call token counts to the orchestrator, so the orchestrator derives the estimate from two signals it *does* control: - -- **Model tier** of the dispatched worker (`## Model Selection`): a per-attempt base cost — `sonnet` cheaper, `opus` more expensive (opus ≈ 5× sonnet as a rough tier multiplier). -- **Worker turn volume**: a small multiplier for retries / long attempts (a `stopped`-then-`opus`-retry attempt costs more than a clean first pass). +1. Flag overrides `cost.budget` for this invocation only; empty/unset ⇒ unlimited. +2. Unit: estimated US-dollar model spend (`cost_estimate_num` in ledger) — tier-weighted estimate, not a token meter. +3. Enforce at Step 3b budget gate; stop at next REQ boundary with budget-stop report. Round estimates up. -Compute a single per-attempt dollar estimate from `tier_base × turn_factor` and pass it to `run-ledger.sh --cost-estimate ` at Step 3b. **Document the imprecision honestly in the budget-stop report**: this is a tier-weighted estimate, not a token meter, so the stop fires when the *estimated* cumulative spend crosses the budget. Round per-attempt estimates conservatively (round up) so the gate trips early rather than overshooting silently — the promise is "do not silently exceed", and an estimate that errs toward stopping honours it. +Full budget/parallel resolution text: original detail lives in [run-loop.md](../references/run-loop.md) / [run-parallel.md](../references/run-parallel.md) consumers via When Invoked in those flows. --- ## Load Config -Read and follow the **Load Config** section of [config.md](config.md). +Read and follow the **Load Config** section of [config.md](config.md), including **step 8** (resolve `$SKILL_ROOT` / `{skill-root}` via walk-up from the loaded instruction file with marker requirements, or inherit a valid pre-set `$SKILL_ROOT`; hard-stop if unknown). -Keep `model.default`, `model.escalation`, `cost.budget`, and `ledger.enabled` in context for the run. Use `model.default` for ordinary worker dispatch and `model.escalation` for high-risk or retry-worthy work as described in model selection. Resolve the **effective budget** once at startup per `## When Invoked → Budget (--budget )`: the `--budget` flag overrides `cost.budget` for this invocation; empty/unset means unlimited. If the effective budget is non-empty, surface it in the run summary and ledger, and **enforce it at the Step 3b budget gate** — do not silently exceed an explicit user-provided budget; stop gracefully at the next REQ boundary with the budget-stop report. +Keep `model.default`, `model.escalation`, `cost.budget`, `ledger.enabled`, and the resolved `$SKILL_ROOT` in context. Resolve effective budget once at startup. If non-empty, enforce at the Step 3b budget gate. ---- - -## Agent Identity - -Each `/do-work run` process derives a stable `hostname.pid` identifier once at startup and reuses it for the lifetime of that run loop. - -### ID derivation - -```bash -AGENT_ID="$(hostname).$$" -# Example result: mbp-tom.42137 -``` +## Tracker load path -- `hostname` — machine name, distinguishes agents on different machines sharing a repo -- `$$` — the shell PID of the current `/do-work run` process, unique per process on the same machine -- The combined string is computed **once** when the orchestrator starts and stored in the shell variable `AGENT_ID` +Work-item storage goes **only** through named tracker port ops after config is loaded: -### Ownership stamp format +1. Resolve effective `tracker.backend` (missing/empty/whitespace → `markdown`). +2. Read `agents/tracker/port.md`. +3. Read `agents/tracker/.md` (e.g. `markdown.md` or `linear.md`). +4. Call **only** named port ops — never raw `.do-work/REQ-*` paths or raw Linear tools outside the backend doc. -When the orchestrator claims a REQ into `working/`, it inserts the following block at the top of the REQ file, immediately under the `# REQ-NNN:` heading and before the existing `**UR:** ...` field: +**Hard rules:** -```markdown - -**Claimed by:** -**Claimed at:** -**Heartbeat:** - -``` - -Example of a claimed REQ header: - -```markdown -# REQ-115: Pre-flight concurrent-slot check +- **No silent fallback** from `linear` to `markdown`. +- If backend is **`linear`** but `agents/tracker/linear.md` is missing/unreadable → **hard-stop**. +- Markdown backend: ops map — **invoke** coordination scripts as `bash {skill-root}/lib/...` after Load Config step 8 resolves `$SKILL_ROOT`; **catalog identity** remains `lib/*.sh` in `markdown.md` — use those ops; do not re-implement store details here. - -**Claimed by:** mbp-tom.42137 -**Claimed at:** 2026-05-15T14:03:22Z -**Heartbeat:** 2026-05-15T14:03:22Z - +### Claim / pick / heartbeat / archive — backend branch -**UR:** UR-025 -**Status:** in-progress -``` - -### Stamp lifecycle +| Concern | Markdown | Linear | +|---------|----------|--------| +| Pick | `list_claimable_reqs` → `lib/pick-req.sh` | `list_claimable_reqs` in linear.md / [linear-ops.md](../references/linear-ops.md) | +| Claim | `claim_req` → `lib/claim-req.sh` | `claim_req` — workflow + claim comment; never steal assignee | +| Heartbeat | `heartbeat_req` → `lib/heartbeat.sh` | `heartbeat_req` — claim-protocol comment | +| Archive | working/ → archive/ | `archive_req` after evidence + review gates | +| Run notes | `append_run_note` / ledger | Issue comment authoritative; local ledger optional telemetry | +| Commits / branches | `feat(REQ-NNN):` + `req/REQ-NNN` | `feat(ENG-123):` + `req/` | -| Phase | Actor | Action | -|---|---|---| -| Claim time | Orchestrator (REQ-114) | Inserts `` block after claiming the file into `working/` | -| Pre-flight | Sibling orchestrators (REQ-115) | Read `working/REQ-*.md` files; parse the block to attribute each slot to its owning agent | -| Archive time | Worker (this file) | Strips the `` block before moving the file to `archive/` | +**When `linear`:** do not call pick/claim/heartbeat bash as the store; leave claimed on mid-flight MCP death; no silent-release; no markdown fallback. Full table and Linear steps: [run-loop.md](../references/run-loop.md) (Tracker load path section retained in agent above is authoritative for hard rules). -The stamp is a filesystem-visible, human-readable contract. Archived REQs do not retain ownership metadata — only the git commit message records which agent committed the change. +**When `markdown`:** keep `lib/pick-req.sh` / `claim-req.sh` / `heartbeat.sh` sequences in [run-loop.md](../references/run-loop.md). --- -## Pre-flight Check +## Agent Identity (outline) -> **Default behaviour:** By default the orchestrator claims unblocked backlog work; stale-slot triage is a fallback that fires only when the backlog is empty for this agent. The `working/` scan at §3 is informational — it populates buckets used by the picker's overlap exclusion and, if the backlog is drained, the fallback prompt. It is NOT a gate on starting work. +- `AGENT_ID="$(hostname).$$"` once at startup. +- Markdown claim stamp: `` … `Claimed by` / `Claimed at` / `Heartbeat` … ``. +- Linear claim: workflow + claim-protocol comment (``) — see linear backend. -Before starting the loop: - -### 1. Branch and working-directory checks - -- Confirm you are on the correct git branch. -- Confirm your working directory is `{project}` (the user's repo), NOT the skill clone at `~/.claude/skills/do-work/`. All file edits and git commits must happen in `{project}`. If you are in the skills directory, `cd` to `{project}` before proceeding. -- **Ensure `.do-work/state/` exists.** Run `mkdir -p {project}/.do-work/state` defensively. Subsequent steps (stale reclaim, milestone mode, deadlock surfacing, gate-owner writes, final-suite lockfile) write here; installs from before REQ-170 may not have created the directory. - -### 2. Resolve agent id - -Compute `AGENT_ID` per `## Agent Identity`: - -```bash -AGENT_ID="$(hostname).$$" -``` - -### 2a. Resolve `{skill-root}` to a concrete absolute path - -`{skill-root}` is the directory these agent instructions were loaded from — the root of the do-work skill clone (the directory containing `agents/`, `lib/`, `SKILL.md`). The lib invocations throughout this file (`{skill-root}/lib/scan-stale.sh`, etc.) and in `agents/run-worker.md` (heartbeat, file-feedback) only resolve when `{skill-root}` is a real absolute path. A worker `cd`'d into a consumer project's worktree has no `lib/` of its own, so the orchestrator must resolve `{skill-root}` **once here** and substitute the concrete path into every `{skill-root}/lib/...` call it makes, and pass it to the worker (Step 2 dispatch) so the worker substitutes it too. - -Resolve it from the absolute path of the loaded agent file: - -```bash -# These instructions live at {skill-root}/agents/run.md, so the parent of agents/ is the root. -SKILL_ROOT="$(cd "$(dirname "")/.." && pwd)" -# Example: /Users/you/.claude/skills/do-work -``` - -When this project IS the do-work skill itself, `SKILL_ROOT` resolves to the project root and the lib calls work directly. When the project is any other repo, `SKILL_ROOT` points back at the skill clone where `lib/` actually lives. Use the resolved `$SKILL_ROOT` value everywhere the steps below write `{skill-root}`. - -### 2b. Generate or refresh the project context pack - -Workers run context-starved by design (their "When Invoked" rule). To raise implementation quality without making each worker re-explore the repo, the orchestrator maintains **one** project context pack at `{project}/.do-work/state/context-pack.md` and passes its path to every worker. One orchestrator-level scan amortises across every worker in every run. - -**Staleness rule (documented, pick-one): the pack is stale when it is older than 14 days OR more than 50 commits behind `HEAD`.** Regenerate only when stale or absent — a fresh pack costs no per-run scan. - -```bash -PACK="{project}/.do-work/state/context-pack.md" -REGEN=0 -if [ ! -f "$PACK" ]; then - REGEN=1 # absent → must generate -else - PACK_MTIME=$(stat -f %m "$PACK" 2>/dev/null || stat -c %Y "$PACK") - AGE_DAYS=$(( ($(date +%s) - PACK_MTIME) / 86400 )) - # Commits landed on HEAD since the pack was last written. - COMMITS_SINCE=$(git rev-list --count --since="@$PACK_MTIME" HEAD 2>/dev/null || echo 0) - if [ "$AGE_DAYS" -ge 14 ] || [ "$COMMITS_SINCE" -ge 50 ]; then - REGEN=1 # stale → refresh - fi -fi -``` - -**If `REGEN=0` (pack is fresh): skip generation entirely.** Do not scan, do not rewrite the file. This is the common case and it must carry zero per-run scan cost. - -**If `REGEN=1` (absent or stale): scan the project once and write a ~200-line pack.** Keep it to roughly 200 lines — a map, not a copy of the codebase. Cover: - -- **Architecture** — the top-level shape of the system (layers, services, entry points) in a few sentences. -- **Directory roles** — one line per significant top-level directory (what lives there, what it is for). -- **Key services / modules** — the handful of files or modules a worker is most likely to touch or extend, with a one-line role each. -- **Naming & test conventions** — how files, tests, and symbols are named; where tests live; the dominant test idiom. -- **How to run the suite** — the exact command(s) to run the project's tests (mirror `config.test.suite_command` when set). - -Write the result to `$PACK` (filesystem only — `.do-work/state/` is orchestrator-owned; do not commit it from here). The pack is project-level state, regenerated on the staleness cadence above, and read by every dispatched worker. - -### 3. Scan and classify working/ slots (informational — hold all buckets in memory, do not prompt) - -**Staleness detection — delegate to `lib/scan-stale.sh`:** - -```bash -STALE_SLOTS=$(bash {skill-root}/lib/scan-stale.sh) -``` - -`scan-stale.sh` (REQ-149, extended in REQ-172) reads `parallel.stale_threshold_seconds` from `.do-work/config.yml` (default 300 s) and prints one line per stale slot in the form ` age=`. Slots with a missing or malformed `**Heartbeat:**` are treated as stale by the script and emit `age=unknown`. The orchestrator does not re-implement this logic inline. - -**Ownership classification — inline (cheap deterministic read):** - -Glob `{project}/.do-work/working/REQ-*.md`. For each file found, read its ownership stamp (the `` block) and classify the slot into one of three buckets. **Retain all three buckets in memory. Do not prompt at this stage regardless of what the stale bucket contains.** - -| Bucket | Condition | Action | -|---|---|---| -| **`mine`** | `**Claimed by:**` in the stamp matches `AGENT_ID` | Resume this REQ — skip the claim step and jump directly to worker dispatch for it | -| **`sibling`** | `**Claimed by:**` is set, differs from `AGENT_ID`, AND the slot path is NOT in `$STALE_SLOTS` | Leave alone — another live orchestrator owns it | -| **`out-of-milestone`** | Milestone mode is active (`.do-work/state/active-milestone.md` exists) AND the slot's milestone id (parsed from the filename: `REQ-M-NNN-slug.md` → `M`) differs from the active milestone | Silently ignore — treat the same as `sibling` (a previous-milestone REQ still in flight during a milestone transition is informational only) | -| **`stale`** | Slot path appears in `$STALE_SLOTS` output | Hold in memory — surface only as fallback when backlog has no claimable REQ | - -### 3a. Timestamp reasoning rule - -All timestamps in REQ files (`**Claimed at:**`, `**Heartbeat:**`, and any -`` value) are UTC with a `Z` suffix. The local wall-clock -date may differ from the UTC date by ±1 day depending on the host's -timezone. Do NOT decide whether a slot is fresh by comparing the -heartbeat's calendar date to "today" — that reasoning will misclassify -recent slots as stale across the UTC/local date boundary. - -Slot staleness is determined solely by `$STALE_SLOTS` (the output of -`lib/scan-stale.sh`, which compares UTC epochs deterministically). -When you need to surface "how long ago" to the user, use the `age=` -token from `scan-stale.sh`'s output — not the raw ISO timestamp. - -### 3b. Legacy stranded REQ triage (advisory — no automatic state change) - -While classifying `working/` slots in §3, also identify **legacy stranded REQs**: files whose `**Status:**` is `stopped` and whose `**Reason:**` value is not in the documented stopper enum (`tests-failing`, `verification-failing`, `missing-creds`, `ambiguous-criteria`, `scope-creep`, `dependency-missing`, `concurrent-conflict`, `unknown-error`). The canonical example is `awaiting-human-verification`, an improvised reason used before the `pending-validation` state existed. - -**Detection:** for each `working/REQ-*.md` file, read `**Status:**` and `**Reason:**`. If `**Status:** stopped` AND `**Reason:**` is non-empty AND the reason does not match any enum value above, record the file as a **legacy stranded slot**. - -**Advisory output (emit once per run, immediately after §3 classification — do NOT block the run or prompt):** - -If any legacy stranded slots were found, print a triage notice before proceeding to §4: - -``` -⚠ Legacy stranded REQ(s) detected in working/: - - REQ-NNN reason: () - ... -These REQs stopped with an unrecognized reason and were never migrated to the -pending-validation state. Triage guidance (advisory — take the appropriate action manually): - • If the code for this REQ was already merged into the base branch: - → Move the REQ to .do-work/pending/ (set Status: pending-validation, strip the claim - block, empty the Closure proof field) and resolve via /do-work approve REQ-NNN or - /do-work reject REQ-NNN. - • If the code was NOT merged (the req/ branch still exists with unmerged commits): - → Unblock the REQ: /do-work unblock REQ-NNN (returns it to backlog for re-dispatch). -Run continues — no automatic state change was made. -``` - -This triage report is informational only. The orchestrator does NOT automatically move files, rewrite status fields, or modify any REQ. The human (or a subsequent operator) takes the appropriate action based on the guidance. The legacy slots are classified into the `stale` bucket for footprint exclusion purposes (same as any stopped slot). - -### 4. Resume any `mine` slot - -If the `mine` bucket is non-empty, resume that REQ — skip the claim step and jump directly to worker dispatch for it. - -### 5. Try the backlog (primary path) - -No `mine` slot is present. Immediately attempt `lib/pick-req.sh`: - -```bash -PICK_STDERR=$(mktemp) -REQ_PATH=$(bash {skill-root}/lib/pick-req.sh "$SCOPE" "$AGENT_ID" 2>"$PICK_STDERR") -``` - -`pick-req.sh` already excludes any REQ whose `**Files:**` overlaps with a slot in `working/` — **both `sibling` and `stale` slots are treated as in-flight** for the purpose of footprint exclusion. You do not need to communicate the stale list to the picker separately; it reads `working/` directly. - -- **If `pick-req.sh` returns a path:** claim it (proceed to The Loop, Step 1 claim sequence). **Do not surface any stale-slot prompt**, regardless of what `$STALE_SLOTS` contains. -- **If `pick-req.sh` returns nothing:** continue to §6. - -### 6. Fallback: backlog drained — evaluate working set - -Reached only when `pick-req.sh` returned no candidate AND the `mine` bucket is empty. Now the stale bucket matters: - -- **`stale` is non-empty:** prompt the user once (batch all stale slots into a single message — do NOT prompt per slot): - - ``` - N stale REQ(s) found in working/: - - REQ-NNN (claimed by , last activity ago) - - ... - These appear abandoned. Reclaim into this run, return to backlog, or abort? - ``` - - Where `` is derived from the `age=` token in `$STALE_SLOTS` output: convert seconds to the coarsest human unit that is non-zero (e.g. `42s`, `7m`, `2h`, `3d`). When `age=unknown`, render `unknown` in place of a duration. Do NOT use the raw ISO heartbeat timestamp to fill this field. - - - **Reclaim into this run:** For each stale REQ, rewrite its stamp to the local `AGENT_ID` and a fresh `**Claimed at:**` (ISO-8601 UTC). These REQs become the first ones this orchestrator processes in the loop — treat them as `mine`. - - Before rewriting the stamp, classify *why* the slot went stale and emit feedback (best-effort, non-blocking) iff there has been **no commit activity** touching any path under the REQ's `**Files:**` declaration in the last hour: - - ```bash - LAST_COMMIT_AGE_SEC=$(($(date +%s) - $(git log -1 --format=%ct -- 2>/dev/null || echo 0))) - if [ "$LAST_COMMIT_AGE_SEC" -gt 3600 ] || [ "$LAST_COMMIT_AGE_SEC" -eq "$(date +%s)" ]; then - # Classify the reason using the age= token from $STALE_SLOTS — not the raw - # ISO heartbeat. One of: - # no-heartbeat — age=unknown AND heartbeat field absent or malformed in the REQ file - # heartbeat-frozen — age= present (numeric) AND older than the stale threshold - # no-progress — age= present and under threshold but no commits on REQ's files - # Do NOT decide reason class by comparing **Heartbeat:** calendar dates to "today". - REASON_CLASS="" - FINGERPRINT="stale-slot:${REASON_CLASS}" - bash {skill-root}/lib/file-feedback.sh stale-slot \ - "$FINGERPRINT" \ - '{"req":"REQ-NNN","prior_owner":"","reason_class":"'"$REASON_CLASS"'","last_commit_age_sec":'"$LAST_COMMIT_AGE_SEC"'}' \ - "Stale-slot reclaim: REQ-NNN (${REASON_CLASS})" \ - "REQ-NNN sat in working/ with no commit progress in over an hour before reclaim. Prior owner appears abandoned; this orchestrator is taking the slot." \ - || true - fi - ``` - - > **JUDGMENT:** The title carries the REQ id and the reason class so an inbox skim tells you whether agents are dying silently (no-heartbeat) versus making progress without committing (no-progress). The body is one sentence — a single stale reclaim is routine; the inbox's fingerprint dedup surfaces the recurrence pattern. - - - **Return to backlog:** For each stale REQ, `git mv` it back to the backlog root, strip its ownership stamp, reset `**Status:**` to `backlog`, and commit per REQ. Stage **only** that REQ's file path — do not sweep `.do-work/`. Example: - ```bash - git mv {project}/.do-work/working/REQ-NNN-slug.md {project}/.do-work/REQ-NNN-slug.md - # edit the file to strip the claim block and reset Status - git add {project}/.do-work/REQ-NNN-slug.md - git commit -m "chore(REQ-NNN): return stale claim to backlog" - ``` - - **Abort:** Exit pre-flight and halt this orchestrator. - -- **`stale` is empty AND `sibling` is non-empty:** fall through to `## When the Backlog is Empty` — siblings are still doing the remaining work. - -- **`stale` is empty AND `sibling` is empty:** fall through to `## When the Backlog is Empty`. - -### 7. Backlog emptiness check - -If both `pick-req.sh` returned nothing (§5) AND the stale set is empty (§6 fallback was not triggered or returned to backlog), fall through to `## When the Backlog is Empty`. +Full stamp lifecycle: [run-loop.md](../references/run-loop.md) § Agent Identity. --- -## REQ Classification - -Before dispatching a worker for a REQ, classify the REQ to pick the most appropriate `subagent_type` for the `Agent` tool. Classification is config-driven: the routing rules live in `config.routing` (see `agents/config.md`), not hard-coded here, so the stock skill ships portable and each user routes specialist work to whatever subagents exist on their own machine. - -### Apply the routing config - -Read `config.routing` — the ordered list of `{match, agent}` rules loaded at startup (see `## Load Config` in `agents/config.md`). Then: - -1. Scan the REQ's `## Task`, `## Context`, `## Acceptance Criteria`, and `## Verification Steps`. -2. Walk the `routing` rules **top to bottom, first match wins**. Each rule's `match` is a signal description or keyword list; if the REQ's content fits it, the chosen `subagent_type` is that rule's `agent`, and you stop scanning. -3. If no rule matches — or `routing` is empty (the shipped default) — the `subagent_type` is `general-purpose`. +## Pre-flight Check (outline) -There are no hard-coded specialist agents in this section. Portable agents (`Explore` for pure exploration, `feature-dev:*` for architecture/review) are routed only when a `routing` rule names them — they are not assumed present. `agents/config.md` ships a commented example `routing` block reproducing the original specialist table; a user restores that behaviour by uncommenting it and confirming each named agent exists locally. +> Default: claim unblocked backlog; stale-slot triage is fallback when backlog empty. Working/ scan is informational, not a start gate. -### Fallback rule - -When no `routing` rule matches with confidence — or none is configured — **fall back to `general-purpose` silently**. Never block, never ask the user, never stop the loop on classification ambiguity. The cost of picking `general-purpose` for a specialist task is small; the cost of stalling the loop is large. - -### Logging - -Include the chosen `subagent_type` in the per-REQ progress line so the user can see routing decisions: - -``` -Starting REQ-NNN [type=general-purpose]: [title] -``` +1. Branch + working-directory checks; `mkdir -p {project}/.do-work/state`. +2. Resolve `AGENT_ID`; resolve `{skill-root}` / `$SKILL_ROOT` via **Load Config step 8** (`agents/config.md` — single home; hard-stop if unknown); refresh context pack. +3. Scan/classify working slots (markdown) or in-flight Linear claims — mine / sibling / stale buckets. +4. Resume any `mine` slot. +5. Try backlog (primary); else evaluate working set; else empty-backlog path. -This is the only "progress" signal the orchestrator emits before the worker returns — the worker runs in a separate session and its output does not stream back. Plan accordingly. +Full pre-flight sequences: [run-loop.md](../references/run-loop.md) § Pre-flight Check. --- -## Model Selection +## REQ Classification / Model Selection (outline) -After classifying `subagent_type`, pick a `model` for the dispatch. Default to `sonnet` to save tokens. Escalate to `opus` only when the REQ shows signals of genuine difficulty. +- Classification: first matching `config.routing` rule → `subagent_type`; else `general-purpose`. +- Model: Size / risk signals → `model.default` or `model.escalation` (typically sonnet / opus); log on announce line. -### Primary signals → model - -Two structural signals are read directly from the REQ header and take precedence over everything below — check them first, in order: - -| Primary signal | model | -|---|---| -| REQ has a previous `status: stopped` attempt recorded in its body (retry after Sonnet failed) | `opus` | -| REQ header carries `**Size:** L` (capture sized this REQ large from its file count / layer span / criteria count) | `opus` | - -If either primary signal fires, select `opus` and skip the lexical scan. The `**Size:**` field, when present, is capture's own up-front difficulty estimate — trust it over re-deriving difficulty from prose. - -### Fallback signals → model (REQs without `**Size:**`) - -When the REQ has **no `**Size:**` field** (legacy REQs, or capture left it off because the shape was ambiguous), fall back to scanning the REQ's `## Task`, `## Context`, and `## Acceptance Criteria` (top to bottom; first match wins). When `**Size:** S` or `**Size:** M` is present, these lexical rules still apply as a secondary check but never downgrade a `Size: L`: - -| Fallback signal in REQ | model | -|---|---| -| Task touches 4+ distinct files, OR spans 3+ layers (e.g. controller + model + view + test) | `opus` | -| Task introduces new architecture: new service, new abstraction, new module boundary, schema design, or "design X" | `opus` | -| Task involves debugging across layers, race conditions, concurrency, or performance investigation | `opus` | -| `subagent_type` is `feature-dev:code-architect` or `feature-dev:code-reviewer` | `opus` | -| Anything else: single-file edits, doc/markdown updates, agent/skill/config edits, mechanical refactors, scoped bug fixes, test additions, exploration | `sonnet` | - -### Fallback rule - -When in doubt, **default to `sonnet`**. The worker's stopping-rules already catch failures: if Sonnet can't make tests pass after 3 attempts, it returns `status: stopped` and the orchestrator's retry path picks `opus` automatically (signal #1 above). - -### Logging - -The chosen `model` appears in the per-REQ announce line alongside `subagent_type` (see Step 1). +Full tables: [run-loop.md](../references/run-loop.md). --- -## The Loop - -Repeat until the backlog is empty: - -### Step 1: Claim the next REQ - -#### Step 1.0 — Milestone filter (milestone mode only) - -Before globbing the backlog, check whether `{project}/.do-work/state/active-milestone.md` exists. - -- **File absent (non-milestone mode):** skip this step entirely — proceed to the backlog glob as written below, behaviour unchanged from REQ-114. -- **File present (milestone mode):** - 1. Read the file. Its contents are a single line such as `M1` or `M2`. Trim whitespace to obtain ``. - 2. **Constrain the candidate glob** to `{project}/.do-work/REQ-M-*.md` instead of `{project}/.do-work/REQ-*.md`. Sort ascending and iterate exactly as the steps below describe. - 3. **No fallback to other milestones.** If the constrained glob returns no files, the active milestone's backlog is drained — fall through to **Step 1.0a: Sibling idle-waiting** below. The orchestrator MUST NOT silently widen the glob to pick up REQs from other milestones. The deploy gate (Step 7b) is the only mechanism that advances `active-milestone.md` to the next milestone. - -#### Step 1.0a — Sibling idle-waiting (milestone mode, empty active-milestone backlog) - -Reached only when Step 1.0 found the active milestone's backlog empty. The local orchestrator may be a *sibling* — another orchestrator could already be handling the deploy gate. Do not fall through to `## When the Backlog is Empty` yet; first check whether a gate is in progress. - -1. Re-read `{project}/.do-work/state/active-milestone.md` and capture its contents as ``. -2. Check `{project}/.do-work/state/gate-owner.md`: - - **File absent:** No sibling has claimed the gate. This orchestrator has finished its in-flight REQ and the milestone backlog is empty, but no one has surfaced the gate yet. Fall through to `## When the Backlog is Empty` — this is the genuine drain path for a single-orchestrator run, or the loser of a race where the gate-owner will detect milestone completion on its own next worker return. - - **File present:** Read the single line — the ``. If it equals the local `AGENT_ID`, this orchestrator already owns the gate (re-entry after a restart mid-prompt) — jump to Step 7b. Otherwise enter **idle-waiting** mode. -3. **Idle-waiting loop.** Log exactly once: - - ``` - [] Idle — waiting on milestone M deploy gate (handled by ). - ``` - - Then poll `{project}/.do-work/state/active-milestone.md` every 30 seconds: - - **File contents changed** (new milestone id, e.g. `M`): the gate-owner advanced. Exit idle-waiting and restart the loop at Step 1 (which will re-read the new active milestone and glob accordingly). - - **File deleted:** the gate-owner stopped the run (user answered `n` to the gate prompt). Exit idle-waiting and fall through to `## When the Backlog is Empty` — the sibling exits cleanly. - - **File unchanged AND `gate-owner.md` deleted while `active-milestone.md` is also gone:** treat as stop. Fall through to `## When the Backlog is Empty`. - - **File unchanged after 30 minutes:** the gate-owner appears stuck. Surface to the user: `Gate owner has not resolved milestone M after 30 minutes. Continue waiting, or abort?` and act on the user's response. - - **Otherwise:** continue polling. - -No commits are made while idle-waiting — the orchestrator is reading state files only. - -**Compute your agent-id** using the rule in `## Agent Identity`: - -```bash -AGENT_ID="$(hostname).$$" -``` - -**Scope argument:** `SCOPE` is derived from the optional `UR-NNN` argument at startup (see `## When Invoked`). Default is `any`. When `/do-work run UR-NNN` is invoked, `SCOPE=UR-NNN` and the picker filters out REQs whose `**UR:**` field does not match. The picker is also milestone-aware: when `state/active-milestone.md` exists it constrains its glob to `REQ-M-*.md` regardless of `SCOPE`. - -**Pick the next claimable REQ — delegate to `lib/pick-req.sh`:** - -```bash -PICK_STDERR=$(mktemp) -REQ_PATH=$(bash {skill-root}/lib/pick-req.sh "$SCOPE" "$AGENT_ID" 2>"$PICK_STDERR") -``` - -`pick-req.sh` (REQ-145) applies the full scope / dependency / footprint-overlap filter in one pass and prints the absolute path of the first claimable REQ to stdout (exit 0), or nothing (exit 1) if no candidate survives. Its stderr carries one `:` line per rejected candidate. - -**If `pick-req.sh` returns empty (exit 1) — classify and branch:** - -```bash -CLASSIFICATION=$(cat "$PICK_STDERR" | bash {skill-root}/lib/drain-classify.sh) -rm -f "$PICK_STDERR" -``` - -`drain-classify.sh` (REQ-152) reads the stderr lines and emits one of four labels, precedence `overlap-blocked > deps-blocked > scope-blocked > truly-empty`: - -| Classification | Meaning | Action | -|---|---|---| -| `overlap-blocked` | At least one candidate blocked by footprint overlap with a sibling slot | Idle-wait (see below) | -| `deps-blocked` | All survivors blocked on unsatisfied dependencies | Idle-wait (see below) | -| `scope-blocked` | All candidates excluded by the `` filter | Idle-wait (see below) — a new capture or a scope change can add eligible REQs | -| `truly-empty` | No candidates considered at all (backlog drained for this picker view) | Fall through to `## When the Backlog is Empty` | - -**Idle-wait loop** (entered on `overlap-blocked`, `deps-blocked`, or `scope-blocked`). Log the entry classification once, then poll every **30 seconds**, max **30 minutes**: - -```bash -ELAPSED=0 -while [ "$ELAPSED" -lt 1800 ]; do - sleep 30 - ELAPSED=$((ELAPSED + 30)) - # Refresh heartbeat on a still-owned slot, if any. No-op when CURRENT_SLOT is unset. - if [ -n "${CURRENT_SLOT:-}" ] && [ -e "$CURRENT_SLOT" ]; then - bash {skill-root}/lib/heartbeat.sh "$CURRENT_SLOT" >/dev/null 2>&1 || true - fi - # Re-pick. - REQ_PATH=$(bash {skill-root}/lib/pick-req.sh "$SCOPE" "$AGENT_ID" 2>"$PICK_STDERR") - if [ -n "$REQ_PATH" ]; then - break # back to the claim step - fi -done -``` - -On 30-minute timeout, **run deadlock detection before falling back to the generic prompt**: - -```bash -DEADLOCK_OUT=$(bash {skill-root}/lib/deadlock-check.sh) -``` - -`deadlock-check.sh` (REQ-156) prints empty stdout when no deadlock is detected and a structured report otherwise. Branch on its output: - -**If `DEADLOCK_OUT` is empty (no deadlock):** Surface the generic prompt to the user: `Claim blocked () — still no claimable REQ after 30 min. Continue waiting, or abort?` Act on user response. - -**If `DEADLOCK_OUT` is non-empty (deadlock detected):** - -1. Parse the report. Extract `signal`, `fingerprint`, `diagnosis`, `live-slots`, `stale-slots`, `backlog-size`, `last-commit-age`. -2. **Ensure `state/` exists.** Run `mkdir -p {project}/.do-work/state` before any lock acquisition or state write. This is defensive — installs created before REQ-170 may not have `state/`, and orchestrators must not crash on a missing directory. -3. **Acquire the surfacing lock** via `flock -n` on `.do-work/state/feedback.lock` so only one orchestrator writes `deadlock.md` and surfaces to the user. Siblings that fail to acquire the lock skip steps 4–6 and exit the idle-wait loop quietly (they will pick up via their own timeout if the deadlock persists). -4. **Lock-holder only:** write `{project}/.do-work/state/deadlock.md` containing the full `deadlock-check.sh` output plus a timestamp. This file is the cross-process signal that the deadlock has been surfaced. -5. **Lock-holder only:** emit feedback by calling `bash {skill-root}/lib/file-feedback.sh deadlock "" ''` where `` is a single-line JSON object with `signal`, `live-slots`, `stale-slots`, `backlog-size`, `last-commit-age`, `classification` (the idle-wait entry classification). The script handles its own enable/disable, deduplication, and lock-on-feedback.lock — call it best-effort and continue regardless of exit code. -6. **Lock-holder only — surface to the user**, gated on standalone mode only (recovery prompts are workflow-critical and must not depend on `config.next_steps.enabled`): - - **If standalone** (not running as a delegate inside go): use the `AskUserQuestion` tool with options: - 1. **"Reset stale slots"** — return any slots listed in `stale-slots` to the backlog (per the Pre-flight stale-slot return path). - 2. **"Show situation room"** — print the suggestion `Run /do-work status` and exit cleanly. - 3. **"Unblock a REQ"** — ask which REQ id; print `Run /do-work unblock REQ-NNN` and exit cleanly. - 4. **"Abort"** — exit this orchestrator cleanly. - - **If delegate** (running inside go): print the diagnosis block (the `deadlock-check.sh` output plus a one-line summary) and exit cleanly. Do not prompt. - -> **JUDGMENT:** The deadlock diagnosis must distinguish a stuck deadlock from a slow-but-live backlog. `deadlock-check.sh` returning a report is strong evidence (no commits in 5 min OR all slots stale OR runtime cycle) — trust it and surface. Empty output means heartbeats are still advancing or commits are landing; in that case the generic "continue waiting?" prompt is correct. Never silently keep idling past the 30-minute mark — either the deadlock path or the user prompt must fire. - -**If `pick-req.sh` returns a path (exit 0) — claim it atomically via `lib/claim-req.sh`:** - -```bash -COMMIT_HASH=$(bash {skill-root}/lib/claim-req.sh "$REQ_PATH" "$AGENT_ID") -``` - -`claim-req.sh` (REQ-146) performs the `git mv` → stamp insertion → `Status: in-progress` update → stage → commit sequence atomically and prints the commit short hash to stdout. On failure it writes a diagnostic to stderr and exits non-zero: - -- **Exit 2 (`Claim lost: REQ-NNN`)** — a sibling won the race on this exact file. Re-run `pick-req.sh` from the top of Step 1 (the lost candidate is now in `working/` and will be excluded by the overlap filter). -- **Any other non-zero exit** — log the stderr diagnostic and re-run `pick-req.sh` after a 2 s backoff. After 3 consecutive non-race failures, stop and report to the user. +## The Loop (outline) -After a successful `claim-req.sh`: +Repeat until backlog empty (or budget/stopper). **Full step bodies:** [run-loop.md](../references/run-loop.md) § The Loop. -**Announce:** - -``` -[] [scope=] Starting REQ-NNN [type=, model=, isolation=]: [title] -``` - -### Step 2: Dispatch the worker subagent - -Read all of [agents/run-worker.md](run-worker.md) — that is the worker's full instruction set. You will pass it inline to the dispatched subagent. - -Determine `subagent_type` using the rules in `## REQ Classification` above. Default to `general-purpose`. -Determine `model` using the rules in `## Model Selection` above. Default to `sonnet`. - -#### Step 2a: Criteria provenance note - -Read the REQ header's `**Criteria approved:**` value when present, but do not block worker dispatch based on it. `agent-drafted` is provenance, not a pre-run approval requirement. If a REQ exists in the backlog and its dependencies, footprint, scope, and policy gates allow it to run, dispatch the worker. - -Unexpected ambiguity still stops the run: if the acceptance criteria are missing, contradictory, impossible to verify, or become invalid during implementation, the worker must return `status: stopped` with `reason: ambiguous-criteria` or `verification-failing`. Do not ask for approval merely because criteria were generated by capture. - -Identify the **prior-REQ archived paths** for the same UR — these provide the worker context about what has already been built: - -1. Read the REQ's `**UR:**` field -2. Glob `{project}/.do-work/archive/REQ-*.md` -3. For each archived REQ, read its `**UR:**` field and keep only those matching the current UR -4. Pass the resulting absolute paths to the worker - -Dispatch via the `Agent` tool. Pass the worker **five named inputs** — REQ path, UR path, prior-REQ paths, the project context-pack path (from Pre-flight Step 2b), and the resolved skill-root (from Pre-flight Step 2a) — plus the run-worker.md instructions inline. Substitute the concrete `$SKILL_ROOT` value for `{skill-root}` in the instructions you paste so the worker's `{skill-root}/lib/...` calls resolve to a real path: - -``` -Agent( - description: "Run worker for REQ-NNN", - subagent_type: , - model: , - prompt: """ -You are the Run Worker. Follow the instructions below exactly. Prefer the inputs given; bounded exploration of files your implementation genuinely touches is allowed (see your When Invoked rule). Do not load other REQs or URs. - - -REQ: {absolute path to working/REQ-NNN-slug.md} -UR: {absolute path to user-requests/UR-NNN/input.md} -Prior REQs from this UR (may be empty): - - {absolute path} - - {absolute path} -Context pack: {absolute path to .do-work/state/context-pack.md} -Skill root: {resolved absolute $SKILL_ROOT — the directory containing lib/; your {skill-root}/lib/... calls use this value} - - - -{full contents of agents/run-worker.md verbatim, with {skill-root} replaced by the resolved $SKILL_ROOT} - - -Return your structured YAML report as your final message. Nothing else. -""" -) -``` - -The worker performs: create worktree → read REQ → read context → TDD red → implement → verify green → run affected tests → check acceptance criteria → execute verification steps → commit on feature branch → return YAML, all in its own session. **The worker does NOT merge, archive, or tear down its worktree** — those are the orchestrator's Step 4 (Integrate) responsibilities. - -The worker's stdout does not stream back to the orchestrator — only its final structured report is visible. Do not poll, do not babysit. Wait for the dispatch to return. - -### Step 3: Process the worker report - -The worker's final message is a fenced YAML block matching the schema defined in [agents/run-worker.md](run-worker.md) `## Return Report`. Parse it. Branch on `status`: - -| `status` | Action | -|---|---| -| `done` | Capture `commit` hash and `outputs`. Continue to Step 4 (Integrate). | -| `stopped` | The worker hit a stopper (`reason` enum: `tests-failing`, `verification-failing`, `missing-creds`, `ambiguous-criteria`, `scope-creep`, `dependency-missing`, `concurrent-conflict`, `unknown-error`). Continue to Step 5 (Recover) — handle per `## Stopping Rules`. Skip Step 4. **Workers never report a human-wait stopper** — there is no `awaiting-human-verification` reason. Inherently non-executable verification steps are *deferred* by the worker (returned in `pending_validation:`, per REQ-231), which routes the REQ to the `pending-validation` Status at Step 4.0, not to a stopper. | -| `failed` | The worker crashed before completing. Treat as `stopped` with `reason: unknown-error`. | - -If the worker's report is missing or unparseable, treat as `status: failed` with `reason: unknown-error` and surface the raw output to the user. - -If the worker reports `status: stopped` with `reason: verification-failing`, parse `last_good_step`, `failed_step`, and `checkpoint_log` from the report. Include the localized failure in the user-facing stopper report, e.g. `Verification failed at step ; last good step was ; handoff: .` - -If the worker reports `status: done`, validate acceptance evidence before Step 4 integration: - -```bash -bash lib/check-acceptance-evidence.sh {project}/.do-work/working/REQ-NNN-slug.md -``` - -If validation fails, treat the result as `status: stopped`, `reason: verification-failing`, surface the validator diagnostics, and do not merge, write closure proof, review, or archive. This gate extends the checkpoint/closure-proof model; it does not replace `closure_proof`. - -After acceptance evidence validation passes, run the post-build review gate before Step 4 integration. **Review is dispatched as a fresh, independent subagent — never followed inline in the orchestrator's own context.** The orchestrator that wants the run to finish must not grade its own work; the reviewer runs cold, with no run history, seeing only the artifacts you hand it. Worker says done is not final until this evidence gate and the review gate both pass. - -Before dispatching review, run deterministic policy checks using changed files, command evidence, and REQ metadata: - -```bash -bash lib/check-policy.sh \ - --project {project} \ - --files \ - --commands \ - --req {project}/.do-work/working/REQ-NNN-slug.md -``` - -Capture both the exit code and stdout/stderr — they are an input to the review dispatch. - -- **Exit `1`:** treat the result as `status: stopped`, `reason: policy-blocked`, surface the blocked path or blocked command diagnostics, leave the REQ in `working/`, and do not review, merge, archive, or write completion state. -- **Exit `2`:** a `risk.require_review` signal fired. Continue into review and pass the `review_required` diagnostics as mandatory review context. This exit code is also the trigger for **adversarial mode** (below). -- **Exit `0`:** continue into review normally. - -The helper reads `security.blocked_paths`, `security.blocked_commands`, and `risk.require_review` from `.do-work/config.yml`. - -#### 3a. Dispatch the review subagent - -Read all of [agents/review.md](review.md) — that is the reviewer's full instruction set. Pass it inline to the dispatched subagent, exactly as Step 2 does for the worker. The reviewer receives **five named inputs and nothing else** — no run narrative, no prior-REQ context, no memory of the worker's reasoning: - -``` -Agent( - description: "Post-build review for REQ-NNN", - subagent_type: general-purpose, - model: , - prompt: """ -You are the Review agent. Follow the instructions below exactly. You run as an independent subagent with no run history — judge only the artifacts handed to you. - - -Working REQ: {absolute path to working/REQ-NNN-slug.md} -UR: {absolute path to user-requests/UR-NNN/input.md} -Worker report: {the worker's returned YAML report, inline} -Diff / commit: {the implementation diff, or the feature-branch commit reference} -Policy check: {the captured check-policy.sh output and exit code} - - - -{full contents of agents/review.md verbatim} - - -Return your structured YAML review report as your final message. Nothing else. -""" -) -``` - -Parse the reviewer's returned YAML (schema in [agents/review.md](review.md) `## Output`). Branch on its `status`: - -- **`status: passed`:** continue to Step 4 (Integrate). -- **`status: failed`:** treat the result as `status: stopped`, `reason: review-failed`, surface the review `findings`, leave the REQ in `working/`, and do not merge, write closure proof, archive, or record completion. - -#### 3b. Adversarial mode (config-gated, risk-triggered) - -Read `review.adversarial` (loaded at startup; default `false`). - -- **`review.adversarial` is `false` (default), OR `check-policy.sh` exited `0`:** dispatch exactly **one** reviewer as in §3a. This is the shipped path. -- **`review.adversarial` is `true` AND `check-policy.sh` exited `2`:** dispatch **three** reviewers in parallel, each scoped to a distinct lens — **correctness**, **security**, **regression**. Use the same §3a dispatch shape per reviewer, adding a line to the prompt naming the lens (e.g. `Review lens: security — weight your findings toward this lens; still report blockers you see outside it.`). Aggregate the three returned reports into one verdict: - 1. **Majority gate:** the gate passes only when at least **2 of 3** reviewers return `status: passed`. - 2. **Blocker override:** any `severity: blocker` finding from **any** reviewer fails the gate regardless of the majority outcome. Blockers are never out-voted. - 3. On failure (majority not met OR any blocker present), apply the same handling as a single failed review: `status: stopped`, `reason: review-failed`, surface the union of all three reviewers' `findings`, leave the REQ in `working/`. - - Default stays single-reviewer to contain token cost until run-level budget enforcement (REQ-226) exists. - -### Step 3b: Run Ledger - -When `ledger.enabled` is true, record one append-only run ledger entry per worker attempt under `{project}/.do-work/runs/RUN-NNN.yml` using `lib/run-ledger.sh`. Collect the ledger inputs while the run progresses: REQ id, agent id, selected model, branch, started and ended timestamps, command evidence, test evidence, changed files, result, cost estimate or budget note, review outcome, and derived proof status. - -Finalize the ledger after the attempt reaches a terminal outcome: - -```bash -bash lib/run-ledger.sh \ - --project {project} \ - --req \ - --agent \ - --model \ - --branch \ - --started \ - --ended \ - --result \ - --review \ - --cost \ - --cost-estimate \ - --pr \ - --commands \ - --tests \ - --changed-files -``` +| Step | Intent | +|------|--------| +| **1** | Claim next REQ — milestone filter (1.0 / 1.0a); `list_claimable_reqs` + `claim_req` (backend-specific) | +| **2** | Dispatch worker subagent (`run-worker.md`) with five-input contract + model/routing | +| **3** | Process worker report — acceptance evidence, policy, review gates | +| **3b** | Run ledger / `append_run_note`; **budget gate** | +| **4** | Integrate — merge, archive (`archive_req` or file move), worktree teardown, metadata commit | +| **5** | Recover on stopper | +| **7** | Report progress | +| **7b** | Milestone deploy-gate (milestone mode only) — human y/n; local gate-owner | +| **8** | Loop (unless budget-stop) | -For stopped workers, write the ledger before returning control to the user, with `result: stopped:` and the best available evidence lists. For policy-blocked or acceptance-evidence failures before review, use `review: not-run`. If `ledger.enabled` is false, skip ledger creation. - -For a **pending-validation-bound** REQ (detected in Step 4.0), write `result: pending-validation` with the normal review and evidence fields — delivery still happened and all automated gates passed, so the review and evidence lists are populated exactly as for a done REQ; only human/device sign-off remains. Write this ledger entry on the same path as a done REQ (after integration), not the stopped path. - -The worker also reports `milestone_complete` (boolean) and `milestone` (id when true). Step 7b uses these. - -#### Step 3b.1: Budget gate (enforcement hook) - -Run this **immediately after** the ledger write above, on every worker attempt — serial mode here, and at the same point inside the merge queue's Stage B for parallel mode (P3 reuses Step 3b verbatim; the gate rides along). - -**Inert unless armed.** If the effective budget (resolved at startup) is empty/unset, **skip this gate entirely** — never sum, never stop. This preserves today's unlimited behaviour with zero overhead. Likewise skip when `ledger.enabled` is false (no ledger to sum). - -When the budget is non-empty: - -1. Sum cumulative estimated spend for this run from the ledger: - ```bash - SPENT="$(bash lib/run-ledger.sh --sum-run {project}/.do-work/runs)" - ``` -2. Compare `SPENT` against the effective `BUDGET` (numeric, same dollar unit): - - **`SPENT < BUDGET` ⇒ under budget.** Continue normally to Step 4 (Integrate) and loop. - - **`SPENT >= BUDGET` ⇒ budget exhausted.** Do **not** abandon the current attempt. **Finish the current REQ's integration first** (complete Step 4 fully — merge/archive/teardown/commit, or the PR delivery sequence — so the loop never stops mid-merge or mid-archive). Then, at the REQ boundary (where Step 8 would normally claim the next REQ), **stop gracefully** instead of looping: emit the **budget-stop report** and end the run. - -> **JUDGMENT:** The gate trips *after* the attempt that crossed the line, never mid-attempt. An in-flight integration always completes — abandoning a half-merged REQ would corrupt state, which is a worse failure than a small budget overshoot. The estimate is tier-weighted (see budget unit above), so the report names spend as an estimate, not a metered total. - -**Budget-stop report** (print before ending; under `next_steps.enabled` + standalone, surface via `AskUserQuestion` like a stopper, else print and stop): - -``` -Budget reached — stopping at REQ boundary. - -Estimated spend: $ / budget $ (tier-weighted estimate, not a metered bill) -REQs completed this run: -REQs remaining in backlog: -Last integrated: REQ-NNN - -Re-run with a higher --budget (or raise cost.budget) to continue. -``` - -The in-parallel variant is identical: when the gate trips inside Stage B, finish that report's Step 4 integration, then **stop admitting new reports to Stage B and stop refilling the window (P2)** — let live workers drain naturally (their integrations still complete), then emit the budget-stop report. No worker is killed mid-attempt; the window simply stops being refilled past the budget boundary. - -### Step 4: Integrate (worker = code, orchestrator = state) - -> **JUDGMENT:** The integration sequence below is the orchestrator's responsibility BECAUSE workers run in isolated worktrees. The worker has committed implementation files to `req/REQ-NNN`; the orchestrator now merges that branch into the base branch, archives the REQ, tears down the worktree, and commits the metadata change. This is the only place where `.do-work/` lifecycle writes happen. - -Reached only when `status: done` and both acceptance evidence validation and post-build review passed. - -#### 4.0 Detect pending-validation-bound REQs (before delivery) - -A REQ is **pending-validation-bound** when, after every automated gate above has passed (acceptance evidence, policy, review — all unchanged), human or device sign-off still remains. Detect it from either signal: - -1. The worker report's `pending_validation:` list is **non-empty** (the worker deferred one or more inherently non-executable verification steps — see [agents/run-worker.md](run-worker.md) `## Return Report`, added by REQ-231), **OR** -2. The REQ file carries a **non-empty `## Post-merge validation`** section. - -If neither signal holds, the REQ is fully done — take the normal delivery-then-archive path below. If either holds, the REQ is pending-validation-bound: it still takes the **identical delivery** path (merge or PR, including worktree teardown — the code is never held back), but **parks instead of archives** (4b-pending replaces 4b in merge mode; the PR path parks in 4-pr.4). This is a terminal `pending-validation` Status, not a stopper — the loop continues afterward (Step 8). The `/do-work approve REQ-NNN` flow (REQ-236) consumes `.do-work/pending/` and completes closure later. - -**Delivery mode dispatch.** Read `config.delivery.mode` (default `merge`): - -- **`merge`** (default) — execute substeps **4a → 4b → 4c → 4d** below, in order; each must succeed before the next. This is the historical local-merge behaviour, unchanged. For a pending-validation-bound REQ, substitute **4b-pending** for 4b — delivery (4a), teardown (4c), and the metadata commit (4d) are otherwise identical. -- **`pr`** — skip 4a–4d entirely and execute the **PR delivery** sequence (`#### 4-pr`) instead. PR mode never runs the local merge. A pending-validation-bound REQ follows 4-pr identically, parking in 4-pr.4 instead of archiving. - -The guards in 4b / 4b-pending (path-unit closure, non-empty closure proof for the done path) and the closure-proof model are identical in both delivery modes — only the delivery vehicle differs. Whichever path runs, proceed to Step 7 when it completes. - -#### 4a. Merge the feature branch - -From the orchestrator's checkout (the main working tree, NOT the worktree): - -```bash -git merge --no-ff req/REQ-NNN -m "merge(REQ-NNN): integrate" -``` - -On text-level conflict (any file contains `<<<<<<<`): - -1. `git merge --abort`. -2. Apply the 5-retry exponential-backoff policy (5s / 15s / 30s / 60s waits): - - `git pull --rebase origin ` (if remote exists; otherwise local fetch). - - Re-attempt the merge. -3. On the 5th failure, leave the feature branch alive (do NOT delete it), transition the REQ to `**Status:** stopped`, `**Reason:** concurrent-conflict` (handled in the Recover step below), and surface to the user. The branch can be resumed via `/do-work resume REQ-NNN` which checks out the worktree and re-runs the worker on the same branch. - -#### 4b. Archive the REQ file - -Read the worker's YAML report's `outputs:` list and `closure_proof` value. Rewrite the REQ file in place under `.do-work/working/REQ-NNN-slug.md`: - -0. **Path-unit closure guard.** Before any archive mutation, read `**Entry point:**` and `**Terminal state:**` from the REQ file. If either field is present, both must be present and non-empty. If a path-unit is missing either value, do not archive it. Transition the REQ to `**Status:** stopped`, add `**Reason:** path-unit-incomplete`, and surface: `REQ-NNN cannot close: path-unit requires non-empty Entry point and Terminal state.` Non-path REQs with both fields absent are unaffected. -1. Require non-empty `closure_proof` when the worker returned `status: done`. If it is missing or empty, transition the REQ to `**Status:** stopped`, add `**Reason:** missing-closure-proof`, and do not archive. -2. Strip the ownership stamp (``). -3. Update `**Status:**` to `done`. -4. Write the worker's `closure_proof` value into `**Closure proof:**`. If the header is absent, insert it before `**Files:**`. -5. Append a `## Outputs` section based on the `outputs:` array from the worker's YAML report. One bullet per entry: `- `. -5b. **Archive-integrity gate.** With the working file now fully rewritten, run the deterministic guardrail on it before the move: - ```bash - bash {skill-root}/lib/check-archive-integrity.sh {project}/.do-work/working/REQ-NNN-slug.md - ``` - It asserts the final on-disk state is internally consistent: `**Status:** done`, a non-empty `**Closure proof:**`, and zero unchecked `- [ ]` items inside `## Acceptance Criteria`. **Exit non-zero ⇒ do not archive:** transition the REQ to `**Status:** stopped`, add `**Reason:** archive-integrity`, surface the script's stderr diagnostics, and leave the file in `working/`. This is the persistence-boundary enforcement of the invariants steps 3–4 and the worker's acceptance-criteria ticking (`agents/run-worker.md` Step "Mark each `- [x]`") are supposed to satisfy — those are prose an LLM can silently skip; this gate cannot be skipped. (`archive-integrity` is an orchestrator-assigned reason like `path-unit-incomplete` and `missing-closure-proof`; it is not a worker reason.) -6. Move the file to `archive/`: - ```bash - mv {project}/.do-work/working/REQ-NNN-slug.md {project}/.do-work/archive/REQ-NNN-slug.md - ``` - -#### 4b-pending. Park the REQ file (pending-validation-bound REQs only) - -Runs **instead of 4b** when 4.0 detected a pending-validation-bound REQ. The merge (4a) has already landed — the code is on the base branch — so the only difference from 4b is *where the REQ file goes and what state it carries*. Read the worker's YAML report's `outputs:` list. Rewrite the REQ file in place under `.do-work/working/REQ-NNN-slug.md`: - -0. **Path-unit closure guard.** Identical to 4b step 0 — if `**Entry point:**` / `**Terminal state:**` are partially present, do not park; transition to `**Status:** stopped`, `**Reason:** path-unit-incomplete`, and surface. Both-absent (non-path) REQs are unaffected. -1. **No closure-proof requirement.** Closure proof is the *done* oracle; a pending REQ is delivered-but-unclosed, so leave `**Closure proof:**` **empty** — the `/do-work approve` flow writes it at sign-off. -2. Strip the ownership stamp (``) — pending lives outside `working/`, so it carries no live claim or heartbeat. -3. Update `**Status:**` to `pending-validation`. -4. **Consolidate the outstanding checklist into `## Post-merge validation`.** Merge the worker report's `pending_validation:` entries into the REQ's `## Post-merge validation` section so the entire outstanding checklist lives in one place: append one unchecked bullet per deferred step, e.g. `- [ ] (: )`. If the section does not exist, create it. If the REQ already had a `## Post-merge validation` section, keep its existing items and append the worker's. This section is what `/do-work approve` reads. -5. Append a `## Outputs` section based on the `outputs:` array from the worker's YAML report. One bullet per entry: `- `. -6. Move the file to `pending/` (create the directory on first use): - ```bash - mkdir -p {project}/.do-work/pending - mv {project}/.do-work/working/REQ-NNN-slug.md {project}/.do-work/pending/REQ-NNN-slug.md - ``` - -7. **Fire the pending-validation notification hook.** Read `config.notifications.on_pending_validation`. If non-empty, substitute placeholders and run the command **once**, best-effort: - - - `{req}` → the REQ id (e.g. `REQ-234`) - - `{title}` → first non-blank line of the REQ's `## Task` section (strip leading `##`) - - `{checks}` → newline-joined text of every unchecked bullet (`- [ ] ...`) from `## Post-merge validation` in the now-parked REQ file; empty string when there are none - - Execute via `bash -c ""`. A non-zero exit code or missing binary logs a one-line warning (`⚠ pending-notification: `) and continues — it never stops the park flow or surfaces a stopper. If `config.notifications.on_pending_validation` is empty or absent, skip this step entirely with no output. - -Then run 4c (teardown — identical) and 4d (metadata commit), with the 4d commit message and staged paths adjusted for the park as noted in 4d. - -#### 4c. Tear down the worktree - -```bash -git worktree remove {project}/.worktrees/req-NNN -git branch -d req/REQ-NNN # safe delete; refuses if not fully merged -``` - -If `git branch -d` refuses (the merge somehow incomplete), surface to the user; leave the branch alive for manual investigation. Never use `-D`. - -#### 4d. Commit the metadata change - -If `.do-work/` is tracked in this project, stage only the archive (or park) move and commit. - -For the normal **archive** path (4b): - -```bash -git add {project}/.do-work/archive/REQ-NNN-slug.md -git add {project}/.do-work/working/REQ-NNN-slug.md # stages the removal -git commit -m "chore(REQ-NNN): archive - -REQ: {project}/.do-work/archive/REQ-NNN-slug.md -UR: {project}/.do-work/user-requests/UR-NNN/input.md" -``` - -For the **park** path (4b-pending), stage the `pending/` move and use the `pending validation` message: - -```bash -git add {project}/.do-work/pending/REQ-NNN-slug.md -git add {project}/.do-work/working/REQ-NNN-slug.md # stages the removal -git commit -m "chore(REQ-NNN): pending validation - -REQ: {project}/.do-work/pending/REQ-NNN-slug.md -UR: {project}/.do-work/user-requests/UR-NNN/input.md" -``` - -If `.do-work/` is gitignored: skip this commit silently. The move is filesystem-only, and the worker's `feat(REQ-NNN): ...` commit (now on the base branch via the merge) is the authoritative record. - -Proceed to Step 7. - -#### 4-pr. PR delivery (delivery.mode: pr) - -Runs *instead of* 4a–4d when `config.delivery.mode` is `pr`. The closure-proof model is unchanged — evidence still gates archive; the PR is the delivery vehicle, not the proof. Execute these substeps in order; each must succeed before the next. - -**4-pr.0 Precondition — remote + `gh` (never a silent merge fallback).** Before any push, verify both: - -```bash -git remote get-url origin # a remote must be configured -gh auth status # the gh CLI must be installed and authenticated -``` - -If a remote is missing **or** `gh` is absent/unauthenticated, **stop**: do NOT merge, do NOT push, do NOT archive. Leave the REQ in `working/` (and the branch alive), transition it to `**Status:** stopped`, `**Reason:** missing-creds`, and surface to the user per `## Stopping Rules`. PR mode must **never** silently fall back to `merge` mode. - -**4-pr.1 Push the REQ branch.** With the precondition met, push the worker's branch to the remote: - -```bash -git push -u origin req/REQ-NNN -``` - -**4-pr.2 Determine the PR target by granularity.** Read `config.delivery.pr.granularity` (default `req`): - -- **`req`** (default) — open the PR immediately, from `req/REQ-NNN` into the base branch. Continue to 4-pr.3. -- **`ur`** — do NOT open a per-REQ PR. Instead accumulate this REQ onto the UR's shared integration branch: - 1. Resolve the UR id from the REQ's `**UR:**` field → integration branch `ur/UR-NNN`. - 2. If `ur/UR-NNN` does not yet exist on the remote, create it from the base branch and push it. - 3. Merge `req/REQ-NNN` into `ur/UR-NNN` (`git merge --no-ff`, applying the same conflict/retry policy as 4a) and push `ur/UR-NNN`. - 4. Archive this REQ now (4-pr.4) recording the `ur/UR-NNN` branch, but **defer PR creation**: the single PR opens at UR drain. After the last REQ for this UR archives and the UR's backlog is empty (see `## When the Backlog is Empty` drain check), open one PR from `ur/UR-NNN` into the base branch using the same title/body shape as 4-pr.3 (title/body keyed to the UR rather than a single REQ; the body links the UR and lists each integrated REQ). Record that PR's URL on the UR. Then continue past 4-pr.5 to Step 7. - -**4-pr.3 Open the PR (`req` granularity, or the single UR-drain PR).** - -```bash -gh pr create \ - --base \ - --head req/REQ-NNN \ - --title "" \ - --body "" -``` - -PR body mirrors the commit convention (see SKILL.md / README `## Commit Convention`) and ends with the standard generated-with footer: - -``` -REQ: .do-work/archive/REQ-NNN-slug.md -UR: .do-work/user-requests/UR-NNN/input.md -Output: - -🤖 Generated with [Claude Code](https://claude.com/claude-code) -``` - -Capture the PR URL printed by `gh pr create`. - -**4-pr.4 Archive the REQ — or park it (pending-validation-bound).** For a fully-done REQ, apply the **same** archive logic as 4b (path-unit closure guard, non-empty closure-proof requirement, strip ownership stamp, set `**Status:** done`, write `**Closure proof:**`, append `## Outputs`, **archive-integrity gate (4b step 5b — `bash {skill-root}/lib/check-archive-integrity.sh` on the rewritten file; non-zero ⇒ stop with `**Reason:** archive-integrity`, do not archive)**, `mv` to `archive/`) — with one addition: append the PR URL to `## Outputs` as a bullet, e.g. `- PR — `. For `ur` granularity where the PR opens later, record the integration branch in `## Outputs` now and append the PR URL bullet when the UR-drain PR opens. - -For a **pending-validation-bound** REQ (detected in 4.0), apply the **same** park logic as **4b-pending** instead (path-unit guard, empty closure proof, strip ownership stamp, set `**Status:** pending-validation`, consolidate the worker's `pending_validation:` steps into `## Post-merge validation`, append `## Outputs`, `mkdir -p` + `mv` to `pending/`, fire the `on_pending_validation` notification hook) — with the same addition: append the PR URL to `## Outputs` as a `- PR — ` bullet. The PR has already opened (4-pr.3) and delivers the code; only human sign-off waits, exactly as in merge mode. There is **no fallback to a local merge** — PR mode parks the REQ but never changes its delivery vehicle. - -**4-pr.5 Tear down the worktree — but keep the branch.** Remove the worktree; do **not** delete the branch (the PR owns it): - -```bash -git worktree remove {project}/.worktrees/req-NNN -# NO `git branch -d` — the open PR owns req/REQ-NNN (or it lives on in ur/UR-NNN). -``` - -**4-pr.6 Record the PR URL in the ledger.** When `ledger.enabled` is true, pass the captured URL to the ledger via `--pr` (see Step 3b) so the run record's `pr_url` field carries it. If the metadata commit (4d-equivalent) runs for a tracked `.do-work/`, stage and commit the move per 4d — `chore(REQ-NNN): archive` for the archived path, or `chore(REQ-NNN): pending validation` (staging the `pending/` move) for the parked path. - -Proceed to Step 7. - -### Step 5: Recover (on stopper) - -Reached only when `status: stopped` or `failed`. The REQ file is still in `working/` (worker didn't move it). Handle per `## Stopping Rules`. - -For `reason: concurrent-conflict` after Step 4a's 5-retry exhaustion: leave the feature branch alive; update the REQ to `**Status:** stopped` and `**Reason:** concurrent-conflict`. `/do-work resume REQ-NNN` is the recovery path. - -For other stoppers: surface to the user via `AskUserQuestion` (existing stopping-rules behaviour). - -Do not proceed to Step 7. - -### Step 6 — (reserved; removed in an earlier revision) - -### Step 7: Report progress - -``` -✅ REQ-NNN complete: [title] - Output: [path] - Commit: [short hash] - -Remaining in backlog: N -``` - -### Step 7b: Milestone deploy-gate check (milestone mode only) - -The deploy-gate prompt is **owned by the orchestrator, not the worker**. The worker has no user-interaction surface and is explicitly forbidden from auto-confirming any gate. Under parallelism, only **one** orchestrator surfaces the prompt to the user — the first to detect milestone completion *and* observe a fully drained milestone backlog. - -If `{project}/.do-work/state/active-milestone.md` does NOT exist (non-milestone mode), the worker always reports `milestone_complete: false` and the orchestrator simply continues until the backlog is empty. Skip the rest of this step. - -If `{project}/.do-work/state/active-milestone.md` exists (milestone mode): - -1. Read `milestone_complete` from the worker's most recent return report. -2. If `milestone_complete` is `false`, continue the loop normally — claim the next REQ. -3. If `milestone_complete` is `true`, run the **first-to-detect drain check** before showing any prompt. First-to-detect doesn't mean first-to-finish-its-REQ; it means *first whose worker reports milestone-complete AND whose drain check passes*. - -#### Step 7b.1 — Drain confirmation - -Let `` be the trimmed contents of `{project}/.do-work/state/active-milestone.md`. - -1. Glob `{project}/.do-work/REQ-M-*.md` (backlog root). **Must return zero files.** If non-zero, a sibling can still claim more work in this milestone — abort the gate detection, continue the loop normally (Step 8). Some other return-report will trigger the gate later. -2. Glob `{project}/.do-work/working/REQ-M-*.md`. For each file, read its `` ownership stamp: - - Slots whose `**Claimed by:**` equals the local `AGENT_ID` are expected — at most one (the just-archived REQ's transient state) and not a blocker. - - Any slot owned by a **different** agent-id is a sibling's in-flight REQ for the same milestone. The milestone is not yet drained. -3. **If sibling slots are present**, poll every 30 seconds, up to 30 minutes: - - Re-run the working/ glob and re-classify on each tick. - - When no sibling-owned slots remain, the milestone is drained — proceed to Step 7b.2. - - On 30-minute timeout, surface to the user: `Milestone M appears stuck — sibling slot(s) have not drained after 30 minutes. Continue waiting, or abort?` Act on the user's response (continue → resume polling; abort → exit this orchestrator cleanly without writing `gate-owner.md`). -4. **If both globs come back clean on the first check (or after polling completes)**, this orchestrator owns the gate. Proceed to Step 7b.2. - -#### Step 7b.2 — Claim the gate - -1. Write `{project}/.do-work/state/gate-owner.md` containing a single line: the local `AGENT_ID`. (This file is the cross-process signal that the gate is being handled — siblings reading it in Step 1.0a use the id to attribute the wait.) -2. Read the deploy gate text for the active milestone from `{project}/.do-work/user-requests/UR-NNN/input.md`. The deploy gate is the line beginning `**Deploy gate:**` under the active milestone's `#### M` heading. -3. Halt the loop and print: - - ``` - Milestone M REQs complete. - - Deploy gate: - - Has the deploy gate been satisfied? (y/n) - ``` - -4. Wait for user input. - -#### Step 7b.3 — Advance on `y` - -- Update `{project}/.do-work/state/milestones.md` to mark M as `deployed`. -- Identify the next pending milestone (lowest M with status `pending` in milestones.md). - - **If one exists:** update `{project}/.do-work/state/active-milestone.md` to that milestone id. **This file change is the signal that wakes idle siblings** (see Step 1.0a). - - **If none exists** (all milestones deployed): delete `{project}/.do-work/state/active-milestone.md` so idle siblings fall through to `## When the Backlog is Empty`. -- Delete `{project}/.do-work/state/gate-owner.md`. -- Ask: "Begin capture for the next milestone? (y/n)" - - On **y**: print: "Run `/do-work capture UR-NNN` to decompose milestone M." Exit. - - On **n**: exit cleanly. The user can return later. - -#### Step 7b.4 — Stop on `n` - -- Ask: "What needs to change? Describe the gap." Capture the user's description. -- Delete `{project}/.do-work/state/gate-owner.md`. -- Delete `{project}/.do-work/state/active-milestone.md`. **This deletion wakes idle siblings into the empty-backlog path** (see Step 1.0a) so they exit cleanly without further user prompts. -- Print: "Run `/do-work capture UR-NNN` to add new REQs for the gap, or edit the UR's milestone definition. Idle siblings will exit when active-milestone.md is removed." -- Exit. - -#### State file: `gate-owner.md` - -| Action | Actor | When | -|---|---|---| -| **Write** | Gate-owning orchestrator (Step 7b.2) | After drain confirmation passes, before printing the gate prompt | -| **Read** | Sibling orchestrators (Step 1.0a) | When their active-milestone backlog is empty, to attribute the idle log line | -| **Delete** | Gate-owning orchestrator (Step 7b.3 or Step 7b.4) | After the user answers y or n, before exit | - -Contents: a single line — the gate-owner's `AGENT_ID`. No header, no trailing data. If the file is ever found with malformed contents, treat as absent and continue. - -#### Non-delegation - -- **Sign-off is non-delegable.** The orchestrator must NOT auto-confirm the deploy gate. The orchestrator must NOT attempt to deploy or test deployment itself. The worker is also forbidden from these actions (see [agents/run-worker.md](run-worker.md)). -- Only the *which orchestrator owns showing the prompt* changes under parallelism. The prompt text and the requirement for an explicit human y/n answer are unchanged. - -### Step 8: Loop - -If the Step 3b.1 budget gate tripped on the REQ just integrated, **do not loop** — the budget-stop report has already been emitted and the run ends here. Otherwise, go back to Step 1 and claim the next REQ. - -A REQ parked as `pending-validation` (Step 4.0 / 4b-pending / 4-pr.4) is **not a stopper** — its code merged and its worktree was torn down, so the run continues looping exactly as it does after a fully-done REQ. Do not stop the loop, do not surface a stopper prompt; just proceed to the next claim. - -**Dependency note.** A REQ whose `**Depends on:**` names a pending-validation REQ is **claimable** — the dependency's code is already merged to the base branch; only human sign-off is outstanding. `lib/check-deps.sh` treats a parked dep as satisfied by globbing `.do-work/pending/` alongside `.do-work/archive/` (REQ-239), so `lib/pick-req.sh` will hand out a dependent of a parked REQ at claim time just as it would a dependent of an archived REQ. +Backend branch notes for Step 1 / 4 live in the full loop reference. Linear issue ids replace `REQ-NNN` in branch/commit naming. --- ## Parallel Run Mode -> **Entered only when the effective window width `N > 1`** (see `## When Invoked → Parallel window width`). When `N == 1` this entire section is skipped and `## The Loop` runs serially, byte-for-byte unchanged. -> -> **Authority:** this section transcribes `docs/design/single-session-parallel.md` (REQ-220). Each subsection cites its design decision. If any decision proves unimplementable as written, stop with `ambiguous-criteria` naming the design section — do not improvise coordination semantics. - -Single-session parallel mode is **one orchestrator dispatching up to `N` concurrent workers from one terminal**, then integrating their results through a **serialized merge queue**. Every safety primitive (`pick-req.sh` overlap exclusion, `claim-req.sh` atomicity, dependency ordering, worktree isolation, heartbeat staleness, the final-suite lockfile) is reused unchanged. What changes is only the shape of the hot path: from serial `claim → dispatch → wait → integrate` to windowed `claim K → dispatch K → integrate as each returns → refill`. - -Pre-flight (`## Pre-flight Check`) runs exactly as in serial mode — branch/dir checks, `mkdir -p state/`, `AGENT_ID`, context pack, the informational `working/` scan. A non-empty `mine` bucket is resumed first (those REQs occupy window slots before any new claim). - -### P1. Fan-out mechanism — concurrent `Agent` dispatches (design §1) - -Fan out via **N concurrent `Agent`-tool dispatches in one turn** — the same worker-dispatch surface serial mode uses at `## The Loop` Step 2. The harness runs concurrent tool calls in a single turn in parallel. Do **not** delegate fan-out to a Workflow/scheduler primitive: the `Agent` tool is the only dispatch surface guaranteed wherever serial mode works, it keeps the announce/return checkpoints that logging, the ledger (Step 3b), and stopper-surfacing all hang off, and it keeps resume granularity at one REQ. Each dispatch carries the identical five-input worker contract from Step 2 (REQ path, UR path, prior-REQ paths, context-pack path, resolved `$SKILL_ROOT`, plus `run-worker.md` inline). Announce each at claim time exactly as Step 1's announce line. - -### P2. Window fill — claim-as-slot-frees (design §2) - -**Claim one REQ immediately before each dispatch — never a batch up front.** `pick-req.sh` reads `working/` directly to build its footprint-exclusion set, so a claim must be *visible in `working/`* before the next pick or two picked candidates could overlap each other. - -**Fill loop** (run at start, and again on every refill): - -``` -while live workers < N: - REQ_PATH = pick-req.sh "$SCOPE" "$AGENT_ID" # Step 1 picker, unchanged - if REQ_PATH is empty: break # nothing claimable right now - claim-req.sh "$REQ_PATH" "$AGENT_ID" # Step 1 claim, atomic, lands in working/ - classify + select model (## REQ Classification, ## Model Selection) - dispatch worker (P1) and count it as a live worker -``` - -- Each `claim-req.sh` updates `working/` before the next `pick-req.sh`, so the next pick automatically skips overlapping and now-claimed REQs. **Never `pick-req.sh × K` then claim.** -- **Overlapping candidates:** the first claimed wins the slot; the rest are excluded by the overlap filter on the next pick and stay in the backlog. They become claimable again only when the winning slot drains (its REQ is integrated and leaves `working/`). Identical to multi-terminal behaviour — no new arbitration. -- A claimed-but-not-dispatched gap is impossible: each claim is immediately followed by its dispatch in the same fan-out turn. -- **Claim races / errors** are handled exactly as `## The Loop` Step 1 (`claim-req.sh` exit 2 ⇒ re-pick; other non-zero ⇒ backoff/re-pick, stop after 3 consecutive non-race failures). -- **Picker returns empty while slots are free:** do not idle-wait the way serial Step 1 does — live workers are still running and will free footprints as they integrate. Break the fill loop and go drain the merge queue (P3); refill again after each slot frees. Only when the window is fully empty **and** `pick-req.sh` returns empty do you fall through to `## When the Backlog is Empty`. - -> **JUDGMENT:** J8 — when the window has free slots but the picker returns empty, prefer draining ready workers over blocking. A footprint freed by an integrating peer may unblock the next pick. Surface to the user only via the existing empty-backlog / deadlock paths once **no** workers are live. - -### P3. The merge queue (design §3) +Entered only when `N > 1`. Full body: [references/run-parallel.md](../references/run-parallel.md). -Workers return on `req/REQ-NNN` branches concurrently and **in any order** (a fast REQ dispatched second can return before a slow one dispatched first). Every gate after dispatch must serialize the single-writer tail. The merge queue is one in-orchestrator **FIFO of returned worker reports awaiting integration, ordered by arrival** — not by REQ number. - -Arrival order is correct because footprints are disjoint by construction (no two queued REQs touch the same files) and dependency ordering is already enforced upstream at claim time (`pick-req.sh` will not hand out a REQ whose `**Depends on:**` are unarchived). The queue never needs to reorder for deps, and arrival order avoids head-of-line blocking. - -Each dequeued report runs **exactly the existing serial Steps 3–4, internals unchanged**, split into two stages: - -**Stage A — concurrent, read-only (safe N-wide).** May run across multiple queued reports in parallel; writes nothing to the base branch or `.do-work/` lifecycle state: -1. Step 3 — acceptance-evidence gate (`check-acceptance-evidence.sh`) -2. Step 3 — policy gate (`check-policy.sh`) -3. Step 3a/3b — **independent review dispatch** (`review.md` as a fresh subagent; adversarial mode per Step 3b when `review.adversarial` + policy exit 2). Review reads only `(REQ, diff, evidence)` with no run context, so its dispatches may be fanned out concurrently — the same `Agent`-tool concurrency used for workers — keeping review latency off the critical path. - -**Stage B — serial, single-writer (one REQ at a time).** A report enters Stage B only after passing **every** Stage A gate. Run the existing Step 3b ledger + Step 4 substeps in order: -4. Step 3b — ledger entry (`run-ledger.sh`) -5. Step 4a — `git merge --no-ff req/REQ-NNN` from the **main working tree** (never a worktree) -6. Step 4b — archive the REQ file (closure-proof + path-unit guards unchanged) -7. Step 4c — tear down the worktree + `git branch -d` -8. Step 4d — commit the metadata change - -**Serialization invariant:** at most one Stage B sequence touches the main working tree and `.do-work/` at any instant — exactly as serial mode. Only after an entry finishes Step 4d (or diverts to Recover) do you admit the next report to Stage B. After each Stage B completion the freed slot triggers a P2 refill. - -**Conflict handling mid-queue — reuse Step 4a verbatim, do not invent a new retry path.** On text-level conflict (`<<<<<<<`): `git merge --abort`, then the existing **5-retry exponential backoff** (5s / 15s / 30s / 60s), each attempt re-syncing the base branch and re-merging. On the 5th failure: leave the `req/REQ-NNN` branch alive, transition the REQ to `**Status:** stopped`, `**Reason:** concurrent-conflict`, surface to the user, and **continue draining the rest of the queue** — a conflict on one queued REQ must not abort the others. Resumable via `/do-work resume REQ-NNN`. Because footprints are disjoint, a content conflict between two queued REQs should not occur; the retry path absorbs conflicts against concurrent multi-terminal siblings or a remote (P5). - -> **JUDGMENT:** J9 — admit reports to Stage B in arrival order, one at a time; never hold a ready report waiting for a slower lower-numbered REQ. A Stage A failure or a Stage B 5-retry exhaustion on one report diverts only that report to Recover (P4) and never blocks its siblings. - -### P4. Failure isolation (design §5) - -**One worker (or one queued integration) stopping must never abort its siblings.** - -- A worker returning `status: stopped` / `failed`, or a Stage A gate failure on its report, is handled **exactly** as serial Step 5 (Recover) and `## Stopping Rules`: the REQ stays in `working/` with `**Status:** stopped` + `**Reason:**`; the orchestrator surfaces the stopper. The difference under parallelism: **do not halt the loop** — record the stopper, **free that window slot**, and (if backlog remains) claim a refill (P2). The other workers and any queued ready reports proceed untouched. -- **Stoppers are queued per-REQ and surfaced in arrival order**, one decision at a time. When `next_steps.enabled` is true **and** standalone, each stopper surfaces via `AskUserQuestion` (Show details / Retry / Skip) as in serial mode. When the gate is closed (delegate mode or `next_steps.enabled=false`), each stopper prints its `details` and the loop continues — no auto-retry. The per-REQ retry counter and ambiguous-criteria feedback (`## Stopping Rules`) apply per REQ, unchanged. -- **No new stopper reasons.** The `## Stopping Rules` enum is complete; `concurrent-conflict` (P3 5-retry exhaustion) is already in it. -- **Drain accounting.** A stopped REQ left in `working/` is, for the empty-backlog drain check (`## When the Backlog is Empty` Step B), a slot owned by *this* `AGENT_ID` — `mine`, tolerated, not a blocker. The single-session orchestrator finishes its loop when the backlog is empty **and** its window has no live workers, then runs the final-suite path (P5). - -### P5. Coexistence with multi-terminal orchestrators (design §6) - -Single-session mode is **just one more agent-id in the existing claim arbitration — no special-casing.** - -- **Claim arbitration.** This orchestrator has one `AGENT_ID = hostname.pid`; its N claimed slots all carry it. A multi-terminal sibling's `pick-req.sh` excludes those slots by footprint just as it excludes any terminal's slots, and vice-versa. N-wide claiming from one process is indistinguishable, to the picker, from N processes each claiming once. -- **Heartbeat / staleness.** Each dispatched worker keeps its own slot's heartbeat fresh (`run-worker.md` checkpoint-stamping). A sibling's stale scan treats a single-session worker's slot like any other. This mode does **not** change the heartbeat mechanism. -- **Merge contention.** Both this orchestrator and a multi-terminal sibling merge into the same base branch. The Step 4a / P3 5-retry backoff is precisely what absorbs a merge that collides with a sibling's just-landed commit — a sync conflict resolved by rebase-and-retry. -- **Final-suite lockfile.** `## When the Backlog is Empty` (Steps B–E) is reused unchanged. The single-session orchestrator runs its drain check (backlog empty + no `other`-owned slots) only after its own N workers have all returned and drained, then races for the committed `final-suite-running.md` lockfile like any other contender. Its internal N-way fan-out is invisible at the lockfile layer. - -### P6. Out of scope — deploy gates stay single-flow (design §7) - -- **Milestone deploy gates are NOT parallelised.** Step 7b (the deploy-gate y/n prompt) is non-delegable and owned by exactly one orchestrator. When a worker reports `milestone_complete: true`, run the existing first-to-detect drain check (Step 7b) and surface the single y/n prompt. The N-way fan-out **pauses new claims while the gate is open** (the active-milestone backlog is, by definition, drained when the gate fires). No change to gate semantics. -- **The coordination lib is untouched.** `pick-req.sh`, `claim-req.sh`, `check-footprint.sh`, `scan-stale.sh`, `deadlock-check.sh`, `run-ledger.sh` keep their current contracts. This mode is a run-loop shape change, not a primitive change. No new state files, no new stopper reasons. +Outline: concurrent Agent dispatches (P1) → claim-as-slot-frees window fill (P2) → FIFO merge queue Stage A concurrent / Stage B serial (P3) → failure isolation (P4) → multi-terminal coexistence (P5) → deploy gates stay single-flow (P6). --- ## When the Backlog is Empty -The final cross-REQ test suite must run exactly once per drained backlog — fired by the **last orchestrator to finish**, not whichever orchestrator happens to observe the empty backlog first. Under N-way parallelism, this section guarantees that property via an explicit drain check and a committed lockfile. - -### Step A — Trigger - -Reached when the claim step (Step 1, REQ-114) returns no claimable REQ **and** this orchestrator has just archived its previous REQ. (Pre-flight empty-backlog also lands here — see `## Pre-flight Check` Step 5.) - -### Step B — Drain check (am I the last?) - -Before running the suite, classify the live state by reading ownership stamps (per `## Agent Identity` and REQ-113): - -1. **Backlog root:** glob `{project}/.do-work/REQ-*.md`. Must be empty. - - In milestone mode (`{project}/.do-work/state/active-milestone.md` exists), glob `{project}/.do-work/REQ-M-*.md` instead. -2. **Working slots:** glob `{project}/.do-work/working/REQ-*.md` (milestone mode: `working/REQ-M-*.md`). For each slot file, read its `` block and classify by `**Claimed by:**`: - - | Classification | Condition | - |---|---| - | `mine` | Stamp's `**Claimed by:**` equals local `AGENT_ID`. Tolerated — at most one, the just-archived REQ's transient state. Not a blocker. | - | `other` | Stamp's `**Claimed by:**` differs from local `AGENT_ID`. A sibling is still in flight — drain check **fails**. | - | `other` (defensive) | No stamp present (legacy / malformed slot). Treat as `other` — the local agent must not run the suite without checking with siblings. | - -3. **Drain check passes** iff backlog glob is empty AND no slot is classified `other`. Proceed to Step C. -4. **Drain check fails** (one or more `other` slots): proceed to Step E (sibling idle exit). - -### Step C — Lockfile acquisition (sibling-also-drained race) - -Two orchestrators can both pass the drain check at near-the-same instant (each just archived its own REQ, neither sees the other's slot). The lockfile is the tiebreaker. +Full drain / final-suite lockfile sequence: [references/run-parallel.md](../references/run-parallel.md) § When the Backlog is Empty. -Lockfile path: -- Non-milestone mode: `{project}/.do-work/state/final-suite-running.md` -- Milestone mode: `{project}/.do-work/state/final-suite-M-running.md` - -Acquisition sequence (first-to-commit wins): - -1. Check whether the lockfile already exists. If yes, another orchestrator already holds the suite — proceed to Step E (sibling idle exit), substituting "Sibling is running the final suite" framing. -2. Write the lockfile with a single block: - - ```markdown - **Held by:** - **Started at:** - ``` - -3. Stage and commit atomically: - - ```bash - git add {project}/.do-work/state/final-suite-running.md # or final-suite-M-running.md - git commit -m "chore: final-suite lock" - ``` - - - **Commit succeeds:** this orchestrator holds the lock. Proceed to Step D. - - **Commit fails** (e.g. sibling won the race, working tree shows their lockfile already committed, or merge conflict on the lockfile): treat as lost race. Discard local lockfile changes (`git checkout -- ` then `rm -f ` if still present), then proceed to Step E. - -The lockfile is intentionally committed *before* running the suite so other orchestrators can observe the lock even if the suite hangs. - -### Step D — Run the suite (lock-holder only) - -This orchestrator holds the lockfile. Run the project's full test suite as a cross-REQ safety net. - -1. **Suite command resolution** — unchanged. Use `config.test.suite_command` if set; otherwise try defaults in order (`npm test`, `npx vitest run`, `./vendor/bin/pest`), checking the runner exists before executing. If none found, log `No test suite configured or detected — skipping full suite run` and skip to Step D.4. -2. **Execute** the suite command. -3. **On failure** — apply the existing failure-attribution + 3-attempt fix loop unchanged: map failing test files to REQ commits via `git diff-tree --no-commit-id --name-only -r `, report the likely responsible REQ, fix the implementation, re-run; after 3 failed attempts, stop and report to the user. -4. **Release the lockfile** (regardless of pass/fail): - - ```bash - git rm {project}/.do-work/state/final-suite-running.md # or final-suite-M-running.md - git commit -m "chore: final-suite lock released" - ``` - -5. Proceed to the completion report below. - -### Step E — Sibling idle exit (drain check failed OR lockfile already held) - -This orchestrator does NOT run the suite. Emit exactly one idle log line, then exit cleanly: - -``` -[] Backlog drained for this orchestrator. sibling slot(s) still in flight ([, ...]). -Sibling will run the final suite when it finishes. -``` - -The user will see one final-suite report from whichever sibling finishes last. No further work, no polling, no lockfile writes. - -### Completion report and prompt - -Output the completion report: - -``` -Do Work loop complete. - -Processed: N REQs -Full suite: [passed / skipped — no test runner found] -All outputs committed. -Archive: {project}/.do-work/archive/ -``` - -When the effective budget is armed (non-empty), append a budget line to this report: `Estimated spend: $ / budget $ (tier-weighted estimate)`. This is the natural-exhaustion case (backlog emptied before the budget was hit); the **budget-stop report** (Step 3b.1) is the distinct early-stop case where the budget was reached with REQs still remaining. - -**Then, immediately after the report**, check whether to present next-step options: - -If `config.next_steps.enabled` is `true` **and** this agent is running standalone (not as a delegate inside the go agent): - -**Use the `AskUserQuestion` tool** (do NOT just print the options as text) with these options: - -1. **"Start new work"** — Run intake for a new UR -2. **"Review outputs"** — List archived REQs and their output paths -3. **"Skip"** — End the interaction - -If `config.next_steps.enabled` is `false`, missing, or this agent is running as a delegate inside go: skip the AskUserQuestion and stop. +Outline: drain check (last orchestrator) → lockfile race → run suite once → sibling idle exit → completion report + optional next-steps prompt. --- -## Stopping Rules - -Workers cannot pause and ask the user — they have no interaction surface. Every stopper must surface to the user **through the orchestrator**, never inline from the worker. The worker emits `status: stopped` with a structured `reason`; the orchestrator decides what to show the user. - -### Stopper category → worker `reason` enum - -| Situation | Worker emits `reason` | -|-----------|----------------------| -| Tests cannot be made to pass after 3 attempts | `tests-failing` | -| Verification steps fail after 3 attempts | `verification-failing` | -| A REQ has unmet dependencies on another REQ not yet complete | `dependency-missing` | -| Task requires external credentials or access not available | `missing-creds` | -| Acceptance criteria are ambiguous and cannot be interpreted | `ambiguous-criteria` | -| A change would affect files outside the REQ's stated scope | `scope-creep` | -| Commit or merge conflict unresolved after 5 retries (see run-worker.md `## Concurrent-Conflict Retry`) | `concurrent-conflict` | -| Any other unrecoverable error | `unknown-error` | - -The worker captures relevant details in the report's `details` field. The worker does not retry beyond what's defined in [agents/run-worker.md](run-worker.md) and never asks the user a question — it exits with the structured report. - -### Orchestrator handles user interaction - -When the worker returns `status: stopped`, the orchestrator surfaces the stopper to the user. Recover the REQ from `working/` if it was not archived, then: - -If this agent is running **standalone** (not as a delegate inside the go agent): +## Stopping Rules (outline) -**Use the `AskUserQuestion` tool** (do NOT just print the options as text) with these options: +Workers emit `status: stopped` + `reason` enum (`tests-failing`, `verification-failing`, `dependency-missing`, `missing-creds`, `ambiguous-criteria`, `scope-creep`, `concurrent-conflict`, `unknown-error`). Orchestrator surfaces to user (AskUserQuestion when standalone); per-REQ retry counter for ambiguous-criteria recurrence. -1. **"Show blocker details"** — Display the worker's `details` field and any captured output -2. **"Retry current REQ"** — Re-dispatch the worker for the same REQ (fresh subagent session) -3. **"Skip"** — End the interaction +Full enum handling + feedback path: [run-loop.md](../references/run-loop.md) § Stopping Rules. -If this agent is running as a **delegate** inside go: print the stopper and the worker's `details` field, then stop. Do not loop, do not silently retry, do not auto-resolve. - -### Per-REQ retry counter (ambiguous-criteria recurrence) - -The orchestrator tracks per-REQ stopped-reason occurrences so a *second* `ambiguous-criteria` stop on the same REQ can surface as feedback (a single ambiguity is normal; a second on the same REQ means the user-facing clarification did not stick or the REQ wording is genuinely defective). - -Counter store: `{project}/.do-work/state/retry-counters.md`. Format — one Markdown table row per (REQ, reason) pair: - -```markdown -| REQ-NNN | ambiguous-criteria | 2 | 2026-05-21T02:14:22Z | -``` - -Columns: REQ id, reason, count, last-seen ISO-8601 UTC. The orchestrator keeps this in memory for the lifetime of the loop and flushes to the file after each update. If the file is missing on startup, treat all counters as zero. - -When the worker returns `status: stopped`, `reason: ambiguous-criteria`: - -1. Increment the (REQ-NNN, ambiguous-criteria) counter in memory and persist to `retry-counters.md`. -2. If the new count is **≥ 2**, emit feedback (best-effort, non-blocking) before surfacing the stopper to the user: - - ```bash - FINGERPRINT="ambiguous-req:REQ-NNN" - bash {skill-root}/lib/file-feedback.sh ambiguous-criteria \ - "$FINGERPRINT" \ - '{"req":"REQ-NNN","occurrence":'"$COUNT"',"first_seen":"","last_seen":""}' \ - "Ambiguous-criteria recurrence: REQ-NNN (occurrence #$COUNT)" \ - "Worker has now stopped on REQ-NNN with reason ambiguous-criteria $COUNT times. The acceptance criteria likely need a rewrite, not another retry." \ - || true - ``` +--- -3. Proceed to the existing user-interaction step above (AskUserQuestion or stop-and-print). +## Rules -> **JUDGMENT:** Fire feedback only on the 2nd+ occurrence — the first stop is the worker doing its job; the second is the signal. Title states the REQ id and occurrence count so the human inbox immediately knows which REQ needs editing. The body must point at the *criteria* as the problem (not the worker, not the model) so the human reaches for the REQ file rather than a retry button. +- One REQ per orchestrator in `working/` at a time in serial mode; under `--parallel N` up to N in-flight REQs per orchestrator. Multi-terminal multi-agent is normal. +- TDD is not optional: failing tests must exist before implementation begins. +- Never skip tests because "it's a simple change". +- Never modify REQs in `archive/` after they are committed. +- Never commit without running tests; never commit until all Verification Steps pass. +- Verification failures are feedback — fix and re-verify; after 3 failed attempts on the same REQ, stop and ask the user. +- If `runtime` or `ui` steps need a server, start it and confirm health first. +- The loop runs until the backlog is empty or a stopper is hit. +- Tracker hard rules: no linear→markdown silent fallback; leave Linear claims on mid-flight MCP death. --- -## Rules +## References -- One REQ per orchestrator in `working/` at a time in serial mode; under `--parallel N` (see `## Parallel Run Mode`) up to N in-flight REQs per orchestrator is the norm. Multiple in-flight REQs across parallel orchestrators (multi-terminal) are normal in either mode. -- TDD is not optional: failing tests must exist before implementation begins -- Never skip tests because "it's a simple change" -- Never modify REQs in `archive/` after they are committed -- Never commit without running tests -- Never commit until all Verification Steps pass -- Verification failures are not blockers — they are feedback. Fix the implementation and re-verify. -- After 3 failed verification attempts on the same REQ, stop and ask the user for guidance -- If `runtime` or `ui` steps require a running server, start it in the background and confirm it is healthy before executing those steps -- The loop runs until the backlog is empty or a stopper is hit +- [references/run-loop.md](../references/run-loop.md) — Agent identity, pre-flight, classification, model selection, serial loop steps, stopping rules +- [references/run-parallel.md](../references/run-parallel.md) — Parallel window + empty-backlog drain / final suite +- [agents/run-worker.md](run-worker.md) — Worker contract +- [agents/tracker/linear.md](tracker/linear.md) — Linear backend index (when `tracker.backend: linear`) diff --git a/agents/start.md b/agents/start.md index 9791d77..1877787 100644 --- a/agents/start.md +++ b/agents/start.md @@ -24,17 +24,43 @@ You will be given: Read and follow the **Load Config** section of [config.md](config.md). Keep the loaded config in context — sub-agents will load config independently but the orchestrator should also have it available. +### 0a. Tracker load path + +Work-item storage (URs, REQs, decisions, verify/close reports, run notes) goes **only** through named tracker port ops after config is loaded: + +1. Resolve effective `tracker.backend` (missing/empty/whitespace → `markdown`). +2. Read `agents/tracker/port.md` (shared op catalog + rules). +3. Read `agents/tracker/.md` (e.g. `markdown.md` or `linear.md`). +4. For work-item storage, call **only** named port ops from that backend file — never raw `.do-work/REQ-*` paths or raw Linear tools outside the backend doc. + +**Hard rules:** +- **No silent fallback** from `linear` to `markdown`. If backend is `linear`, do not substitute UR/REQ markdown as the store. +- If backend resolves to **`linear`** but `agents/tracker/linear.md` is **missing or unreadable**, **hard-stop** with setup instructions (restore the Linear backend doc / connect Linear skill). Never fall through to markdown paths. +- Markdown backend: ops map — **invoke** coordination scripts as `bash {skill-root}/lib/...` after Load Config step 8 resolves `$SKILL_ROOT`; **catalog identity** remains `lib/*.sh` in `markdown.md` — use those ops; do not re-implement store details here. + +### Start store — backend branch (ORI-9) + +| Backend | Intake home | Ideate home | Capture home | +|---------|-------------|-------------|--------------| +| **linear** | **`create_ur`** → UR Project Milestone; report Linear ids | **`append_ideate`** on that milestone; **`read_ur`** for brief/ideate | **`create_req`** Issues on product Project + UR milestone; **`list_reqs_for_ur`** to list | +| **markdown** | Local `user-requests/UR-NNN/input.md` | Local `ideate.md` | Local `REQ-*.md` backlog files | + +When backend is **`linear`**, start **must not** require or create `.do-work/user-requests/` as the work-item store. Hard-stop if Linear MCP unusable — never silent markdown fallback. + ### 1. Run Intake Read and follow [intake.md](intake.md) in full. -Execute all intake steps: find next UR number, create the folder, write `input.md`. +- **Markdown:** execute intake steps: find next UR number, create the folder, write `input.md`. +- **Linear:** execute intake **Linear path** only — port op **`create_ur`** (no local folder). Note **UR slug + product project id/name + milestone id/name** from the intake report. **Do not stop after intake.** Unlike standalone intake, the start agent continues immediately. -Note the UR number created (e.g. `UR-003`) — you will need it for the next steps. +Note the UR number created (e.g. `UR-003`) — you will need it for the next steps. Under Linear, also keep the milestone id in context. -**Number conflict guard:** Intake scans existing UR folders and uses max+1. Capture scans existing REQ files across backlog, working, and archive and uses max+1. Both use zero-padded 3-digit numbers. If the filesystem has gaps (e.g., UR-001, UR-003), the next number is max+1 (UR-004), not the gap fill (UR-002). This prevents conflicts with deleted or moved items. +**Number conflict guard:** +- **Markdown:** Intake scans existing UR folders and uses max+1. Capture scans existing REQ files across backlog, working, and archive and uses max+1. Both use zero-padded 3-digit numbers. If the filesystem has gaps (e.g., UR-001, UR-003), the next number is max+1 (UR-004), not the gap fill (UR-002). +- **Linear:** UR slugs come from **`create_ur`** milestone scan; REQ ids are **Linear issue identifiers** allocated by Linear (no local `REQ-NNN`). ### 2. Run Ideate (default — skip with `--no-ideate`) @@ -42,15 +68,23 @@ Unless the `--no-ideate` flag was specified: Read and follow [ideate.md](ideate.md) in full. -Pass it the UR folder path from Step 1. +Pass it: +- **Markdown:** the UR folder path from Step 1 +- **Linear:** the UR slug (and milestone id if known) — not a local folder path Ideate now ends with a mandatory interactive gate (Grill / Continue / Stop). Honor the gate's outcome: - **Grill** chosen by user → ideate.md will already have invoked question.md inline. Continue to Step 3 (Run Capture) when ideate returns. - **Continue** chosen by user (or empty input default) → Continue to Step 3 (Run Capture) when ideate returns. -- **Stop** chosen by user → **Halt the start orchestrator.** Do not run Capture. Output: `Start halted at ideate gate — revise UR-NNN/input.md and re-run start.` Return. +- **Stop** chosen by user → **Halt the start orchestrator.** Do not run Capture. + - **Markdown:** Output: `Start halted at ideate gate — revise UR-NNN/input.md and re-run start.` + - **Linear:** Output: `Start halted at ideate gate — revise UR-NNN brief on Linear (UR milestone) and re-run start.` + - Return. -After ideate returns (and unless Stop was chosen), read `{project}/.do-work/user-requests/UR-NNN/ideate.md` — keep its observations in context for Step 3. +After ideate returns (and unless Stop was chosen), load ideate observations for Step 3: + +- **Markdown:** read `{project}/.do-work/user-requests/UR-NNN/ideate.md` +- **Linear:** **`read_ur`** and use `## Ideate` (do not require local `ideate.md`) If `--no-ideate` was specified, skip this step entirely (no gate runs). @@ -59,14 +93,15 @@ If `--no-ideate` was specified, skip this step entirely (no gate runs). Read and follow [capture.md](capture.md) in full. Pass it: -- The UR folder path from Step 1 +- **Markdown:** the UR folder path from Step 1 +- **Linear:** the UR slug (+ milestone id); capture uses **`read_ur`** / **`create_req`** — no local `input.md` required - The `--no-layers` flag if it was set on the start invocation (capture reads it in its Step 2c) -If ideate was run in Step 2, the Capture agent should read `ideate.md` alongside `input.md` when decomposing — treating the observations as additional context (not as requirements to blindly follow). +If ideate was run in Step 2, Capture should treat ideate observations as additional context (not requirements to blindly follow) — from local `ideate.md` (markdown) or `## Ideate` via **`read_ur`** (linear). ### 4. Report and prompt -Output the combined summary: +**Markdown report:** ``` Start complete for UR-NNN @@ -75,13 +110,28 @@ Intake: {project}/.do-work/user-requests/UR-NNN/input.md Ideate: [yes/no] REQs written: - REQ-NNN-slug.md — Short title REQ-NNN-slug.md — Short title ... Total: N tasks in backlog ``` +**Linear report (report Linear ids):** + +``` +Start complete for UR-NNN + +Intake: Linear UR milestone (product project ) +Ideate: [yes/no — ## Ideate on UR milestone via append_ideate] + +REQs written (Linear issue ids): + ENG-123 — Short title + ENG-124 — Short title + ... + +Total: N issues for UR-NNN +``` + **Then, immediately after the report**, check whether to present next-step options: If `config.next_steps.enabled` is `true`: @@ -104,10 +154,12 @@ If any sub-agent (Intake, Ideate, or Capture) fails mid-flow: 1. **Intake fails:** Stop immediately. Report the exact error. The UR was not created — no cleanup needed. Output: `"Start failed at intake: {error}. No UR was created."` 2. **Question fails:** Output the failure to the user: `"Question failed: {error}. Proceeding without clarifications."` Continue to Ideate (or Capture if `--no-ideate`). Do not block the pipeline for an advisory step. -3. **Ideate fails:** Output the failure to the user: `"Ideate failed: {error}. Proceeding without ideate observations."` Continue to Capture as if `--no-ideate` was specified. Do not block the pipeline for an advisory step. Do not write a partial `ideate.md` — if the file was partially written, delete it before continuing. +3. **Ideate fails:** Output the failure to the user: `"Ideate failed: {error}. Proceeding without ideate observations."` Continue to Capture as if `--no-ideate` was specified. Do not block the pipeline for an advisory step. + - **Markdown:** do not leave a partial local `ideate.md` — if partially written, delete it before continuing. + - **Linear:** do not invent a local `ideate.md` dual-write; partial milestone append failures → hard-stop / report per linear.md (leave remote state as-is for operator). 4. **Capture fails:** Stop immediately. Report the exact error and the UR number so the user can resume. Output: `"Start failed at capture: {error}. UR-NNN was created but has no REQs. Resume with: /do-work capture UR-NNN"` -In all cases, never leave partial state without reporting it. If a UR was created but Capture failed, tell the user the UR number so they can resume. +In all cases, never leave partial state without reporting it. If a UR was created but Capture failed, tell the user the UR number (and under Linear, the milestone id) so they can resume. --- @@ -118,3 +170,4 @@ In all cases, never leave partial state without reporting it. If a UR was create - If Intake encounters an existing UR conflict, resolve it per intake.md's rules before proceeding - Ideate runs by default — use `--no-ideate` to skip it - Do not run Verify or Run — that is the Go agent's job +- **Linear:** no dual-write to `user-requests/`; intake reports Linear ids; hard-stop if MCP unusable diff --git a/agents/status.md b/agents/status.md index c0b0b0b..4339626 100644 --- a/agents/status.md +++ b/agents/status.md @@ -21,70 +21,82 @@ You will be given: Read and follow the **Load Config** section of [config.md](config.md). -### 1. Render situation +### 0a. Tracker load path + +Work-item storage (URs, REQs, decisions, verify/close reports, run notes) goes **only** through named tracker port ops after config is loaded: + +1. Resolve effective `tracker.backend` (missing/empty/whitespace → `markdown`). +2. Read `agents/tracker/port.md` (shared op catalog + rules). +3. Read `agents/tracker/.md` (e.g. `markdown.md` or `linear.md`). +4. For work-item storage, call **only** named port ops from that backend file — never raw `.do-work/REQ-*` paths or raw Linear tools outside the backend doc. + +**Hard rules:** +- **No silent fallback** from `linear` to `markdown`. If backend is `linear`, do not substitute UR/REQ markdown as the store. +- If backend resolves to **`linear`** but `agents/tracker/linear.md` is **missing or unreadable**, **hard-stop** with setup instructions (restore the Linear backend doc / connect Linear skill). Never fall through to markdown paths. +- Markdown backend: ops map — **invoke** coordination scripts as `bash {skill-root}/lib/...` after Load Config step 8 resolves `$SKILL_ROOT`; **catalog identity** remains `lib/*.sh` in `markdown.md` — use those ops; do not re-implement store details here. + +**Branch the render path on effective backend** (after load path): + +| Backend | Work-item situation room | +|---------|--------------------------| +| **`markdown`** (default) | Steps **1–2** below (`{skill-root}/lib/synth-status.sh`, `derive-status`, `coverage-rollup`, `deadlock-check`) | +| **`linear`** | Step **1L** — Linear claimers / heartbeats via port ops in `agents/tracker/linear.md` (**Status reporting**). Do **not** glob `.do-work/working/` or treat local REQ files as the live store. | + +### 1. Render situation (markdown backend) + +*Skip this step when effective backend is `linear` — use **1L** instead.* Run: ```bash -bash lib/synth-status.sh [UR-NNN] # passes the optional scope +bash {skill-root}/lib/synth-status.sh [UR-NNN] # passes the optional scope ``` Print stdout verbatim to the user. -If `lib/synth-status.sh` is missing, report `"lib/synth-status.sh not found — cannot render status."` and stop. +Unscoped output prioritizes live work: backlog + working list fully; archive is capped to recent completed rows with a note when more exist. Scope with `UR-NNN` to list every matching archived REQ. Archive rows always show Status `done` even if a file header is stale. + +If `$SKILL_ROOT/lib/synth-status.sh` is missing, report `"$SKILL_ROOT/lib/synth-status.sh not found — cannot render status."` and stop. Then render a proof-backed status view. Glob REQ files in backlog, `working/`, and `archive/` (respecting `UR-NNN` scope when provided), and run: ```bash -bash lib/derive-status.sh ... +bash {skill-root}/lib/derive-status.sh ... ``` -Print the result under a `Proven` heading. This is a derived view: `proven` means the REQ is done/archived and has a non-empty `**Closure proof:**`; `unproven` means either proof is missing or the REQ is not done. If `lib/derive-status.sh` is missing, report `"lib/derive-status.sh not found — skipping proven view."` and continue. +Print the result under a `Proven` heading. This is a derived view: `proven` means the REQ is done/archived, has a non-empty `**Closure proof:**`, and does not carry `**Suite:** not-run`; `unproven` means proof is missing, the REQ is not done, or it carries the `**Suite:** not-run` marker (its own test/build suite could not be run — see `agents/run-worker.md` §6 and `agents/run.md` Step 4b sub-step 5a). If `$SKILL_ROOT/lib/derive-status.sh` is missing, report `"$SKILL_ROOT/lib/derive-status.sh not found — skipping proven view."` and continue. Then render the intended-vs-proven Coverage section: ```bash -bash lib/coverage-rollup.sh [UR-NNN] +bash {skill-root}/lib/coverage-rollup.sh [UR-NNN] ``` -Print stdout under a `Coverage` heading. Each line shows `intended= proven= unproven= pending=`, any `unproven_ids`, and a trailing `closed=` end-to-end closure field. `pending` counts REQs in the `pending-validation` state — code merged, human/device sign-off outstanding — as their own bucket, so a UR whose work is all merged but unsigned-off reads as pending, not as an unproven coverage gap or a completion. `closed` reports whether the UR has been validated end-to-end by `/do-work close` (per docs/design/ur-closure.md), distinct from per-REQ proof: `yes` = `UR-NNN/closure.md` exists with `overall: closed`; `no` = closure.md reports gaps, or the UR has path-unit REQs but no closure.md yet (run `/do-work close UR-NNN`); `n/a` = the UR declares no path-unit REQs to walk. `proven` still means per-REQ closure proof; `closed` means the merged whole was walked. Also compute and print a project total by summing the rows. If there are no REQs yet, show `Coverage: no REQs captured yet.` If `lib/coverage-rollup.sh` is missing, report `"lib/coverage-rollup.sh not found — skipping coverage rollup."` and continue. - -### 1b. Render Pending validation - -Surface the merged-but-unsigned-off queue so parked REQs are a queue the user sees, not a stall they discover. Glob `{project}/.do-work/pending/REQ-*.md` (respecting `UR-NNN` scope when provided — match the REQ's `**UR:**` field). - -If the `pending/` directory is absent or contains no matching REQ files, **render nothing** — omit the section entirely. - -Otherwise, print a `Pending validation` heading followed by one row per pending REQ. For each `.do-work/pending/REQ-NNN-*.md`: - -- **REQ id** — from the filename / `# REQ-NNN:` heading. -- **Title** — the text after `REQ-NNN:` in the `#` heading. -- **Age** — how long it has been pending. Prefer the metadata commit time: - - ```bash - git -C "{project}" log -1 --format=%cr -- ".do-work/pending/REQ-NNN-*.md" - ``` +Print stdout under a `Coverage` heading. Each line shows `intended= proven= unproven=`, any `unproven_ids`, and a trailing `closed=` end-to-end closure field. `closed` reports whether the UR has been validated end-to-end by `/do-work close` (per docs/design/ur-closure.md), distinct from per-REQ proof: `yes` = `UR-NNN/closure.md` exists with `overall: closed`; `no` = closure.md reports gaps, or the UR has path-unit REQs but no closure.md yet (run `/do-work close UR-NNN`); `n/a` = the UR declares no path-unit REQs to walk. `proven` still means per-REQ closure proof; `closed` means the merged whole was walked. Also compute and print a project total by summing the rows. If there are no REQs yet, show `Coverage: no REQs captured yet.` If `$SKILL_ROOT/lib/coverage-rollup.sh` is missing, report `"$SKILL_ROOT/lib/coverage-rollup.sh not found — skipping coverage rollup."` and continue. - If that yields nothing (file not yet committed, or not a git checkout), fall back to the file mtime: +### 1L. Render situation (Linear backend) - ```bash - # macOS (bash 3.2): stat -f; GNU: stat -c. Try BSD first, then GNU. - stat -f '%Sm' -t '%Y-%m-%d %H:%M' "" 2>/dev/null \ - || stat -c '%y' "" 2>/dev/null - ``` +*Only when effective `tracker.backend` is `linear`. Sequences live in `agents/tracker/linear.md` — **Status reporting (claimers / heartbeats)** and **Helper: read active claim**. Rediscover Linear tools live; hard-stop if MCP unusable (never fall back to `synth-status.sh` as the work-item store).* -- **Outstanding checklist** — the unchecked items from the REQ's `## Post-merge validation` section (lines beginning `- [ ]`). List each. If the section is absent or has no unchecked items, note `(no outstanding items recorded)`. +1. **Scope** — optional `UR-NNN` → Project `do-work/{UR-id}` (config `project_name_pattern`). No UR → all team Projects matching `do-work/UR-*` (or `list_urs` then per-project issues). +2. **List issues** in scope via port list ops (`list_reqs_for_ur` / list-by-project sequences). Identify rows by **Linear issue id** only (e.g. `ENG-123`). +3. **For each issue** with workflow mapping to `in_progress` or `stopped` (and optionally recent `released` for audit): + - Parse the latest claim-protocol comment (`tracker.linear.agent_claim_marker`, default ``) via **Helper: read active claim**. + - Report: **id**, title, do-work status (via inverted `status_map`), **claimer** (`agent_id`), **claimed_at**, **heartbeat**, **fresh/stale** vs effective stale max (`heartbeat_max_age_seconds` or `parallel.stale_threshold_seconds`), claim `status` (`active` / `released`). +4. **Stale banner** — if any active claim is stale, prepend a clear warning (parity with markdown stale/deadlock intent). Surface deps from authoritative `blocks` relations when tools exist. +5. **Do not** invent local REQ paths, run `{skill-root}/lib/synth-status.sh` / glob `.do-work/working/` as the live claim source, or change Linear state (read-only). +6. Optional local telemetry (e.g. gate-owner files under `state/`) may be mentioned separately; they are **not** the work-item store. -After the rows, name the resolution command explicitly: `Resolve with: /do-work approve REQ-NNN` (or reject). State that each REQ's code is already merged — only sign-off is outstanding. +Print a compact table or list under a `Linear status` heading, then stop (skip markdown Step 2 unless a local deadlock helper is useful for **runtime** locks only — never treat markdown REQ globs as Linear truth). -This section is read-only: glob, read, and render. Make no writes and no commits. +### 2. Check for deadlock (markdown backend) -### 2. Check for deadlock +*Skip when backend is `linear` (stale claims already surfaced in **1L**). Optional: still run for local gate/runtime diagnostics only; do not treat empty `working/` as “idle” under Linear.* Run: ```bash -bash lib/deadlock-check.sh +bash {skill-root}/lib/deadlock-check.sh ``` If output is non-empty, prepend it to the status report with a clear header: @@ -96,7 +108,7 @@ If output is non-empty, prepend it to the status report with a clear header: ──────────────────── ``` -If `lib/deadlock-check.sh` is missing, report `"lib/deadlock-check.sh not found — skipping deadlock check."` and continue without it. +If `$SKILL_ROOT/lib/deadlock-check.sh` is missing, report `"$SKILL_ROOT/lib/deadlock-check.sh not found — skipping deadlock check."` and continue without it. ### 3. Stop @@ -106,7 +118,7 @@ No prompts, no commits, no state changes. ## Rules -- Read-only. Never write any file under `{project}/.do-work/` or the source tree. +- Read-only. Never write any file under `{project}/.do-work/` or the source tree (and never write Linear issues while rendering status). - No git commits, no AskUserQuestion prompts. -- If `lib/synth-status.sh` or `lib/deadlock-check.sh` are missing, report the missing script and stop (synth-status missing) or continue without the check (deadlock-check missing). -- The deadlock banner always renders above the synth-status output when present. +- **Markdown:** If `$SKILL_ROOT/lib/synth-status.sh` or `$SKILL_ROOT/lib/deadlock-check.sh` are missing, report the missing script and stop (synth-status missing) or continue without the check (deadlock-check missing). The deadlock banner always renders above the synth-status output when present. +- **Linear:** Use only `agents/tracker/linear.md` status / claim-comment sequences; hard-stop if Linear MCP is unusable; no silent markdown situation room. diff --git a/agents/tracker/linear.md b/agents/tracker/linear.md new file mode 100644 index 0000000..d2da673 --- /dev/null +++ b/agents/tracker/linear.md @@ -0,0 +1,238 @@ +# Tracker backend: linear (opt-in) + +Implements the tracker port (`agents/tracker/port.md`) with **Linear as the sole work-item store** when `tracker.backend: linear`. Agent steps invoke the Linear skill / MCP; there is **no** Linear-aware bash in v1 and **no** dual-write to local UR/REQ markdown. + +**This is not the default.** When `tracker.backend` is missing, empty, or `markdown`, agents load `markdown.md` instead — Linear tools are not required. + +--- + +## When to load + +After config load and backend resolution (`port.md` load path + `agents/config.md` Load Config step 7): + +1. Effective backend is **`linear`**. +2. Linear validation passes (team resolvable, MCP discoverable, every `status_map` state exists on the team) — or agent **hard-stops** (see below). +3. Read `agents/tracker/port.md`. +4. Read this file (index + hard rules). +5. When executing a named op sequence, read the one-hop reference listed in the **Port op index** below. + +**Exception — idle migration:** `/do-work upgrade migrate` / `migrate_markdown_to_linear` loads while backend is still **`markdown`**. Sequence: [references/linear-paths.md](../../references/linear-paths.md) (migration path section). + +Do **not** load this file for ordinary work-item ops when backend is `markdown` (except the migration path). + +--- + +## Hierarchy (authoritative) + +``` +Team (config) +└── Project product_project — one shared product Project per local product (not per UR) + ├── Project Milestone (UR) — §9.1 + └── Issue (REQ) — attached to that UR milestone + └── Sub-issue (layer child) +``` + +| Entity | Naming / config | +|--------|-----------------| +| Product Project | `tracker.linear.product_project` — **shared** name or UUID; **default empty**. Resolve via config chain (explicit `product_project` → `project.name` → git-root basename); `ensure_product_container` create-if-missing + **always persist UUID**. Never fall through to skill name `do-work` for empty config. Example for this skill repo only: name `do-work`. | +| UR | **Project Milestone** on that project; name `ur_milestone_name_pattern` (default `{ur_id}: {title}`) | +| REQ | **Linear issue id only** (e.g. `ENG-123`) — no parallel `REQ-NNN` | +| Issue scope | product Project + UR Project Milestone membership | + +### Hard rules (hierarchy) + + +1. **No Initiative-as-UR** — MCP has no reliable Initiative create path; URs are Project Milestones. +2. **`product_project` is shared per local product** — do not create per-UR Projects (including `do-work/{UR-id}` patterns) as the UR container. +3. **Atomic `create_ur`** — product Project ensure + milestone create; no partial UR; hard-stop on failure. +4. **Rediscover, never invent** — every op begins with `search_tool`; hard-stop if tools missing. +5. **No dual-write** — Linear is sole work-item store while `backend: linear`. + +### Disambiguation: Milestone-as-UR vs path-milestone mode (M1/M2) + +| | **Milestone-as-UR** | **Path-milestone mode (M1/M2)** | +|--|---------------------|--------------------------------| +| What | The UR *entity* | Optional delivery mode *inside* one UR | +| Trigger | Every Linear UR | Brief has `source: /saas-thesis handoff` **and** `### Milestones` with `#### M1`+ | +| Store | Linear Project Milestone | Cursor `` on that **same** UR milestone description; Issues marked `M1`/`M2` | +| Ops | `create_ur` / `read_ur` / `list_urs` | `read_active_milestone` / `set_active_milestone` / `list_milestone_reqs` | +| Detail | This section + [linear-ops.md](../../references/linear-ops.md) | [linear-path-milestones.md](../../references/linear-path-milestones.md) | + +--- + +## Tool rediscovery (hard rule) + +1. Call **`search_tool`** scoped to Linear. +2. Call **`use_tool`** only with a **qualified** name + **`input_schema`** from that search. +3. Never hard-code tool names; never fabricate issues/milestones when MCP is down; never silent-fallback to markdown. + +Official remote MCP: `https://mcp.linear.app/mcp`. Setup: Linear skill `SKILL.md`. + +--- + +## Port op index + +Read the pointed reference **when executing that op** (one hop only — no references→references chains for further sequences). + +| Port op / surface | When to load | Reference | +|-------------------|--------------|-----------| +| `ensure_product_container` | Before first CRUD in session; intake | [linear-ops.md](../../references/linear-ops.md) § ensure_product_container | +| **`create_ur`** | Intake / start | [linear-ops.md](../../references/linear-ops.md) § create_ur | +| `read_ur` / `list_urs` | Any phase needing brief / UR list | [linear-ops.md](../../references/linear-ops.md) | +| `create_req` / `update_req` / `read_req` / `list_reqs_for_ur` | Capture / workers | [linear-ops.md](../../references/linear-ops.md) | +| `append_ideate` / `append_clarifications` | Ideate / question | [linear-ops.md](../../references/linear-ops.md) | +| `set_blocked_by` / `set_files` | Deps / footprint writers | [linear-ops.md](../../references/linear-ops.md) | +| `list_claimable_reqs` / `claim_req` / `heartbeat_req` | Run pick/claim | [linear-ops.md](../../references/linear-ops.md) | +| `set_req_status` / `unblock_req` / Resume / Status | Stop / unblock / resume / status | [linear-ops.md](../../references/linear-ops.md) | +| **`archive_req`** / `append_run_note` | Post-worker integrate | [linear-ops.md](../../references/linear-ops.md) | +| `append_decision` / calibration Doc | Capture / retro | [linear-ops.md](../../references/linear-ops.md) | +| `write_verify_report` / `write_close_report` | Verify / close | [linear-ops.md](../../references/linear-ops.md) | +| `read_active_milestone` / `set_active_milestone` / `list_milestone_reqs` / `write_gate_state` | Path-milestone mode only | [linear-path-milestones.md](../../references/linear-path-milestones.md) | +| §9 templates / labels / path-units | Creating or parsing bodies | [linear-ops.md](../../references/linear-ops.md) § Templates | +| Commits / branch sanitize (§6.5) | Worker / merge under Linear | [linear-path-milestones.md](../../references/linear-path-milestones.md) (commits section) or [linear-ops.md](../../references/linear-ops.md) | +| Path narratives / capability matrix / migration | Spike fill, upgrade migrate | [linear-paths.md](../../references/linear-paths.md) | + +### Templates (pointers only) + +| Entity | Marker | Full template | +|--------|--------|---------------| +| UR Project Milestone | `` | [linear-ops.md](../../references/linear-ops.md) §9.1 | +| Issue (REQ) | `` | [linear-ops.md](../../references/linear-ops.md) §9.2 | + +On read/update: missing marker → **stop the op**; do not invent headers. + +### Non-ticket artifact homes (summary) + +| Artifact | Home | Op | +|----------|------|-----| +| Decisions | Team Doc `decisions_doc_title` | `append_decision` | +| Calibration | Team Doc `calibration_doc_title` | Write/read calibration | +| Run notes | Issue comment `` | `append_run_note` | +| Verify / close | UR Project Milestone `## Verify` / `## Closure` + comment | `write_verify_report` / `write_close_report` | +| Path-milestone cursor | UR Project Milestone description `` | milestone ops | +| Gate locks | **Local** `state/gate-owner.md` only | `write_gate_state` | + +Full sequences: [linear-ops.md](../../references/linear-ops.md). + +--- + +## status_map + +| do-work status | Config key | Default Linear state name | +|----------------|------------|---------------------------| +| `backlog` | `status_map.backlog` | `Todo` | +| `in_progress` | `status_map.in_progress` | `In Progress` | +| `stopped` | `status_map.stopped` | `Canceled` | +| `done` | `status_map.done` | `Done` | + +**Hard-fail:** when `backend: linear`, every mapped state **name** must exist on the team workflow. Missing → hard-stop (rename team state or override map). **Never** invent states; **never** pick “close enough”; **never** fall back to markdown. + +Live sandbox validation (when MCP was missing) and matrix notes: [linear-paths.md](../../references/linear-paths.md). + +--- + +## Hard-stop when Linear MCP is missing or unusable + +When `tracker.backend` is **`linear`**, failure is a **hard stop**. **Never** silent-fallback to markdown work-item ops, invent tickets, or write substitute UR/REQ files under `.do-work/`. + +### Operator-facing message (use as template) + +```text +HARD STOP: Linear tracker backend is configured but Linear MCP is not usable. + +do-work will not fall back to markdown work-item storage while tracker.backend is "linear". +No issues, Initiative-as-UR entities, or local REQ/UR substitutes were invented. + +What failed: + +Fix — connect Linear MCP (from Linear skill setup): + +1. Preferred (API key): + - Create a Personal API key in Linear → Settings → Account → Security & access + - Export in the shell that launches the agent (do not paste the key into chat): + export LINEAR_API_KEY='lin_api_...' + - Configure MCP server `linear` at https://mcp.linear.app/mcp with + Authorization: Bearer ${LINEAR_API_KEY} + - Restart the agent / refresh MCP (`/mcps` → r) and verify tools via search_tool "linear" + +2. OAuth alternative (if your host supports it): + - Add HTTP MCP server `linear` → https://mcp.linear.app/mcp + - Authenticate in `/mcps` (or host equivalent) + - If OAuth sticks on "authenticating", use the API key path instead + +3. Grok CLI examples (host-specific): + - grok mcp add --transport http linear https://mcp.linear.app/mcp + - grok mcp enable linear + - grok mcp doctor linear + +4. Team + product Project config (when MCP works but team/project fails): + - Set tracker.linear.team_id (UUID) and/or tracker.linear.team_key in .do-work/config.yml + - product_project resolve (Load Config step 8 / ensure_product_container): explicit + tracker.linear.product_project (name|UUID) if set; else project.name; else git-root + directory basename. Default product_project is empty — never invent skill name `do-work` + for empty config. ensure_product_container create-if-missing (name path) and always + persists the Project UUID back to tracker.linear.product_project. + - Empty-name failure: product_project, project.name, and basename all empty/unusable → + set project.name or product_project explicitly; do not invent a name; do not markdown-fallback + - Multi-match failure: more than one team Project shares the target name → set + tracker.linear.product_project to the desired Project UUID (names are ambiguous) + - Do not guess a team + +5. status_map (when team loads but a workflow state name is missing): + - Defaults: backlog→"Todo", in_progress→"In Progress", stopped→"Canceled", done→"Done" + - Rename the team workflow state to match, OR override tracker.linear.status_map. + to an existing state name on that team + - Missing states are never invented + +Then re-run the phase. If a claim was already active when MCP died mid-flight, leave it; +use /do-work resume or unblock after MCP recovers (port: leave claimed). +``` + +### Conditions → stop (summary) + +| Condition | Behavior | +|-----------|----------| +| `search_tool` returns no Linear tools | Hard stop + setup steps above | +| MCP offline / unauthenticated mid-session | Hard stop; if already claimed → leave claimed | +| Team id/key unresolved | Hard stop; do not guess | +| `product_project` empty-name after resolve chain | Hard stop; set `project.name` or `product_project`; **no** skill-name invent; **no** markdown fallback | +| `product_project` multi-match by name on team | Hard stop; require UUID in `tracker.linear.product_project` | +| `product_project` unresolved / uncreatable | Hard stop | +| Any `status_map` value missing on team workflow | Hard stop + rename / override instructions | +| Milestone / issue create tools missing for `create_ur` / `create_req` | Hard stop; **no** Initiative-as-UR substitute; **no** markdown dual-write | +| Relation tools missing after spike documents **missing** | Prefer body-only deps + one-time warning — still no markdown fallback | + +### Claim / mid-flight (summary) + +| Event | Behavior | +|-------|----------| +| Fresh foreign active claim | `concurrent-conflict`; resume for owner | +| MCP dies after successful `claim_req` | **Leave claimed**; stop for resume/unblock; never silent-release; never markdown fallback | +| Human assignee | Sacred — agents never steal Linear assignee for claim | + +Full claim/archive sequences: [linear-ops.md](../../references/linear-ops.md). + +--- + +## Run-loop rules (pointers) + +| Concern | Rule | Detail | +|---------|------|--------| +| Deps | Native **`blocks`** relations authoritative; body `**Depends on:**` mirror | [linear-ops.md](../../references/linear-ops.md) | +| Footprint | Issue `**Files:**` vs in-flight claims; empty = free | [linear-ops.md](../../references/linear-ops.md) | +| Pick order | Priority DESC (missing→2) → created_at ASC → id ASC | `list_claimable_reqs` | +| Archive | Only via **`archive_req`** after evidence + review gates | [linear-ops.md](../../references/linear-ops.md) | +| Commits | `feat(ENG-123):` + `Issue:` footer; branch `req/` | [linear-path-milestones.md](../../references/linear-path-milestones.md) | +| No Linear bash in `lib/` (v1) | Sequences are agent/MCP only | — | + +--- + +## References + +- [references/linear-ops.md](../../references/linear-ops.md) — CRUD, claim, archive, artifacts, templates +- [references/linear-path-milestones.md](../../references/linear-path-milestones.md) — path-milestone (M1/M2) ops + commits/branch sanitize +- [references/linear-paths.md](../../references/linear-paths.md) — path narratives, capability matrix, migration +- `agents/tracker/port.md` — shared ops + hard-stop / leave-claimed +- `agents/config.md` — `tracker.*` schema +- `agents/intake.md` / phase agents — Milestone-as-UR consumers (ORI-9) +- Design: `docs/superpowers/specs/2026-07-31-do-work-multi-tracker-design.md` diff --git a/agents/tracker/markdown.md b/agents/tracker/markdown.md new file mode 100644 index 0000000..2d3dfe9 --- /dev/null +++ b/agents/tracker/markdown.md @@ -0,0 +1,412 @@ +# Tracker backend: markdown (default) + +Implements the tracker port (`agents/tracker/port.md`) with local files under `.do-work/` and existing `lib/*.sh` helpers. + +**This is the default backend.** When `tracker.backend` is missing, empty, or `markdown`, agents load this file and run today's file-based loop. No Linear tools, credentials, or dual-write. + +--- + +## When to load + +After config load and backend resolution (see `port.md` load path and each phase agent's **Tracker load path** block): + +1. Backend resolves to `markdown` (including unset/empty default). +2. Read `agents/tracker/port.md`. +3. Read this file. +4. Perform work-item ops only via the mappings below. + +Do **not** load `agents/tracker/linear.md` on this path. + +--- + +## Bash surface (markdown only) + +**No Linear-aware bash is required for the markdown backend.** Every `lib/*.sh` script listed here is file/git coordination against `.do-work/`. Scripts do not call Linear MCP, GraphQL, or Linear APIs. Linear reimplements the same *semantics* in `agents/tracker/linear.md` via agent/MCP steps — it does not reuse these scripts as Linear clients. + +| Script | Role on this backend | +|--------|----------------------| +| `lib/pick-req.sh` | Pick first claimable backlog REQ (scope, deps, footprint) | +| `lib/claim-req.sh` | Atomic claim: `git mv` / `mv` + stamp + status | +| `lib/check-deps.sh` | Unsatisfied `**Depends on:**` vs `archive/` | +| `lib/check-footprint.sh` | Footprint overlap vs `working/` slots | +| `lib/heartbeat.sh` | Refresh `**Heartbeat:**` on a claimed working/ REQ (FS-only) | +| `lib/scan-stale.sh` | List working/ slots past stale threshold (orchestrator) | +| `lib/check-archive-integrity.sh` | Gate before archive: status done, proof, AC checked | +| `lib/run-ledger.sh` | Append `.do-work/runs/RUN-NNN.yml` when ledger enabled | +| `lib/score-coverage.sh` | Verify-phase confidence arithmetic | +| `lib/deadlock-check.sh` | Parallel drain diagnosis (orchestrator) | +| `lib/derive-status.sh` | proven/unproven from closure proof + Suite header | + +**Composition note:** `list_claimable_reqs` is implemented by `pick-req.sh`, which inlines deps + footprint filters equivalent to `check-deps.sh` and `check-footprint.sh`. The standalone check scripts remain the explicit ops/helpers for single-REQ diagnostics and tests; pick is the run-loop picker. + +--- + +## Store layout (unchanged) + +| Artifact | Location | +|----------|----------| +| UR brief | `.do-work/user-requests/UR-NNN/input.md` | +| Ideate | `.do-work/user-requests/UR-NNN/ideate.md` | +| Clarifications | `## Clarifications` section inside `input.md` | +| REQ backlog | `.do-work/REQ-NNN-*.md` (or `REQ-M-NNN-*.md` in milestone mode) | +| In-flight | `.do-work/working/REQ-NNN-*.md` | +| Done | `.do-work/archive/REQ-NNN-*.md` | +| Decisions | `.do-work/decisions.md` | +| Close report | `.do-work/user-requests/UR-NNN/closure.md` (+ optional `closure-evidence/`) | +| Verify report | Console/agent output from `agents/verify.md` (no single fixed file path; scoring via `lib/score-coverage.sh`) | +| Run notes / ledger | `.do-work/runs/RUN-NNN.yml` via `lib/run-ledger.sh` when `ledger.enabled` | +| Milestone cursor | `.do-work/state/active-milestone.md`, checklist `.do-work/state/milestones.md` | +| Gate lock | `.do-work/state/gate-owner.md` | +| Final-suite lock | `.do-work/state/final-suite-*.md` (runtime; not a port work-item field) | + +Runtime/git (worktrees, merges, events, config) stay local and are outside the port op surface. + +--- + +## Op → implementation map + +Every op name from `port.md` appears below with **script path and/or file glob**. Paths under `lib/` are only listed when the file exists in this repo. Where no dedicated script exists, the implementation is **agent playbook + file edit** (called out as such — not invented as a fake `lib/*.sh`). + +### Catalog index (quick) + +| Port op | Primary implementation | +|---------|------------------------| +| `ensure_product_container` | `mkdir -p` / install layout under `.do-work/` — no dedicated lib | +| `create_ur` | `agents/intake.md` → `.do-work/user-requests/UR-NNN/input.md` | +| `read_ur` | Read `.do-work/user-requests/UR-NNN/input.md` (+ `ideate.md` if present) | +| `list_urs` | Glob `.do-work/user-requests/UR-*/` | +| `append_ideate` | `agents/ideate.md` → `user-requests/UR-NNN/ideate.md` | +| `append_clarifications` | `agents/question.md` → append `## Clarifications` in `input.md` | +| `create_req` | `agents/capture.md` → `.do-work/REQ-NNN-*.md` | +| `update_req` | Edit REQ file in place (capture/audit/worker/orchestrator) | +| `read_req` | Read REQ from backlog / `working/` / `archive/` | +| `list_reqs_for_ur` | Glob REQs with matching `**UR:**` across backlog/working/archive | +| `list_claimable_reqs` | **`lib/pick-req.sh`** (uses deps + footprint filters; peers: **`lib/check-deps.sh`**, **`lib/check-footprint.sh`**) | +| `claim_req` | **`lib/claim-req.sh`** | +| `heartbeat_req` | **`lib/heartbeat.sh`** | +| `set_req_status` | Edit `**Status:**` on REQ file (agent/orchestrator; no dedicated lib) | +| `set_blocked_by` | Edit `**Depends on:**` header (no dedicated lib) | +| `set_files` | Edit `**Files:**` header (no dedicated lib) | +| `archive_req` | Orchestrator: status/proof/outputs + move to `archive/`; gate **`lib/check-archive-integrity.sh`** | +| `unblock_req` | `agents/unblock.md` — strip claim stamp, status backlog, move out of `working/` | +| `append_decision` | Append line to `.do-work/decisions.md` | +| `write_verify_report` | `agents/verify.md` + **`lib/score-coverage.sh`** (console report; no fixed durable path) | +| `write_close_report` | `agents/close.md` → `user-requests/UR-NNN/closure.md` | +| `append_run_note` | **`lib/run-ledger.sh`** → `.do-work/runs/RUN-NNN.yml` when ledger enabled | +| `read_active_milestone` | Read `.do-work/state/active-milestone.md` | +| `set_active_milestone` | Write/delete `.do-work/state/active-milestone.md` (+ `milestones.md` checklist) | +| `list_milestone_reqs` | Glob `.do-work/REQ-M-*.md` (and working/archive forms) for active `M` | +| `write_gate_state` | Write/delete `.do-work/state/gate-owner.md` (local lock) | + +--- + +### Op contracts (markdown sequences) + +#### `ensure_product_container` + +| | | +|---|---| +| **Intent** | Ensure local product store dirs exist. | +| **Implementation** | Agent/install: `mkdir -p .do-work/{user-requests,working,archive,state,runs}` as needed. Install path: `install.sh` / first-use conventions. | +| **lib/*.sh** | **None** — no `lib/ensure-product-container.sh`. Gap: intentional; directory creation is playbook/install. | +| **File globs** | `.do-work/` tree | + +#### `create_ur` + +| | | +|---|---| +| **Intent** | Record intake brief. | +| **Implementation** | `agents/intake.md`: allocate next `UR-NNN`, `mkdir -p .do-work/user-requests/UR-NNN/assets`, write `input.md`. | +| **lib/*.sh** | **None** | +| **File paths** | `.do-work/user-requests/UR-NNN/input.md` | + +#### `read_ur` + +| | | +|---|---| +| **Intent** | Load brief (+ ideate if present). | +| **Implementation** | Read `input.md`; optionally read `ideate.md` and `## Clarifications` in the brief. | +| **lib/*.sh** | **None** | +| **File paths** | `.do-work/user-requests/UR-NNN/input.md`, `…/ideate.md` | + +#### `list_urs` + +| | | +|---|---| +| **Intent** | Enumerate URs. | +| **Implementation** | Glob directories under `.do-work/user-requests/UR-*/` (exclude non-UR siblings such as `archive/` if present). | +| **lib/*.sh** | **None** | +| **File globs** | `.do-work/user-requests/UR-*/` | + +#### `append_ideate` + +| | | +|---|---| +| **Intent** | Write ideate onto UR. | +| **Implementation** | `agents/ideate.md` writes `.do-work/user-requests/UR-NNN/ideate.md`. | +| **lib/*.sh** | **None** | +| **File paths** | `user-requests/UR-NNN/ideate.md` | + +#### `append_clarifications` + +| | | +|---|---| +| **Intent** | Question-phase Q&A. | +| **Implementation** | `agents/question.md` appends `## Clarifications` (and Q&A entries) to `input.md`. Never overwrites the original brief above that section. | +| **lib/*.sh** | **None** | +| **File paths** | `user-requests/UR-NNN/input.md` | + +#### `create_req` + +| | | +|---|---| +| **Intent** | Create one backlog REQ for a UR. | +| **Implementation** | `agents/capture.md` writes `.do-work/REQ-NNN-slug.md` (or `REQ-M-NNN-slug.md`) with template headers, `**Status:** backlog`, footprint/deps as known. | +| **lib/*.sh** | **None** for create; later eligibility uses pick/deps/footprint. | +| **File globs** | `.do-work/REQ-*.md` | + +#### `update_req` + +| | | +|---|---| +| **Intent** | Edit body/fields without claim/archive lifecycle. | +| **Implementation** | In-place edit of the REQ file wherever it lives (backlog / working / archive for allowed phases). Prefer dedicated ops for status, deps, footprint, claim. | +| **lib/*.sh** | **None** | +| **File paths** | matching `REQ-*.md` in backlog, `working/`, or `archive/` | + +#### `read_req` + +| | | +|---|---| +| **Intent** | Load full REQ. | +| **Implementation** | Read the REQ file; resolve by id across backlog root, `working/`, `archive/`. | +| **lib/*.sh** | **None** (optional downstream: `lib/derive-status.sh` for proven/unproven view). | +| **File globs** | `.do-work/REQ--*.md`, `.do-work/working/REQ--*.md`, `.do-work/archive/REQ--*.md` | + +#### `list_reqs_for_ur` + +| | | +|---|---| +| **Intent** | All REQs for a UR, any status. | +| **Implementation** | Glob REQ files; filter on header `**UR:** UR-NNN`. | +| **lib/*.sh** | **None** — no `lib/list-reqs-for-ur.sh`. | +| **File globs** | `.do-work/REQ-*.md`, `working/REQ-*.md`, `archive/REQ-*.md` | + +#### `list_claimable_reqs` + +| | | +|---|---| +| **Intent** | Backlog REQs that are unclaimed, deps-satisfied, footprint-free — pick order. | +| **Implementation** | **`lib/pick-req.sh `** from project root. Scope: `any` or `UR-NNN`. Prints absolute path of first claimable REQ (exit 0) or empty (exit 1). Stderr: `dep:…` / `overlap:…` / `scope:…` rejects. | +| **Related scripts** | **`lib/check-deps.sh `** — missing deps (stdout one id per line). **`lib/check-footprint.sh `** — overlap lines vs `working/`. **`lib/scan-stale.sh`** — stale slots for reclaim policy (orchestrator, not a pick filter input). | +| **Authoritative deps** | Markdown: `**Depends on:**` on the REQ file (satisfied iff each id has `.do-work/archive/-*.md`). | +| **File globs** | Candidates: `.do-work/REQ-*.md` or `.do-work/REQ-M-*.md` when `state/active-milestone.md` exists. In-flight exclusion: `.do-work/working/REQ-*.md`. | + +#### `claim_req` + +| | | +|---|---| +| **Intent** | Optimistic claim + in-progress. | +| **Implementation** | **`lib/claim-req.sh `** where `` is a backlog-root file (`.do-work/REQ-*.md`, not under `working/`). | +| **Sequence (script)** | 1) Validate backlog-root REQ. 2) Move into `.do-work/working/`: **tracked** `.do-work/` → `git mv`; **untracked** → plain `mv`. 3) Insert claim stamp (`Claimed by` / `Claimed at` / `Heartbeat`, optional Session) under the `# REQ-…:` heading. 4) Set `**Status:**` to `in-progress`. 5) If tracked: stage + commit `chore(REQ-NNN): claim by `; print short hash. If untracked: print `untracked`. | +| **Claim atomicity (mv / git mv race)** | Concurrent claim is a **filesystem race on the move**, not a distributed lock. Loser semantics match the script: | +| | • **Exit 0** — claim succeeded. | +| | • **Exit 2** — race lost (source no longer at backlog root). Stderr: `Claim lost: `. Caller re-runs `pick-req.sh`; do not force-claim. | +| | • **Exit 1** — other failure; script attempts to reverse the move. | +| **lib/*.sh** | **`lib/claim-req.sh`** (exists). | +| **File paths** | Source: `.do-work/REQ-NNN-*.md` → dest: `.do-work/working/REQ-NNN-*.md` | + +#### `heartbeat_req` + +| | | +|---|---| +| **Intent** | Refresh liveness on active claim. | +| **Implementation** | **`lib/heartbeat.sh `** with path under `.do-work/working/`. Updates `**Heartbeat:**` inside the claim stamp to current UTC ISO. **No git commit** — filesystem only. | +| **lib/*.sh** | **`lib/heartbeat.sh`** (exists). Related consumer: **`lib/scan-stale.sh`**. | +| **File paths** | `.do-work/working/REQ-*.md` | + +#### `set_req_status` + +| | | +|---|---| +| **Intent** | Set workflow status without full archive. | +| **Implementation** | Edit `**Status:**` on the REQ file (e.g. `stopped`, `in-progress`). `claim_req` already sets `in-progress`. Resume: `agents/resume.md` (stopped → in-progress + heartbeat). Archive/done → `archive_req`. Clear claim → `unblock_req`. | +| **lib/*.sh** | **None** for generic status write. | +| **File paths** | REQ file at current location | + +#### `set_blocked_by` + +| | | +|---|---| +| **Intent** | Write depends-on graph. | +| **Implementation** | Set/clear header `**Depends on:**` (comma and/or whitespace separated REQ ids). Eligibility readers: `pick-req.sh` / `check-deps.sh`. | +| **lib/*.sh** | **None** for write; **`lib/check-deps.sh`** for read/eligibility. | +| **File paths** | REQ header field | + +#### `set_files` + +| | | +|---|---| +| **Intent** | Set footprint list. | +| **Implementation** | Set/clear header `**Files:**` (space-separated paths/globs). Overlap evaluated at pick/claim via `pick-req.sh` / `check-footprint.sh`. | +| **lib/*.sh** | **None** for write; **`lib/check-footprint.sh`** for read/eligibility. | +| **File paths** | REQ header field | + +#### `archive_req` + +| | | +|---|---| +| **Intent** | Done + closure proof / outputs; leave in-flight. | +| **Implementation** | Orchestrator (`agents/run.md` post-worker): set `**Status:** done`, write `**Closure proof:**` and `## Outputs`, optional `**Suite:**`, then move `.do-work/working/REQ-*.md` → `.do-work/archive/REQ-*.md` (git-aware when tracked). **Before archive write, gate with `lib/check-archive-integrity.sh `** (requires done status, non-empty closure proof, no unchecked `- [ ]` ACs). | +| **lib/*.sh** | **`lib/check-archive-integrity.sh`** (gate). **No** `lib/archive-req.sh` — move/status/outputs are orchestrator playbook. Optional: **`lib/derive-status.sh`**, **`lib/check-acceptance-evidence.sh`**. | +| **File paths** | `working/` → `archive/` | + +#### `unblock_req` + +| | | +|---|---| +| **Intent** | Return to backlog; clear claim. | +| **Implementation** | `agents/unblock.md`: locate `.do-work/working/REQ-NNN-*.md`, strip claim stamp block (`` … ``), set `**Status:** backlog`, move file back to `.do-work/` backlog root. | +| **lib/*.sh** | **None** — no `lib/unblock-req.sh`. Gap: agent-only today. | +| **File paths** | `working/REQ-*.md` → `.do-work/REQ-*.md` | + +#### `append_decision` + +| | | +|---|---| +| **Intent** | Append standing decision line. | +| **Implementation** | Append one line to `.do-work/decisions.md` (create if absent when writer is capture/etc.). Format: `YYYY-MM-DD \| UR/REQ ref \| decision \| rationale`. | +| **lib/*.sh** | **None** | +| **File paths** | `.do-work/decisions.md` | + +#### `write_verify_report` + +| | | +|---|---| +| **Intent** | Persist verify-phase coverage report for a UR. | +| **Implementation** | `agents/verify.md` produces the coverage report (console-primary). Scoring arithmetic: **`lib/score-coverage.sh`**. | +| **lib/*.sh** | **`lib/score-coverage.sh`** (exists). **No** `lib/write-verify-report.sh`. | +| **File paths** | **Gap:** no single durable path equivalent to `closure.md`; report is agent console output unless the operator asks to save it. Do not invent a path. | + +#### `write_close_report` + +| | | +|---|---| +| **Intent** | Persist close-phase report for a UR. | +| **Implementation** | `agents/close.md` writes `.do-work/user-requests/UR-NNN/closure.md` (+ optional `closure-evidence/`). | +| **lib/*.sh** | **None** | +| **File paths** | `user-requests/UR-NNN/closure.md` | + +#### `append_run_note` + +| | | +|---|---| +| **Intent** | Ledger-ish / cost note for a REQ or run. | +| **Implementation** | When `ledger.enabled`: **`lib/run-ledger.sh`** with flags (`--project`, `--req`, `--agent`, `--model`, `--branch`, timestamps, `--result`, `--cost-estimate`, evidence paths, etc.) appends `.do-work/runs/RUN-NNN.yml`. | +| **lib/*.sh** | **`lib/run-ledger.sh`** (exists). | +| **File paths** | `.do-work/runs/RUN-NNN.yml` | + +#### `read_active_milestone` + +| | | +|---|---| +| **Intent** | Read milestone cursor. | +| **Implementation** | Read `.do-work/state/active-milestone.md` (absent ⇒ not milestone mode). | +| **lib/*.sh** | **None** (pick-req.sh reads it for glob constraint). | +| **File paths** | `.do-work/state/active-milestone.md` | + +#### `set_active_milestone` + +| | | +|---|---| +| **Intent** | Set or advance milestone cursor. | +| **Implementation** | Write milestone id into `active-milestone.md`; maintain checklist in `milestones.md` (capture / run deploy-gate). Delete cursor when all milestones deployed / run stop. | +| **lib/*.sh** | **None** | +| **File paths** | `.do-work/state/active-milestone.md`, `.do-work/state/milestones.md` | + +#### `list_milestone_reqs` + +| | | +|---|---| +| **Intent** | REQs for active (or named) milestone. | +| **Implementation** | Glob `REQ-M-*.md` under backlog (and optionally working/archive) for `M` from active cursor or argument. | +| **lib/*.sh** | **None** dedicated; **`lib/pick-req.sh`** constrains claimable backlog to active milestone when cursor exists. | +| **File globs** | `.do-work/REQ-M-*.md`, `working/REQ-M-*.md`, `archive/REQ-M-*.md` | + +#### `write_gate_state` + +| | | +|---|---| +| **Intent** | Deploy-gate ownership coordination. | +| **Implementation** | Write single-line `AGENT_ID` to `.do-work/state/gate-owner.md`; delete when gate resolves. Local runtime lock — allowed even if a future backend stores work items remotely. | +| **lib/*.sh** | **None** — no `lib/write-gate-state.sh`. | +| **File paths** | `.do-work/state/gate-owner.md` | + +--- + +## Gaps (explicit — do not invent scripts) + +These port ops have **no** dedicated `lib/*.sh` writer/reader. Implementations are agent playbooks and file globs only. **Do not invent** names such as `lib/create-ur.sh`, `lib/archive-req.sh`, or `lib/unblock-req.sh` in callers. + +| Op | Gap | +|----|-----| +| `ensure_product_container` | No lib; mkdir/install | +| `create_ur` / `read_ur` / `list_urs` | No lib; intake + globs | +| `append_ideate` / `append_clarifications` | No lib; ideate/question agents | +| `create_req` / `update_req` / `read_req` / `list_reqs_for_ur` | No lib; capture + file IO | +| `set_req_status` / `set_blocked_by` / `set_files` | No lib writers; header edits | +| `archive_req` | Integrity gate only (`check-archive-integrity.sh`); move is orchestrator | +| `unblock_req` | Agent-only (`agents/unblock.md`) | +| `append_decision` | File append only | +| `write_verify_report` | Score lib only; no durable report path | +| `write_close_report` | Agent-only (`closure.md`) | +| `read_active_milestone` / `set_active_milestone` / `list_milestone_reqs` | State files + globs; pick-req consumes cursor | +| `write_gate_state` | State file only | + +Coordination ops that **do** have real scripts: `list_claimable_reqs` → `pick-req.sh` (+ `check-deps.sh`, `check-footprint.sh`); `claim_req` → `claim-req.sh`; `heartbeat_req` → `heartbeat.sh`; `append_run_note` → `run-ledger.sh`; archive integrity → `check-archive-integrity.sh`; verify score → `score-coverage.sh`. + +--- + +## Claim atomicity (summary) + +Matches `lib/claim-req.sh` and `agents/run.md` claim step: + +1. Pick via `lib/pick-req.sh` (does not claim). +2. Claim via `lib/claim-req.sh` — **atomic unit is `git mv` (tracked) or `mv` (untracked)** of the REQ from backlog root into `working/`, then stamp + status rewrite. +3. **Race lost → exit 2** (`Claim lost: REQ-NNN`); re-pick. Same stopper class as Linear concurrent-conflict for multi-agent recovery. +4. Heartbeats via `lib/heartbeat.sh` keep the slot fresh; `lib/scan-stale.sh` detects abandoned slots. + +--- + +## Rules specific to markdown + +- **Default / no hard-stop:** unset or empty `tracker.backend` → this backend; never require Linear. +- **No Linear-aware bash required** for any markdown port op (see **Bash surface**). +- **No dual-write:** do not create Linear Initiatives/Issues/comments as part of markdown ops. +- **Bash remains authoritative** for claim, pick, deps, footprint, heartbeat, and archive integrity on this path. +- **Never invent `lib/*.sh` paths** that are not in the repo; use the **Gaps** table. +- **Regression:** `bash lib/tests/run-all.sh` and `bash lib/conformance-scan.sh {project}` stay runnable without Linear. + +--- + +## Related runtime helpers (not port ops) + +Listed so agents do not confuse them with the catalog; still markdown-local scripts: + +| Script | Use | +|--------|-----| +| `lib/scan-stale.sh` | Orchestrator pre-flight stale slots | +| `lib/deadlock-check.sh` | Parallel drain diagnosis | +| `lib/provision-worktree.sh` | Worker worktree deps | +| `lib/file-feedback.sh` | Trend feedback inbox | +| `lib/emit-event.sh` / `lib/session-hook.sh` / `lib/stamp-session.sh` / `lib/resolve-session.sh` | Session telemetry | +| `lib/check-acceptance-evidence.sh` | Acceptance evidence validation | +| `lib/derive-status.sh` | proven/unproven derivation | + +--- + +## Out of scope for this file + +- Linear MCP sequences → `agents/tracker/linear.md`. +- Changing TDD, worktree isolation, or review philosophy — documentation of the existing store only. +- Implementing new bash for gap ops — out of scope for this mapping REQ. diff --git a/agents/tracker/port.md b/agents/tracker/port.md new file mode 100644 index 0000000..6b20667 --- /dev/null +++ b/agents/tracker/port.md @@ -0,0 +1,481 @@ +# Tracker port (shared contract) + +Shared work-item operation catalog and load path for do-work multi-tracker backends. +Phase agents that touch URs/REQs (or other work-item artifacts) resolve storage **only** through this port and the active backend file — never by inventing raw store paths or tools outside the backend doc. + +This file freezes **op names**, **preconditions**, and **backend-independent semantic rules** (claim, deps, footprint, hard-stop, mid-flight failure). Backend files implement each op; they must not invent alternate op names or weaken these rules. + +--- + +## Path: markdown-default (REQ-283) + +| | | +|---|---| +| **Entry point** | `/do-work` phase agents with `tracker.backend` **unset**, **empty**, or explicitly `markdown` | +| **Terminal state** | All work-item ops resolve through `agents/tracker/port.md` + `agents/tracker/markdown.md`; existing `lib/tests` and conformance pass **without** Linear MCP, Linear credentials, or dual-write | + +This path is the happy path for every project that has not opted into Linear. Product behavior (TDD-per-REQ, worktrees, claim/deps/footprint, review gate) is unchanged; only the **documented store surface** is named so a second backend can plug in later. + +**Child work under this path (do not re-implement here):** + +| Area | Responsibility | +|------|----------------| +| Config schema (`tracker.*`) | Full keys, validation, migrate-to-disk | +| Port op catalog body | This file — preconditions, claim/deps/footprint, hard-stop, mid-flight | +| Markdown backend mapping | Op → `lib/*.sh` + `.do-work/` path sequences (`markdown.md`) | +| Linear backend mapping | Op → Linear skill/MCP sequences (`linear.md`; no tool inventing here) | +| Phase-agent load-path wiring | Each agent that touches work items loads port + backend | + +--- + +## Load path (every work-item phase) + +1. Load config (`agents/config.md`). +2. Resolve `tracker.backend`: + - **missing key, empty string, or whitespace-only** → treat as **`markdown`** + - **`markdown`** → continue; **no Linear tools required**, no hard-stop + - **`linear`** → Linear backend path; Linear must be usable (see **Hard-stop**) + - **any other value** → hard-stop with a clear config error (do not guess) +3. Read `agents/tracker/port.md` (this file). +4. Read `agents/tracker/.md` (for default: `agents/tracker/markdown.md`). +5. For work-item storage, call **only** named port ops documented in the backend file. + +Phase agents keep product logic (TDD, review, decomposition). They do not re-implement store details and do not dual-write across backends. + +--- + +## Backend files + +| File | Role | +|------|------| +| `agents/tracker/port.md` | Shared op names, rules, load path (this document) | +| `agents/tracker/markdown.md` | File + `lib/*.sh` implementation of port ops (default) | +| `agents/tracker/linear.md` | Linear skill/MCP sequences for the same ops (opt-in) | + +Later backends (e.g. GitHub Issues, Jira) add sibling files; they are not part of the markdown-default path. + +--- + +## Work-item vs runtime split (design §5.5) + +From the storage inventory: **work-item** data is what the active tracker backend owns; **runtime / git / config** always stay local regardless of backend. + +### Must map through port ops (work-item store) + +| Domain | Examples (ops) | +|--------|----------------| +| UR lifecycle | `create_ur`, `read_ur`, `list_urs`, `append_ideate`, `append_clarifications` | +| REQ lifecycle | `create_req`, `update_req`, `read_req`, `list_reqs_for_ur`, `set_req_status`, `archive_req` | +| Claim / pick | `list_claimable_reqs`, `claim_req`, `heartbeat_req`, `unblock_req` | +| Deps / footprint fields | `set_blocked_by`, `set_files` | +| Non-ticket artifacts | `append_decision`, `write_verify_report`, `write_close_report`, `append_run_note` | +| Milestone cursor content | `read_active_milestone`, `set_active_milestone`, `list_milestone_reqs` | +| Product container | `ensure_product_container` | + +In Linear mode these live only in Linear (Initiatives, Projects, Issues, Docs, comments) — **no dual-write** to UR/REQ markdown as source of truth. + +### Stay local (not port work-item storage) + +| Domain | Notes | +|--------|--------| +| Worktrees, branches, merges, PRs | Git isolation; branch names may reference Linear issue ids | +| `state/*` locks, events, context-pack, retry counters | Orchestrator coordination | +| `config.yml`, install, conformance | Config load path; tracker backend selection | +| Gate-owner / final-suite locks | Deploy-gate coordination; `write_gate_state` may still use a **local** lock file even when work-items are remote | +| Optional local ledger telemetry | If `ledger.enabled`, local `.do-work/runs/RUN-NNN.yml` may mirror cost notes for offline tooling — **telemetry only**, not a second work-item store | + +Claim **semantics** are port rules; claim **representation** is backend-specific (markdown: claim stamp on the REQ file; Linear: workflow status + claim **comment**, not a local claim file). + +--- + +## Hard-stop (Linear unusable) + +When `tracker.backend` resolves to **`linear`**, an unusable Linear backend is a **hard stop** — never silent markdown fallback. + +| Condition | Behavior | +|-----------|----------| +| `agents/tracker/linear.md` missing or unreadable | **Hard stop** with setup instructions (restore the Linear backend doc from the skill install; do not invent Linear sequences) | +| Linear MCP missing, offline, or unauthenticated | **Hard stop** with setup instructions from the Linear skill | +| Team id / team key unresolved | **Hard stop**; do not guess a team | +| Required `status_map` workflow state missing on the team | **Hard stop** with rename / map-fix instructions | +| MCP dies mid-op before a safe commit point | **Hard stop** — see **Mid-flight MCP failure** | + +**Never silent markdown fallback.** Agents must not: + +- switch to `markdown` ops “to keep going” (including when `linear.md` is missing) +- write UR/REQ files under `.do-work/` as a substitute store while backend is `linear` +- invent partial local mirrors of Linear work items + +When `tracker.backend` resolves to **`markdown`** (including unset/empty), Linear unavailability is irrelevant — no Linear tools required, no hard-stop for Linear. + +--- + +## Mid-flight MCP failure (leave claimed) + +If Linear MCP fails **after** a successful `claim_req` (issue is in-progress + active claim/heartbeat) but **before** `archive_req` / clean `unblock_req`: + +1. **Leave claimed** — do **not** clear the claim, force backlog, or silently release the slot. +2. Issue stays in-progress with the last claim / heartbeat as written. +3. Operator recovers with `/do-work resume` or `/do-work unblock` after MCP is healthy again (same multi-agent recovery story as markdown concurrent-conflict). +4. The failing agent exits stopped (e.g. concurrent-conflict / missing-creds / dependency-missing as appropriate to the surface); it does **not** invent a “claimed-but-abandoned” cleanup that races siblings. + +Markdown mid-flight failures follow the same spirit: a claimed working/ slot is not auto-released on worker crash; stale heartbeat + resume/unblock repair it. + +--- + +## Deps authority (relations authoritative) + +| Backend | Authoritative deps for eligibility | Mirror / display | +|---------|------------------------------------|------------------| +| **markdown** | `**Depends on:**` header on the REQ (file is the store) | same field | +| **linear** | Native Linear **`blocks` relations** | `**Depends on:**` line in Issue body is a **mirror** only | + +Rules (backend-independent intent): + +1. **`list_claimable_reqs` / deps checks** use the **authoritative** graph for the active backend — never a stale mirror when relations exist. +2. On Linear, if native `blocks` relations and body `**Depends on:**` diverge, **relations win** for claim eligibility. +3. **`set_blocked_by`** always updates the authoritative store; when relation tools exist on Linear, it updates **both** relations and body mirror. +4. If relation tools are unavailable on Linear, backends may fall back to description-only deps with a one-time warning (documented in `linear.md`) — still no silent markdown fallback. +5. A dependency is **satisfied** only when the depended-on REQ is **archived/done** (backend equivalent). Unsatisfied deps block claim. + +--- + +## Claim, deps, and footprint semantic rules + +These rules are shared. Backends implement the representation; they must preserve the semantics. + +### Claim + +| Concept | Rule | +|---------|------| +| Unclaimed | Backlog-equivalent status **and** no active claim (or last claim released / unblocked) | +| Claim (`claim_req`) | **Optimistic:** re-read before write; if another agent holds an active claim with a fresh heartbeat → fail (`concurrent-conflict`); else mark in-progress and record claim + heartbeat | +| Heartbeat (`heartbeat_req`) | Refresh liveness timestamp on the active claim; consumers take the latest active claim | +| Stale | Latest active heartbeat older than configured max age (`parallel.stale_threshold_seconds` or backend override) — recoverable by claim takeover / resume / unblock per multi-agent rules | +| Unblock (`unblock_req`) | Return to backlog-equivalent; clear / release claim | +| Resume | stopped → in-progress; refresh heartbeat; do not steal human assignee semantics on Linear | +| Atomicity | Markdown: FS claim stamp + move. Linear: re-read + comment protocol (no true distributed lock — intentional) | + +### Footprint + +| Concept | Rule | +|---------|------| +| Representation | Structured `**Files:**` (and related header fields) on the REQ — **not** ad-hoc custom fields | +| Free footprint | No other **in-flight** (claimed / working) REQ’s footprint overlaps the candidate’s declared paths | +| Overlap | Blocks `list_claimable_reqs` / claim until the overlapping in-flight REQ archives or changes footprint | +| `set_files` | Updates the footprint list; does not by itself claim or unclaim | + +### Deps (eligibility) + +| Concept | Rule | +|---------|------| +| Graph | Declared depends-on edges (authoritative store per backend — see **Deps authority**) | +| Satisfied | Every depended-on work item is done/archived | +| Unsatisfied | REQ is not claimable | +| `set_blocked_by` | Writes the graph (and mirror when applicable) | + +### Pick order (`list_claimable_reqs`) + +A REQ is claimable only when **all** of the following hold: + +1. Status is backlog-equivalent (not in-progress, stopped-held, or done). +2. Unclaimed (or stale claim eligible for recovery per multi-agent rules). +3. Deps satisfied (authoritative graph). +4. Footprint free vs other in-flight REQs. +5. Within scope filters the caller applies (e.g. active milestone, single UR project). + +Backends return pick-order suitable for the run loop; exact ordering policy lives in the backend / pick implementation. + +--- + +## Operation catalog (design §5.4) + +Names freeze intent. Exact field shapes and store sequences live in each backend file. Markdown may compose several ops from existing `lib/*.sh` scripts. **Do not invent Linear tool call names in this file** — those belong only in `linear.md`. + +### Catalog index + +| Op | Intent | +|----|--------| +| `ensure_product_container` | Product/team container ready (markdown: dirs; Linear: shared product Project create/bind + persist UUID) | +| `create_ur` | Record intake brief | +| `read_ur` | Load brief (+ ideate if present) | +| `list_urs` | Enumerate URs for prompts/status | +| `append_ideate` | Write ideate onto UR | +| `append_clarifications` | Question-phase Q&A | +| `create_req` | Create one REQ in backlog | +| `update_req` | Edit REQ body/fields | +| `read_req` | Load full REQ | +| `list_reqs_for_ur` | All REQs for a UR (any status) | +| `list_claimable_reqs` | Backlog, deps ok, footprint ok, unclaimed — pick order | +| `claim_req` | Optimistic claim + in-progress | +| `heartbeat_req` | Refresh liveness | +| `set_req_status` | stopped / in-progress / etc. | +| `set_blocked_by` | Deps graph | +| `set_files` | Footprint list | +| `archive_req` | Done + closure proof / outputs | +| `unblock_req` | Return to backlog, clear claim | +| `append_decision` | Standing decisions memory | +| `write_verify_report` | Verify output for a UR | +| `write_close_report` | Close output for a UR | +| `append_run_note` | Ledger-ish / cost note for a REQ or run | +| `read_active_milestone` | Milestone cursor | +| `set_active_milestone` | Advance / set milestone | +| `list_milestone_reqs` | REQs for active milestone | +| `write_gate_state` | Deploy-gate coordination (local lock still allowed) | +| `migrate_markdown_to_linear` | One-shot idle markdown→Linear cutover (design §12); dry-run supported | + +### Op contracts + +Each op lists **intent**, **preconditions**, and **notes**. Inputs/outputs are conceptual; backends map them to files or remote entities. + +#### `ensure_product_container` + +| | | +|---|---| +| **Intent** | Ensure the product/team container for work items is ready. **Markdown:** local `.do-work/` dirs. **Linear:** team resolvable; **create or bind** the shared product Project when missing (`product_project` resolve chain + list/create + **persist UUID**); optional labels ready. | +| **Preconditions** | Config loaded; backend resolved. For Linear: team resolvable or hard-stop. | +| **Notes** | Idempotent. Does not create a UR or REQ. Linear never falls through to skill name `do-work` for empty `product_project`. Multi-match by name and empty-name failures hard-stop (no markdown substitute store). | + +#### `create_ur` + +| | | +|---|---| +| **Intent** | Record a new intake brief as a UR. | +| **Preconditions** | `ensure_product_container` satisfied; next UR slug allocatable; backend store writable. | +| **Notes** | Allocates sequential `UR-NNN` slug (machine-stable). Does not create REQs. | + +#### `read_ur` + +| | | +|---|---| +| **Intent** | Load the UR brief and attached sections (ideate, clarifications, etc. if present). | +| **Preconditions** | UR id known and exists. | +| **Notes** | Read-only. | + +#### `list_urs` + +| | | +|---|---| +| **Intent** | Enumerate URs for prompts, status, and migration. | +| **Preconditions** | Product container ready. | +| **Notes** | May return ids + titles only; use `read_ur` for full body. | + +#### `append_ideate` + +| | | +|---|---| +| **Intent** | Append or write ideate content onto an existing UR. | +| **Preconditions** | UR exists; ideate phase allowed for that UR. | +| **Notes** | Prefer append over overwrite of intake brief. | + +#### `append_clarifications` + +| | | +|---|---| +| **Intent** | Append question-phase Q&A onto the UR. | +| **Preconditions** | UR exists. | +| **Notes** | Does not create REQs. | + +#### `create_req` + +| | | +|---|---| +| **Intent** | Create one REQ in the backlog for a UR (optionally under a path-unit parent). | +| **Preconditions** | UR exists; capture/schema fields available; backend writable. | +| **Notes** | Starts unclaimed, backlog-equivalent status. Footprint/deps may be set at create or via `set_files` / `set_blocked_by`. | + +#### `update_req` + +| | | +|---|---| +| **Intent** | Edit REQ body or structured fields without changing claim/archive lifecycle. | +| **Preconditions** | REQ exists; caller is allowed to edit in the current phase (capture/audit/worker rules). | +| **Notes** | Prefer dedicated ops for status, deps, footprint, claim when those are the intent. | + +#### `read_req` + +| | | +|---|---| +| **Intent** | Load the full REQ (headers + body sections). | +| **Preconditions** | REQ id known; present in backlog, in-flight, or archive store. | +| **Notes** | Read-only. | + +#### `list_reqs_for_ur` + +| | | +|---|---| +| **Intent** | List all REQs for a UR in any status. | +| **Preconditions** | UR exists (or UR id known). | +| **Notes** | Scope is the UR’s project/container; not global product backlog unless caller expands. | + +#### `list_claimable_reqs` + +| | | +|---|---| +| **Intent** | Return REQs that are backlog, deps-satisfied, footprint-free, and unclaimed — in pick order. | +| **Preconditions** | Backend readable; claim/deps/footprint rules evaluable. | +| **Notes** | Uses **authoritative** deps (relations on Linear). Does not claim. Empty list is valid. | + +#### `claim_req` + +| | | +|---|---| +| **Intent** | Optimistically claim a REQ and move it to in-progress. | +| **Preconditions** | REQ appears claimable under **Claim / deps / footprint** rules at re-read time; agent id available. | +| **Notes** | Re-read before write; loser → concurrent-conflict / stop; resume allowed. On Linear, human assignee is not stolen for claim. | + +#### `heartbeat_req` + +| | | +|---|---| +| **Intent** | Refresh liveness on an active claim so siblings do not treat the slot as stale. | +| **Preconditions** | REQ is claimed by this agent (or caller is the claim owner); claim still active. | +| **Notes** | Filesystem-only / comment-only — no git commit for stamps. | + +#### `set_req_status` + +| | | +|---|---| +| **Intent** | Set workflow status (e.g. stopped, in-progress) without full archive. | +| **Preconditions** | REQ exists; target status is valid for the backend `status_map` / schema. | +| **Notes** | Archive/done should use `archive_req`. Unclaim/backlog return should use `unblock_req` when clearing a claim. | + +#### `set_blocked_by` + +| | | +|---|---| +| **Intent** | Write the depends-on graph for a REQ. | +| **Preconditions** | REQ exists; dependency ids valid (or empty to clear). | +| **Notes** | Updates authoritative store; on Linear with relation tools, updates **blocks relations + body mirror**. | + +#### `set_files` + +| | | +|---|---| +| **Intent** | Set the footprint (`**Files:**`) list for a REQ. | +| **Preconditions** | REQ exists. | +| **Notes** | Does not claim. Overlap is evaluated by consumers at pick/claim time. | + +#### `archive_req` + +| | | +|---|---| +| **Intent** | Mark REQ done with closure proof and outputs; move to archive-equivalent store. | +| **Preconditions** | Acceptance / verification evidence complete per run-worker rules; claim owned by orchestrating flow as required by backend. | +| **Notes** | Releases in-flight footprint. Does not delete historical data. | + +#### `unblock_req` + +| | | +|---|---| +| **Intent** | Return a REQ to backlog and clear/release the claim. | +| **Preconditions** | REQ is in-flight or stopped with a claim (or explicitly targeted by unblock). | +| **Notes** | Used after mid-flight failure recovery and operator-driven unblock. | + +#### `append_decision` + +| | | +|---|---| +| **Intent** | Append one standing decision line to decisions memory. | +| **Preconditions** | Decisions store reachable (markdown file or Linear team Doc). | +| **Notes** | Append-only; readers treat lines as constraints. | + +#### `write_verify_report` + +| | | +|---|---| +| **Intent** | Persist verify-phase output for a UR. | +| **Preconditions** | UR exists; verify phase has produced a report. | +| **Notes** | Backend chooses home (UR tree vs Initiative section/comment). | + +#### `write_close_report` + +| | | +|---|---| +| **Intent** | Persist close-phase output for a UR. | +| **Preconditions** | UR exists; close phase has produced a report. | +| **Notes** | Backend chooses home (UR tree vs Initiative section/comment). | + +#### `append_run_note` + +| | | +|---|---| +| **Intent** | Append a ledger-ish / cost / run note for a REQ or run. | +| **Preconditions** | Target REQ or run context exists when required. | +| **Notes** | Authoritative work-item note is backend store; optional local ledger file is telemetry only when `ledger.enabled`. | + +#### `read_active_milestone` + +| | | +|---|---| +| **Intent** | Read the active milestone cursor (if any). | +| **Preconditions** | None beyond readable state; missing cursor means not in milestone mode. | +| **Notes** | Content is work-item-ish; representation may be local file or Project description marker. | + +#### `set_active_milestone` + +| | | +|---|---| +| **Intent** | Set or advance the active milestone cursor. | +| **Preconditions** | Milestone mode applicable; target milestone id valid. | +| **Notes** | Deploy-gate human y/n remains orchestrator-owned; this op only persists the cursor. | + +#### `list_milestone_reqs` + +| | | +|---|---| +| **Intent** | List REQs belonging to the active (or named) milestone. | +| **Preconditions** | Milestone id known or active cursor set. | +| **Notes** | Used by run loop and milestone-complete detection. | + +#### `write_gate_state` + +| | | +|---|---| +| **Intent** | Coordinate deploy-gate ownership / state. | +| **Preconditions** | Milestone / gate flow active. | +| **Notes** | **Local lock still allowed** (e.g. `state/gate-owner.md`) even when work-items are remote. Not a dual-write of work items. | + +#### `migrate_markdown_to_linear` + +| | | +|---|---| +| **Intent** | One-shot, idle-only cutover from the markdown work-item store to Linear (design §12). Creates Initiatives / Projects / Issues for existing URs and REQs (backlog + archive), Team Docs for decisions/calibration, then flips `tracker.backend` to `linear`. After cutover, local UR/REQ trees are **read-only historical** — not dual-write. | +| **Preconditions** | Effective backend is still **`markdown`** (cutover target is Linear). **`working/` empty.** No active claims. Operator confirms (or explicit dry-run). Linear team resolvable and MCP usable **before** any write. Surfaced via `/do-work upgrade migrate` (or upgrade migrate step) — see `agents/upgrade.md` + `agents/tracker/linear.md`. | +| **Notes** | **Not a normal lifecycle op.** Sequences and dry-run live in `linear.md`. **Refuse entirely** if `working/` non-empty or active claims exist — leave config + markdown trees unchanged (no partial cutover). **Hard-stop** if Linear MCP is unusable mid-migration — leave markdown trees + config backend unchanged (no partial cutover). Supports **dry-run** (report planned creates; zero Linear writes; config untouched). | + +--- + +## Shared rules (backend-independent summary) + +- **No dual-write.** One active backend owns work-item truth. Markdown does not mirror to Linear; Linear does not write UR/REQ markdown as source of truth. +- **Port-only storage API.** Phase agents call named ops only — never raw `.do-work/REQ-*` paths or raw Linear tools outside the backend file. +- **Claim eligibility** requires deps satisfied + footprint free + unclaimed (or stale claim recoverable per multi-agent rules). +- **Optimistic claim:** re-read before write; loser stops with concurrent-conflict / resume allowed. +- **Footprint** is structured `**Files:**` (and related headers) on the REQ — not ad-hoc custom fields. +- **Deps authority:** Linear native **blocks relations** are authoritative for eligibility; body `**Depends on:**` is mirror. Markdown file header is the store. +- **Hard-stop on unusable Linear** when `tracker.backend: linear` — **never silent markdown fallback**. +- **Mid-flight MCP failure:** **leave claimed**; resume/unblock repair after recovery. +- **Work-item vs runtime:** work-item data through port ops; git/worktrees/`state/*`/config/gate locks stay local. +- **Idle markdown→Linear migration (design §12 / `migrate_markdown_to_linear`):** only when idle (`working/` empty, no active claims) + operator confirm (or dry-run). No partial cutover: refuse preflight or hard-stop MCP failure leaves `tracker.backend` and markdown trees unchanged. After successful cutover, ops **stop reading** local `user-requests/` and `archive/` as the work-item store (historical read-only only). + +--- + +## Regression (markdown-default terminal) + +When `tracker.backend` resolves to `markdown`: + +- Existing `lib/*.sh` coordination remains the implementation surface. +- `bash lib/tests/run-all.sh` and `bash lib/conformance-scan.sh` remain the regression gates. +- No Linear MCP discovery, team resolution, or credentials are required. +- Agents must not invent Linear tools or dual-write “for safety.” + +--- + +## Out of scope for this file + +- Concrete Linear MCP / skill tool call sequences (including **`migrate_markdown_to_linear`** agent sequence + dry-run report format) → `agents/tracker/linear.md`. +- Concrete `lib/*.sh` step lists → `agents/tracker/markdown.md`. +- Upgrade/conformance UX that surfaces the migrate step → `agents/upgrade.md`. +- Config key schema → `agents/config.md`. +- Changing TDD, worktree isolation, or review philosophy — store contract only. diff --git a/agents/unblock.md b/agents/unblock.md index 892634e..366c107 100644 --- a/agents/unblock.md +++ b/agents/unblock.md @@ -33,22 +33,83 @@ Invoked via `/do-work unblock REQ-NNN`. Read and follow the **Load Config** section of [config.md](config.md). +### 0a. Tracker load path + +Work-item storage (URs, REQs, decisions, verify/close reports, run notes) goes **only** through named tracker port ops after config is loaded: + +1. Resolve effective `tracker.backend` (missing/empty/whitespace → `markdown`). +2. Read `agents/tracker/port.md` (shared op catalog + rules). +3. Read `agents/tracker/.md` (e.g. `markdown.md` or `linear.md`). +4. For work-item storage, call **only** named port ops from that backend file — never raw `.do-work/REQ-*` paths or raw Linear tools outside the backend doc. + +**Hard rules:** +- **No silent fallback** from `linear` to `markdown`. If backend is `linear`, do not substitute UR/REQ markdown as the store. +- If backend resolves to **`linear`** but `agents/tracker/linear.md` is **missing or unreadable**, **hard-stop** with setup instructions (restore the Linear backend doc / connect Linear skill). Never fall through to markdown paths. +- Markdown backend: ops map — **invoke** coordination scripts as `bash {skill-root}/lib/...` after Load Config step 8 resolves `$SKILL_ROOT`; **catalog identity** remains `lib/*.sh` in `markdown.md` — use those ops; do not re-implement store details here. + +**Branch on effective backend** after load path: + +| Backend | Work-item unblock | +|---------|-------------------| +| **`markdown`** | Steps **1–8** below (working/ stamp strip + backlog move) | +| **`linear`** | Steps **L1–L4** — port op **`unblock_req`** in `agents/tracker/linear.md`. Id is a **Linear issue id** (e.g. `ENG-123`). No `.do-work/working/` claim stamps. | + +Invocation under Linear may be `/do-work unblock ENG-123` (or the issue identifier the operator passes). Treat `REQ-NNN` in the markdown steps as the issue identifier only for markdown. + +--- + +## Linear backend (`unblock_req`) + +### L1. Resolve the issue + +Caller supplies a Linear issue id. Run **Helper: read active claim** + get issue (linear.md). If the issue is missing → report nothing to unblock and stop. If already backlog with no active claim / latest claim `released` → report already unblocked and stop (idempotent). + +### L2. Detect implementation commits (local git — same judgment) + +Run: + +```bash +git log --grep "" --oneline +``` + +Also accept historical `REQ-NNN` greps if the operator still uses that form in commit messages. Filter to implementation commits (`feat(…)` / `fix(…)`). Record as `IMPL_COMMITS`. + +### L3. Handle partial commits (judgment gate) + +Same **J1** as markdown Step 3 (`AskUserQuestion`: revert / keep / fold). Execute the chosen git action **before** the port op. Git recovery stays local; it is **outside** the tracker port. + +### L4. Call port op `unblock_req` + +Follow **`unblock_req`** in `agents/tracker/linear.md` exactly: + +1. Rediscover Linear tools (`search_tool` → `use_tool`); hard-stop if undiscoverable. +2. Post claim-protocol comment with `status: released` (`agent_claim_marker` / ``). +3. Set workflow state → `status_map.backlog`. **Do not** change human **assignee**. +4. Do **not** write local backlog files or invent markdown dual-write. + +Report: + +``` +Unblock complete (Linear). + + → backlog (status_map.backlog) +Claim status: released +Assignee: unchanged +Commit decision: +Implementation commits affected: +``` + +Stop. Do not invoke run/verify next-step prompts. + +--- + +## Markdown backend + ### 1. Locate the REQ Check whether `{project}/.do-work/working/REQ-NNN-*.md` exists. -- If **no match**: check whether the REQ is in `.do-work/pending/`: - - **Found in `pending/`**: refuse with: - ``` - REQ-NNN is in pending-validation state — it cannot be unblocked. - Unblock's contract is "force out of working/ back to the backlog", which would - orphan an already-merged REQ's bookkeeping. The code is merged; only human - sign-off remains. - To return to backlog (the pending-validation equivalent of unblock): /do-work reject REQ-NNN - To close: /do-work approve REQ-NNN - ``` - Stop. Do not strip claim stamps, do not move the file, do not commit. - - **Not in `pending/` either**: report `"REQ-NNN is not in working/ — nothing to unblock."` and stop. +- If **no match**: report `"REQ-NNN is not in working/ — nothing to unblock."` and stop. - If **multiple matches** in `working/`: report the ambiguity and stop. Do not guess. - If **exactly one match** in `working/`: record the absolute path as `REQ_PATH` and continue. @@ -88,7 +149,7 @@ Execute the chosen action before continuing: Read `REQ_PATH`. Locate the block delimited by `` and `` (inclusive of both markers). -Remove the entire block, including the trailing blank line if one separates it from the next content. The strip must be atomic — never leave a half-removed stamp (e.g. dangling `claimed-end` marker, orphaned `**Heartbeat:**` line). +Remove the entire block, including the trailing blank line if one separates it from the next content. The block includes any optional `**Session:**` line (stamped by `lib/claim-req.sh` to correlate the REQ with a live session) — it is removed along with `**Claimed by:**`, `**Claimed at:**`, and `**Heartbeat:**`. The strip must be atomic — never leave a half-removed stamp (e.g. dangling `claimed-end` marker, orphaned `**Heartbeat:**` or `**Session:**` line). If no stamp is present, continue silently — the REQ may already have been partially cleaned up. @@ -145,10 +206,10 @@ Stop. Do not invoke run, verify, or any next-step prompt. ## Rules -- **Never unblock a pending-validation REQ.** A REQ in `.do-work/pending/` has already had its code merged — unblock would orphan its bookkeeping. The sanctioned return-to-backlog path is `/do-work reject REQ-NNN `, which carries a note and leaves the merged code intact. Refuse with that guidance and stop without touching claim stamps or files. -- Refuse to unblock a REQ that is not in `working/`. Backlog REQs are not blocked; archived REQs are done — neither needs unblocking. -- Refuse to operate on multiple REQs in one invocation. One REQ per call. +- **Markdown:** Refuse to unblock a REQ that is not in `working/`. Backlog REQs are not blocked; archived REQs are done — neither needs unblocking. +- **Linear:** Unblock via **`unblock_req`** only; refuse to invent local working/ files; assignee never changed. +- Refuse to operate on multiple REQs in one invocation. One REQ / issue per call. - Always surface implementation commits before discarding them — never silently revert. -- Strip the claim stamp atomically. A half-edited stamp is worse than no edit at all. -- Commit message follows the `chore(REQ-NNN): ...` convention. Never use `feat:` or `fix:` — unblock is housekeeping, not implementation. +- **Markdown:** Strip the claim stamp atomically. A half-edited stamp is worse than no edit at all. Commit message follows the `chore(REQ-NNN): ...` convention. +- **Linear:** Claim release is a `status: released` comment + backlog workflow state (linear.md); no half-updated protocol. - No `AskUserQuestion` next-step prompt after the report. Unblock is a terminal action. diff --git a/agents/upgrade.md b/agents/upgrade.md new file mode 100644 index 0000000..59ce883 --- /dev/null +++ b/agents/upgrade.md @@ -0,0 +1,575 @@ +# Upgrade Agent + +You are the Upgrade agent in the Do Work system. Your job is to bring an +existing project into conformance with the current do-work filesystem contract. +You use a manifest of cheap state detectors and explicit fixes. Safe rows may +auto-apply; destructive rows require user confirmation. + +--- + +## When Invoked + +You will be given a project checkout where `/do-work upgrade` was invoked. + +Resolve `{project}` at startup: + +```bash +git rev-parse --show-toplevel +``` + +If this fails because the current directory is not a git repo, use the current +working directory. + +--- + +## Conformance Manifest + +Rows accrete over time. When a future maintenance row is added, add its detector +to `lib/conformance-scan.sh` and add its fix contract here in the same change. + +| row-id | detector | fix | class | +|---|---|---|---| +| `legacy-dir` | `safe-blocking` drift line from `bash {skill-root}/lib/conformance-scan.sh {project}` when `do-work/` exists and `.do-work/` does not | `git mv do-work .do-work` with fallback plain `mv`, then `.gitignore` rewrite, then consumer-ref advisory scan | auto-apply | +| `dir-conflict` | `blocking` drift line from `bash {skill-root}/lib/conformance-scan.sh {project}` when both `do-work/` and `.do-work/` exist | none — halt with the existing conflict message | manual | +| `config-keys` | `safe-silent` missing or incomplete `.do-work/config.yml`, detected and migrated by the `agents/config.md` loader | load config per `agents/config.md`; its missing-key migration has already applied by Step 0 | auto-apply | +| `pending-dir` | `destructive` drift line from `bash {skill-root}/lib/conformance-scan.sh {project}` when `.do-work/pending/` exists, including when empty | archive parked REQs and delete `.do-work/pending/` after explicit `AskUserQuestion` confirmation | interactive confirm | +| `stale-config-key` | `destructive` drift line from `bash {skill-root}/lib/conformance-scan.sh {project}` when a tombstoned config key is present | remove the key line(s) from `.do-work/config.yml` — and the parent section if the removal leaves it empty — after explicit `AskUserQuestion` confirmation | interactive confirm | +| `session-hooks` | `bash {skill-root}/lib/install-hooks.sh --check {project}` prints `absent` (session telemetry hooks missing from `.claude/settings.json`) | run `bash {skill-root}/lib/install-hooks.sh {project}` — idempotent, additive merge | auto-apply | +| `migrate-linear` | **Optional / opt-in only** — not an auto-scan drift row (`lib/conformance-scan.sh` never emits it; see header comment there). Operator runs `/do-work upgrade migrate` (or upgrade Step 9) when they want design §12 idle markdown→Linear cutover. Detector for *eligibility* is preflight in Step 9 (working empty, no active claims, backend still markdown, Linear MCP usable) | invoke port op **`migrate_markdown_to_linear`** sequences in `agents/tracker/linear.md` (dry-run or apply). **Apply mode is destructive** — requires explicit operator confirm gate. Dry-run is non-destructive. | **destructive** interactive confirm (or dry-run) | + +**`session-hooks` detector location.** This row is the one exception to the +accretion rule below: its detector lives in `lib/install-hooks.sh --check`, not +in `lib/conformance-scan.sh`. The hooks are written to +`{project}/.claude/settings.json`, which is outside the `.do-work/` tree that +`conformance-scan.sh` scans, so the scan is the wrong home for it. The installer +owns both detection (`--check`) and the idempotent fix. + +**`migrate-linear` is not scanned by `conformance-scan.sh`.** It is surfaced only +via explicit `/do-work upgrade migrate` (Step 9). Do not invent a blocking +conformance failure for “still on markdown” — markdown remains the default +backend. Accretion rule for scanner rows does not apply to this opt-in path. +The scanner file documents this contract (REQ-301) so conformance and upgrade +stay aligned without auto-flagging markdown projects. + +**Tombstone list.** This manifest is the curated documentation of tombstoned +`.do-work/config.yml` keys — key paths the skill itself has removed, which +`stale-config-key` flags if still present. v1: `notifications.on_pending_validation` +(removed by the UR-039 cleanup). The executable list lives in +`lib/conformance-scan.sh` (`STALE_CONFIG_KEYS`); the two must be updated +together whenever a key is tombstoned, per the accretion rule above. + +--- + +## Steps + +### 0. Load Config + +Read and follow the **Load Config** section of [config.md](config.md). + +### 0a. Tracker load path + +Work-item storage (URs, REQs, decisions, verify/close reports, run notes) goes **only** through named tracker port ops after config is loaded: + +1. Resolve effective `tracker.backend` (missing/empty/whitespace → `markdown`). +2. Read `agents/tracker/port.md` (shared op catalog + rules). +3. Read `agents/tracker/.md` (e.g. `markdown.md` or `linear.md`). +4. For work-item storage, call **only** named port ops from that backend file — never raw `.do-work/REQ-*` paths or raw Linear tools outside the backend doc. + +**Hard rules:** +- **No silent fallback** from `linear` to `markdown`. If backend is `linear`, do not substitute UR/REQ markdown as the store. +- If backend resolves to **`linear`** but `agents/tracker/linear.md` is **missing or unreadable**, **hard-stop** with setup instructions (restore the Linear backend doc / connect Linear skill). Never fall through to markdown paths. +- Markdown backend: ops map — **invoke** coordination scripts as `bash {skill-root}/lib/...` after Load Config step 8 resolves `$SKILL_ROOT`; **catalog identity** remains `lib/*.sh` in `markdown.md` — use those ops; do not re-implement store details here. + +This is also the `config-keys` manifest row. If the loader creates or migrates +config, report `config-keys: converged`. If it makes no changes, report +`config-keys: already-conformant`. + +### 1. Run The Conformance Scan + +Run: + +```bash +bash {skill-root}/lib/conformance-scan.sh "{project}" +``` + +Interpret exit codes: + +- `0` with no output: no scanned drift. Continue to Step 8 so `config-keys` + still appears in the report. +- `1`: parse stdout as drift lines. Each line is ` `. +- `2`: report the usage error and stop; this indicates an invocation bug. + +Ignore unknown row ids safely in the report as outstanding drift. Do not invent +a fix for a row that is not in the manifest table. + +### 2. Apply Safe Row: legacy-dir + +If the scan output contains `legacy-dir`, migrate from the legacy `do-work/` +location to `.do-work/`. + +Apply these detection branches: + +| State at `{project}` | Action | +|---|---| +| `.do-work/` exists AND `do-work/` does not exist | Already migrated. Continue silently. | +| `do-work/` exists AND `.do-work/` does not exist | Migrate, then continue. | +| Both `do-work/` and `.do-work/` exist | Halt. Output the conflict message in Step 3 and stop the subcommand. | +| Neither exists | No migration needed. Continue. | + +Migration procedure: + +```bash +# Prefer `git mv` so history follows the rename. Fall back to plain `mv` if the path is +# gitignored or this is not a git repo (both make `git mv` fail). +git mv "{project}/do-work" "{project}/.do-work" 2>/dev/null \ + || mv "{project}/do-work" "{project}/.do-work" +``` + +Then rewrite `.gitignore` if it contains a line matching `^do-work/?$`: + +```bash +if [ -f "{project}/.gitignore" ] && grep -Eq '^do-work/?$' "{project}/.gitignore"; then + sed -i.bak -E 's|^do-work/?$|.do-work/|' "{project}/.gitignore" && rm "{project}/.gitignore.bak" +fi +``` + +After the directory rename and `.gitignore` rewrite succeed, scan the consumer +project for hardcoded `do-work/` references and print a warning if any are found. + +Consumer-ref scan targets: + +- All `*.md` files in `{project}` recursively, excluding `.git/`, + `node_modules/`, `vendor/`, `.worktrees/`, `dist/`, and `build/`. +- `{project}/.gitignore` itself, in case it contains other `do-work/` patterns + beyond the one already rewritten. + +Pattern: regex `(^|[^.])do-work/`. This matches the literal legacy form; the +leading `[^.]` guard excludes post-migration `.do-work/` references so only +genuine stale references surface. + +If matches are found, print: + +```text +Migration warning: consumer files still reference the legacy do-work/ path. +Review and update these manually — migration does NOT auto-rewrite consumer docs: + + CLAUDE.md:14: Run intake: read systems/do-work/agents/intake.md + CLAUDE.md:18: Identify which project the work is for in {project}/do-work/ + README.md:42: See do-work/ for backlog state + ... + +(Total: N references across M files.) +``` + +If zero matches, print nothing. + +Advisory only: never auto-rewrite consumer files. If the scan command fails +(permission denied, regex error, or other non-zero exit), skip the warning +silently. Migration already succeeded; the consumer-ref scan is best-effort. + +When this skill is invoked against its own source clone, that clone may +intentionally gitignore `.do-work/`. Do not treat self-references as fatal; the +consumer-ref scan remains advisory. + +Output: + +```text +Migrated do-work/ → .do-work/ +``` + +Record `legacy-dir: converged`. + +### 3. Handle Manual Row: dir-conflict + +If the scan output contains `dir-conflict`, do not migrate and do not modify the +project. Halt the subcommand with this exact text: + +```text +Migration conflict: both do-work/ and .do-work/ exist at {project}. Resolve manually before re-running. +``` + +Record `dir-conflict: manual-required` and include it in the outstanding rows. + +### 4. Confirm Destructive Row: pending-dir + +If the scan output contains `pending-dir`, inspect `{project}/.do-work/pending/` +before touching it. + +Build the prompt body: + +- If parked REQ files exist, list each basename matching `REQ-*.md`. +- If no parked REQ files exist, list `empty directory`. + +Use one `AskUserQuestion` confirmation gate with the prompt: + +```text +Archive pending/ REQs and remove .do-work/pending/? + +Affected: + +``` + +Use these options: + +1. **"Archive pending now"** - apply the destructive fix. +2. **"Skip pending cleanup"** - leave `.do-work/pending/` unchanged. + +If the user declines, cancels, or gives no clear affirmative answer, do not +modify `.do-work/pending/`. Record `pending-dir: skipped-by-user` and include +`pending-dir` in the outstanding rows. + +### 5. Apply Destructive Row: pending-dir + +Only run this step after the affirmative `AskUserQuestion` answer from Step 4. + +For each parked REQ file under `{project}/.do-work/pending/` matching +`REQ-*.md`: + +1. Strip any claim block delimited by `` and + ``, inclusive. Remove the blank line left behind when + one directly follows the claim block. +2. Change `**Status:**` to `done`. +3. Set `**Closure proof:**` to: + `upgrade: pending/ removal — human validation moved outside the system` +4. Convert a legacy `## Post-merge validation` section into + `## Manual checks (advisory)` by renaming only the heading and preserving the + checklist items exactly. Do not check or delete unchecked advisory items. +5. Do not modify `## Acceptance Criteria` checklist state. The archival rewrite + must not hide unchecked acceptance criteria; `lib/check-archive-integrity.sh` + remains the gate. +6. Run: + + ```bash + bash {skill-root}/lib/check-archive-integrity.sh "" + ``` + + If the check fails for a file, stop before moving that file and report the + failure. Do not delete `pending/`. +7. Move the rewritten file into `{project}/.do-work/archive/`, preserving its + basename. Prefer `git mv`; fall back to plain `mv` when `.do-work/` is + gitignored: + + ```bash + mkdir -p "{project}/.do-work/archive" + git mv "" "{project}/.do-work/archive/" 2>/dev/null \ + || mv "" "{project}/.do-work/archive/" + ``` + +After all parked REQ files are archived, attempt to remove the now-empty +directory: + +```bash +rmdir "{project}/.do-work/pending/" +``` + +Check the exit status. `rmdir` only succeeds when the directory is empty: + +- **Success (exit `0`):** the directory is gone. Continue below. +- **Failure (nonzero exit):** stray non-`REQ-*.md` files remain (for example + `.DS_Store`, which Finder creates near-universally once the directory has + been opened, or arbitrary user notes). **Never force-delete them — a + recursive, forced removal of `.do-work/pending/` or its contents is + forbidden; they may be user files.** Leave the directory and its remaining + contents exactly as found. List every remaining entry + (`ls -a "{project}/.do-work/pending/"`, excluding `.` and `..`) in the + report body so the user knows what to remove manually before re-running + `upgrade`. + +If `.do-work/` is tracked and the archive/delete operation produced staged or +unstaged tracked changes, commit them: + +```bash +git add "{project}/.do-work/archive" "{project}/.do-work/pending" 2>/dev/null || true +git commit -m "chore(upgrade): archive pending/ REQs and remove directory" +``` + +If `.do-work/` is gitignored or there are no tracked changes to commit, skip the +commit silently. + +Do not record a `pending-dir` outcome here. Step 8's re-scan is the +authoritative source of truth for whether `pending-dir` converged — the +directory may still exist if `rmdir` failed above. + +### 6. Confirm Destructive Row: stale-config-key + +If the scan output contains `stale-config-key`, inspect +`{project}/.do-work/config.yml` for each tombstoned key reported by the scan. + +Build the prompt body: for each reported ``, show its exact +key line and, if removing it would leave the parent section empty, the parent +section header line too. + +Use one `AskUserQuestion` confirmation gate with the prompt: + +```text +Remove stale config key(s) from .do-work/config.yml? + +Affected: +: +``` + +Use these options: + +1. **"Remove now"** - apply the destructive fix. +2. **"Skip config cleanup"** - leave `.do-work/config.yml` unchanged. + +If the user declines, cancels, or gives no clear affirmative answer, do not +modify `.do-work/config.yml`. Record `stale-config-key: skipped-by-user` and +include `stale-config-key` in the outstanding rows. + +### 7. Apply Destructive Row: stale-config-key + +Only run this step after the affirmative `AskUserQuestion` answer from Step 6. + +For each tombstoned key reported by the scan: + +1. Remove the key's line from `.do-work/config.yml`. +2. If removing the key leaves its parent section with no other nested keys, + remove the now-empty parent section header line too. +3. Never touch a key that is not named in a `stale-config-key` scanner row — + this is a curated, opt-in removal, not a general prune of unrecognized + keys. User-added custom keys are never touched, even if unfamiliar. + +If `.do-work/` is tracked and the removal produced staged or unstaged tracked +changes, commit them: + +```bash +git add "{project}/.do-work/config.yml" 2>/dev/null || true +git commit -m "chore(upgrade): remove stale config key(s)" +``` + +If `.do-work/` is gitignored or there are no tracked changes to commit, skip +the commit silently. + +Do not record a `stale-config-key` outcome here. Step 8's re-scan and report +converged is the authoritative source of truth for whether it converged. + +### 7a. Apply Safe Row: session-hooks + +`session-hooks` keeps the project's Claude Code session telemetry hooks in sync +with the current skill. It is detected by the installer itself (`--check`), not +by `conformance-scan.sh`, because the hooks live in +`{project}/.claude/settings.json`, outside the `.do-work/` tree. + +1. Check current state: + + ```bash + bash {skill-root}/lib/install-hooks.sh --check "{project}" + ``` + +2. If it prints `present`, record `session-hooks: already-conformant` and + continue. +3. If it prints `absent`, apply the idempotent installer: + + ```bash + bash {skill-root}/lib/install-hooks.sh "{project}" + ``` + + - On `installed`, record `session-hooks: converged`. + - On `skipped` (python3 unavailable), record `session-hooks: skipped` — the + hooks were not written, but telemetry degrades gracefully and upgrade does + not fail. + +The installer merges a `SessionStart` and `Stop` hook (each calling +`lib/session-hook.sh`) into `.claude/settings.json`, deduping by command string, +so running upgrade twice yields exactly one entry per hook. + +### 8. Re-scan And Report + +Run the scanner again: + +```bash +bash {skill-root}/lib/conformance-scan.sh "{project}" +``` + +`pending-dir`'s outcome is derived from this re-scan, never pre-declared in +Step 5: if the re-scan output no longer contains a `pending-dir` line, record +`pending-dir: converged`. If the re-scan output still contains a `pending-dir` +line — meaning the `rmdir` in Step 5 failed because stray non-`REQ-*.md` files +remain — record `pending-dir` as an outstanding row (not `converged`), and +reference the remaining files listed in Step 5 so the user knows to remove +them manually before re-running. + +`stale-config-key`'s outcome is likewise derived from this re-scan, never +pre-declared in Step 7: if the re-scan output no longer contains a +`stale-config-key` line for a given key, record `stale-config-key: converged` +for it. If a `stale-config-key` line for that key was already recorded as +`skipped-by-user` in Step 6, keep that outcome and include it in the +outstanding rows instead. + +Build a per-row outcome report for every row in the manifest: + +- `converged` - drift existed and this invocation fixed it. +- `already-conformant` - the row has no drift after scan and no fix was needed. +- `skipped-by-user` - the row required confirmation and the user declined or did + not affirm. +- `manual-required` - the row cannot be safely fixed by this agent. + +Report each row exactly once: + +```text +legacy-dir: +dir-conflict: +config-keys: +pending-dir: +stale-config-key: +session-hooks: +``` + +If no outstanding rows remain, end with: + +```text +Project is conformant. +``` + +If outstanding rows remain, end with: + +```text +Outstanding conformance rows: +- : +``` + +Second run idempotence requirement: on a conformant project, the scan produces +no drift lines, no files are modified, the row outcomes are +`already-conformant`, and the final line is `Project is conformant.` + +### 9. Optional: Idle markdown→Linear migration (`migrate-linear`) + +Design **§12** one-shot cutover (path: REQ-300; upgrade/conformance wiring: +REQ-301). **Not** part of automatic conformance — only when the operator +invokes **`/do-work upgrade migrate`** (or explicitly asks upgrade to migrate +to Linear after Steps 0–8). + +Port op: **`migrate_markdown_to_linear`**. Full agent sequence, dry-run report +format, status/parent/deps mapping, and failure matrix live in +`agents/tracker/linear.md` (**Path: Idle markdown→Linear migration**). +Shared refuse / hard-stop / no-partial-cutover rules live in +`agents/tracker/port.md`. Scanner relationship: `lib/conformance-scan.sh` +documents that `migrate-linear` is never a drift row. + +#### 9a. When this step runs + +| Invocation | Action | +|------------|--------| +| `/do-work upgrade` only (no migrate) | **Skip** Step 9 entirely. Markdown backend remains default. | +| `/do-work upgrade migrate` | Run Step 9 after Steps 0–8 (or after conformance if already conformant). | +| `/do-work upgrade migrate --dry-run` | Step 9 in **dry-run** mode only (lists planned creates; no writes). | + +#### 9b. Preflight (refuse = entire abort, no partial cutover) + +Before any Linear write or config flip: + +1. Load config. Effective `tracker.backend` must be **`markdown`** (missing/empty → markdown). + If already **`linear`**, report **`migrate-linear: already-linear`** (already-migrated) + and **stop immediately**: + - **Do not re-migrate.** + - **Do not create, update, or rewrite Linear Issues** (or Initiatives / Projects / Docs). + - **Do not** re-inventory markdown trees as a write plan. + - Config and Linear store left unchanged. + This makes re-running `/do-work upgrade migrate` **idempotent by clear refuse** + when cutover already happened. +2. **`working/` empty** — zero `REQ-*.md` under `{project}/.do-work/working/`. If any exist → **refuse entirely**: + + ```text + Migration refused: .do-work/working/ is non-empty (active or stranded REQs). + Finish, unblock, or archive in-flight work first. tracker.backend unchanged. + ``` + + Record `migrate-linear: refused-working-non-empty`. **Do not** create Linear + entities. **Do not** change config. +3. **No active claims** — with working empty, markdown claims are clear; if any + active claim protocol is still detected, refuse the same way + (`migrate-linear: refused-active-claims`). +4. **Linear MCP usable** — `search_tool` must discover Linear tools; team + resolvable via `tracker.linear.team_id` / `team_key`; every `status_map` + state present on the team. If unusable → **hard-stop** with Linear skill + setup instructions. Markdown trees + config **unchanged** + (`migrate-linear: hard-stop-linear-unusable`). **No partial cutover.** +5. **Destructive / confirm gate** (apply only) — migration **apply** is a + **destructive** row: it creates remote Linear entities and flips + `tracker.backend`. Never apply without an explicit operator confirmation gate + (`AskUserQuestion` or equivalent) with options: + + 1. **"Migrate to Linear now"** — apply mode (confirmed destructive cutover). + 2. **"Dry-run only"** — planned creates report, no writes. + 3. **"Skip migration"** — leave markdown backend. + + Decline / skip → `migrate-linear: skipped-by-user`. No writes. + Absence of affirmative “Migrate now” → **refuse** (do not write). + +Dry-run mode skips the affirmative “Migrate now” requirement but still runs +preflight 1–4 so the report is honest about readiness. Dry-run is **not** +destructive and does not require the confirm gate. + +#### 9c. Execute + +Follow `agents/tracker/linear.md` → **`migrate_markdown_to_linear`**: + +| Mode | Behavior | +|------|----------| +| **dry-run** | Inventory markdown URs/REQs + decisions/calibration; **list planned** Initiatives / Projects / Issues / Docs / config flip **without writing**; **zero** Linear writes; **zero** config changes. Record `migrate-linear: dry-run-reported`. | +| **apply** | Only after destructive confirm. Team Docs → Initiatives/Projects/Issues (map status, relations, parents, AC checkboxes) → set `tracker.backend: linear` + team ids in `.do-work/config.yml` → leave `user-requests/`, backlog `REQ-*.md`, and `archive/` on disk as **read-only historical** (do not delete). **Post-cutover work-item ops ignore historical markdown trees** (Linear-only store). Record `migrate-linear: converged`. | + +On mid-migration MCP failure: **hard-stop** per linear.md failure matrix — config +backend left **markdown**; list any orphan Linear ids; markdown trees unchanged +(`migrate-linear: hard-stop-partial-orphans` or equivalent detail in the body). +**Never** leave `tracker.backend: linear` after a partial create run. + +#### 9d. Report line + +Include one of: + +```text +migrate-linear: skipped (not requested) +migrate-linear: dry-run-reported +migrate-linear: converged +migrate-linear: already-linear +migrate-linear: refused-working-non-empty +migrate-linear: refused-active-claims +migrate-linear: skipped-by-user +migrate-linear: hard-stop-linear-unusable +migrate-linear: hard-stop-partial-orphans +``` + +When Steps 0–8 ran in the same invocation, append the migrate line after the +conformance row report. When only migrate was requested, still run Load Config +(Step 0 / 0a) then Step 9; conformance Steps 1–8 may be skipped if the operator +only asked for migrate — but preflight remains mandatory. + +--- + +## Rules + +- Never apply a destructive fix without the explicit `AskUserQuestion` + confirmation in its confirm step (Step 4 for `pending-dir`, Step 6 for + `stale-config-key`, Step **9b.5** for `migrate-linear` **apply**). +- Never rewrite consumer docs during `legacy-dir`; the consumer-ref scan is + advisory only. +- Do not use a config version stamp. Detectors are ground truth. +- The manifest accretes: future **scanner** rows must be added here and in + `lib/conformance-scan.sh` together. Opt-in `migrate-linear` is documented + here **and** in the `lib/conformance-scan.sh` header (never emitted as a + drift line — REQ-301). +- Do not invent fixes for unknown scanner row ids. +- `dir-conflict` is manual-only. The agent must not choose between two data + directories. +- Pending archival keeps human validation outside the system: unchecked manual + checks remain advisory and never block archive by themselves. +- `stale-config-key` removal is curated and opt-in: only touch a key reported + by a scanner drift row. Never remove a key absent from the tombstone list, + even one that looks unfamiliar — that may be a user-added custom key. +- Do not mark unchecked acceptance criteria as complete during upgrade. If + `lib/check-archive-integrity.sh` rejects a parked REQ, stop and report the + file instead of forcing archive. +- **markdown→Linear migration (Step 9 / REQ-301 wiring):** + - **Destructive/confirm gate** for apply; dry-run lists planned creates + without writing. + - Idle-only: refuse when `working/` non-empty or active claims exist. + - Hard-stop when Linear MCP is unusable; **no partial cutover**. + - **Idempotent refuse:** if `tracker.backend` is already `linear`, report + `already-linear` / already-migrated and **stop without rewriting Issues**. + - After success: leave markdown trees historical read-only; **post-cutover + work-item ops ignore historical markdown trees**; set + `tracker.backend: linear`. + - Sequence details only in `agents/tracker/linear.md`. +- No next-step prompt after the report. diff --git a/agents/verify.md b/agents/verify.md index b786a88..1cf01ac 100644 --- a/agents/verify.md +++ b/agents/verify.md @@ -21,11 +21,39 @@ UR-001 ← brief to verify against Read and follow the **Load Config** section of [config.md](config.md). +### 0a. Tracker load path + +Work-item storage (URs, REQs, decisions, verify/close reports, run notes) goes **only** through named tracker port ops after config is loaded: + +1. Resolve effective `tracker.backend` (missing/empty/whitespace → `markdown`). +2. Read `agents/tracker/port.md` (shared op catalog + rules). +3. Read `agents/tracker/.md` (e.g. `markdown.md` or `linear.md`). +4. For work-item storage, call **only** named port ops from that backend file — never raw `.do-work/REQ-*` paths or raw Linear tools outside the backend doc. + +**Hard rules:** +- **No silent fallback** from `linear` to `markdown`. If backend is `linear`, do not substitute UR/REQ markdown as the store. +- If backend resolves to **`linear`** but `agents/tracker/linear.md` is **missing or unreadable**, **hard-stop** with setup instructions (restore the Linear backend doc / connect Linear skill). Never fall through to markdown paths. +- Markdown backend: ops map — **invoke** coordination scripts as `bash {skill-root}/lib/...` after Load Config step 8 resolves `$SKILL_ROOT`; **catalog identity** remains `lib/*.sh` in `markdown.md` — use those ops; do not re-implement store details here. + +### Verify report home — backend branch (REQ-296 / REQ-297) + +| Backend | Where the verify report lives | +|---------|-------------------------------| +| **markdown** | Console-primary (Step 5c). No fixed durable path required (`markdown.md` `write_verify_report`). | +| **linear** | Port op **`write_verify_report`** — Initiative description **`## Verify`** + Initiative comment with the full report (`agents/tracker/linear.md`). Fixed home; do not invent alternate sections or local files as the store. Description size spill → section pointer + Initiative comment only (§10). If description **and** Initiative comment both fail → hard-stop; never invent Issue comments or alternate Docs. | + +After producing the report in Step 5c, when backend is **linear**, call **`write_verify_report`** with the full report body for this UR. Still print the report to the console for the operator. Scoring arithmetic remains `lib/score-coverage.sh` (local). + ### 1. Read the brief -Read `{project}/.do-work/user-requests/UR-NNN/input.md` in full. +**Backend branch (REQ-297):** + +| Backend | Brief / REQs | +|---------|--------------| +| **markdown** | Read `{project}/.do-work/user-requests/UR-NNN/input.md` in full. Read every file in `UR-NNN/assets/` if present. Backlog REQs from `.do-work/` as today. | +| **linear** | **`read_ur`** for brief (Initiative `## Brief` + sections). **`list_reqs_for_ur`** for Issues in Project `do-work/{UR-id}`. Optional local assets only if the operator keeps them on disk — not a dual work-item store. | -Read every file in `UR-NNN/assets/` if present. +**Markdown path (default):** Read `{project}/.do-work/user-requests/UR-NNN/input.md` in full. Read every file in `UR-NNN/assets/` if present. **Legacy UR detection.** Read the first 10 lines of `input.md`. If they do not begin with a `---` line followed by a YAML frontmatter block ending in `---`, this UR predates the gap-aware capture refactor. Mark it as legacy. Verify will: - Run all pre-existing checks (coverage scoring, ideate observation tracking, vague-criteria scan). @@ -205,17 +233,17 @@ For every UR (legacy and non-legacy), scan each REQ in the UR's REQ set (backlog 1. Read the REQ's `## Verification Steps` block. 2. For each numbered step, check whether its text matches any indicator phrase from the four categories above. 3. If a match is found, record a named issue on that REQ: include the REQ id, the step number, the matched indicator phrase, and the category it falls under. -4. The suggested fix for each hit: move the step out of `## Verification Steps` and into `## Post-merge validation` (creating the section if absent). +4. The suggested fix for each hit: move the step out of `## Verification Steps` and into `## Manual checks (advisory)` (creating the section if absent). **Scoring:** non-executable step hits are reported in the Issues section of the verify report. They lower confidence the same way other REQ-quality issues do (each counts as a gap; deduction formula is the same as vague-criteria hits — -5 per hit, capped at -20 total). **Auto-fix:** when invoked with `--auto-fix`: -1. Move the offending step out of `## Verification Steps` and append it to `## Post-merge validation` as a checklist item: `- [ ] [original step text] — Observable outcome: [infer from step context or leave blank for manual fill]`. +1. Move the offending step out of `## Verification Steps` and append it to `## Manual checks (advisory)` as a checklist item: `- [ ] [original step text] — Observable outcome: [infer from step context or leave blank for manual fill]`. 2. Renumber any remaining `## Verification Steps` entries so numbering stays contiguous. -3. Create `## Post-merge validation` if absent, using the section header from `agents/capture.md`'s REQ template. +3. Create `## Manual checks (advisory)` if absent, using the section header from `agents/capture.md`'s REQ template. 4. Re-report the REQ as clean once all non-executable steps have been moved. -Report each auto-fix action in the verify report as: `[AUTO-FIXED] REQ-NNN step N — moved "[indicator phrase]" to ## Post-merge validation`. +Report each auto-fix action in the verify report as: `[AUTO-FIXED] REQ-NNN step N — moved "[indicator phrase]" to ## Manual checks (advisory)`. ### 5. Score the coverage, then produce the report @@ -241,8 +269,8 @@ Non-executable step hits (Step 4g) are reported as named Issues on individual RE Run `{skill-root}/lib/score-coverage.sh` with the manifest flags and use its printed integer as the Confidence Score. Do **not** compute the deductions or base ratio by hand — the script is the single arithmetic authority. Example for an 8-full / 1-partial / 1-missing backlog with 2 layer-coverage gaps: -``` -{skill-root}/lib/score-coverage.sh --full 8 --partial 1 --missing 1 --layer-gaps 2 +```bash +bash {skill-root}/lib/score-coverage.sh --full 8 --partial 1 --missing 1 --layer-gaps 2 # → 65 ``` @@ -293,6 +321,8 @@ Recommendation: [Approved — run the loop / Fix gaps first — re-run capture / The Confidence Score is whatever `lib/score-coverage.sh` printed in Step 5b. Do not recompute it — see the composition formula documented there. +**Persist (Linear only):** when effective `tracker.backend` is `linear`, call port op **`write_verify_report`** (`agents/tracker/linear.md`) with this full report for `UR-NNN` — Initiative `## Verify` + Initiative comment. Do not invent another home. When backend is `markdown`, leave console-only unless the operator asks to save. + **Then, immediately after the report**, check whether to present next-step options: If `config.next_steps.enabled` is `true` **and** this agent is running standalone (not as a delegate inside the go agent): diff --git a/docs/HOW-IT-WORKS.md b/docs/HOW-IT-WORKS.md index db693ff..e5e5c4e 100644 --- a/docs/HOW-IT-WORKS.md +++ b/docs/HOW-IT-WORKS.md @@ -1,16 +1,18 @@ # How do-work Works +> **Operators / first run:** use [getting-started.md](getting-started.md) for install and the happy path. This page is the phase-by-phase deep dive (design rationale included). + A walkthrough of the do-work system — every phase, every file it produces, and the design reasoning behind each choice. --- ## What it is -do-work is a Claude Code skill that turns a natural-language brief into a sequence of small, traceable, individually-committed tasks — executed autonomously with TDD. +do-work is an agent-harness skill that turns a natural-language brief into a sequence of small, traceable, individually-committed tasks — executed autonomously with TDD. It runs on any agent that loads skills from a shared hub. -It is **file-based**: every artifact (brief, decomposed task, claim stamp, commit) is a file in the project's git history. There is no daemon, no database, no in-memory queue, no central coordinator. +It is **file-based by default**: every artifact (brief, decomposed task, claim stamp, commit) is a file in the project's git history. There is no daemon, no database, no in-memory queue, no central coordinator. An optional **Linear** backend stores the same work items in Linear only (see [Multi-tracker](#multi-tracker-work-item-backends) below); runtime and git isolation stay local either way. -**Why file-based:** The alternative is a stateful tool (a queue, a server, an MCP backend). Files give you four things for free that a stateful tool charges for: +**Why file-based (default):** The alternative is a stateful tool (a queue, a server, an MCP backend). Files give you four things for free that a stateful tool charges for: 1. **Auditability** — `git log` *is* the audit log. 2. **Resumability** — kill the process, the state survives. 3. **Multi-agent coordination** — `git mv` is an atomic primitive across processes; no lock service needed. @@ -18,6 +20,80 @@ It is **file-based**: every artifact (brief, decomposed task, claim stamp, commi --- +## Multi-tracker (work-item backends) + +Work items (URs, REQs, decisions, verify/close reports, run notes) go through a **tracker port**. Config key `tracker.backend` selects the store: + +| `tracker.backend` | Work-item store | +|-------------------|-----------------| +| **unset / empty / `markdown`** | Default: local `.do-work/` + `lib/*.sh` | +| **`linear`** | Linear only (product Project / **UR milestones** / Issues) — **no dual-write** | + +**Load path** (every phase agent that touches work items): + +1. Load config (`agents/config.md`) +2. Resolve `tracker.backend` (missing/empty → `markdown`) +3. Read `agents/tracker/port.md` (shared op catalog + rules) +4. Read `agents/tracker/.md` (`markdown.md` or `linear.md`) +5. Call **only** named port ops for storage — never raw `.do-work/REQ-*` paths or raw Linear tools outside the backend doc + +**What stays local on every backend:** worktrees, feature branches, merges, `state/*` locks, events, `config.yml`, optional local run ledger telemetry. + +### No dual-write and hard-stop + +- With `backend: linear`, Linear is the sole work-item source of truth. Agents do not keep a parallel markdown UR/REQ store. +- If Linear is unusable (MCP missing/unauthenticated, team unresolved, missing `status_map` state) **or** `agents/tracker/linear.md` is missing, agents **hard-stop** with setup instructions. They never silently fall back to markdown. +- Mid-flight MCP failure after a claim leaves the Issue **claimed**; recover with `/do-work resume` or `/do-work unblock` after MCP recovers — not by inventing local REQ files. + +### Linear hierarchy (when `backend: linear`) + +``` +Team (config team_id / team_key) +└── Product Project (tracker.linear.product_project — one shared Project per local product) + ├── Project Milestone (UR brief / ideate / verify / close) + │ └── Issue (REQ / path-unit) ± sub-issues (layer children) + └── Project Milestone (next UR) + └── Issue … +``` + +**`product_project` resolve (default empty):** explicit `tracker.linear.product_project` (name|UUID) if set; else `project.name`; else git-root directory basename. Then `ensure_product_container` create-if-missing and **always persists** the Project UUID back to config. Empty config never falls through to the skill name `do-work` — that name is only an example when this skill's own repo is the local product. + +**Why milestones, not Initiatives:** official Linear MCP exposes Project Milestone create/list/get, but not Initiative create/list. do-work therefore homes each UR on a **Project Milestone**. + +REQs use **Linear issue ids** only (e.g. `ENG-123`). `UR-NNN` remains the UR-milestone slug. + +### Commit convention (Linear) + +``` +feat(ENG-123): short title + +Issue: ENG-123 +UR: UR-007 +Output: path/to/primary/output +``` + +Branch / worktree: `req/ENG-123` (sanitized for git refs). Markdown mode still uses `feat(REQ-NNN): …` and `req/REQ-NNN`. + +### Claim protocol warning (human operators) + +Under Linear, the **human** remains Issue **assignee**; agents claim with a workflow state change plus a claim-protocol comment (`` by default) carrying `agent_id`, heartbeats, and `status: active`. + +**Do not clear, edit, or delete agent claim comments in the Linear UI while a run is live.** That breaks claim/heartbeat arbitration. Use `/do-work status`, then `resume` / `unblock` when the agent has stopped. + +### Migration (markdown → Linear) + +One-shot, **idle-only** cutover: working set empty, no active claims, operator confirms (or dry-run). + +```text +/do-work upgrade migrate +``` + +Surfaced under `/do-work upgrade` (and conformance — not a separate forever command). After cutover: `tracker.backend: linear`; historical markdown trees stay as read-only history; **no dual-write**. Details: `agents/tracker/linear.md` + `agents/upgrade.md` Step 9. + +**Operator setup** (MCP + `team_id`): [troubleshooting.md § Linear tracker backend](troubleshooting.md#linear-tracker-backend). Config schema: `agents/config.md`. Skill summary: `SKILL.md` § Tracker backends. + +--- + ## The two-command surface Most users only ever type two commands: @@ -328,9 +404,15 @@ Defaults are picked from REQ shape (parallel claim ordering, layer enforcement) ## Reference -- `SKILL.md` — full command reference and migration semantics +- [getting-started.md](getting-started.md) — install and first run (optional Linear pointer) +- [concepts.md](concepts.md) — user-facing mental model +- [commands.md](commands.md) — command reference +- [troubleshooting.md](troubleshooting.md) — failure symptoms (including Linear MCP / team_id) +- `SKILL.md` — full command reference, tracker backends, migration semantics +- `agents/tracker/port.md` — shared work-item op catalog +- `agents/tracker/markdown.md` / `agents/tracker/linear.md` — backend implementations - `agents/*.md` — per-phase agent instructions -- `lib/*.sh` — coordination primitives (claim, footprint, deps, heartbeat, deadlock, cycle) +- `lib/*.sh` — coordination primitives (markdown backend claim, footprint, deps, heartbeat, deadlock, cycle) - `.do-work/state/` — runtime coordination files (gate-owner, lockfiles, milestone tracking) -- `.do-work/config.yml` — per-project configuration (layers, log, parallel, test, next_steps) +- `.do-work/config.yml` — per-project configuration (layers, log, parallel, test, next_steps, `tracker.*`) - `.do-work/archive/REQ-144-extend-req-template-schema.md` — canonical REQ header schema reference diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..0037402 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,43 @@ +# Do Work — user documentation + +Task-based guides for people who install and run `/do-work` in a project. Works with any agent harness wired to the shared skills hub. + +## Start here + +| If you want to… | Read | +|-----------------|------| +| Install the skill and run your first brief end-to-end | [Getting started](getting-started.md) | +| Understand UR, REQ, `start` / `go`, and the gates | [Concepts](concepts.md) | +| Look up a command or flag | [Commands](commands.md) | +| Fix a failure symptom | [Troubleshooting](troubleshooting.md) | + +## Deeper reference + +| Page | Audience | +|------|----------| +| [How it works](HOW-IT-WORKS.md) | Operators who want phase-by-phase design detail | +| [../README.md](../README.md) | Install one-liner, quick start, config overview | +| [../agents/config.md](../agents/config.md) | Full `config.yml` schema | +| [../SKILL.md](../SKILL.md) | Skill entrypoint and full behavioural reference | +| [../CONTRIBUTING.md](../CONTRIBUTING.md) | Contributors changing the skill itself | + +## Happy path (two commands) + +```text +/do-work start I need a user settings page with email and password change +/do-work go UR-001 +``` + +`start` records the brief and builds the backlog. `go` checks coverage, then runs the backlog when confidence meets the project threshold (default 90%). + +## Docs map + +```text +docs/ +├── README.md ← you are here (index) +├── getting-started.md ← install → first start → first go +├── concepts.md ← mental model +├── commands.md ← command reference +├── troubleshooting.md ← symptoms → fixes +└── HOW-IT-WORKS.md ← deep dive (design + phases) +``` diff --git a/docs/commands.md b/docs/commands.md new file mode 100644 index 0000000..e2c83ad --- /dev/null +++ b/docs/commands.md @@ -0,0 +1,248 @@ +# Commands reference + +Lookup for `/do-work` commands: what each one does, when to use it, and which flags exist in the skill today. + +Invoke with no subcommand for help plus suggested next steps: + +```text +/do-work +``` + +## Command map (when to use which) + +| Goal | Command | +|------|---------| +| First-time project folders | `/do-work install` | +| Align old `.do-work/` with current skill | `/do-work upgrade` | +| New work end-to-end (define) | `/do-work start [brief]` | +| Execute a defined UR | `/do-work go UR-NNN` | +| Record brief only | `/do-work intake [brief]` | +| Creative review only | `/do-work ideate UR-NNN` | +| Grill the brief | `/do-work question UR-NNN` | +| Decompose only | `/do-work capture UR-NNN` | +| Score coverage only | `/do-work verify UR-NNN` | +| Sharpen REQ quality | `/do-work audit UR-NNN` | +| Run backlog (no verify gate) | `/do-work run [UR-NNN]` | +| Live situation room | `/do-work status [UR-NNN]` | +| Stuck REQ → backlog | `/do-work unblock REQ-NNN` | +| Re-dispatch stopped REQ | `/do-work resume REQ-NNN` | +| Validate integrated UR paths | `/do-work close UR-NNN` | +| Learn from run history | `/do-work retro` | +| Draft social posts | `/do-work log` | + +--- + +## Orchestrators + +### `/do-work start [brief]` + +**Job:** Record a brief and build the REQ backlog in one shot. + +**Pipeline:** intake → ideate (default) → capture. Does **not** run verify or implementation. + +| Flag | Effect | +|------|--------| +| `--no-ideate` | Skip ideate and its Grill/Continue/Stop gate | +| `--no-layers` | Skip layer-coverage checks for this UR; records `layers_in_scope: []` | + +**Notes:** + +- Auto-installs `.do-work/` if missing +- Ideate gate: **Grill** / **Continue** / **Stop** (Stop halts before capture) +- After success, may offer next steps (Run Go / Verify only / Skip) when `next_steps.enabled` is true + +**Example:** + +```text +/do-work start Add password reset email with rate limiting +/do-work start Quick typo fix in README --no-ideate --no-layers +``` + +### `/do-work go [UR-NNN]` + +**Job:** Verify coverage for a UR, then audit and run when the confidence gate passes. + +**Pipeline:** verify → (if gate passes) audit → run → optional close offer → optional log. + +| Flag | Effect | +|------|--------| +| `--force` | Run even if score < threshold; verify still runs | +| `--auto-fix` | One verify pass that creates missing REQs, re-scores; run only if ≥ threshold afterward | +| `--no-layers` | Skip layer-coverage checks; passed through to capture if `--auto-fix` re-runs capture | + +**Threshold:** `verify.threshold` in `.do-work/config.yml` (default **90**). + +**Example:** + +```text +/do-work go UR-001 +/do-work go UR-001 --auto-fix +/do-work go UR-001 --force +``` + +--- + +## Setup + +### `/do-work install` + +Creates the per-project `.do-work/` folder structure and default `config.yml` in the current project. + +Use when you want structure before the first brief. `/do-work start` also installs automatically. + +### `/do-work upgrade` + +Brings existing `.do-work/` state into conformance with the current skill (detectors + fixes). Destructive rows require interactive confirmation. Idempotent. + +Use after upgrading the skill install when help or startup mentions pending migration / stale config keys. + +--- + +## Define work (granular) + +### `/do-work intake [brief]` + +Records the brief **verbatim** as the next `UR-NNN/input.md`. No decomposition. + +Use when you want the UR on disk before ideate/capture, or to script the pipeline yourself. + +### `/do-work ideate [UR-NNN]` + +Surfaces assumptions, risks, and connections into `UR-NNN/ideate.md`. Ends with the interactive gate when run in flows that expect it. + +Use to pressure-test a brief without starting capture yet (or re-run after edits). + +### `/do-work question [UR-NNN]` + +Grills you one question at a time about the brief (assumptions, gaps, constraints). + +Use from the ideate **Grill** path or standalone when the brief is thin. + +### `/do-work capture [UR-NNN]` + +Decomposes `input.md` (and `ideate.md` if present) into backlog `REQ-NNN-slug.md` files. Applies layer rules, integration blocks, dependency cycle checks. + +Use to resume after a failed start-at-capture, or to re-decompose after you edited the brief. + +--- + +## Check quality + +### `/do-work verify [UR-NNN]` + +Scores REQ coverage against the original brief (0–100%) and lists gaps. Includes layer, integration-block, and partial-confidence structural checks. + +| Flag | Effect | +|------|--------| +| `--auto-fix` | Create missing REQs, then re-score | + +Use before `run` when you are not using `go`, or after manual REQ edits. + +### `/do-work audit [UR-NNN]` + +Interrogates acceptance criteria quality; auto-fixes vague spots; reports changes. Always runs inside `go` when execution will proceed; does not re-score verify. + +Use standalone to sharpen REQs without starting the run loop. + +--- + +## Execute and observe + +### `/do-work run [UR-NNN]` + +Executes the backlog: claim REQ → worker TDD loop → evidence validation → policy checks → post-build review → archive/ledger. Optional `UR-NNN` limits work to that UR’s REQs. + +Does **not** run the verify confidence gate (unlike `go`). + +| Flag | Effect | +|------|--------| +| `--parallel N` | One terminal dispatches up to N concurrent workers (default serial; capped at 10). Uses `parallel.max_workers` defaults when applicable | +| `--budget ` | Cap estimated model spend for this run; overrides `cost.budget`. Stops at the next REQ boundary after the in-flight REQ finishes integration. Empty budget = unlimited | + +**Parallelism without flags:** open multiple terminals and run `/do-work run` in each; claims coordinate via the filesystem/`git mv`. + +### `/do-work status [UR-NNN]` + +Read-only situation room: live REQs first (working + backlog), claimers (`hostname.pid`), heartbeats, deadlock warnings, coverage rollup. Unscoped archive is capped to recent completed rows; pass `UR-NNN` to list every matching archived REQ. + +Use whenever something looks stuck or you are running parallel workers. + +### `/do-work unblock REQ-NNN` + +Forces a REQ out of `working/` back to the backlog: strips claim stamp, resets status. Includes judgment when partial commits exist. + +Use when a worker died, heartbeat is stale, or you need to break a deadlock after triage with `status`. + +Requires a REQ id (example: `/do-work unblock REQ-042`). + +### `/do-work resume REQ-NNN` + +Re-dispatches a fresh worker for a **stopped** REQ while preserving the claim and refreshing the heartbeat. + +Use after `concurrent-conflict` or a transient worker failure—not as a substitute for `unblock` when the claim should be cleared. + +Requires a REQ id (example: `/do-work resume REQ-042`). + +### `/do-work close UR-NNN` + +Validates the integrated result of a UR against the verbatim brief: walks path-unit entry points to terminal states and writes a closure report under the UR folder. + +Requires a UR id. `go` may offer close after a clean drain when path-unit REQs exist and no `closure.md` yet. Closure gaps do not block the log step. + +--- + +## Learn and publish drafts + +### `/do-work retro` + +Mines the run ledger (and related feedback signals) into a human report and regenerates `.do-work/state/calibration.md` as advisory capture guidance. + +Use after several runs when you want capture to learn from history. + +### `/do-work log` + +Generates build-in-public **draft** posts for platforms listed in `log.platforms` (for example `x`, `linkedin`, `blog`). You choose drafts; history is recorded so the same work is not re-prompted forever. + +Skipped when `log.enabled` is false or `platforms` is empty. `go` can trigger log automatically after a clean run. + +--- + +## Quick reference table + +Same surface as README / SKILL quick reference: + +| Command | What it does | +|---------|--------------| +| `/do-work start [brief]` | Brief + REQs; ideate on by default | +| `/do-work start [brief] --no-ideate` | Skip creative review | +| `/do-work start [brief] --no-layers` | Skip layer checks for this UR | +| `/do-work go [UR-NNN]` | Verify; auto-run if ≥ threshold | +| `/do-work go [UR-NNN] --force` | Verify + run regardless of score | +| `/do-work go [UR-NNN] --auto-fix` | Verify, fix gaps once, run if ≥ threshold | +| `/do-work go [UR-NNN] --no-layers` | Verify + run; skip layer checks | +| `/do-work install` | Create `.do-work/` | +| `/do-work upgrade` | Conformance fixes for `.do-work/` | +| `/do-work intake [brief]` | Verbatim UR only | +| `/do-work capture [UR-NNN]` | UR → REQ files | +| `/do-work question [UR-NNN]` | Interactive grilling | +| `/do-work audit [UR-NNN]` | REQ quality pass | +| `/do-work ideate [UR-NNN]` | Assumptions and risks | +| `/do-work verify [UR-NNN]` | Coverage score + gaps | +| `/do-work verify [UR-NNN] --auto-fix` | Verify + create missing REQs | +| `/do-work run [UR-NNN]` | Execute backlog (optional UR scope) | +| `/do-work run --parallel N` | Single-session parallel workers | +| `/do-work run --budget ` | Spend cap for the run | +| `/do-work status [UR-NNN]` | Situation room | +| `/do-work close UR-NNN` | Integrated UR closure report | +| `/do-work unblock REQ-NNN` | Stuck REQ → backlog | +| `/do-work resume REQ-NNN` | Re-dispatch stopped REQ | +| `/do-work retro` | Ledger → calibration report | +| `/do-work log` | Build-in-public drafts | +| `/do-work` | Help | + +## Related + +- [Getting started](getting-started.md) +- [Concepts](concepts.md) +- [Troubleshooting](troubleshooting.md) +- Config schema: [`agents/config.md`](../agents/config.md) diff --git a/docs/concepts.md b/docs/concepts.md new file mode 100644 index 0000000..7eea5f4 --- /dev/null +++ b/docs/concepts.md @@ -0,0 +1,140 @@ +# Concepts + +Minimal mental model for do-work: what a brief becomes, how work is gated, and what “done” means. + +## Why it matters + +do-work turns a natural-language brief into small, traceable tasks and runs them with tests and evidence—not one opaque “agent did stuff” blob. Knowing the nouns (UR, REQ) and the two-command loop (`start` → `go`) keeps you in control of the gates. + +## How it works (minimal model) + +```text +Your brief + │ + ▼ + UR-NNN user request (verbatim input + side artifacts) + │ + ▼ + REQ-NNN-… backlog tasks (one file each) + │ + ▼ + working/ claimed, in flight (one worker / worktree per REQ) + │ + ▼ + archive/ done, with proof metadata +``` + +**Two-command surface** + +| Command | Role | +|---------|------| +| `/do-work start …` | Define work: intake → ideate (default) → capture | +| `/do-work go UR-NNN` | Execute work: verify → audit → run (then optional close/log) | + +Granular commands (`intake`, `capture`, `verify`, `run`, …) are the same building blocks; `start` and `go` chain them with defaults and human gates. + +**File-based state.** Everything lives under the project’s `.do-work/` (config, URs, backlog REQs, `working/`, `archive/`, `runs/`, `state/`). There is no separate do-work server. `git` history is the audit trail. + +## Key terms + +### UR (user request) + +- Folder: `.do-work/user-requests/UR-NNN/` +- Core file: `input.md` — your brief **verbatim** (do-work does not “improve” the wording on intake) +- May also hold `ideate.md`, `assets/`, later `closure.md` +- Numbering: sequential, zero-padded (`UR-001`, `UR-002`, …); next id is max+1 (gaps are not filled) + +### REQ (requirement / task) + +- Backlog file: `.do-work/REQ-NNN-slug.md` +- One discrete unit of work with acceptance criteria, verification steps, optional dependencies, layer, and file footprint +- Lifecycle locations: backlog root → `working/` (claimed) → `archive/` (done) +- Each completed REQ normally produces **one git commit** on a `req/REQ-NNN` branch, then delivery per `delivery.mode` (`merge` default, or `pr`) + +### Start vs go + +- **start** = record + shape the backlog (does not run implementation) +- **go** = score the backlog against the brief, then run if the gate passes + +That split is intentional: you get a human decision point before autonomous execution. + +### Ideate gate + +On `start` (unless `--no-ideate`), ideate ends with: + +- **Grill** — one-at-a-time questions (`question`) +- **Continue** — capture as-is +- **Stop** — you revise `input.md` yourself; capture does not run + +### Verify gate (confidence) + +`go` runs verify first. Score is 0–100% coverage of the brief, plus structural checks: + +1. Layer coverage (declared layers represented or explicitly skipped) +2. Integration block on new-surface feature REQs +3. Partial-confidence acknowledgements from capture + +Default threshold: **`verify.threshold: 90`** in `.do-work/config.yml`. + +| Outcome | Behaviour | +|---------|-----------| +| Score ≥ threshold | Proceed to audit + run | +| Score < threshold | Halt; show gaps (unless `--force` or successful `--auto-fix`) | + +### Layers + +Project-declared slices of the stack in config, for example: + +```yaml +layers: [frontend, backend] +``` + +Capture tags each REQ with a layer (or `none` for bug-fix / pure refactor). Feature briefs that ignore a declared layer get a prompt—or halt if layers are empty and you did not pass `--no-layers`. See README “Layers and Integration”. + +### Integration block + +For feature REQs that add **new surface** (page, route, command, endpoint, …), capture writes `## Integration` with codebase-checked answers: + +- Reachability +- Data dependencies +- Service dependencies + +Stops “compiles but unreachable” work from looking done at decomposition time. + +### TDD loop (per REQ) + +During run, a fresh worker typically: + +1. Works in a dedicated git worktree on `req/REQ-NNN` +2. Writes a failing test for an acceptance criterion +3. Implements until it passes; repeats for remaining criteria +4. Runs the project suite when configured +5. Returns structured evidence to the orchestrator + +### Evidence and review gates + +A worker report alone does **not** mean archived. The orchestrator validates acceptance evidence, runs policy checks (blocked paths/commands), runs post-build review, writes closure proof fields, and only then archives. Failed review is a **stopper**, not a successful REQ. + +### Delivery modes + +From `.do-work/config.yml` (`delivery.mode`): + +- **`merge`** (default) — merge `req/REQ-NNN` into the base branch locally, archive, tear down worktree, delete branch +- **`pr`** — push and open a GitHub PR via `gh`; requires remote + `gh`. Missing credentials → `missing-creds` stopper (no silent merge fallback) + +### Parallelism (short) + +- Multi-terminal: several `/do-work run` sessions claim different REQs via atomic `git mv` +- Single-session: `/do-work run --parallel N` (N capped at 10) + +Claim stamps, heartbeats, footprint checks, and dependency checks reduce collisions. Recovery: `status`, `unblock`, `resume`. + +### Build-in-public log + +Optional drafts for X / LinkedIn / blog from completed archive work. Runs after a clean `go` when `log.enabled` is true and `log.platforms` is non-empty—or on demand via `/do-work log`. + +## What to do next + +1. [Getting started](getting-started.md) — install and first `start` / `go` +2. [Commands](commands.md) — when to use each command +3. [Troubleshooting](troubleshooting.md) — gate failures and stuck REQs diff --git a/docs/design/ur-closure.md b/docs/design/ur-closure.md index aa2dac6..26ce6c5 100644 --- a/docs/design/ur-closure.md +++ b/docs/design/ur-closure.md @@ -169,7 +169,7 @@ overall: gaps - `**Closure proof:**` stays exactly as today: written by run.md Step 4 from the worker's `closure_proof` YAML value (a checkpoint-log + commit reference), and read by `lib/derive-status.sh` to derive per-REQ `proven`/`unproven`. The closure agent does not touch it. - The UR-level closure verdict lives only in `UR-NNN/closure.md`. Per-path-unit `evidence_ref` values in that file are the closure analogue of `**Closure proof:**` — they reference observed end-to-end evidence (command output, screenshot, test name, human-confirm id) rather than a per-REQ checkpoint log. -- **Coverage distinction (REQ-213):** `lib/coverage-rollup.sh` gains an end-to-end tier. Today it prints `intended=N proven=N unproven=N` per UR by aggregating `derive-status.sh`. REQ-213 extends it: when `UR-NNN/closure.md` exists, the rollup reads `overall` and the `verdict_summary` and appends a closure column, e.g. `closed=1 gaps=1` or `closure=none` when no `closure.md` exists yet. This lets `status` (which already prints the rollup under its `Coverage` heading) distinguish "proven per-REQ" from "proven end-to-end" without changing the existing per-REQ math — the new field is additive, preserving the existing line format and the lib's test contract (`lib/coverage-rollup.test.sh`). +- **Coverage distinction (REQ-213):** `lib/coverage-rollup.sh` gains an end-to-end tier. Today it prints `intended=N proven=N unproven=N` per UR by aggregating `derive-status.sh`. REQ-213 extends it: when `UR-NNN/closure.md` exists, the rollup reads `overall` and the `verdict_summary` and appends a closure column, e.g. `closed=1 gaps=1` or `closure=none` when no `closure.md` exists yet. This lets `status` (which already prints the rollup under its `Coverage` heading) distinguish "proven per-REQ" from "proven end-to-end" without changing the existing per-REQ math — the new field is additive, preserving the existing line format and the lib's test contract (`lib/tests/coverage-rollup.test.sh`). **Rationale.** Two proof tiers answer two different questions: `**Closure proof:**` answers "was this REQ correctly built and committed?"; closure answers "does the merged whole do what the user asked?". Keeping them separate preserves the meaning of every existing field and the determinism of the lib scripts, while making the end-to-end signal visible in the one place users already look (`status` Coverage). Folding closure into `**Closure proof:**` would conflate per-REQ correctness with integration reachability and silently change what `proven` means across the whole system. @@ -181,7 +181,7 @@ overall: gaps |---|---|---|---| | REQ-211 | agents | `agents/close.md` (new) | The closure agent: cold dispatch (Decision 1), walk mechanics + degraded routing (Decisions 2–3), writes `closure.md` per the schema (Decision 4), surfaces gaps without fixing (Decision 5). Loads config via config.md. | | REQ-212 | commands | `SKILL.md` | New `### close [UR-NNN]` subcommand section mirroring the `status` block; add `close` to the Quick Reference. Wire the go offer note if it lives in SKILL.md routing; the go-side change itself is `agents/go.md`. | -| REQ-213 | agents | `lib/coverage-rollup.sh` (+ `lib/coverage-rollup.test.sh`) | Add the additive end-to-end closure column read from `UR-NNN/closure.md`; extend the test to cover the `closure=none` / `closed=N gaps=N` / `no-path-units` cases. | +| REQ-213 | agents | `lib/coverage-rollup.sh` (+ `lib/tests/coverage-rollup.test.sh`) | Add the additive end-to-end closure column read from `UR-NNN/closure.md`; extend the test to cover the `closure=none` / `closed=N gaps=N` / `no-path-units` cases. | **go wiring note for REQ-211/212:** the post-run closure offer (Decision 5) is an edit to `agents/go.md` Step 4/6 area. Whichever child owns the go edit must keep it ungated by `next_steps.enabled` (R9) and gated only on go being top-level (it always is). diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..0946a20 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,206 @@ +# Getting started with do-work + +Install the skill, wire it into your agent, then run your first brief with `/do-work start` and `/do-work go`. + +## Before you start + +- Any agent harness that can load skills from a shared hub (Claude Code, Codex CLI, Cursor, and others pointed at the same hub) +- `git` available on your PATH +- A project directory where you want work tracked under `.do-work/` +- Optional: a project test command you can put in `.do-work/config.yml` as `test.suite_command` (for example `npx vitest run` or `./vendor/bin/pest`) + +Time: a few minutes to install; first `start` + `go` depends on the size of your brief. + +Related: [Concepts](concepts.md) · [Commands](commands.md) + +## Steps + +### 1. Install the skill into the skills hub + +Default hub path: `~/.agents/skills/do-work`. + +Override the hub with `AGENTS_SKILLS_HUB` if your agents load skills from somewhere else. + +**Option A — clone this repository (recommended for rawphp/do-work):** + +```bash +git clone https://github.com/rawphp/do-work.git ~/.agents/skills/do-work +``` + +**Option B — run `install.sh` from a checkout:** + +```bash +git clone https://github.com/rawphp/do-work.git +cd do-work +bash install.sh +``` + +`install.sh` clones or updates into `$AGENTS_SKILLS_HUB/do-work` (default `~/.agents/skills/do-work`). + +If you use the script’s default remote without a local checkout, note the current default in `install.sh`: + +```bash +# Default REPO_URL inside install.sh (override if needed): +# DO_WORK_REPO_URL defaults to https://github.com/agent-native/do-work.git +DO_WORK_REPO_URL=https://github.com/rawphp/do-work.git bash install.sh +``` + +**Option C — live symlink from a checkout (development):** + +```bash +cd /path/to/do-work +bash install.sh --from-cwd +# or: bash install.sh --source /path/to/do-work +``` + +`--env` is accepted and ignored. All agents share one hub install. + +### 2. Wire your agent to the hub + +Point your agent harness at the skills hub so it can load `do-work` (`SKILL.md` in the install directory). Exact settings differ by product; the install only places files on disk. + +You should be able to invoke `/do-work` (or bare `/do-work` for help) inside a project session. + +### 3. Open your project and start a brief + +In the project root (or any directory where you want `.do-work/` created): + +```text +/do-work start I need a user settings page with email and password change +``` + +What this does: + +1. Creates `.do-work/` on first use if needed (same as `/do-work install`) +2. Records your brief **verbatim** as the next `UR-NNN` under `.do-work/user-requests/UR-NNN/input.md` +3. Runs **ideate** by default (assumptions, risks, connections), then an interactive gate: **Grill** / **Continue** / **Stop** +4. **Capture** decomposes the brief into backlog REQ files (`.do-work/REQ-NNN-slug.md`) + +Useful flags: + +- `--no-ideate` — skip creative review and the ideate gate +- `--no-layers` — skip layer-coverage checks for this UR (recorded for audit) + +If capture stops because layers are undeclared, either set layers in config or pass `--no-layers` (see [Troubleshooting](troubleshooting.md)). + +### 4. Configure layers and tests (recommended before a large feature) + +On first install, edit `.do-work/config.yml`: + +```yaml +project: + name: "my-project" + +layers: [frontend, backend] # example for a web app; [] opts out until you set them + +test: + suite_command: "npx vitest run" # your real suite command + +verify: + threshold: 90 # go auto-runs only at or above this score unless --force +``` + +Empty `layers: []` opts out of layer gap-checks, but **feature** briefs may halt capture until you declare layers or pass `--no-layers`. + +Full key list: [`agents/config.md`](../agents/config.md). + +### Optional: Linear as work-item store + +By default, work items live under `.do-work/` (`tracker.backend` unset or `markdown`). You can point do-work at **Linear** instead so URs/REQs live only in Linear (no dual-write). + +Minimal config: + +```yaml +tracker: + backend: linear + linear: + team_id: "" # required UUID — or set team_key + team_key: "" # optional alternate team resolve + # product_project: "" # empty by default — resolve → project.name → basename; ensure binds UUID + # status_map / labels / claim marker: defaults in agents/config.md +``` + +**Before enabling `backend: linear`:** + +1. Connect **Linear MCP** in your agent host (API key preferred: `LINEAR_API_KEY` + MCP URL `https://mcp.linear.app/mcp`). Details: [Troubleshooting → Linear tracker backend](troubleshooting.md#linear-tracker-backend). +2. Set a real `team_id` or `team_key` — agents hard-stop if the team cannot be resolved (they never guess). +3. Confirm team workflow states match `tracker.linear.status_map` defaults (`Todo` / `In Progress` / `Canceled` / `Done`) or override the map. +4. Product Project is **per local product**, not a universal Project named `do-work`. Leave `product_project` empty (default) to resolve via `project.name` → git-root basename, or set a name/UUID explicitly. First ensure create-if-missing and **persists the UUID**. + +**Rules that matter day one:** + +- **Markdown is the default** — skip this section entirely if you only want local files. +- **No dual-write** — Linear mode does not keep a second markdown store of URs/REQs. +- **Hard-stop** — if Linear MCP is down or misconfigured, agents stop with setup instructions; they do not fall back to markdown. +- **Human assignee** — you stay assignee on Issues; agents claim via comments. **Do not clear agent claim comments in Linear while a run is live** (see troubleshooting). +- **Migrate existing markdown projects** only when idle: `/do-work upgrade migrate` (dry-run first). See [How it works → Multi-tracker](HOW-IT-WORKS.md#multi-tracker-work-item-backends). + +### 5. Execute with go + +Use the UR number from the start report (example `UR-001`): + +```text +/do-work go UR-001 +``` + +What this does: + +1. **Verify** — scores REQ coverage against your original brief (0–100%) plus structural checks +2. If score ≥ `verify.threshold` (default **90**): **audit** (sharpen acceptance criteria), then **run** the backlog +3. Each REQ: claim → worktree on `req/REQ-NNN` → TDD → evidence and review gates → archive + commit +4. Optional **close** offer (path-unit flows) and **log** drafts if logging is enabled + +Flags: + +- `--force` — run even when confidence is below threshold (verify still runs so you see the report) +- `--auto-fix` — create missing REQs once, re-score, then run only if still ≥ threshold +- `--no-layers` — skip layer-coverage checks for this UR (threaded into verify/capture when auto-fix runs) + +### 6. Check status while work runs + +```text +/do-work status +/do-work status UR-001 +``` + +Read-only situation room: backlog vs working first, recent completed (archive capped when unscoped), claimers, heartbeats, deadlock warnings, coverage rollup. + +## How you know it worked + +After install: + +- Directory exists: `~/.agents/skills/do-work` (or `$AGENTS_SKILLS_HUB/do-work`) with a `SKILL.md` inside +- Your agent exposes `/do-work` / do-work help + +After `start`: + +- `.do-work/user-requests/UR-NNN/input.md` holds your brief under `## Request` +- One or more `.do-work/REQ-*-*.md` files appear in the backlog root +- Start report lists REQs and totals + +After a successful `go`: + +- Message like `Go complete for UR-NNN` with verify %, audit outcome, and run count +- Completed REQs under `.do-work/archive/` with status done +- Git history includes commits shaped like `feat(REQ-NNN): short title` +- Optional: `.do-work/runs/RUN-NNN.yml` when `ledger.enabled` is true + +## If something goes wrong + +| Symptom | What to do | +|---------|------------| +| `/do-work` not found | Confirm hub path and agent skill loading; re-run install | +| Capture halts on layers | Set `layers` in config or use `--no-layers` | +| `go` stops below 90% | Read verify gaps; fix REQs, or use `--auto-fix` / `--force` | +| UR not found | Check `.do-work/user-requests/` for the real `UR-NNN` | +| REQ stuck in `working/` | `/do-work status` then `/do-work unblock REQ-NNN` or `/do-work resume REQ-NNN` | +| Linear hard-stop / no MCP | Connect Linear MCP + set `tracker.linear.team_id` (see [troubleshooting](troubleshooting.md#linear-tracker-backend)) | + +Full table: [Troubleshooting](troubleshooting.md). + +## Related + +- [Concepts](concepts.md) — UR, REQ, gates, evidence +- [Commands](commands.md) — full command list +- [How it works](HOW-IT-WORKS.md) — phase design deep dive (includes multi-tracker) +- [Troubleshooting](troubleshooting.md) — Linear MCP, claim comments, gates diff --git a/docs/skill-best-practices-findings.md b/docs/skill-best-practices-findings.md new file mode 100644 index 0000000..e535356 --- /dev/null +++ b/docs/skill-best-practices-findings.md @@ -0,0 +1,179 @@ +# Skill best-practices findings — do-work (ORI-5 / ORI-6) + +**Issue:** ORI-5 (inventory) · ORI-6 (confirm + N/A completeness) · **UR:** UR-001 +**Scope:** Inventory only — progressive disclosure, description routing, lean SKILL.md, references/scripts layout, anti-patterns, Linear store consistency. +**Out of scope:** Structural migration (no splits, no renames, no agent body rewrites in this REQ). + +Measured in worktree `.worktrees/req-ori-5` on branch `req/ORI-5` (base `linear`); confirmed on `req/ORI-6` with explicit N/A table (ORI-6 AC). + +--- + +## 1. Rubric (which checklist items apply) + +Source: `effective-agent-skills` (`~/.grok/skills/effective-agent-skills/SKILL.md`) — progressive disclosure, description routing, lean body, references one-level deep, anti-patterns, ship checklist. + +| Rubric item | Applies? | Notes for do-work | +|-------------|----------|-------------------| +| **L1 description routing** (what + when + differentiator; no how-summary) | Yes | Frontmatter description is the only discovery signal | +| **L2 lean SKILL.md** (activation budget; aim well under ~5k tokens) | Yes | SKILL.md is the always-loaded body on match | +| **L3 progressive disclosure** (`references/`, on-demand load) | Yes | Skill has no `references/`; detail lives in `agents/` + `docs/` | +| **`scripts/` for determinism** (fragile/repetitive → code) | Partial | Determinism lives in `lib/*.sh` (project shape), not skill-standard `scripts/` | +| **Bash-first, prose-second** | Yes | Many good command examples; also large procedural prose | +| **One skill, one concern** / no mega-skill | Yes | do-work is a full PM loop (intake→archive) in one skill | +| **No human-facing docs inside skill folder** | Yes | README / CHANGELOG / CONTRIBUTING at skill root | +| **Relative paths only** | Yes | Prefer `{project}` / relative; some `~/.claude/skills` examples | +| **State-check before action** | Yes | Strong (conformance scan, install check, Load Config) | +| **Validation loops** | Yes | TDD checkpoints, review gate, archive-integrity, verify | +| **Output formats documented** | Yes | Return reports, checkpoint YAML, claim blocks | +| **Failure modes documented** | Yes | Hard-stops, stopper reasons, dual-write bans | +| **Keep references one level deep** | Yes | SKILL → agents/* is one hop; linear.md internal path chains are deep | +| **No dual-write / single source of truth** (project multi-tracker rule) | Yes | Port contract: Linear sole store when `backend: linear` | +| **Ship checklist** (name match, triggers, relative paths, compose, VCS) | Yes | Spot-check only | + +### N/A checklist items (no silent omission) + +Items from `effective-agent-skills` that do **not** apply as hard gates or ship criteria for this inventory. Each row is deliberate — not omitted. + +| Checklist / guide item | Status | One-line rationale | +|------------------------|--------|--------------------| +| Pattern A “30–80 line skill” length target | **N/A** | do-work is intentionally Pattern B (process + large `lib/` surface); line-count budget still measured under L2, not Pattern A. | +| Security checklist (third-party skill install audit) | **N/A** | Evaluating first-party do-work package, not installing untrusted third-party skills. | +| `assets/` folder (templates/fonts/static) | **N/A** | No static skill assets required; templates live under project/skill `templates/` when present, not skill-standard `assets/`. | +| `disable-model-invocation` frontmatter | **N/A** | Skill is meant to auto-route on PM/backlog phrases; manual-only flag would break primary UX. | +| “Don’t write style-only variants” anti-pattern | **N/A** | do-work is a full workflow skill, not a tone/format preference pack. | +| “Don’t bundle library code” (paste npm/pip sources into skill) | **N/A** | Helpers are project shell under `lib/`, not vendored third-party library source trees. | +| “Defer to `--help` for completeness” | **N/A** as ship gate | Most agent ops are Markdown sequences + MCP rediscovery, not a single CLI with `--help`; `lib/*.sh` scripts are thin and documented in agents. | +| Ship: tested with weak and strong models | **N/A** this REQ | Inventory/docs only; model matrix testing is out of scope for ORI-5/ORI-6 (belongs to runtime eval, not findings file). | +| Ship: eval suite of trigger prompts | **N/A** this REQ | No trigger-eval harness in this inventory; routing quality noted qualitatively under F6 only. | +| Compose interfaces between *separate* published skills | **N/A** short-term | Product is one skill + `lib/`; multi-skill composition is a future migrate option (see §4 item 6), not current packaging. | + +--- + +## 2. Metrics (re-measured) + +```text +$ wc -l SKILL.md agents/tracker/linear.md agents/run.md + 767 SKILL.md + 2113 agents/tracker/linear.md + 1433 agents/run.md +``` + +| Path | Lines | Words | ~Tokens (words×1.3) | +|------|------:|------:|--------------------:| +| `SKILL.md` | 767 | 6983 | ~9077 | +| `agents/tracker/linear.md` | 2113 | 21609 | ~28091 | +| `agents/run.md` | 1433 | 15597 | ~20276 | +| `agents/capture.md` | 819 | 8726 | ~11343 | +| `agents/run-worker.md` | 595 | 6363 | ~8271 | +| `agents/upgrade.md` | 575 | 3469 | ~4509 | +| `agents/tracker/port.md` | 481 | 3540 | ~4602 | +| `agents/tracker/markdown.md` | 412 | 2806 | ~3647 | +| `agents/config.md` | 264 | 3790 | ~4927 | +| All `agents/**/*.md` (23 files) | ~9848 | — | — | +| `lib/*.sh` | 62 scripts | — | (determinism home) | + +**Layout gaps (skill standard):** + +| Expected (effective-agent-skills) | Actual | +|-----------------------------------|--------| +| `references/` | **Missing** | +| `scripts/` | **Missing** (helpers under `lib/`) | +| Lean `SKILL.md` | 767 lines / ~9k tokens (over L2 budget) | + +**Activation stack estimate (full body reads, worst case):** + +| Scenario | Files typically forced into context | ~Tokens | +|----------|-------------------------------------|--------:| +| Markdown `/do-work run` | SKILL + config + port + markdown + run + run-worker | ~50.8k | +| Linear `/do-work run` | SKILL + config + port + **linear** + run + run-worker | ~75.2k | + +**Human-facing docs at skill root:** `README.md` (369), `CHANGELOG.md` (154), `CONTRIBUTING.md` (303) — anti-pattern vs “skills are for agents.” + +--- + +## 3. Findings (ordered: identify → migrate later) + +Findings are ranked so **identify / measure / document** work precedes **split / migrate** work. Severity: `blocker` | `major` | `minor`. Fix class: `docs` | `split` | `code` | `leave`. + +### Identify-first (do before any structural migrate) + +| ID | Title | Severity | Evidence | Fix class | +|----|-------|----------|----------|-----------| +| F1 | **SKILL.md far exceeds L2 activation budget** | **blocker** | 767 lines, ~6983 words, ~9k tokens vs guide “activation <5k tokens”. Body includes full Quick Reference, tracker essay (§ Tracker backends), parallel execution design summary, REQ schema, and ~315 lines of per-subcommand stubs that restate agent files. | **split** (later): keep routing + minimal stubs in SKILL; move schema/parallel/tracker essays to `references/` or keep only pointers to `agents/*`. | +| F2 | **`agents/tracker/linear.md` is a mega-file (runtime + path history)** | **blocker** | 2113 lines / ~28k tokens. Opens with path-unit scaffolding (REQ-288…REQ-301 design diary) then operational CRUD, claim, milestone, migration sequences. Loading backend=`linear` forces agents to absorb historical path narrative + full op catalog. | **split** (later): operational sequences vs path-history/design appendix; optional `references/linear-*.md` one level deep from a short `linear.md` index. | +| F3 | **No `references/` progressive-disclosure tree** | **blocker** | `ls references` → missing. Detail is either inlined in SKILL or in always-linked fat `agents/*.md`. L3 (“load only when needed”) has no standard hook; agents that follow “read X in full” load entire files. | **split** (later): introduce `references/` for schema, parallel, Linear ops, milestone; SKILL/agents point with just-in-time “read when …”. | +| F4 | **Worst-case Linear run stack ~75k tokens of instruction** | **blocker** | Stack SKILL+config+port+linear+run+run-worker ≈ 57.9k words. Context pressure → missed hard-stops, partial sequence following, dual-write “safety” inventiveness. | **split** + **docs**: shrink mandatory full-file reads; index + section pointers; keep hard rules in a short always-loaded contract. | +| F5 | **Markdown path assumptions still dominate SKILL subcommand stubs** | **major** | SKILL subcommands hardcode `.do-work/user-requests/`, backlog `REQ-*.md`, `working/` (≈27 matches in SKILL alone; `agents/run.md` ≈58). Under `tracker.backend: linear`, work-item truth is Linear — stubs can steer agents toward markdown list/confirm steps before backend load. Phase agents *do* restate Load Config + port path, but SKILL is read first. | **docs** first (clarify “paths below are markdown default; Linear uses port ops”), then **split** stubs so store I/O is never specified in SKILL. | +| F6 | **Description routing is OK but incomplete for multi-tracker** | **major** | Frontmatter what+when+triggers present. Still says “file-based autonomous loop” / “REQ files” only — weak differentiator vs Linear mode and vs generic task skills. No explicit triggers for “linear backlog”, “tracker.backend”, “migrate to Linear”. Risk: wrong mental model at L1. | **docs**: extend description what/when/differentiator; keep no how-summary. | +| F7 | **Human-facing docs live inside the skill package** | **major** | Root `README.md`, `CHANGELOG.md`, `CONTRIBUTING.md` (~826 lines). Rubric: “No human-facing docs inside the skill folder.” Agents may load README instead of SKILL/agents. (Acceptable if package is also a product repo — still a skill-ship smell.) | **leave** if dual product+skill is intentional; else **docs** relocate human guides outside install path or mark “humans only / do not load”. | +| F8 | **Monolithic mega-skill (many concerns in one folder)** | **major** | One skill covers install, intake, capture, verify, run, review, close, retro, log, upgrade, multi-tracker, parallel, milestones. Rubric anti-pattern: “Don’t write monolithic mega-skills.” Composition would be multiple skills + shared substrate (already partially `lib/` + `.do-work/`). | **leave** short-term (product identity); long-term **split** only after inventory of phase boundaries (this doc). | +| F9 | **Missing skill-standard `scripts/` name (uses `lib/`)** | **minor** | 62 shell helpers under `lib/` — correct determinism placement, nonstandard vs agentskills.io `scripts/`. Install and SKILL already teach `lib/`. | **leave** (project convention) or **docs** note “`lib/` ≡ scripts/”. | +| F10 | **Absolute / home-path examples in agent docs** | **minor** | e.g. SKILL “loaded from `~/.claude/skills/do-work/`”; run.md example `/Users/you/.claude/skills/do-work`. Rubric prefers relative + placeholders. Functional, slightly host-specific. | **docs** | + +### Linear dual-write / phase-agent path gaps + +| ID | Title | Severity | Evidence | Fix class | +|----|-------|----------|----------|-----------| +| F11 | **No dual-write policy is documented and repeated — treat as intentional strength** | **minor** (positive) | SKILL, `port.md`, `linear.md`, `markdown.md`, capture/run/run-worker hard-stop “no silent markdown fallback”. Context pack: “No dual-write.” | **leave** — do not “fix” by adding mirrors. | +| F12 | **`linear.md` path-unit diary mixed with live op sequences** | **major** | Sections `## Path: Linear MCP capability spike (REQ-288)` … migration REQ-300/301 sit above/ beside production sequences (CRUD, claim, hard-stop). Agents instructed to “read linear.md” get implementation archaeology + matrix “unavailable” narratives that can undercut live MCP use. | **split** later: runtime ops file vs `references/linear-path-history.md` or archive under `docs/design/`. | +| F13 | **Phase agents wire Load Config + port — consistent** | **minor** (positive / residual risk) | All phase agents under `agents/*.md` mention `tracker.backend` / Load Config / tracker. Residual: **order-of-read** risk if SKILL stub runs markdown filesystem checks before agent file (F5). | **docs** (ordering: config → port → backend → then any store I/O). | +| F14 | **Within-Linear “dual-write” of deps (relations + body)** | **minor** | `linear.md`: `set_blocked_by` may write native relations **and** body `**Depends on:**` mirror. This is **not** markdown dual-write; still two Linear representations — document authority (relations win) is present; keep explicit in any split. | **leave** / **docs** clarify in short contract card. | +| F15 | **Markdown historical trees after migrate are easy to re-activate by mistake** | **major** | Post-cutover: local UR/REQ trees remain as **read-only history** while `backend: linear`. SKILL install/list steps and many agents still name those paths. Conformance/upgrade document ignore rules; a rushed agent can still `ls .do-work/` and treat files as live. | **docs** + later **code** (conformance detector / guardrails already partial — extend messaging in SKILL stubs). | +| F16 | **`lib/` remains markdown-centric under Linear (by design)** | **minor** | linear.md: “No Linear-aware bash in lib/ (v1)”. Port ops are agent/MCP sequences. Gap: workers must not call `claim-req.sh` etc. as store of truth when backend=linear — stated in agents; easy to miss under token pressure (F4). | **docs** first; optional **code** thin wrappers later (out of inventory scope). | + +### Ship-checklist snapshot + +| Checklist item | Status | +|----------------|--------| +| Frontmatter `name` matches folder (`do-work`) | Pass | +| Description what + when + triggers | Partial (F6) | +| Differentiator vs related skills | Weak | +| No human-facing docs in skill folder | Fail (F7) | +| No time-sensitive “as of …” rot | Pass (mostly) | +| Relative paths only | Partial (F10) | +| State-check before action | Pass | +| Validation loop | Pass | +| Output format | Pass | +| One concern / composes cleanly | Fail as pure skill (F8); pass as product | +| Version controlled | Pass | + +--- + +## 4. Recommended migrate order (after identify; not this REQ) + +Do **not** execute here — sequencing only so later REQs stay identify-first. + +1. **Contract card** (docs): one-page “always load” rules — backend resolution, no dual-write, hard-stop, claim protocol, commit footers. +2. **SKILL.md diet** (split): Quick Reference + agent index + “read agent file” only; move schema/parallel/tracker essays to `references/`. +3. **Linear runtime extract** (split): `linear.md` → short index + `references/linear/{crud,claim,milestone,migrate}.md`; archive path-unit diary. +4. **SKILL subcommand path neutrality** (docs/split): no markdown filesystem prechecks in stubs; defer store I/O to phase agent after port load. +5. **Description refresh** (docs): multi-tracker what/when/differentiator. +6. **Optional composition** (leave until 1–5): only if still over budget — phase skills with shared `lib/` substrate. +7. **Human docs packaging** (leave/docs): decide product-repo vs pure skill install layout. + +--- + +## 5. Acceptance map (ORI-5 / ORI-6) + +| AC | How satisfied | +|----|----------------| +| Rubric explicit | §1 | +| Each finding: title, severity, evidence, fix class | §3 tables (≥5; F1–F16) | +| SKILL.md line count, top agent sizes, missing references/scripts | §2 | +| Linear dual-write / phase-agent path gaps | F11–F16 | +| Identify-first before migrate | §3 order + §4 explicit sequencing | +| Non-applicable checklist items documented as N/A | §1 **N/A checklist items** table (no silent omission) | +| No structural migration in this REQ | Findings file only | + +--- + +## 6. Verification commands (operator) + +```bash +# File exists with severity column +test -s docs/skill-best-practices-findings.md +rg -n 'severity|blocker|major|minor' docs/skill-best-practices-findings.md + +# Metrics cited above (re-run anytime) +wc -l SKILL.md agents/tracker/linear.md agents/run.md +``` diff --git a/docs/superpowers/specs/2026-07-31-do-work-multi-tracker-design.md b/docs/superpowers/specs/2026-07-31-do-work-multi-tracker-design.md new file mode 100644 index 0000000..5931f49 --- /dev/null +++ b/docs/superpowers/specs/2026-07-31-do-work-multi-tracker-design.md @@ -0,0 +1,400 @@ +# Design: Do-work multi-tracker (markdown + Linear) + +**Date:** 2026-07-31 +**Status:** approved for implementation planning +**Source:** `.scratch/do-work-multi-tracker/` wayfinder + brainstorming session + +## 1. Problem + +do-work stores work items (URs, REQs, decisions, verify/close reports) only as local markdown under `.do-work/`. Operators who want Linear as the system of record cannot run the full do-work loop without dual-maintaining tickets. The skill needs a second backend without rewriting product philosophy (TDD-per-REQ, worktrees, review gate, multi-agent claim/deps/footprint). + +## 2. Goals + +1. **Markdown remains the default backend** — current UR/REQ files and `lib/*.sh` behavior stay the happy path when `tracker.backend` is unset or `markdown`. +2. **Linear is a full second backend** — with `tracker.backend: linear`, work items live **only** in Linear (no dual-write, no local UR/REQ markdown as source of truth). +3. **Tracker port** — one conceptual op catalog; backends plug in. GitHub Issues / Jira can follow later as new backend files (not built in this effort). +4. **v1 ship surface = full map destination** — core loop (intake → ideate → capture → verify → run claim/deps/footprint → status/close), **milestone mode**, Linear homes for ledger notes / decisions / verify / close / calibration, and **idle one-shot markdown→Linear migration**. + +### Done when + +- Default/markdown: behavior matches today (regression). +- Linear configured: an agent can complete intake → ideate → capture → verify → run (claim / deps / footprint) → status / close against Linear via the Linear skill/MCP, preserving multi-agent safety semantics. +- Milestone deploy gates work under Linear mode. +- Non-ticket artifacts have fixed Linear homes; agents do not invent ad-hoc locations. +- Idle migration moves a markdown project to Linear without dual-write. + +## 3. Non-goals + +- Implementing GitHub Issues or Jira backends (pattern only). +- Dual-write or markdown mirror while on Linear. +- Changing TDD-per-REQ, worktree isolation, or post-build review philosophy — only the **store** for work items changes. +- Requiring Linear for all users. +- True distributed locks on Linear (optimistic claim only). + +## 4. Decisions (locked) + +| Decision | Choice | +|----------|--------| +| Architecture | Tracker port docs: `agents/tracker/{port,markdown,linear}.md` | +| Hierarchy | **UR = Project Milestone** on a shared product Project (`tracker.linear.product_project`); REQs = Issues in that Project with `milestone` = UR milestone. *(2026-07-31: supersedes Initiative + per-UR Project — Linear MCP has no Initiative create tools.)* | +| Product container | Team + config — **not** one long-lived product Project for all URs | +| Linear IDs | Linear mode uses Linear issue identifiers only (e.g. `ENG-123`). No parallel `REQ-NNN` allocation | +| UR naming slug | Sequential `UR-NNN` still used as Project name / Initiative metadata slug only | +| Path-units | Parent Issue + layer children as sub-issues (`parentId`) | +| Deps | Native Linear relation type `blocks` (+ mirrored `**Depends on:**` line in body) | +| Footprint | Structured `**Files:**` (and related header fields) in Issue description — no custom fields | +| Claim | Human operator remains Linear **assignee**; agents claim via workflow status + heartbeat **comment** protocol | +| Claim atomicity | Optimistic re-read before write; loser → concurrent-conflict / stop; resume allowed | +| Linear unusable | Hard stop — never silent fallback to markdown | +| Migration | One-shot when idle (`working/` empty); then Linear-only | +| Non-ticket park | Decisions + calibration = team Docs; verify/close = Initiative; run notes = Issue comments (+ optional Project update) | +| Runtime/git | Stay local: worktrees, merges, `state/*` locks, events, config.yml | + +## 5. Architecture + +### 5.1 File layout + +``` +agents/tracker/port.md # shared contract: op names, preconditions, agent-callable surface +agents/tracker/markdown.md # file + lib/*.sh implementation of those ops +agents/tracker/linear.md # Linear skill/MCP sequences for the same ops +# later: agents/tracker/github.md, jira.md +``` + +### 5.2 Load path + +Every phase agent that touches work items: + +1. Load config (`agents/config.md`) +2. Resolve `tracker.backend` (default `markdown` if missing/empty) +3. Read `agents/tracker/port.md` +4. Read `agents/tracker/.md` +5. For work-item storage, call **only** named port ops (never raw `.do-work/REQ-*` paths or raw Linear tools outside the backend file) + +Phase agents keep product logic (TDD, review, decomposition). They do not re-implement store details. + +### 5.3 Bash vs agent steps + +| Backend | Work-item ops | Runtime | +|---------|---------------|---------| +| **markdown** | Existing `lib/*.sh` + file paths, documented in `markdown.md` | worktrees, git, events, state locks — unchanged | +| **linear** | Agent steps invoking Linear skill/MCP, documented in `linear.md`. No Linear-aware bash required for v1 | same local runtime/git | + +Shared **rules** (when to claim, what “deps satisfied” means, footprint overlap) live in `port.md`. Shared **shell** only for the file store. + +### 5.4 Operation catalog + +Coarse lifecycle (~12–25 ops). Names freeze intent; exact set may grow slightly when templates land: + +| Op | Intent | +|----|--------| +| `ensure_product_container` | Team/product labeling ready; no single product Project required | +| `create_ur` | Record intake brief | +| `read_ur` | Load brief (+ ideate if present) | +| `list_urs` | Enumerate URs for prompts/status | +| `append_ideate` | Write ideate onto UR | +| `append_clarifications` | Question phase Q&A | +| `create_req` | Create one REQ in backlog | +| `update_req` | Edit REQ body/fields | +| `read_req` | Load full REQ | +| `list_reqs_for_ur` | All REQs for a UR (any status) | +| `list_claimable_reqs` | Backlog, deps ok, footprint ok, unclaimed — pick order | +| `claim_req` | Optimistic claim + in-progress | +| `heartbeat_req` | Refresh liveness | +| `set_req_status` | stopped / in-progress / etc. | +| `set_blocked_by` | Deps graph | +| `set_files` | Footprint list | +| `archive_req` | Done + closure proof / outputs | +| `unblock_req` | Return to backlog, clear claim | +| `append_decision` | Standing decisions memory | +| `write_verify_report` | Verify output for a UR | +| `write_close_report` | Close output for a UR | +| `append_run_note` | Ledger-ish / cost note for a REQ or run | +| `read_active_milestone` | Milestone cursor | +| `set_active_milestone` | Advance / set milestone | +| `list_milestone_reqs` | REQs for active milestone | +| `write_gate_state` | Deploy-gate coordination (local lock still allowed) | + +Markdown may implement several ops by composing existing scripts. Linear maps each to skill/MCP sequences. + +### 5.5 Work-item vs runtime split + +From storage inventory (~88 ops): **work-item** data moves to Linear in Linear mode; **runtime/git/config** stay local. + +**Must map to Linear:** UR create/read/update; REQ create/edit; status transitions; deps/footprint fields; archive (done + proof + outputs); decisions; close/verify reports; ideate/clarifications; run cost notes; calibration; milestone cursor content. + +**Stay local:** claim stamp equivalent is comments (not files) but **local** still includes worktrees, branches, merges, PRs, `state/events.jsonl`, gate-owner, final-suite locks, feedback.lock, context-pack, retry-counters, config.yml, conformance/install. + +## 6. Linear hierarchy and identifiers + +### 6.1 Hierarchy + +**(Updated 2026-07-31.)** UR home is a **Project Milestone**, not an Initiative — Linear MCP exposes milestone CRUD but not Initiative create/list. + +``` +Team (config) +└── Product Project (tracker.linear.product_project, e.g. do-work) + ├── Project Milestone (UR) — brief, ideate, verify, close + │ └── Issue (path-unit parent) [milestone = UR] + │ └── Sub-issue (layer child) + └── Project Milestone (next UR) + └── Issue … +``` + +### 6.2 Naming + +| Entity | Naming | +|--------|--------| +| Product Project | `tracker.linear.product_project` (default `do-work`) — shared; not one Project per UR | +| UR Milestone (human-facing) | `ur_milestone_name_pattern` (default `{ur_id}: {title}`); body has `**UR-id:** UR-NNN` | +| Issue | Linear identifier only (`ENG-123`). Titles short and actionable; body holds do-work schema | + +### 6.3 List / scope + +| Need | How | +|------|-----| +| `list_reqs_for_ur` | `list_issues` filtered by **product Project** + **UR milestone** | +| `list_claimable_reqs` | Same project filter + status + deps + footprint + unclaimed | +| `status` for a UR | Issues for that milestone + claim comments | +| `read_ur` | UR milestone description (and comments if needed) | +| Product-wide backlog | All issues in product Project (optionally all milestones) | + +### 6.4 Intake create sequence (Linear) + +1. Ensure product Project exists (`product_project`). +2. Allocate next `UR-NNN` slug (scan existing Project Milestones for `**UR-id:**` / name). +3. Create **Project Milestone** (name from pattern; description = §9.1 template with verbatim brief). +4. Capture creates Issues (and sub-issues) on the product Project with `milestone` set to that UR milestone. + +### 6.5 Commits and PRs (Linear mode) + +Commit / PR messages reference the Linear issue id: + +``` +feat(ENG-123): short title + +Issue: ENG-123 +UR: UR-007 +Output: path/to/primary/output +``` + +No `.do-work/archive/REQ-…` path required. Worktree branch naming may use `req/ENG-123` (sanitize for git ref rules). + +## 7. Config schema + +```yaml +tracker: + backend: markdown # markdown | linear + linear: + team_id: "" # required when backend=linear (or resolve via team_key) + team_key: "" # optional alternate resolve + default_assignee_id: "" # human operator; set on issue create when configured + project_name_pattern: "do-work/{ur_id}" + initiative_title_pattern: "{ur_id}: {title}" + status_map: + backlog: "Todo" + in_progress: "In Progress" + stopped: "Canceled" # override if team has a dedicated Stopped state + done: "Done" + labels: + layer_prefix: "Layer/" + path_unit: "path-unit" + size_prefix: "Size/" + agent_claim_marker: "" + heartbeat_max_age_seconds: null # null → use parallel.stale_threshold_seconds + decisions_doc_title: "do-work/decisions" + calibration_doc_title: "do-work/calibration" +``` + +**Validation when `backend: linear`:** hard fail if team cannot be resolved or Linear MCP tools are undiscoverable. Message must tell the operator how to connect Linear (skill setup), not invent data. + +**Interaction with existing keys:** `ledger`, `parallel`, `delivery`, `review`, `layers` remain valid. In Linear mode, **authoritative** run/cost notes are Linear Issue comments via `append_run_note`. If `ledger.enabled: true`, the orchestrator may **also** append local `.do-work/runs/RUN-NNN.yml` for offline retro tooling — that local file is telemetry only, not a second work-item store. Retro prefers Linear run notes when `backend: linear`, falling back to local runs if comments are unavailable. + +## 8. Claim protocol (Linear) + +Human always owns **assignee** (config `default_assignee_id` on create; agents do not steal assignee for claim). + +| Concept | Rule | +|---------|------| +| Unclaimed | Workflow state maps to backlog **and** no active claim comment (or last claim is `released` / unblocked) | +| Claim | Re-read issue; if another agent has active claim and fresh heartbeat → fail; else set state → in_progress; post comment with `agent_claim_marker`, `agent_id`, `claimed_at`, `heartbeat`, optional `session`, `status: active` | +| Heartbeat | New claim-protocol comment (or append) with updated `heartbeat` ISO timestamp; consumers take the latest active claim block | +| Stale | Latest active heartbeat older than `heartbeat_max_age_seconds` or `parallel.stale_threshold_seconds` | +| Unblock | State → backlog; claim comment `status: released` | +| Resume | stopped → in_progress; refresh heartbeat; assignee unchanged | +| Concurrent conflict | Same stopper semantics as markdown multi-agent mode | + +**Atomicity story:** MCP has no filesystem atomic rename. Good enough = re-read + comment protocol + timestamp. Document as intentional. + +### Example claim comment + +```markdown + +agent_id: hostname.pid +claimed_at: 2026-07-31T12:00:00Z +heartbeat: 2026-07-31T12:05:00Z +session: optional-uuid +status: active +``` + +## 9. Templates + +### 9.1 Initiative (UR) + +Machine-stable sections in Initiative description: + +```markdown + +**UR-id:** UR-007 +**Class:** feature +**Created:** YYYY-MM-DD +**Project:** do-work/UR-007 +**Project-id:** {linear-project-uuid} + +## Brief +{verbatim intake} + +## Clarifications + +## Ideate + +## Open gaps + +## Capture summary + +## Verify + +## Closure +``` + +Prefer description appends; fall back to Initiative comments if size limits require it. + +### 9.2 Issue (REQ) + +```markdown + +**UR:** UR-007 +**Layer:** agents | none | … +**Parent:** ENG-100 | none +**Entry point:** … # path-unit parents only +**Terminal state:** … # path-unit parents only +**Files:** path1 path2 +**Depends on:** ENG-101 ENG-102 +**Size:** S|M|L +**Priority:** 1-3 +**Criteria approved:** agent-drafted +**Closure proof:** +**Suite:** + +## Task + +## Acceptance Criteria +- [ ] … + +## Verification Steps +1. … + +## Integration + +## Manual checks (advisory) +- [ ] … + +## Outputs +``` + +**Labels:** `Layer/{name}`, `Size/{S|M|L}`, `path-unit` on parents. +**Estimate:** map Size to team T-shirt when enabled. +**States:** via `status_map`. +**Deps:** create `blocks` relations and mirror ids in `**Depends on:**`. +**Path-units:** parent Issue + sub-issues; children set Linear `parentId` and `**Parent:**`. + +## 10. Non-ticket artifact homes + +| Artifact | Linear home | Format | Writers / readers | +|----------|-------------|--------|-------------------| +| Decisions | Team Doc `do-work/decisions` (create-if-missing) | One line per decision (same as today) | capture write; capture/ideate/question/worker read | +| Run / cost notes | Comment on Issue after attempt; optional Project update for run rollup | YAML fenced block (ledger fields) | run | +| Verify report | Initiative `## Verify` + Initiative comment | Full report markdown | verify, go | +| Close report | Initiative `## Closure` + comment | Per path-unit results | close | +| Calibration | Team Doc `do-work/calibration` | Full calibration body | retro write; capture read | +| Milestone cursor | Project description `` | active M + checklist | capture, run | +| Gate locks | **Local** `state/gate-owner.md`, `final-suite-*.md` | unchanged | run | + +## 11. Milestone mode (Linear) + +- Trigger unchanged (UR shape with `source: /saas-thesis handoff` + `### Milestones`). +- REQs for a milestone are Issues in the UR Project, filterable by milestone marker (Project milestone entity when MCP supports it, else label `M1` / section metadata). +- Active milestone cursor on Project description marker. +- Deploy gate: first orchestrator owns gate via **local** `state/gate-owner.md`; human y/n advances cursor; siblings idle as today. + +## 12. Migration (markdown → Linear) + +One-shot, idle-only: + +1. **Preflight:** no files in `working/`; no active claims; operator confirms. +2. Create/update Team Docs for decisions (and empty calibration if missing). +3. For each UR: create Initiative + Project `do-work/UR-NNN` + link; body from `input.md` / ideate / closure. +4. For each REQ in backlog + archive: create Issue in that Project; map status; relations; parent/sub-issues; preserve checkboxes. In-flight forbidden by preflight. +5. Set `tracker.backend: linear` and resolved team ids in config. +6. Leave `.do-work/user-requests/` and `archive/` as **read-only historical** trees (do not delete); work-item ops stop reading them. +7. No dual-write after cutover. + +Surface via `/do-work upgrade` conformance/migrate path or an explicit migrate step documented in upgrade agent — implementation plan chooses the exact command UX without changing these rules. + +## 13. Agents and libraries in scope + +**Must load port and branch on backend:** +intake, capture, ideate, question, audit, verify, run, run-worker, review, status, close, unblock, resume, start, go, upgrade, retro, log, help (docs pointers). + +**lib/*.sh:** remain markdown-backend implementations. Linear reimplements pick/claim/deps/footprint/heartbeat/archive-integrity **semantics** in `linear.md` via MCP. No requirement for Linear-aware bash in v1. + +**SKILL.md + config.md:** document `tracker.*`, load path, hard-stop rules, commit convention for Linear ids. + +## 14. Error handling + +| Failure | Behavior | +|---------|----------| +| Linear MCP missing / unauthenticated | Hard stop with setup instructions from Linear skill | +| Team id unresolved | Hard stop; do not guess | +| Claim race lost | Stop with concurrent-conflict; `/do-work resume` allowed | +| Relation tool missing | Prefer GraphQL/fallback documented in `linear.md`; if unavailable, description-only deps + one-time warning | +| Template parse failure | Stop REQ; do not invent fields | +| Budget reached | Same boundary as today; costs from Linear run notes | + +## 15. Testing and proof + +1. **Markdown regression:** existing `lib/tests` + conformance pass with `backend: markdown` (default). +2. **Port contract:** checklist that both backend docs implement every op name in `port.md`. +3. **Linear capability spike:** before full Linear CRUD, sandbox-team harness rediscovers tools live (`search_tool`) and commits a capability matrix in `agents/tracker/linear.md` (Initiatives, Projects, InitiativeToProject, `blocks` relations, Team Docs, comments, workflow/`status_map` states). Tool names stay **unknown** until proven; no secrets in repo; no production work-item migration during the spike. +4. **Linear integration:** sandbox team manual/agent harness after matrix fill; no secrets in repo. +5. **Migrate dry-run:** report planned creates without writing when flag set. + +## 16. Implementation phasing (for writing-plans) + +Suggested dependency order (single plan, multi-PR REQs): + +1. Config schema + load path + `port.md` stub ops + `markdown.md` mapping existing behavior +2. **Linear MCP capability spike** — `agents/tracker/linear.md` skeleton + live matrix (rediscover tools; hard-stop copy; `status_map` validation notes) **before** wiring full CRUD +3. Initiative/Issue templates + `linear.md` CRUD for UR/REQ (only after spike cells for hierarchy/relations/Docs are known) +4. Claim/heartbeat/unblock/resume + status +5. Capture/ideate/question/verify against port +6. Run loop pick/claim/deps/footprint/archive on Linear +7. Close, decisions doc, run notes, calibration +8. Milestone mode on Linear +9. Migration one-shot + upgrade wiring +10. Docs (SKILL.md, getting-started, troubleshooting) + +## 17. Open risks + +1. **Linear MCP offline / thin tools** — initiative link, issue relations may need GraphQL; agents must rediscover tools live. Mitigation: capability matrix + hard-stop copy in `agents/tracker/linear.md` (spike path before CRUD). +2. **No custom fields** — all structure is markdown conventions; parse discipline is mandatory. +3. **Optimistic claim** — weaker than FS rename; acceptable with documented conflict/resume. +4. **Linear IDs only** — breaks continuity with markdown `REQ-NNN` history after migrate (by design). +5. **Human assignee + agent claim comments** — humans can still edit Linear UI and break protocol; status/docs should warn “do not clear agent claim comments while run is live.” + +## 18. References + +- `.scratch/do-work-multi-tracker/map.md` and issues 01–10 +- `docs/superpowers/specs/2026-05-21-do-work-parallel-coordination-design.md` +- `agents/config.md`, `SKILL.md` +- Linear skill: MCP-first, rediscover tools live diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000..0aaf277 --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,383 @@ +# Troubleshooting do-work + +Symptom-first fixes for install, `start` / `go` gates, capture, and stuck runs. + +## Quick checks + +1. Skill installed where the agent loads skills (`$AGENTS_SKILLS_HUB/do-work` or `~/.agents/skills/do-work`) and contains `SKILL.md` +2. You are in the **project** directory that should own `.do-work/` +3. `UR-NNN` / `REQ-NNN` ids match folders and filenames under `.do-work/` +4. Read `/do-work status` before force-changing in-flight work +5. For feature work: either declare `layers` in `.do-work/config.yml` or pass `--no-layers` + +--- + +## Install and discovery + +### `/do-work` is not available in the agent + +**Cause:** Skill not installed, or agent not wired to the skills hub. + +**Fix:** + +1. Confirm the directory exists and has `SKILL.md`: + ```bash + ls "${AGENTS_SKILLS_HUB:-$HOME/.agents/skills}/do-work/SKILL.md" + ``` +2. Reinstall: + ```bash + git clone https://github.com/rawphp/do-work.git ~/.agents/skills/do-work + # or from a checkout: + DO_WORK_REPO_URL=https://github.com/rawphp/do-work.git bash install.sh + ``` +3. Point the agent at the same hub path your install used. + +### Install cloned a different GitHub org than expected + +**Cause:** `install.sh` defaults `DO_WORK_REPO_URL` to `https://github.com/agent-native/do-work.git` unless overridden. + +**Fix:** Set the URL explicitly: + +```bash +DO_WORK_REPO_URL=https://github.com/rawphp/do-work.git bash install.sh +``` + +Or clone `rawphp/do-work` directly into the hub (see [Getting started](getting-started.md)). + +### Existing hub directory blocked update + +**Cause:** Non-git directory or leftover symlink at the skill path. + +**Fix:** Re-run `install.sh`. It backs up non-git directories under `$HUB/.backups/` and replaces symlinks when reinstalling from git. For a live dev link, use `bash install.sh --from-cwd` or `--source `. + +--- + +## Start and capture + +### Capture halts because layers are not declared + +**Cause:** Feature-class brief with `layers: []` (or unset equivalent) and no `--no-layers`. Capture expects either declared layers or an explicit opt-out. + +**Fix:** + +1. Set layers in `.do-work/config.yml`, for example: + ```yaml + layers: [frontend, backend] + ``` +2. Or skip for this UR: + ```text + /do-work start "…" --no-layers + /do-work capture UR-NNN --no-layers + ``` + (Pass `--no-layers` on the orchestrator you use; start/go thread it into capture.) + +### Start stopped at ideate gate + +**Cause:** You chose **Stop** after ideate (or the gate halted the orchestrator). + +**Fix:** Edit `.do-work/user-requests/UR-NNN/input.md`, then either: + +```text +/do-work capture UR-NNN +``` + +or run start again only if you intend a **new** UR (intake always creates the next number). Prefer `capture` on the existing UR after revising the brief. + +### Start failed at capture; UR exists with no REQs + +**Cause:** Capture error after intake succeeded. + +**Fix:** Read the error from the agent output, then: + +```text +/do-work capture UR-NNN +``` + +### Feature REQs missing UI or wiring + +**Cause:** Layers undeclared or integration block skipped/incomplete; brief under-specified. + +**Fix:** + +1. Declare layers and re-capture or use verify `--auto-fix` +2. Run `/do-work ideate UR-NNN` / `/do-work question UR-NNN` before capture +3. Run `/do-work verify UR-NNN` and add REQs for gaps +4. Ensure new-surface REQs have a filled `## Integration` section + +### Warning: REQs missing verification steps + +**Cause:** Help/status heuristics found backlog REQs without typed `## Verification Steps`. + +**Fix:** + +```text +/do-work verify UR-NNN --auto-fix +``` + +Or edit each REQ to add test/build/runtime/ui verification steps before `/do-work run`. + +--- + +## Go and verify + +### `go` stops with score below threshold + +**Cause:** Verify confidence < `verify.threshold` (default 90) and neither `--force` nor a successful `--auto-fix` applied. + +**Fix:** + +1. Read the gap list from verify +2. Add or edit REQs manually, or: + ```text + /do-work go UR-NNN --auto-fix + ``` +3. If you accept the risk of incomplete coverage: + ```text + /do-work go UR-NNN --force + ``` +4. Lowering `verify.threshold` in config changes the gate for future runs—prefer fixing coverage when you can + +### `UR-NNN not found` + +**Cause:** Wrong number, or `.do-work` lives in another directory. + +**Fix:** List requests: + +```bash +ls .do-work/user-requests/ +``` + +Confirm `user-requests/UR-NNN/input.md` exists. Re-run go with the correct id. + +### Auto-fix still below threshold + +**Cause:** One `--auto-fix` pass cannot invent missing product intent; score remains under threshold. + +**Fix:** Manual review of gaps; extend the brief or REQs; re-run verify/go. Do not expect multiple silent auto-fix loops—`go` runs auto-fix **once**. + +### Audit changed my REQs + +**Cause:** Expected. Audit inside `go` sharpens vague acceptance criteria before run. + +**Fix:** Read the audit change report. Adjust criteria if the auto-fix misread intent, then continue or re-run verify if you changed scope substantially (audit alone does not re-score). + +--- + +## Run, parallel, delivery + +### REQ stuck in `working/` + +**Cause:** Worker crashed, session killed, or heartbeat went stale. + +**Fix:** + +```text +/do-work status +/do-work unblock REQ-NNN +``` + +Then `/do-work run` or `/do-work go UR-NNN` again as appropriate. Unblock may ask what to do about partial commits—answer deliberately. + +### `status: stopped`, `reason: concurrent-conflict` + +**Cause:** Parallel workers collided on shared files after retries (~110s backoff). + +**Fix:** + +```text +/do-work resume REQ-NNN +``` + +Reduce overlap (narrow `**Files:**` footprints) or run fewer parallel workers. + +### Deadlock banner in status + +**Cause:** Circular wait among in-flight REQs (`Depends on:` chains). + +**Fix:** + +```text +/do-work status +/do-work unblock REQ-NNN # break the cycle on one participant +``` + +Fix dependency declarations in backlog REQs if the graph is wrong. Capture-time cycle check should prevent many bad graphs; runtime deadlocks still need human triage. + +### `missing-creds` stopper (PR delivery) + +**Cause:** `delivery.mode: pr` but `gh` or git remote is missing/misconfigured. do-work does **not** silently fall back to merge. + +**Fix:** Configure remote + authenticated `gh`, or set `delivery.mode: merge` in `.do-work/config.yml` if local merge is intended. + +### Review or evidence gate failed + +**Cause:** Worker finished coding but orchestrator rejected evidence, policy, or post-build review. + +**Fix:** Read the stopper output and the REQ in `working/`. Fix tests/evidence or policy violations; `resume` or `unblock` per status. Do not treat a worker narrative alone as proof of archive. + +### Budget stop + +**Cause:** `/do-work run --budget …` or `cost.budget` reached after finishing the in-flight REQ’s integration. + +**Fix:** Raise or clear the budget and run again; remaining backlog REQs stay eligible. + +### Worktree / dependency issues + +**Cause:** Isolated worktree missing `node_modules`, `vendor`, etc. + +**Fix:** Configure `worktree.link_paths` and/or `worktree.setup_command` in `.do-work/config.yml` (see `agents/config.md`). Ensure the main checkout has dependency dirs the provisioner can symlink. + +--- + +## Linear tracker backend + +Optional work-item store when `tracker.backend: linear` in `.do-work/config.yml`. Markdown remains the default when the key is unset or `markdown`. Deep dive: [How it works → Multi-tracker](HOW-IT-WORKS.md#multi-tracker-work-item-backends). Canonical hard-stop copy: `agents/tracker/linear.md`. + +### HARD STOP: Linear MCP not usable + +**Cause:** `tracker.backend` is `linear` but Linear MCP tools are missing, unauthenticated, or undiscoverable. do-work **does not** fall back to markdown work-item storage. + +**Fix — connect Linear MCP:** + +1. **Preferred (API key):** + - Create a Personal API key in Linear → Settings → Account → Security & access + - Export in the shell that launches the agent (do not paste the key into chat): + ```bash + export LINEAR_API_KEY='lin_api_...' + ``` + - Configure MCP server `linear` at `https://mcp.linear.app/mcp` with `Authorization: Bearer ${LINEAR_API_KEY}` + - Restart the agent / refresh MCP and verify tools (e.g. `search_tool "linear"`) + +2. **OAuth alternative** (if your host supports it): add HTTP MCP server `linear` → `https://mcp.linear.app/mcp`, authenticate in the host MCP UI. If OAuth sticks on "authenticating", use the API key path. + +3. **Grok CLI examples** (host-specific): + ```bash + grok mcp add --transport http linear https://mcp.linear.app/mcp + grok mcp enable linear + grok mcp doctor linear + ``` + +Then re-run the phase. If a claim was already active when MCP died mid-flight, **leave it** — use `/do-work resume` or `/do-work unblock` after MCP recovers. + +### Team unresolved (`team_id` / `team_key`) + +**Cause:** Linear MCP works but config has empty/wrong team. + +**Fix:** Set a real team in `.do-work/config.yml`: + +```yaml +tracker: + backend: linear + linear: + team_id: "" # preferred + team_key: "" # optional alternate resolve +``` + +Do **not** guess a team. Agents hard-stop until one resolves. Full schema: `agents/config.md`. + +### `status_map` state missing on team + +**Cause:** Defaults (`Todo` / `In Progress` / `Canceled` / `Done`) do not match your team’s workflow names. + +**Fix:** Rename the team workflow state to match, **or** override `tracker.linear.status_map.` to an existing state name. Missing states are never invented. + +### Operator cleared agent claim comments mid-run + +**Cause:** Human edited/deleted `` comments (or equivalent `agent_claim_marker`) in the Linear UI while a worker was live. Assignee stays human; agents rely on those comments for claim + heartbeat. + +**Fix:** + +1. Prefer: **do not clear claim comments while a run is live** +2. If already cleared: treat protocol as broken — stop inventing state; wait until agents stop, then `/do-work status` and `/do-work unblock` / re-claim via a fresh run +3. Recover mid-flight MCP stops with `/do-work resume` only when the claim comment is still intact and status is stopped + +### Want Linear but still on markdown history + +**Cause:** Project has local URs/REQs; you want cutover. + +**Fix:** Idle only (`working/` empty, no active claims): + +```text +/do-work upgrade migrate +``` + +Use dry-run first when offered. After cutover: no dual-write; historical markdown trees are read-only. Refuse if already `backend: linear` or if working/ is non-empty. + +### Accidentally set `backend: linear` without Linear + +**Cause:** Config flipped before MCP/team were ready. + +**Fix:** Either connect MCP + set `team_id` (above), or set `tracker.backend: markdown` (or remove the key) to return to the default local store. Do not dual-write. + +### Intake hard-stops looking for Initiatives + +**Cause:** Older skill text required Initiative + per-UR Project. Current hierarchy uses **Project Milestones** for URs (Linear MCP has milestone CRUD, not Initiative create). + +**Fix:** Use skill version with Milestone-as-UR (`agents/tracker/linear.md` § Hierarchy). Ensure the shared product Project resolves: set `tracker.linear.product_project` (name|UUID) **or** leave it empty so resolve uses `project.name` → git-root basename, then `ensure_product_container` create-if-missing + persists UUID. Do **not** expect a universal default Project named `do-work` (that name is only an example for this skill repo). Confirm milestone tools appear in `search_tool "linear milestone"`. + +--- + +## Upgrade and legacy layout + +### Prompt to run `/do-work upgrade` + +**Cause:** Conformance scan found legacy paths, stale config keys, or similar. + +**Fix:** + +```text +/do-work upgrade +``` + +Confirm destructive rows interactively. Prefer an idle project (no mid-flight `run`) before migrating layouts. + +### State still under legacy `do-work/` (non-hidden) + +**Cause:** Older projects used a visible `do-work/` directory; current default is `.do-work/`. + +**Fix:** Follow skill migration / `/do-work upgrade` guidance in `SKILL.md`. Do not hand-move `working/` files while a run is active. + +--- + +## Log and close + +### Log did nothing after `go` + +**Cause:** Stopper hit; or `log.enabled: false`; or `log.platforms` empty. + +**Fix:** Check `.do-work/config.yml`: + +```yaml +log: + enabled: true + platforms: [x, linkedin] +``` + +Run `/do-work log` manually after archive has new REQs. + +### Close not offered / closure gaps + +**Cause:** Close applies when path-unit REQs (entry point + terminal state) exist; gaps mean a path did not reach the expected terminal state in the merged app. + +**Fix:** Run explicitly: + +```text +/do-work close UR-NNN +``` + +Treat gap rows as product issues to fix with new REQs; they do not by themselves block logging. + +--- + +## Still stuck + +1. `/do-work status UR-NNN` +2. Inspect `.do-work/working/`, `.do-work/archive/`, and latest `.do-work/runs/RUN-*.yml` if ledger is enabled +3. Re-read [Concepts](concepts.md) for gate meaning +4. Deep dive: [How it works](HOW-IT-WORKS.md) and `SKILL.md` + +## Related + +- [Getting started](getting-started.md) +- [Commands](commands.md) +- [Concepts](concepts.md) diff --git a/install.sh b/install.sh index 4966008..05b1653 100755 --- a/install.sh +++ b/install.sh @@ -1,52 +1,31 @@ #!/usr/bin/env bash set -euo pipefail -REPO_URL="https://github.com/rawphp/do-work.git" -ENV_NAME="" +# Install do-work into the active skills hub (~/.agents/skills). +# Default: git clone/update. Use --from-cwd / --source for a live symlink. -resolve_do_work_install_target() { - local env_name="${1:-}" - local home_dir="${2:-$HOME}" +REPO_URL="${DO_WORK_REPO_URL:-https://github.com/agent-native/do-work.git}" +HUB="${AGENTS_SKILLS_HUB:-$HOME/.agents/skills}" +SKILL_DIR="$HUB/do-work" +BACKUP_DIR="$HUB/.backups" +SOURCE_DIR="" - case "$env_name" in - claude) - printf '%s|%s|%s\n' "$home_dir/.claude/skills/do-work" "$home_dir/.claude/backups" "Claude Code" - ;; - codex) - printf '%s|%s|%s\n' "$home_dir/.codex/skills/do-work" "$home_dir/.codex/backups" "Codex" - ;; - *) - echo "Error: invalid --env '$env_name'. Valid values: claude, codex" >&2 - return 1 - ;; - esac -} +usage() { + cat <] -choose_env() { - if [ -n "$ENV_NAME" ]; then - return 0 - fi +Default: git clone (or update) into the skills hub: + $SKILL_DIR - if [ ! -t 0 ]; then - echo "Error: --env is required when stdin is non-interactive. Use --env claude or --env codex." >&2 - exit 1 - fi +Options: + --from-cwd Symlink from the current working directory + --source Symlink from the given path + -h, --help Show this help - echo "Install do-work for which environment?" - echo " 1) Claude Code" - echo " 2) Codex" - printf "Choice [1/2]: " - read -r choice - case "$choice" in - 1|claude|Claude|CLAUDE) ENV_NAME="claude" ;; - 2|codex|Codex|CODEX) ENV_NAME="codex" ;; - *) echo "Error: choose 1 for Claude Code or 2 for Codex." >&2; exit 1 ;; - esac +Note: --env is ignored; all agents share the hub. +EOF } -# Move-aside helper: relocates an existing install OUT of ~/.claude/skills/ -# (where the harness scans for SKILL.md) into ~/.claude/backups/ so the -# backup is not re-registered as a duplicate skill. move_aside() { local src="$1" mkdir -p "$BACKUP_DIR" @@ -55,32 +34,13 @@ move_aside() { echo "Backed up previous install to $dest" } -usage() { - cat < [--from-cwd | --source ] - -Default behavior: git clone (or update) from $REPO_URL into the selected skill directory. - -Options: - --env claude Install into ~/.claude/skills/do-work - --env codex Install into ~/.codex/skills/do-work - --from-cwd Symlink from the current working directory instead of cloning - --source Symlink from the given path instead of cloning - -h, --help Show this help -EOF -} - -SOURCE_DIR="" - while [ $# -gt 0 ]; do case "$1" in --env) - if [ $# -lt 2 ]; then - echo "Error: --env requires claude or codex" >&2 - exit 1 - fi - ENV_NAME="$2" - shift 2 + echo "Note: --env is ignored; skills install into the shared hub only." >&2 + shift + [ $# -ge 1 ] && shift || true + continue ;; --from-cwd) SOURCE_DIR="$(pwd)" @@ -106,38 +66,27 @@ while [ $# -gt 0 ]; do esac done -choose_env -TARGET_INFO="$(resolve_do_work_install_target "$ENV_NAME")" -IFS='|' read -r SKILL_DIR BACKUP_DIR ENV_LABEL <&2 exit 1 fi - if [ ! -f "$SOURCE_DIR/SKILL.md" ]; then echo "Warning: $SOURCE_DIR does not contain a SKILL.md — is this the right directory?" >&2 fi - if [ -d "$SKILL_DIR" ] && [ ! -L "$SKILL_DIR" ]; then echo "Existing do-work directory found at $SKILL_DIR (not a symlink). Backing up..." move_aside "$SKILL_DIR" fi - if [ -L "$SKILL_DIR" ]; then rm "$SKILL_DIR" fi - - mkdir -p "$(dirname "$SKILL_DIR")" ln -s "$SOURCE_DIR" "$SKILL_DIR" - - echo "Symlinked $SOURCE_DIR -> $SKILL_DIR" - echo "Done. The /do-work command is now available in $ENV_LABEL." + echo "Symlinked $SOURCE_DIR -> $SKILL_DIR (skills hub)" + echo "Done." exit 0 fi @@ -148,7 +97,6 @@ if [ -d "$SKILL_DIR/.git" ]; then elif [ -L "$SKILL_DIR" ]; then echo "Existing do-work symlink found at $SKILL_DIR. Removing and reinstalling from $REPO_URL..." rm "$SKILL_DIR" - mkdir -p "$(dirname "$SKILL_DIR")" git clone "$REPO_URL" "$SKILL_DIR" echo "Installed to $SKILL_DIR" elif [ -d "$SKILL_DIR" ]; then @@ -158,10 +106,9 @@ elif [ -d "$SKILL_DIR" ]; then echo "Installed to $SKILL_DIR (old version backed up)" else echo "Installing do-work skill..." - mkdir -p "$(dirname "$SKILL_DIR")" git clone "$REPO_URL" "$SKILL_DIR" echo "Installed to $SKILL_DIR" fi echo "" -echo "Done. The /do-work command is now available in $ENV_LABEL." +echo "Done. Skill available to any agent wired to the hub ($HUB)." diff --git a/lib/check-acceptance-evidence.sh b/lib/check-acceptance-evidence.sh index 48f0efe..3690f5c 100755 --- a/lib/check-acceptance-evidence.sh +++ b/lib/check-acceptance-evidence.sh @@ -13,6 +13,11 @@ # ref: ... # # Keys are AC1..ACn, matching acceptance criteria order in the REQ. +# +# For type: ui (or ui: shorthand), ref MUST be a path to an existing image +# under the project's .do-work/user-requests//ui-evidence/ tree +# (worker contract in agents/run-worker.md). Soft ui claims without a +# screenshot path fail the gate (UR-043 / REQ-279). set -u @@ -32,6 +37,144 @@ if [ ! -e "$REPORT_PATH" ]; then exit 1 fi +# Resolve project root: walk up from the REQ path until we find .do-work/ sibling or parent. +resolve_project_root() { + local start dir + start="$(cd "$(dirname "$REQ_PATH")" && pwd)" + dir="$start" + while [ "$dir" != "/" ]; do + if [ -d "$dir/.do-work" ]; then + printf '%s\n' "$dir" + return 0 + fi + # REQ lives inside .do-work/working or .do-work itself + if [ "$(basename "$dir")" = ".do-work" ]; then + printf '%s\n' "$(dirname "$dir")" + return 0 + fi + dir="$(dirname "$dir")" + done + # Fall back: parent of the directory containing the REQ + printf '%s\n' "$(cd "$(dirname "$REQ_PATH")/../.." && pwd 2>/dev/null || pwd)" +} + +PROJECT_ROOT="$(resolve_project_root)" + +is_image_path() { + local p="$1" + case "$p" in + *.png|*.PNG|*.jpg|*.JPG|*.jpeg|*.JPEG|*.webp|*.WEBP|*.gif|*.GIF) return 0 ;; + *) return 1 ;; + esac +} + +# Pure UI ref policy: strip quotes, require image extension, path must +# resolve under {project}/.do-work/user-requests//ui-evidence/, file exists. +# Emits one diagnostic to stderr and returns non-zero on failure. +check_ui_ref() { + local key="$1" + local ref="$2" + local candidate rest prefix + + # Strip surrounding quotes + ref="$(printf '%s' "$ref" | sed -E 's/^["'\'']//; s/["'\'']$//')" + + if [ -z "$ref" ]; then + echo "acceptance evidence ui missing screenshot ref: $key" >&2 + return 1 + fi + + if ! is_image_path "$ref"; then + echo "acceptance evidence ui ref is not an image path: $key ($ref)" >&2 + return 1 + fi + + # Resolve relative paths against project root + if [ "${ref#/}" = "$ref" ]; then + candidate="$PROJECT_ROOT/$ref" + else + candidate="$ref" + fi + + # Path must live under project .do-work/user-requests//ui-evidence/ + # (not a bare ui-evidence substring — rejects /tmp/ui-evidence/ and not-user-requests/ui-evidence/) + prefix="$PROJECT_ROOT/.do-work/user-requests/" + case "$candidate" in + "$prefix"*) + rest="${candidate#"$prefix"}" + # rest must be: /ui-evidence/ + if ! printf '%s' "$rest" | grep -Eq '^[^/]+/ui-evidence/.+'; then + echo "acceptance evidence ui ref must be under .do-work/user-requests/*/ui-evidence/: $key ($ref)" >&2 + return 1 + fi + ;; + *) + echo "acceptance evidence ui ref must be under .do-work/user-requests/*/ui-evidence/: $key ($ref)" >&2 + return 1 + ;; + esac + + if [ ! -f "$candidate" ]; then + echo "acceptance evidence ui screenshot file missing: $key ($ref)" >&2 + return 1 + fi + + return 0 +} + +# Validate every type: ui evidence item in a YAML acceptance block. +# Walker only extracts refs (shorthand - ui: path | long form type: ui + ref:); +# policy lives entirely in check_ui_ref. +validate_ui_evidence_in_block() { + local key="$1" + local block="$2" + local failed=0 + local line type_line ref + + type_line="" + while IFS= read -r line; do + if echo "$line" | grep -Eq '^[[:space:]]*-[[:space:]]*type:[[:space:]]*ui[[:space:]]*$' \ + || echo "$line" | grep -Eq '^[[:space:]]*type:[[:space:]]*ui[[:space:]]*$' \ + || echo "$line" | grep -Eq '^[[:space:]]*-[[:space:]]*ui:'; then + type_line="$line" + # Shorthand "- ui: path" — path on the same line + if echo "$line" | grep -Eq '^[[:space:]]*-[[:space:]]*ui:'; then + ref="$(echo "$line" | sed -E 's/^[[:space:]]*-[[:space:]]*ui:[[:space:]]*//')" + if ! check_ui_ref "$key" "$ref"; then + failed=1 + fi + type_line="" + fi + continue + fi + + if [ -n "$type_line" ]; then + if echo "$line" | grep -Eq '^[[:space:]]*ref:'; then + ref="$(echo "$line" | sed -E 's/^[[:space:]]*ref:[[:space:]]*//')" + if ! check_ui_ref "$key" "$ref"; then + failed=1 + fi + type_line="" + elif echo "$line" | grep -Eq '^[[:space:]]*-[[:space:]]*(type:|test:|command:|file:|runtime_check:|ui:)'; then + # Next evidence item without a ref on the previous ui item + echo "acceptance evidence ui missing screenshot ref: $key" >&2 + failed=1 + type_line="" + fi + fi + done <&2 + failed=1 + fi + + return "$failed" +} + AC_COUNT="$(awk ' /^## Acceptance Criteria/ { in_ac=1; next } /^## / && in_ac { in_ac=0 } @@ -73,6 +216,13 @@ while [ "$i" -le "$AC_COUNT" ]; do FAILED=1 fi + # UI screenshot hard gate (UR-043): only when block contains a ui evidence item + if echo "$block" | grep -Eq 'type:[[:space:]]*ui|^[[:space:]]*-[[:space:]]*ui:'; then + if ! validate_ui_evidence_in_block "$key" "$block"; then + FAILED=1 + fi + fi + i=$((i + 1)) done @@ -81,4 +231,3 @@ if [ "$FAILED" -ne 0 ]; then fi exit 0 - diff --git a/lib/check-archive-integrity.sh b/lib/check-archive-integrity.sh index d257684..e2cdfa2 100755 --- a/lib/check-archive-integrity.sh +++ b/lib/check-archive-integrity.sh @@ -14,7 +14,7 @@ # # Exit 0 when all hold; exit 1 with a diagnostic on the first/each violation. # -# This is the guardrail behind run.md Step 4b / 4-pr.4 and approve.md: the +# This is the guardrail behind run.md Step 4b / 4-pr.4: the # worker is *instructed* (run-worker.md) to set status, write closure proof, and # tick each `- [x]`, but that is prose an LLM follows unreliably. This check is # the persistence-boundary enforcement that makes the bad write impossible. diff --git a/lib/check-deps.sh b/lib/check-deps.sh index 65d1bf0..1ba8512 100755 --- a/lib/check-deps.sh +++ b/lib/check-deps.sh @@ -7,14 +7,13 @@ # yet satisfied. # # Behavior: -# 1. Parses this REQ's `**Depends on:**` field — comma-separated REQ ids, -# may be empty. The field line may be omitted entirely (treated as empty). +# 1. Parses this REQ's `**Depends on:**` field — REQ ids separated by commas +# and/or runs of whitespace (may be empty). The field line may be omitted +# entirely (treated as empty). # 2. Validates each id against `REQ-\d+` or `REQ-M\d+-\d+` (milestone form). # Malformed ids are logged to stderr and NOT included in the missing-list. -# 3. For each valid id, globs `{project}/.do-work/archive/-*.md` OR -# `{project}/.do-work/pending/-*.md`. If no file matches either -# directory, prints the id to stdout (one per line). An absent `pending/` -# directory is treated as "no match" — never an error. +# 3. For each valid id, globs `{project}/.do-work/archive/-*.md`. +# If no file matches, prints the id to stdout (one per line). # 4. Empty `**Depends on:**` → empty stdout. Exits 0 in all non-error cases. # # Notes: @@ -59,8 +58,11 @@ extract_field() { | sed -E "s/^\*\*${field}:\*\*[[:space:]]*//" } -# Split a comma-separated list, trim whitespace, print one item per line. -split_csv() { +# Split a **Depends on:** value on commas AND/OR runs of whitespace. +# Trim each token, drop empties, print one id per line. Delimiter-tolerant so +# both "REQ-144 REQ-145" and "REQ-144, REQ-145" (and mixed) tokenize the same. +# Must agree with pick-req.sh's split_dep_ids. +split_dep_ids() { local s="$1" if [ -z "$s" ]; then return 0 @@ -70,7 +72,8 @@ split_csv() { # expansion so entries containing `*` don't get expanded during the split. set +u set -f - local IFS=',' + # Normalize commas to spaces, then word-split on whitespace runs. + s="${s//,/ }" # shellcheck disable=SC2206 local arr=($s) set +f @@ -106,26 +109,16 @@ is_valid_req_id() { } # Check if a dep id is satisfied — i.e. at least one file matching -*.md -# exists in `.do-work/archive/` OR `.do-work/pending/`. -# An absent `pending/` directory is handled gracefully (treated as no match). +# exists in `.do-work/archive/`. # Returns 0 if satisfied, 1 otherwise. is_satisfied() { local id="$1" shopt -s nullglob 2>/dev/null || true - # Check archive/ first. # shellcheck disable=SC2206 local archive_matches=( "$DOWORK"/archive/"$id"-*.md ) if [ "${#archive_matches[@]}" -gt 0 ]; then return 0 fi - # Check pending/ — skip gracefully if the directory does not exist. - if [ -d "$DOWORK/pending" ]; then - # shellcheck disable=SC2206 - local pending_matches=( "$DOWORK"/pending/"$id"-*.md ) - if [ "${#pending_matches[@]}" -gt 0 ]; then - return 0 - fi - fi return 1 } @@ -146,6 +139,6 @@ while IFS= read -r dep; do if ! is_satisfied "$dep"; then printf '%s\n' "$dep" fi -done < <(split_csv "$DEPS_RAW") +done < <(split_dep_ids "$DEPS_RAW") exit 0 diff --git a/lib/claim-req.sh b/lib/claim-req.sh index c391755..58f1e11 100755 --- a/lib/claim-req.sh +++ b/lib/claim-req.sh @@ -33,6 +33,8 @@ set -u +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + # --- args ------------------------------------------------------------------- if [ "$#" -lt 2 ]; then @@ -124,6 +126,16 @@ fi # `-u +%Y-%m-%dT%H:%M:%SZ`. NOW_ISO="$(date -u +%Y-%m-%dT%H:%M:%SZ)" +# --- resolve session (optional) --------------------------------------------- + +# Correlate this claim with the current do-work session so the extension can +# map REQ → session for its resume flow. The project root is the parent of the +# backlog-root `.do-work/` directory (REQ_PARENT). resolve-session.sh prints +# the session id, or nothing when it cannot be determined without guessing; +# the `**Session:**` line is stamped only when non-empty. +PROJECT_ROOT="$(dirname "$REQ_PARENT")" +SESSION_ID="$(bash "$SCRIPT_DIR/resolve-session.sh" "$PROJECT_ROOT" 2>/dev/null || true)" + # --- move ------------------------------------------------------------------- if [ "$TRACKED_MODE" = "1" ]; then @@ -187,13 +199,18 @@ revert_move() { # Write the stamp block to a temp file (avoids passing multi-line strings to # awk via -v, which BSD awk on macOS rejects). STAMP_FILE="$(mktemp -t claim-req-stamp.XXXXXX)" -cat > "$STAMP_FILE" < -**Claimed by:** $AGENT_ID -**Claimed at:** $NOW_ISO -**Heartbeat:** $NOW_ISO - -EOF +{ + printf '%s\n' '' + printf '%s\n' "**Claimed by:** $AGENT_ID" + printf '%s\n' "**Claimed at:** $NOW_ISO" + printf '%s\n' "**Heartbeat:** $NOW_ISO" + # Session line is omitted entirely when no session could be resolved (older + # do-work versions and marker-less/ambiguous cases) — absence stays valid. + if [ -n "$SESSION_ID" ]; then + printf '%s\n' "**Session:** $SESSION_ID" + fi + printf '%s\n' '' +} > "$STAMP_FILE" # Insert the stamp block immediately under the first `# REQ-` heading. # Algorithm: @@ -269,14 +286,17 @@ if [ "$TRACKED_MODE" = "1" ]; then # (addition). `git mv` already updates the index for both, but we re-add the # destination explicitly in case the awk/sed rewrite happened after git mv # so the index reflects the stamped content. - if ! git add -- "$DEST_PATH" 2>/dev/null; then + # -f: consumer projects track .do-work/ normally, but the skill source repo + # gitignores .do-work/ while force-tracking REQ files — plain git add rejects + # new paths under an ignored parent (e.g. working/ after first claim move). + if ! git add -f -- "$DEST_PATH" 2>/dev/null; then echo "claim-req.sh: git add failed for $DEST_PATH" >&2 revert_move exit 1 fi # Also ensure the source removal is staged (git mv already does this, but # belt-and-braces for the fallback-mv path above). - git add -- "$REQ_PATH" 2>/dev/null || true + git add -f -- "$REQ_PATH" 2>/dev/null || true COMMIT_MSG="chore(${REQ_ID}): claim by ${AGENT_ID}" if ! git commit -q -m "$COMMIT_MSG" -- "$REQ_PATH" "$DEST_PATH" 2>/dev/null; then diff --git a/lib/conformance-scan.sh b/lib/conformance-scan.sh new file mode 100755 index 0000000..e380a65 --- /dev/null +++ b/lib/conformance-scan.sh @@ -0,0 +1,127 @@ +#!/usr/bin/env bash +# conformance-scan.sh — detect project conformance drift without fixing it. +# +# Usage: +# conformance-scan.sh +# +# Prints one line per detected drift row: +# +# +# Exit codes: +# 0 no drift detected +# 1 one or more drift rows detected +# 2 usage error +# +# --------------------------------------------------------------------------- +# migrate-linear (opt-in; NOT a scanner drift row) — REQ-301 / design §12 +# --------------------------------------------------------------------------- +# Markdown→Linear one-shot migration is operator-invoked only via +# `/do-work upgrade migrate` (agents/upgrade.md Step 9 → port op +# migrate_markdown_to_linear in agents/tracker/linear.md). +# +# This scanner intentionally never emits a `migrate-linear` drift line: +# remaining on markdown is the default backend, not non-conformance. +# Do not invent blocking drift for "still on markdown." +# +# After a successful cutover (`tracker.backend: linear`), leftover +# `.do-work/user-requests/`, backlog `REQ-*.md`, and `archive/` trees are +# historical read-only. They are also not drift — work-item ops ignore them +# as the store (Linear is sole truth); runtime/git/config stay local. +# --------------------------------------------------------------------------- + +set -u + +usage() { + echo "Usage: conformance-scan.sh " >&2 +} + +# Curated tombstone list of .do-work/config.yml keys the skill itself has +# removed. Only keys listed here are ever flagged — user-added custom keys +# and sections are never touched, regardless of whether they appear in the +# canonical template. Documentation home / fix contract: the stale-config-key +# row in agents/upgrade.md's conformance manifest. +STALE_CONFIG_KEYS="notifications.on_pending_validation" + +# stale_key_present +# True when the dotted key path is present as a real nested YAML key, matched +# by indentation rather than a bare substring search — e.g. +# "notifications.on_pending_validation" only matches an on_pending_validation: +# line actually nested under a top-level notifications: section, never a +# same-named key elsewhere in the file or a mention inside a comment. +stale_key_present() { + local dotted_key="$1" + local file="$2" + local result + result="$(awk -v key="$dotted_key" ' + BEGIN { + ncomp = split(key, comp, ".") + depth = 0 + } + { + raw = $0 + content = raw + sub(/^[ \t]*/, "", content) + if (content == "" || content ~ /^#/) next + indent = length(raw) - length(content) + + while (depth > 0 && indent <= stack[depth]) depth-- + + target = comp[depth + 1] + if (content ~ ("^" target "[ \t]*:")) { + if (depth == 0 && indent != 0) next + depth++ + stack[depth] = indent + if (depth == ncomp) { found = 1; exit } + } + } + END { print (found ? 1 : 0) } + ' "$file")" + [ "$result" = "1" ] +} + +if [ "$#" -ne 1 ]; then + usage + exit 2 +fi + +PROJECT_ROOT="$1" +if [ ! -d "$PROJECT_ROOT" ]; then + usage + echo "conformance-scan.sh: project root not found: $PROJECT_ROOT" >&2 + exit 2 +fi + +LEGACY_DIR="$PROJECT_ROOT/do-work" +DOT_DIR="$PROJECT_ROOT/.do-work" +PENDING_DIR="$DOT_DIR/pending" +CONFIG_FILE="$DOT_DIR/config.yml" +DRIFT=0 + +if [ -d "$LEGACY_DIR" ] && [ -d "$DOT_DIR" ]; then + echo "dir-conflict blocking both do-work/ and .do-work/ exist" + DRIFT=1 +elif [ -d "$LEGACY_DIR" ] && [ ! -d "$DOT_DIR" ]; then + echo "legacy-dir safe-blocking do-work/ exists and .do-work/ does not" + DRIFT=1 +fi + +if [ -d "$PENDING_DIR" ]; then + REQ_COUNT="$(find "$PENDING_DIR" -maxdepth 1 -type f -name 'REQ-*.md' -print 2>/dev/null | awk 'END { print NR+0 }')" + echo "pending-dir destructive .do-work/pending/ exists ($REQ_COUNT REQ files)" + DRIFT=1 +fi + +if [ -f "$CONFIG_FILE" ]; then + for key in $STALE_CONFIG_KEYS; do + if stale_key_present "$key" "$CONFIG_FILE"; then + echo "stale-config-key destructive $key" + DRIFT=1 + fi + done +fi + +if [ "$DRIFT" -eq 1 ]; then + exit 1 +fi + +exit 0 diff --git a/lib/coverage-rollup.sh b/lib/coverage-rollup.sh index 42e9faf..2d5990a 100755 --- a/lib/coverage-rollup.sh +++ b/lib/coverage-rollup.sh @@ -5,14 +5,7 @@ # coverage-rollup.sh [UR-NNN] # # Prints one line per UR: -# UR-001 intended=3 proven=1 unproven=2 pending=0 unproven_ids=REQ-002,REQ-003 closed=n/a -# -# The `pending=` field counts REQs whose `**Status:**` is `pending-validation` -# (merged-and-parked in `.do-work/pending/`, sign-off outstanding) as their own -# bucket — separate from proven and unproven. A UR with all code merged but a -# human/device check still outstanding therefore reads as pending, not as a -# coverage gap (unproven) and not as a completion (proven). Pending REQs are -# never listed in `unproven_ids`. +# UR-001 intended=3 proven=1 unproven=2 unproven_ids=REQ-002,REQ-003 closed=n/a # # The trailing `closed=` field reports end-to-end UR closure # (per docs/design/ur-closure.md), derived from the UR's path-unit REQs @@ -67,7 +60,7 @@ closure_overall() { TMP_ROWS="$(mktemp -t coverage-rollup.XXXXXX)" trap 'rm -f "$TMP_ROWS"' EXIT -for dir in "$DOWORK" "$DOWORK/working" "$DOWORK/archive" "$DOWORK/pending"; do +for dir in "$DOWORK" "$DOWORK/working" "$DOWORK/archive"; do [ -d "$dir" ] || continue for req in "$dir"/REQ-*.md; do [ -e "$req" ] || continue @@ -108,9 +101,6 @@ $1 == "ROW" { if (pathunit == 1) has_pathunit[ur]=1 if (state == "proven") { proven[ur]++ - } else if (state == "pending") { - # Merged-and-parked: its own bucket, never an unproven coverage gap. - pending[ur]++ } else { unproven[ur]++ if (unproven_ids[ur] == "") unproven_ids[ur]=id @@ -120,7 +110,7 @@ $1 == "ROW" { END { for (i=1; i<=n; i++) { ur=order[i] - printf "%s intended=%d proven=%d unproven=%d pending=%d", ur, intended[ur]+0, proven[ur]+0, unproven[ur]+0, pending[ur]+0 + printf "%s intended=%d proven=%d unproven=%d", ur, intended[ur]+0, proven[ur]+0, unproven[ur]+0 if ((unproven[ur]+0) > 0) printf " unproven_ids=%s", unproven_ids[ur] # End-to-end closure column (additive). See header comment for semantics. if (!(ur in has_pathunit)) { @@ -136,4 +126,3 @@ END { } } ' "$TMP_ROWS" - diff --git a/lib/derive-status.sh b/lib/derive-status.sh index e98b053..2561e40 100755 --- a/lib/derive-status.sh +++ b/lib/derive-status.sh @@ -4,14 +4,15 @@ # Usage: # derive-status.sh [ ...] # -# Prints one line per REQ: " proven", " pending", or -# " unproven". +# Prints one line per REQ: " proven" or " unproven". # A REQ is proven only when it is done/archived and has a non-empty -# `**Closure proof:**` field. A REQ whose `**Status:**` is `pending-validation` -# derives as `pending` — its code is merged and the worktree torn down, but -# human/device sign-off is outstanding, so it is neither proven (no closure -# proof) nor unproven-in-flight (work is delivered). This deliberately does not -# replace writable `**Status:**`, which remains coordination state. +# `**Closure proof:**` field. This deliberately does not replace writable +# `**Status:**`, which remains coordination state. +# +# An orchestrator-stamped `**Suite:** not-run` header downgrades an +# otherwise-proven REQ to unproven — its test/build suite never ran, so +# "proven" would overclaim. Absent or any-other-value `**Suite:**` leaves +# derivation unchanged. Human/device advisory items never affect this. set -u @@ -48,6 +49,7 @@ for req_path in "$@"; do req_id="$(req_id_from_path "$req_path")" status="$(extract_field "Status" "$req_path")" proof="$(extract_field "Closure proof" "$req_path")" + suite="$(extract_field "Suite" "$req_path")" # Archive location is accepted as done even if an older file's Status line # has drifted. Backlog/working files must explicitly say done to be proven. @@ -56,14 +58,9 @@ for req_path in "$@"; do */.do-work/archive/REQ-*.md|.do-work/archive/REQ-*.md) archived=1 ;; esac - if [ "$status" = "pending-validation" ]; then - # Merged-and-parked: delivered, sign-off outstanding. A distinct state, - # governed by the Status field rather than the file's directory. - printf '%s pending\n' "$req_id" - elif { [ "$status" = "done" ] || [ "$archived" = "1" ]; } && [ -n "$proof" ]; then + if { [ "$status" = "done" ] || [ "$archived" = "1" ]; } && [ -n "$proof" ] && [ "$suite" != "not-run" ]; then printf '%s proven\n' "$req_id" else printf '%s unproven\n' "$req_id" fi done - diff --git a/lib/derive-status.test.sh b/lib/derive-status.test.sh deleted file mode 100755 index 4894882..0000000 --- a/lib/derive-status.test.sh +++ /dev/null @@ -1,119 +0,0 @@ -#!/usr/bin/env bash -# Tests for lib/derive-status.sh -# Plain bash (no bats dependency). Compatible with macOS bash 3.2. - -set -u - -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -SCRIPT="$SCRIPT_DIR/derive-status.sh" - -FAILED=0 -CASES=0 -CURRENT_CASE="" - -fail() { - echo "FAIL [$CURRENT_CASE]: $*" >&2 - FAILED=$((FAILED + 1)) -} - -assert_eq() { - local expected="$1" - local actual="$2" - local label="$3" - if [ "$expected" != "$actual" ]; then - fail "$label: expected '$expected', got '$actual'" - fi -} - -setup_fixture() { - TMP="$(mktemp -d -t derive-status-test.XXXXXX)" - mkdir -p "$TMP/.do-work/archive" "$TMP/.do-work/working" "$TMP/.do-work/pending" -} - -teardown_fixture() { - if [ -n "${TMP:-}" ] && [ -d "$TMP" ]; then - rm -rf "$TMP" - fi -} - -write_req() { - local path="$1" - local id="$2" - local status="$3" - local proof="$4" - cat > "$path" < **JUDGMENT:** Jn ...` marker must have a matching `| Jn |` row in a # Judgment Points table in the SAME file. Orphan markers indicate table drift. @@ -145,6 +186,31 @@ while IFS= read -r file; do scan_judgment_markers "$file" done < "$FILES_LIST" +# Path-scoped pass: SKILL.md + agents/ + references/ (runtime invocation fences). +# SKILL.md is the skill entry (ORI-246). Not folded into DEFAULT_ROOTS so +# references/ is not subject to unrelated patterns (e.g. judgment-marker tables +# that live in agents/). Use a temp list (not a pipeline) so HITS increments in +# report() are not lost to a subshell — bash 3.2 pipelines run the right-hand +# side in a subshell. +BARE_LIST="$(mktemp -t doc-lint-bare.XXXXXX)" +trap 'rm -f "$FILES_LIST" "$BARE_LIST"' EXIT +: > "$BARE_LIST" +if [ -f SKILL.md ]; then + printf '%s\n' "SKILL.md" >> "$BARE_LIST" +fi +for root in agents references; do + [ -d "$root" ] || continue + find "$root" -type f -name '*.md' 2>/dev/null | while IFS= read -r f; do + is_excluded "$f" && continue + printf '%s\n' "$f" + done +done | sort -u >> "$BARE_LIST" +while IFS= read -r file; do + [ -z "$file" ] && continue + [ -f "$file" ] || continue + scan_bare_runtime_bash_lib "$file" +done < "$BARE_LIST" + if [ "$HITS" -ne 0 ]; then echo "" echo "doc-lint: $HITS drift hit(s) — fix the doc or extend the lint." >&2 diff --git a/lib/emit-event.sh b/lib/emit-event.sh new file mode 100755 index 0000000..370953e --- /dev/null +++ b/lib/emit-event.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# emit-event.sh — append one telemetry event line to a project's +# .do-work/state/events.jsonl stream. +# +# Usage: +# emit-event.sh [data-json] +# +# Arguments: +# Path to the consumer project root (the dir containing .do-work/). +# Event type string (e.g. session.start, session.end). Not +# validated against any enum here — the extension ignores +# unknown types. +# Session id string. +# [data-json] Optional raw JSON *object* fragment (e.g. '{"marker":"m1"}'). +# Passed through verbatim as the event's "data" field. Omitted +# from the line when absent or empty. The caller is responsible +# for it being valid JSON. +# +# Emits exactly one line of the form (field order mirrors the extension's +# fixtures; JSON key order is not significant to the parser): +# +# {"ts":"","session":"","type":"","data":} +# +# to {project}/.do-work/state/events.jsonl, creating the state/ directory and +# the file defensively. The append is a single O_APPEND write (atomic for a +# line this size), so concurrent emitters never interleave partial lines. +# +# The extension's parser (src/parsers/eventsParser.ts) requires a string `ts`, +# a string `session`, and a known `type`; `data` is an optional object. This +# script's output satisfies that contract. +# +# Exit codes: +# 0 Line appended. +# 1 Usage error (missing required argument) or state-dir creation failure. +# +# Environment overrides (testing only): +# EMIT_EVENT_TS If set, used verbatim as the `ts` value instead of `date`. +# +# Compatible with macOS bash 3.2 + BSD userland. No jq dependency. + +set -u + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +# shellcheck source=json-bash.sh +. "$SCRIPT_DIR/json-bash.sh" + +PROJECT="${1:-}" +TYPE="${2:-}" +SESSION="${3:-}" +DATA="${4:-}" + +if [ -z "$PROJECT" ] || [ -z "$TYPE" ] || [ -z "$SESSION" ]; then + echo "emit-event.sh: usage: emit-event.sh [data-json]" >&2 + exit 1 +fi + +TS="${EMIT_EVENT_TS:-$(date -u +%Y-%m-%dT%H:%M:%SZ)}" +ESC_SESSION="$(json_escape "$SESSION")" +ESC_TYPE="$(json_escape "$TYPE")" + +LINE="{\"ts\":\"$TS\",\"session\":\"$ESC_SESSION\",\"type\":\"$ESC_TYPE\"" +if [ -n "$DATA" ]; then + LINE="$LINE,\"data\":$DATA" +fi +LINE="$LINE}" + +STATE_DIR="$PROJECT/.do-work/state" +if ! mkdir -p "$STATE_DIR" 2>/dev/null; then + echo "emit-event.sh: cannot create $STATE_DIR" >&2 + exit 1 +fi + +printf '%s\n' "$LINE" >> "$STATE_DIR/events.jsonl" diff --git a/lib/install-hooks.sh b/lib/install-hooks.sh new file mode 100755 index 0000000..13712ba --- /dev/null +++ b/lib/install-hooks.sh @@ -0,0 +1,133 @@ +#!/usr/bin/env bash +# install-hooks.sh — idempotently wire do-work's SessionStart / Stop telemetry +# hooks into a consumer project's .claude/settings.json. +# +# Usage: +# install-hooks.sh # install/merge hooks; report result +# install-hooks.sh --check # report presence only (no writes) +# +# The hook commands point at this repo's lib/session-hook.sh (absolute path): +# SessionStart → "/session-hook.sh start" +# Stop → "/session-hook.sh end" +# +# Idempotent: repeated runs yield exactly one entry per hook (dedup by exact +# command string). Existing, unrelated hooks and settings are preserved. The +# hooks themselves are safe no-ops in projects without .do-work/ (see +# session-hook.sh), so adding them to a shared settings.json is harmless. +# +# Output (stdout, one word): +# installed — hooks were added this run. +# already-conformant — both hooks already present; nothing changed. +# present | absent — for --check mode. +# skipped — python3 unavailable; hooks not installed (non-fatal). +# +# Requires python3 for a safe structural JSON merge (matches the precedent in +# provision-worktree.sh). If python3 is missing, prints a warning to stderr and +# exits 0 — telemetry degrades gracefully and never blocks install/upgrade. +# +# Exit codes: +# 0 Success (installed, already-conformant, present/absent, or skipped). +# 1 Usage error, or settings.json exists but is not valid JSON. +# +# Compatible with macOS bash 3.2 + BSD userland. + +set -u + +MODE="install" +if [ "${1:-}" = "--check" ]; then + MODE="check" + shift +fi + +PROJECT="${1:-}" +if [ -z "$PROJECT" ]; then + echo "install-hooks.sh: usage: install-hooks.sh [--check] " >&2 + exit 1 +fi + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +HOOK_SCRIPT="$SCRIPT_DIR/session-hook.sh" + +if ! command -v python3 >/dev/null 2>&1; then + echo "install-hooks.sh: python3 not found — skipping hook install (telemetry degrades gracefully)" >&2 + echo "skipped" + exit 0 +fi + +python3 - "$PROJECT" "$HOOK_SCRIPT" "$MODE" <<'PY' +import json, os, sys + +project, hook_script, mode = sys.argv[1], sys.argv[2], sys.argv[3] + +settings_dir = os.path.join(project, ".claude") +settings_path = os.path.join(settings_dir, "settings.json") + +# Desired (event -> command) mapping. +wanted = { + "SessionStart": hook_script + " start", + "Stop": hook_script + " end", +} + +# Load existing settings (or start fresh). +if os.path.exists(settings_path): + try: + with open(settings_path) as f: + settings = json.load(f) + except (ValueError, OSError) as e: + sys.stderr.write("install-hooks.sh: %s is not valid JSON: %s\n" % (settings_path, e)) + sys.exit(1) + if not isinstance(settings, dict): + sys.stderr.write("install-hooks.sh: %s is not a JSON object\n" % settings_path) + sys.exit(1) +else: + settings = {} + +hooks = settings.get("hooks") +if not isinstance(hooks, dict): + hooks = {} + +def command_present(event, command): + groups = hooks.get(event) + if not isinstance(groups, list): + return False + for group in groups: + if not isinstance(group, dict): + continue + entries = group.get("hooks") + if not isinstance(entries, list): + continue + for entry in entries: + if isinstance(entry, dict) and entry.get("command") == command: + return True + return False + +# --check: report presence without touching anything. +if mode == "check": + both = all(command_present(ev, cmd) for ev, cmd in wanted.items()) + print("present" if both else "absent") + sys.exit(0) + +changed = False +for event, command in wanted.items(): + if command_present(event, command): + continue + groups = hooks.get(event) + if not isinstance(groups, list): + groups = [] + groups.append({"hooks": [{"type": "command", "command": command}]}) + hooks[event] = groups + changed = True + +if not changed: + print("already-conformant") + sys.exit(0) + +settings["hooks"] = hooks +os.makedirs(settings_dir, exist_ok=True) +tmp = settings_path + ".tmp" +with open(tmp, "w") as f: + json.dump(settings, f, indent=2) + f.write("\n") +os.replace(tmp, settings_path) +print("installed") +PY diff --git a/lib/install-target.sh b/lib/install-target.sh index ff64758..08fdf36 100755 --- a/lib/install-target.sh +++ b/lib/install-target.sh @@ -1,27 +1,22 @@ #!/usr/bin/env bash -# install-target.sh — resolve do-work skill install directories. +# install-target.sh — resolve the shared skills-hub install directory for do-work. +# Aligns with install.sh: single target under AGENTS_SKILLS_HUB / ~/.agents/skills. set -u resolve_do_work_install_target() { - local env_name="${1:-}" - local home_dir="${2:-$HOME}" + local home_dir="${1:-$HOME}" + local hub - case "$env_name" in - claude) - printf '%s|%s|%s\n' "$home_dir/.claude/skills/do-work" "$home_dir/.claude/backups" "Claude Code" - ;; - codex) - printf '%s|%s|%s\n' "$home_dir/.codex/skills/do-work" "$home_dir/.codex/backups" "Codex" - ;; - *) - echo "Error: invalid --env '$env_name'. Valid values: claude, codex" >&2 - return 1 - ;; - esac + if [ -n "${AGENTS_SKILLS_HUB:-}" ]; then + hub="$AGENTS_SKILLS_HUB" + else + hub="$home_dir/.agents/skills" + fi + + printf '%s|%s|%s\n' "$hub/do-work" "$hub/.backups" "skills hub" } if [ "${1:-}" = "--resolve" ]; then - resolve_do_work_install_target "${2:-}" "${3:-$HOME}" + resolve_do_work_install_target "${2:-$HOME}" fi - diff --git a/lib/json-bash.sh b/lib/json-bash.sh new file mode 100644 index 0000000..73512fc --- /dev/null +++ b/lib/json-bash.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# json-bash.sh — shared JSON string primitives for do-work hot-path scripts. +# +# Source this file (do not exec): +# . "$(dirname "$0")/json-bash.sh" +# +# Provides: +# json_escape +# Escape a controlled value for embedding in a JSON string. Escapes +# backslash, double-quote, and tab; strips CR/LF so a value can never +# break the one-line-per-event contract. +# +# json_string_field +# Extract a top-level JSON string field's value via sed. Tolerant of +# optional whitespace around the colon. Feeds sed a trailing newline so +# BSD sed never preserves a missing final newline (which would glue +# successive extracts, e.g. sess-Xsess-Y). +# +# Compatible with macOS bash 3.2 + BSD userland. No jq dependency. No python3. + +# Guard against redefining when sourced multiple times in one shell. +if [ -n "${_DO_WORK_JSON_BASH_LOADED:-}" ]; then + return 0 2>/dev/null || true +fi +_DO_WORK_JSON_BASH_LOADED=1 + +json_escape() { + local s="$1" + s="${s//\\/\\\\}" # backslash first + s="${s//\"/\\\"}" # double quote + s="${s//$'\t'/\\t}" # tab + s="${s//$'\r'/}" # strip CR + s="${s//$'\n'/}" # strip LF + printf '%s' "$s" +} + +json_string_field() { + # $1 = JSON text (line or payload); $2 = field name + # NOTE: feed sed a trailing newline. BSD sed preserves a missing final + # newline, which would glue accumulated tokens together (sess-Xsess-Y). + printf '%s\n' "$1" \ + | sed -n "s/.*\"$2\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p" \ + | head -n1 +} diff --git a/lib/pick-req.sh b/lib/pick-req.sh index 1aee4d9..032715c 100755 --- a/lib/pick-req.sh +++ b/lib/pick-req.sh @@ -58,6 +58,7 @@ extract_field() { } # Split a comma-separated list, trim whitespace around each item, print one per line. +# Used for **Files:** only — do not use for **Depends on:** (see split_dep_ids). split_csv() { local s="$1" if [ -z "$s" ]; then @@ -78,6 +79,34 @@ split_csv() { done } +# Split a **Depends on:** value on commas AND/OR runs of whitespace. +# Trim each token, drop empties, print one id per line. Delimiter-tolerant so +# both "REQ-144 REQ-145" and "REQ-144, REQ-145" (and mixed) tokenize the same. +# Does not validate id shape — callers that need validation do so themselves. +split_dep_ids() { + local s="$1" + if [ -z "$s" ]; then + return 0 + fi + # Normalize commas to spaces, then word-split on whitespace runs. + # bash 3.2 + set -u: relax nounset around empty arrays; disable globbing. + set +u + set -f + s="${s//,/ }" + # shellcheck disable=SC2206 + local arr=($s) + set +f + local item + for item in "${arr[@]}"; do + item="${item#"${item%%[![:space:]]*}"}" + item="${item%"${item##*[![:space:]]}"}" + if [ -n "$item" ]; then + printf '%s\n' "$item" + fi + done + set -u +} + # Extract the REQ id from a REQ filename or first-line heading. # Filename convention: REQ-NNN-slug.md or REQ-M-NNN-slug.md # Returns just the REQ id stem we use in **Depends on:** and stderr labels. @@ -286,30 +315,12 @@ while IFS= read -r candidate; do if [ "$found" -eq 0 ] && [ -e "$DOWORK/archive/$dep.md" ]; then found=1 fi - # Also check pending/ — a dep parked in pending-validation is satisfied - # (its code is merged; only human sign-off is outstanding). - # An absent pending/ directory is handled gracefully via nullglob. - if [ "$found" -eq 0 ] && [ -d "$DOWORK/pending" ]; then - # shellcheck disable=SC2206 - pending=( "$DOWORK"/pending/"$dep"-*.md ) - if [ "${#pending[@]}" -gt 0 ]; then - for p in "${pending[@]}"; do - if [ -e "$p" ]; then - found=1 - break - fi - done - fi - if [ "$found" -eq 0 ] && [ -e "$DOWORK/pending/$dep.md" ]; then - found=1 - fi - fi if [ "$found" -eq 0 ]; then printf 'dep:%s\n' "$dep" >&2 dep_blocked=1 break fi - done < <(split_csv "$deps_raw") + done < <(split_dep_ids "$deps_raw") fi if [ "$dep_blocked" -eq 1 ]; then continue diff --git a/lib/provision-worktree.sh b/lib/provision-worktree.sh new file mode 100755 index 0000000..202add3 --- /dev/null +++ b/lib/provision-worktree.sh @@ -0,0 +1,209 @@ +#!/usr/bin/env bash +# lib/provision-worktree.sh +# Provisions a git worktree's gitignored dependency directories. +# +# Symlinks dependency dirs (vendor, node_modules, .venv) from the main +# checkout into the worktree so test tooling can boot without a fresh +# install. Falls back to a configured setup command if no main copy +# exists. +# +# Usage: provision-worktree.sh +# +# Always exits 0 — failure to provision a path is a reported outcome +# (unprovisionable: ), not a fatal error. The script exits +# non-zero ONLY on usage or argument errors. +# +# Compatible with macOS bash 3.2. No external runtime dependencies. + +set -u + +# ----------------------------------------------------------------------- +# Usage guard +# ----------------------------------------------------------------------- +if [ $# -ne 2 ]; then + echo "Usage: $( basename "$0" ) " >&2 + exit 1 +fi + +MAIN_ROOT="$1" +WT_ROOT="$2" + +if [ ! -d "$MAIN_ROOT" ]; then + echo "Error: main checkout root not a directory: $MAIN_ROOT" >&2 + exit 1 +fi + +if [ ! -d "$WT_ROOT" ]; then + echo "Error: worktree root not a directory: $WT_ROOT" >&2 + exit 1 +fi + +# ----------------------------------------------------------------------- +# Config: parse worktree.link_paths and worktree.setup_command from +# {main}/.do-work/config.yml. Missing file or missing keys → empty. +# Minimal line-by-line state-machine reader; no yq/python required. +# ----------------------------------------------------------------------- +CONFIG_FILE="$MAIN_ROOT/.do-work/config.yml" +LINK_PATHS="" # newline-terminated entries, e.g. "vendor\nserver/vendor\n" +SETUP_CMD="" + +_parse_config() { + [ -f "$CONFIG_FILE" ] || return 0 + local in_wt=0 in_lp=0 + + while IFS= read -r line; do + case "$line" in + # ---- top-level worktree: key ----------------------------------- + "worktree:"*) + in_wt=1; in_lp=0 + ;; + # ---- 4-space list items: must precede the " "* catch-all ----- + " - "*) + if [ "$in_wt" -eq 1 ] && [ "$in_lp" -eq 1 ]; then + local item="${line# - }" + # strip trailing whitespace and optional surrounding quotes + item="$(printf '%s' "$item" | sed 's/[[:space:]]*$//')" + item="${item#\"}" + item="${item%\"}" + item="${item#\'}" + item="${item%\'}" + [ -n "$item" ] && LINK_PATHS="${LINK_PATHS}${item} +" + fi + ;; + # ---- 2-space keys ---------------------------------------------- + " link_paths:"*) + [ "$in_wt" -eq 1 ] && in_lp=1 + ;; + " setup_command:"*) + in_lp=0 + if [ "$in_wt" -eq 1 ]; then + local raw="${line#*setup_command:}" + # trim leading whitespace + raw="${raw# }" + # strip surrounding quotes + raw="${raw#\"}" + raw="${raw%\"}" + raw="${raw#\'}" + raw="${raw%\'}" + SETUP_CMD="$raw" + fi + ;; + # ---- any other 2-space-indented line resets link_paths mode ---- + " "*) + [ "$in_wt" -eq 1 ] && in_lp=0 + ;; + # ---- new top-level key: leave worktree block ------------------- + [A-Za-z_-]*) + in_wt=0; in_lp=0 + ;; + esac + done < "$CONFIG_FILE" +} + +# ----------------------------------------------------------------------- +# Target collection with deduplication +# ----------------------------------------------------------------------- +TARGETS="" # newline-terminated; each entry is one relative path + +_add_target() { + local path="$1" + [ -n "$path" ] || return 0 + # Wrap with newlines so we can do an exact-line match. + case " +${TARGETS}" in + *" +${path} +"*) return 0 ;; # already present + esac + TARGETS="${TARGETS}${path} +" +} + +_manifest_to_dep() { + case "$1" in + "composer.json") echo "vendor" ;; + "package.json") echo "node_modules" ;; + "pyproject.toml") echo ".venv" ;; + "requirements.txt") echo ".venv" ;; + *) ;; + esac +} + +# Auto-detect at worktree root (depth 0 → satisfies depth ≤ 2 overall) +for _m in composer.json package.json pyproject.toml requirements.txt; do + if [ -f "$WT_ROOT/$_m" ]; then + _d="$(_manifest_to_dep "$_m")" + [ -n "$_d" ] && _add_target "$_d" + fi +done + +# Auto-detect in immediate subdirectories (depth 1 → total depth ≤ 2) +for _subdir in "$WT_ROOT"/*/; do + [ -d "$_subdir" ] || continue + _sname="$(basename "$_subdir")" + for _m in composer.json package.json pyproject.toml requirements.txt; do + if [ -f "${_subdir}${_m}" ]; then + _d="$(_manifest_to_dep "$_m")" + [ -n "$_d" ] && _add_target "${_sname}/${_d}" + fi + done +done + +# Config-listed paths (additive, deduplicated) +_parse_config +_save_IFS="$IFS" +IFS=' +' +for _p in $LINK_PATHS; do + [ -n "$_p" ] && _add_target "$_p" +done +IFS="$_save_IFS" + +# ----------------------------------------------------------------------- +# Provision each target +# ----------------------------------------------------------------------- +_setup_done=0 + +_provision() { + local rel="$1" + local wt_path="$WT_ROOT/$rel" + local main_path="$MAIN_ROOT/$rel" + + # Already present in the worktree (real dir/file or an existing symlink) → skip + if [ -e "$wt_path" ] || [ -L "$wt_path" ]; then + return 0 + fi + + # Main checkout has it as a directory → symlink + if [ -d "$main_path" ]; then + mkdir -p "$(dirname "$wt_path")" + ln -s "$main_path" "$wt_path" + echo "linked: $rel" + return 0 + fi + + # Setup command fallback — run at most once, then re-check + if [ -n "$SETUP_CMD" ]; then + if [ "$_setup_done" -eq 0 ]; then + ( cd "$WT_ROOT" && eval "$SETUP_CMD" ) 2>/dev/null || true + _setup_done=1 + fi + if [ -e "$wt_path" ] || [ -L "$wt_path" ]; then + echo "ran-setup: $rel" + return 0 + fi + fi + + echo "unprovisionable: $rel" +} + +_save_IFS="$IFS" +IFS=' +' +for _target in $TARGETS; do + [ -n "$_target" ] && _provision "$_target" +done +IFS="$_save_IFS" + +exit 0 diff --git a/lib/resolve-session.sh b/lib/resolve-session.sh new file mode 100755 index 0000000..15647ca --- /dev/null +++ b/lib/resolve-session.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +# resolve-session.sh — resolve the current do-work session id from a project's +# events.jsonl telemetry stream, for stamping into a REQ claim block's +# `**Session:**` field. +# +# Usage: resolve-session.sh +# Directory containing .do-work/ (events are read from +# /.do-work/state/events.jsonl). +# +# Resolution order: +# 1. Marker correlation. When $DO_WORK_UI_MARKER is set and non-empty, print +# the session id of the LATEST `session.start` line whose `data.marker` +# equals it. This is the terminal→session correlation the extension relies +# on (the marker is exported per-terminal and echoed into session.start by +# the SessionStart hook — see lib/session-hook.sh). Preferred when the +# marker matches; unchanged by fallback semantics. +# 2. Fallback: the single un-ended session (last-event semantics). A session +# is un-ended when its *last* event for that session id is `session.start` +# — i.e. it has a start and no later end. A prior `session.end` does not +# permanently retire the id: start→end→start is un-ended again. +# Implementation: one pass over events.jsonl tracking the last +# start|end type per session id; candidates are ids whose last type is +# start. If exactly one candidate, print it. +# 3. Otherwise print NOTHING — no events file, no candidate, or more than one +# un-ended session with no marker match. The claim block then omits the +# `**Session:**` line entirely rather than guessing between candidates. +# +# Always exits 0. Prints at most one session id (nothing else) to stdout. +# Compatible with macOS bash 3.2 + BSD userland. No jq dependency. + +set -u + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +# shellcheck source=json-bash.sh +. "$SCRIPT_DIR/json-bash.sh" + +PROJECT="${1:-}" +[ -n "$PROJECT" ] || exit 0 + +EVENTS="$PROJECT/.do-work/state/events.jsonl" +[ -f "$EVENTS" ] || exit 0 + +MARKER="${DO_WORK_UI_MARKER:-}" + +# --- 1. marker correlation -------------------------------------------------- +if [ -n "$MARKER" ]; then + # Latest session.start line carrying data.marker == MARKER. Both the event + # type and the full `"marker":""` fragment (with trailing quote) are + # matched as FIXED strings, so m1 never false-matches m10. + line="$(grep -F '"type":"session.start"' "$EVENTS" 2>/dev/null \ + | grep -F -- "\"marker\":\"$MARKER\"" | tail -n1)" + if [ -n "$line" ]; then + sid="$(json_string_field "$line" session)" + if [ -n "$sid" ]; then + printf '%s\n' "$sid" + exit 0 + fi + fi +fi + +# --- 2. fallback: single un-ended session (last-event semantics) ------------ +# One pass over events.jsonl: track last session.start|session.end type per id. +# Candidates = sessions whose last event is session.start. +# Exactly one candidate → print; zero or >1 → omit (never guess). +# bash 3.2: no associative arrays — keep "sid:type" tokens in last_map. + +CANDIDATE="" +COUNT=0 +last_map="" + +while IFS= read -r line || [ -n "$line" ]; do + [ -n "$line" ] || continue + case "$line" in + *'"type":"session.start"'*) typ="start" ;; + *'"type":"session.end"'*) typ="end" ;; + *) continue ;; + esac + sid="$(json_string_field "$line" session)" + [ -n "$sid" ] || continue + # Drop any prior entry for this sid so the final token is the last event. + rebuilt="" + for tok in $last_map; do + case "$tok" in + "${sid}:"*) ;; + *) rebuilt="$rebuilt $tok" ;; + esac + done + last_map="$rebuilt ${sid}:$typ" +done < "$EVENTS" + +for tok in $last_map; do + case "$tok" in + *:start) + CANDIDATE="${tok%:*}" + COUNT=$((COUNT + 1)) + ;; + esac +done + +# Exactly one un-ended session → unambiguous. Zero or >1 → omit (never guess). +if [ "$COUNT" = "1" ]; then + printf '%s\n' "$CANDIDATE" +fi +exit 0 diff --git a/lib/session-hook.sh b/lib/session-hook.sh new file mode 100755 index 0000000..6fc4f7f --- /dev/null +++ b/lib/session-hook.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash +# session-hook.sh — Claude Code SessionStart / Stop hook entry point for +# do-work telemetry. Wired into a consumer project's .claude/settings.json by +# `/do-work install` and `/do-work upgrade` (see lib/install-hooks.sh). +# +# Usage (from a hook command in settings.json): +# session-hook.sh start # SessionStart hook → emits session.start +# session-hook.sh end # Stop / session-end hook → emits session.end +# +# Reads the hook payload JSON on stdin (Claude Code provides at least +# `session_id`, usually `cwd`). Behaviour: +# - Resolve the project dir from the stdin `cwd`, falling back to $PWD. +# - If the project has no .do-work/ directory: exit 0 WITHOUT writing anything +# (do-work is not installed here — telemetry is a no-op). +# - Otherwise emit the event via lib/emit-event.sh: +# * start → session.start, including data.marker from $DO_WORK_UI_MARKER +# when that env var is set and non-empty (omitted otherwise). +# * end → session.end (no data). +# +# Always exits 0 — a telemetry hook must never break the user's session. +# +# Compatible with macOS bash 3.2 + BSD userland. No jq dependency. + +set -u + +MODE="${1:-}" +case "$MODE" in + start) EVENT_TYPE="session.start" ;; + end) EVENT_TYPE="session.end" ;; + *) + echo "session-hook.sh: usage: session-hook.sh " >&2 + exit 0 # never fail the session, even on misconfiguration + ;; +esac + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +# shellcheck source=json-bash.sh +. "$SCRIPT_DIR/json-bash.sh" + +# Read the whole hook payload from stdin. Tolerate an empty stdin. +PAYLOAD="$(cat 2>/dev/null || true)" + +# Model id of the LAST assistant message in a JSONL transcript. Prints nothing +# when the path is empty/absent or no assistant message carries a model. No jq. +transcript_model() { + local tpath="$1" line + [ -n "$tpath" ] || return 0 + [ -f "$tpath" ] || return 0 + line="$(grep '"type"[[:space:]]*:[[:space:]]*"assistant"' "$tpath" 2>/dev/null \ + | grep '"model"' | tail -n1)" + [ -n "$line" ] || return 0 + json_string_field "$line" model +} + +# Most recent model recorded for a session in this project's events.jsonl — the +# last line for the session carrying data.model (session.start / model.change). +recorded_model() { + local sess="$1" events="$PROJECT/.do-work/state/events.jsonl" line + [ -f "$events" ] || return 0 + line="$(grep "\"session\":\"$sess\"" "$events" 2>/dev/null | grep '"model"' | tail -n1)" + [ -n "$line" ] || return 0 + json_string_field "$line" model +} + +SESSION="$(json_string_field "$PAYLOAD" session_id)" +CWD="$(json_string_field "$PAYLOAD" cwd)" +TRANSCRIPT="$(json_string_field "$PAYLOAD" transcript_path)" + +PROJECT="${CWD:-$PWD}" +[ -n "$PROJECT" ] || PROJECT="$PWD" + +# No-op when do-work is not installed in the resolved project. +if [ ! -d "$PROJECT/.do-work" ]; then + exit 0 +fi + +# Without a session id there is no meaningful event to emit — no-op. +if [ -z "$SESSION" ]; then + exit 0 +fi + +# Compose the session.start data object. It may carry: +# marker — from $DO_WORK_UI_MARKER (existing REQ-037 behaviour) +# model — the orchestrator model, from stdin `model` (SessionStart provides +# it, but not always) falling back to the last assistant message's +# message.model in the transcript. Omitted when neither yields one. +DATA="" +if [ "$MODE" = "start" ]; then + MODEL="$(json_string_field "$PAYLOAD" model)" + [ -n "$MODEL" ] || MODEL="$(transcript_model "$TRANSCRIPT")" + FIELDS="" + if [ -n "${DO_WORK_UI_MARKER:-}" ]; then + FIELDS="\"marker\":\"$(json_escape "$DO_WORK_UI_MARKER")\"" + fi + if [ -n "$MODEL" ]; then + [ -n "$FIELDS" ] && FIELDS="$FIELDS," + FIELDS="$FIELDS\"model\":\"$(json_escape "$MODEL")\"" + fi + [ -n "$FIELDS" ] && DATA="{$FIELDS}" +fi + +bash "$SCRIPT_DIR/emit-event.sh" "$PROJECT" "$EVENT_TYPE" "$SESSION" "$DATA" || true + +# Stop hook (end mode): in addition to session.end above, emit a model.change +# event when the orchestrator's current model (last assistant message.model in +# the transcript) differs from the last model recorded for this session. Emits +# nothing when the model is unchanged (no per-turn spam) or undeterminable. +if [ "$MODE" = "end" ]; then + CUR_MODEL="$(transcript_model "$TRANSCRIPT")" + if [ -n "$CUR_MODEL" ]; then + PREV_MODEL="$(recorded_model "$SESSION")" + if [ "$CUR_MODEL" != "$PREV_MODEL" ]; then + MC_DATA="{\"model\":\"$(json_escape "$CUR_MODEL")\"}" + bash "$SCRIPT_DIR/emit-event.sh" "$PROJECT" "model.change" "$SESSION" "$MC_DATA" || true + fi + fi +fi +exit 0 diff --git a/lib/stamp-session.sh b/lib/stamp-session.sh new file mode 100755 index 0000000..c56b9cb --- /dev/null +++ b/lib/stamp-session.sh @@ -0,0 +1,180 @@ +#!/usr/bin/env bash +# stamp-session.sh — insert or replace the optional **Session:** field inside a +# working/ REQ slot's claim stamp (filesystem-only, no git commit). +# +# Usage: stamp-session.sh [session-id] +# Path (relative or absolute) to a REQ file living under +# `.../.do-work/working/`. The file must contain a claim stamp +# wrapped in ` ... `. +# [session-id] Optional session id to stamp. If omitted or empty, the script +# calls lib/resolve-session.sh with the project root derived +# from the REQ path. If resolve prints nothing, any existing +# **Session:** line is left untouched and the script exits 0 +# (resume must not clear a known session by guessing). +# +# Behaviour: +# - If does not exist: print to stderr, exit 1. +# - If is not under a `.do-work/working/` directory: warn on +# stderr, exit 1. Session stamp is only meaningful for in-progress slots. +# - If the claim stamp is missing entirely: exit 1. Never creates a claim +# stamp (resume assumes claim exists). +# - If [session-id] is non-empty (or resolve returns non-empty): +# * If `**Session:**` already present inside the stamp: replace its value. +# * If absent: insert `**Session:** ` immediately before +# `` (after Heartbeat / other stamp fields). +# - If resolved/arg session id is empty: leave existing Session line alone +# (or leave absent), exit 0. +# +# No git commands. No staging. No commit. Sibling agents read the stamp +# directly from the filesystem — same contract as lib/heartbeat.sh. +# +# Exit codes: +# 0 Session updated (insert/replace) or intentionally left untouched. +# 1 Any failure: missing file, file outside working/, no claim stamp, +# sed/awk write failure. +# +# Compatible with macOS bash 3.2 + BSD sed/awk. +# Standard POSIX tools only. + +set -u + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +# --- args ------------------------------------------------------------------- + +if [ "$#" -lt 1 ]; then + echo "Usage: stamp-session.sh [session-id]" >&2 + exit 1 +fi + +REQ_PATH="$1" +SESSION_ARG="${2:-}" + +# --- validate file exists --------------------------------------------------- + +if [ ! -e "$REQ_PATH" ]; then + echo "stamp-session.sh: REQ file not found: $REQ_PATH" >&2 + exit 1 +fi + +if [ ! -f "$REQ_PATH" ]; then + echo "stamp-session.sh: REQ path is not a regular file: $REQ_PATH" >&2 + exit 1 +fi + +# --- validate file is under .do-work/working/ ------------------------------- + +# Inspect the parent directory's basename and its parent's basename. We need +# the file to live in `<...>/.do-work/working/`, so: +# parent basename == "working" +# grandparent basename == ".do-work" +REQ_PARENT="$(dirname "$REQ_PATH")" +REQ_PARENT_BASE="$(basename "$REQ_PARENT")" +REQ_GRANDPARENT="$(dirname "$REQ_PARENT")" +REQ_GRANDPARENT_BASE="$(basename "$REQ_GRANDPARENT")" + +if [ "$REQ_PARENT_BASE" != "working" ] || [ "$REQ_GRANDPARENT_BASE" != ".do-work" ]; then + echo "stamp-session.sh: refusing to stamp session — REQ is outside .do-work/working/: $REQ_PATH" >&2 + exit 1 +fi + +# Project root is the parent of `.do-work/` (grandparent of working/). +PROJECT_ROOT="$(dirname "$REQ_GRANDPARENT")" + +# --- locate claim stamp block ---------------------------------------------- + +START_LINE="$(grep -n '^$' "$REQ_PATH" | head -1 | cut -d: -f1)" +END_LINE="$(grep -n '^$' "$REQ_PATH" | head -1 | cut -d: -f1)" + +if [ -z "$START_LINE" ] || [ -z "$END_LINE" ]; then + echo "stamp-session.sh: claim stamp not found in $REQ_PATH" >&2 + exit 1 +fi + +if [ "$START_LINE" -ge "$END_LINE" ]; then + echo "stamp-session.sh: malformed claim stamp (start >= end) in $REQ_PATH" >&2 + exit 1 +fi + +# --- resolve session id ----------------------------------------------------- + +SESSION_ID="$SESSION_ARG" +if [ -z "$SESSION_ID" ]; then + # Prefer resolve-session next to this script; fall back silently if missing. + RESOLVE="$SCRIPT_DIR/resolve-session.sh" + if [ -x "$RESOLVE" ] || [ -f "$RESOLVE" ]; then + SESSION_ID="$(bash "$RESOLVE" "$PROJECT_ROOT" 2>/dev/null || true)" + fi + # Trim trailing newline / whitespace from resolve output. + SESSION_ID="$(printf '%s' "$SESSION_ID" | tr -d '\r\n')" +fi + +# Empty id → leave any existing Session line untouched; do not clear by guessing. +if [ -z "$SESSION_ID" ]; then + exit 0 +fi + +# --- find existing Session line inside the claim block ---------------------- + +SESS_LINE="" +while IFS= read -r hit; do + [ -z "$hit" ] && continue + hit_n="$(printf '%s' "$hit" | cut -d: -f1)" + if [ "$hit_n" -gt "$START_LINE" ] && [ "$hit_n" -lt "$END_LINE" ]; then + SESS_LINE="$hit_n" + break + fi +done < "$TMP_OUT" || { + rm -f "$TMP_OUT" + echo "stamp-session.sh: awk rewrite failed for $REQ_PATH" >&2 + exit 1 +} + +if [ ! -s "$TMP_OUT" ]; then + rm -f "$TMP_OUT" + echo "stamp-session.sh: rewrite produced empty file for $REQ_PATH" >&2 + exit 1 +fi +if ! grep -q '^\*\*Session:\*\*' "$TMP_OUT"; then + rm -f "$TMP_OUT" + echo "stamp-session.sh: rewrite did not produce a Session line in $REQ_PATH" >&2 + exit 1 +fi + +if ! mv "$TMP_OUT" "$REQ_PATH"; then + rm -f "$TMP_OUT" + echo "stamp-session.sh: failed to write updated REQ to $REQ_PATH" >&2 + exit 1 +fi + +exit 0 diff --git a/lib/synth-status.sh b/lib/synth-status.sh index c11bd24..126b111 100755 --- a/lib/synth-status.sh +++ b/lib/synth-status.sh @@ -13,9 +13,9 @@ # Sources: # - `.do-work/REQ-*.md` (Status: backlog) # - `.do-work/working/REQ-*.md` (Status: in-progress / stopped) -# - `.do-work/archive/REQ-*.md` (Status: done; only shown when -# present so the snapshot reflects -# completed work in this run) +# - `.do-work/archive/REQ-*.md` (Status: always rendered as done; +# unscoped runs cap archive rows so +# the situation room stays scannable) # 3. Empty case: a single "no REQs" message in place of the table when there # are zero REQs across all three buckets. # 4. Footer: deadlock warnings from `lib/deadlock-check.sh` if executable; @@ -51,6 +51,9 @@ ARCHIVE_DIR="$DOWORK/archive" CONFIG="$DOWORK/config.yml" DEFAULT_THRESHOLD=300 FOOTPRINT_MAX=60 +# Unscoped archive rows shown in the table (totals still count all archived). +# Scoped runs (`synth-status.sh UR-NNN`) list every matching archive row. +ARCHIVE_CAP=15 # Resolve sibling lib scripts via $0's directory so the script works from any # cwd inside the project. @@ -238,7 +241,8 @@ md_cell() { # Bucket: backlog | working | archive # Status column is derived from the file's own `**Status:**` field so that # `stopped` is rendered correctly when a worker has marked a working slot -# stopped. +# stopped — except archive rows, which always display as `done` (location is +# authoritative; stale in-progress headers in archive used to confuse operators). render_row() { local path="$1" local bucket="$2" @@ -259,8 +263,12 @@ render_row() { return 0 fi - status="$F_STATUS" - [ -z "$status" ] && status="—" + if [ "$bucket" = "archive" ]; then + status="done" + else + status="$F_STATUS" + [ -z "$status" ] && status="—" + fi layer="$F_LAYER" [ -z "$layer" ] && layer="—" @@ -383,8 +391,13 @@ fi printf '**Totals:** backlog=%d, working=%d, archived=%d\n\n' \ "$BACKLOG_N" "$WORKING_N" "$ARCHIVE_N" +# Idle project with history (unscoped only): no live work. Lead with a scan cue. +if [ -z "$UR_FILTER" ] && [ "$BACKLOG_N" -eq 0 ] && [ "$WORKING_N" -eq 0 ] && [ "$ARCHIVE_N" -gt 0 ]; then + printf '_No live work (backlog empty, nothing in-flight). Showing recent completed REQs only._\n\n' +fi + if [ "$TOTAL_N" -eq 0 ]; then - printf '_no REQs found in backlog, working, or archive._\n' + printf '_no REQs found in backlog, working, or archive. Next: `/do-work start "your brief"`._\n' else # Table header. printf '| REQ | UR | Status | Layer | Claimer | Heartbeat-age | Deps-status | Footprint |\n' @@ -403,9 +416,33 @@ else done fi if [ "$ARCHIVE_N" -gt 0 ]; then - for f in "${ARCHIVE_FILES[@]}"; do - render_row "$f" "archive" - done + # Unscoped: cap archive rows (newest REQ ids last in glob → take tail). + # Scoped (UR-NNN): list every matching archive row — the filter already + # narrows the set and operators asked for that UR deliberately. + if [ -n "$UR_FILTER" ]; then + for f in "${ARCHIVE_FILES[@]}"; do + render_row "$f" "archive" + done + else + archive_shown=0 + archive_start=0 + if [ "$ARCHIVE_N" -gt "$ARCHIVE_CAP" ]; then + archive_start=$(( ARCHIVE_N - ARCHIVE_CAP )) + fi + i=0 + for f in "${ARCHIVE_FILES[@]}"; do + if [ "$i" -ge "$archive_start" ]; then + render_row "$f" "archive" + archive_shown=$(( archive_shown + 1 )) + fi + i=$(( i + 1 )) + done + if [ "$ARCHIVE_N" -gt "$ARCHIVE_CAP" ]; then + hidden=$(( ARCHIVE_N - archive_shown )) + printf '\n_… and %d more archived. Scope with `/do-work status UR-NNN`, or inspect `.do-work/archive/`._\n' \ + "$hidden" + fi + fi fi fi diff --git a/lib/tests/check-acceptance-evidence.test.sh b/lib/tests/check-acceptance-evidence.test.sh index f2daecb..6f57499 100755 --- a/lib/tests/check-acceptance-evidence.test.sh +++ b/lib/tests/check-acceptance-evidence.test.sh @@ -132,6 +132,217 @@ assert_eq "1" "$RC" "$CURRENT_CASE rc" case "$STDERR" in *"acceptance evidence missing evidence item: AC1"*) : ;; *) fail "$CURRENT_CASE stderr" ;; esac teardown_fixture +# --- UR-043: type ui requires existing ui-evidence screenshot --- + +CURRENT_CASE="ui-missing-ref" +CASES=$((CASES + 1)) +setup_fixture +# Simulate project layout so resolve_project_root finds TMP as project +mkdir -p "$TMP/.do-work" +cat > "$REPORT" <<'EOF' +acceptance: + AC1: + status: passed + evidence: + - type: ui + AC2: + status: passed + evidence: + - type: file + ref: README.md +EOF +run_script +assert_eq "1" "$RC" "$CURRENT_CASE rc" +case "$STDERR" in *"acceptance evidence ui missing screenshot ref: AC1"*) : ;; *) fail "$CURRENT_CASE stderr: $STDERR" ;; esac +teardown_fixture + +CURRENT_CASE="ui-text-only-ref" +CASES=$((CASES + 1)) +setup_fixture +mkdir -p "$TMP/.do-work" +cat > "$REPORT" <<'EOF' +acceptance: + AC1: + status: passed + evidence: + - type: ui + ref: looked fine in the browser + AC2: + status: passed + evidence: + - type: file + ref: README.md +EOF +run_script +assert_eq "1" "$RC" "$CURRENT_CASE rc" +case "$STDERR" in *"acceptance evidence ui ref is not an image path: AC1"*) : ;; *) fail "$CURRENT_CASE stderr: $STDERR" ;; esac +teardown_fixture + +CURRENT_CASE="ui-missing-file" +CASES=$((CASES + 1)) +setup_fixture +mkdir -p "$TMP/.do-work" +cat > "$REPORT" <<'EOF' +acceptance: + AC1: + status: passed + evidence: + - type: ui + ref: .do-work/user-requests/UR-001/ui-evidence/REQ-001-step-1.png + AC2: + status: passed + evidence: + - type: file + ref: README.md +EOF +run_script +assert_eq "1" "$RC" "$CURRENT_CASE rc" +case "$STDERR" in *"acceptance evidence ui screenshot file missing: AC1"*) : ;; *) fail "$CURRENT_CASE stderr: $STDERR" ;; esac +teardown_fixture + +CURRENT_CASE="ui-valid-screenshot" +CASES=$((CASES + 1)) +setup_fixture +mkdir -p "$TMP/.do-work/user-requests/UR-001/ui-evidence" +# Minimal non-empty PNG-like file (existence check only) +printf 'fake-png' > "$TMP/.do-work/user-requests/UR-001/ui-evidence/REQ-001-step-1.png" +# REQ lives under project so PROJECT_ROOT resolves to TMP +REQ="$TMP/.do-work/REQ-001-test.md" +cat > "$REQ" <<'EOF' +# REQ-001: Test + +## Acceptance Criteria + +- [ ] First criterion +- [ ] Second criterion + +## Verification Steps +EOF +cat > "$REPORT" <<'EOF' +acceptance: + AC1: + status: passed + evidence: + - type: ui + ref: .do-work/user-requests/UR-001/ui-evidence/REQ-001-step-1.png + AC2: + status: passed + evidence: + - type: file + ref: README.md +EOF +run_script +assert_eq "0" "$RC" "$CURRENT_CASE rc (stderr=$STDERR)" +teardown_fixture + +# --- REQ-279: collapse check_ui_ref + tighten path contract --- + +CURRENT_CASE="ui-shorthand-ui-colon" +CASES=$((CASES + 1)) +setup_fixture +mkdir -p "$TMP/.do-work/user-requests/UR-044/ui-evidence" +printf 'fake-png' > "$TMP/.do-work/user-requests/UR-044/ui-evidence/REQ-279-step-1.png" +REQ="$TMP/.do-work/REQ-001-test.md" +cat > "$REQ" <<'EOF' +# REQ-001: Test + +## Acceptance Criteria + +- [ ] First criterion +- [ ] Second criterion + +## Verification Steps +EOF +cat > "$REPORT" <<'EOF' +acceptance: + AC1: + status: passed + evidence: + - ui: .do-work/user-requests/UR-044/ui-evidence/REQ-279-step-1.png + AC2: + status: passed + evidence: + - type: file + ref: README.md +EOF +run_script +assert_eq "0" "$RC" "$CURRENT_CASE rc (stderr=$STDERR)" +teardown_fixture + +CURRENT_CASE="ui-wrong-dir-ui-evidence-substring" +CASES=$((CASES + 1)) +setup_fixture +# Path contains "ui-evidence" but is NOT under .do-work/user-requests/*/ui-evidence/ +mkdir -p "$TMP/.do-work" "$TMP/not-user-requests/ui-evidence" +printf 'fake-png' > "$TMP/not-user-requests/ui-evidence/evil.png" +REQ="$TMP/.do-work/REQ-001-test.md" +cat > "$REQ" <<'EOF' +# REQ-001: Test + +## Acceptance Criteria + +- [ ] First criterion +- [ ] Second criterion + +## Verification Steps +EOF +cat > "$REPORT" <<'EOF' +acceptance: + AC1: + status: passed + evidence: + - type: ui + ref: not-user-requests/ui-evidence/evil.png + AC2: + status: passed + evidence: + - type: file + ref: README.md +EOF +run_script +assert_eq "1" "$RC" "$CURRENT_CASE rc" +case "$STDERR" in *"acceptance evidence ui ref must be under"*|*"ui-evidence"*) : ;; *) fail "$CURRENT_CASE stderr: $STDERR" ;; esac +teardown_fixture + +CURRENT_CASE="ui-absolute-escape-outside-tree" +CASES=$((CASES + 1)) +setup_fixture +mkdir -p "$TMP/.do-work" +ABS_ESCAPE="$(mktemp -d -t ui-evidence-escape.XXXXXX)" +mkdir -p "$ABS_ESCAPE/ui-evidence" +printf 'fake-png' > "$ABS_ESCAPE/ui-evidence/x.png" +ABS_REF="$ABS_ESCAPE/ui-evidence/x.png" +REQ="$TMP/.do-work/REQ-001-test.md" +cat > "$REQ" <<'EOF' +# REQ-001: Test + +## Acceptance Criteria + +- [ ] First criterion +- [ ] Second criterion + +## Verification Steps +EOF +# Inject absolute path (file exists but outside project user-requests tree) +cat > "$REPORT" < rc 0 --- -# Pending-validation/post-merge checklists legitimately carry unchecked bullets; -# the guardrail only governs the Acceptance Criteria section. +# Manual advisory checklists legitimately carry unchecked bullets; the guardrail +# only governs the Acceptance Criteria section. CURRENT_CASE="unchecked-outside-acceptance" CASES=$((CASES + 1)) new_tmp @@ -171,7 +171,7 @@ cat > "$REQ" <<'EOF' - [x] First criterion -## Post-merge validation +## Manual checks (advisory) - [ ] Manual device check deferred EOF diff --git a/lib/check-deps.test.sh b/lib/tests/check-deps-pending.test.sh similarity index 91% rename from lib/check-deps.test.sh rename to lib/tests/check-deps-pending.test.sh index 19aee30..f6c5485 100755 --- a/lib/check-deps.test.sh +++ b/lib/tests/check-deps-pending.test.sh @@ -1,19 +1,20 @@ #!/usr/bin/env bash -# Tests for lib/check-deps.sh — pending-validation satisfaction cases. +# Tests for lib/check-deps.sh — pending-only dependency cases. # Plain bash (no bats dependency). Compatible with macOS bash 3.2. # # Covers: # (a) archive-only dep → satisfied -# (b) pending-only dep → satisfied -# (c) both archive + pending present → satisfied -# (d) absent from both → reported missing +# (b) pending-only dep → reported missing +# (c) both archive + pending present → satisfied via archive +# (d) absent from archive → reported missing # (e) empty Depends on: → empty stdout, exit 0 # (f) absent pending/ directory → no error (graceful degradation) set -u SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -CHECKER="$SCRIPT_DIR/check-deps.sh" +LIB_DIR="$( cd "$SCRIPT_DIR/.." && pwd )" +CHECKER="$LIB_DIR/check-deps.sh" FAILED=0 CASES=0 @@ -126,20 +127,20 @@ assert_eq "" "$CHK_STDOUT" "$CURRENT_CASE stdout empty (archive dep satisfied)" teardown_fixture # ----------------------------------------------------------------------- -# Case (b): pending-only dep → satisfied (empty stdout) +# Case (b): pending-only dep → reported missing # ----------------------------------------------------------------------- -CURRENT_CASE="pending-only-satisfied" +CURRENT_CASE="pending-only-missing" CASES=$((CASES + 1)) setup_fixture write_stub "$TMP/.do-work/pending/REQ-101-parked.md" "REQ-101" write_req "$TMP/.do-work/REQ-201-target.md" "REQ-201" "REQ-101" run_checker ".do-work/REQ-201-target.md" assert_eq "0" "$CHK_RC" "$CURRENT_CASE rc" -assert_eq "" "$CHK_STDOUT" "$CURRENT_CASE stdout empty (pending dep satisfied)" +assert_eq "REQ-101" "$CHK_STDOUT" "$CURRENT_CASE stdout has missing dep" teardown_fixture # ----------------------------------------------------------------------- -# Case (c): dep present in both archive and pending → satisfied +# Case (c): dep present in both archive and pending → satisfied via archive # ----------------------------------------------------------------------- CURRENT_CASE="both-archive-and-pending-satisfied" CASES=$((CASES + 1)) @@ -149,11 +150,11 @@ write_stub "$TMP/.do-work/pending/REQ-102-parked.md" "REQ-102" write_req "$TMP/.do-work/REQ-202-target.md" "REQ-202" "REQ-102" run_checker ".do-work/REQ-202-target.md" assert_eq "0" "$CHK_RC" "$CURRENT_CASE rc" -assert_eq "" "$CHK_STDOUT" "$CURRENT_CASE stdout empty (dep satisfied via both)" +assert_eq "" "$CHK_STDOUT" "$CURRENT_CASE stdout empty (dep satisfied via archive)" teardown_fixture # ----------------------------------------------------------------------- -# Case (d): dep absent from both archive/ and pending/ → printed as missing +# Case (d): dep absent from archive/ → printed as missing # ----------------------------------------------------------------------- CURRENT_CASE="absent-from-both-missing" CASES=$((CASES + 1)) diff --git a/lib/tests/check-deps.test.sh b/lib/tests/check-deps.test.sh index fb1ec15..a0ff085 100755 --- a/lib/tests/check-deps.test.sh +++ b/lib/tests/check-deps.test.sh @@ -234,6 +234,84 @@ assert_eq "0" "$CHK_RC" "$CURRENT_CASE rc" assert_eq "REQ-005" "$CHK_STDOUT" "$CURRENT_CASE REQ-005 still missing (REQ-0050 must not satisfy it)" teardown_fixture +# ---------------------------------------------------------------------- +# Case 10: space-separated valid ids — no malformed warning, empty missing +# when all archived (UR-041 regression) +# ---------------------------------------------------------------------- +CURRENT_CASE="deps-space-separated-all-satisfied" +CASES=$((CASES + 1)) +setup_fixture +write_archived "$TMP/.do-work/archive/REQ-260-a.md" "REQ-260" +write_archived "$TMP/.do-work/archive/REQ-263-b.md" "REQ-263" +write_archived "$TMP/.do-work/archive/REQ-264-c.md" "REQ-264" +write_req "$TMP/.do-work/REQ-261-target.md" "REQ-261" "REQ-260 REQ-263 REQ-264" +run_checker ".do-work/REQ-261-target.md" +assert_eq "0" "$CHK_RC" "$CURRENT_CASE rc" +assert_eq "" "$CHK_STDOUT" "$CURRENT_CASE stdout empty (all satisfied)" +assert_not_contains "malformed" "$CHK_STDERR" "$CURRENT_CASE no malformed warning for space-separated valid ids" +teardown_fixture + +# ---------------------------------------------------------------------- +# Case 11: space-separated — one missing → that id alone on stdout +# ---------------------------------------------------------------------- +CURRENT_CASE="deps-space-separated-one-missing" +CASES=$((CASES + 1)) +setup_fixture +write_archived "$TMP/.do-work/archive/REQ-260-a.md" "REQ-260" +write_archived "$TMP/.do-work/archive/REQ-263-b.md" "REQ-263" +# REQ-264 not archived +write_req "$TMP/.do-work/REQ-261-target.md" "REQ-261" "REQ-260 REQ-263 REQ-264" +run_checker ".do-work/REQ-261-target.md" +assert_eq "0" "$CHK_RC" "$CURRENT_CASE rc" +assert_eq "REQ-264" "$CHK_STDOUT" "$CURRENT_CASE only REQ-264 missing" +assert_not_contains "malformed" "$CHK_STDERR" "$CURRENT_CASE no malformed for valid space-separated ids" +assert_not_contains "REQ-260 REQ-263 REQ-264" "$CHK_STDOUT" "$CURRENT_CASE not one blob on stdout" +teardown_fixture + +# ---------------------------------------------------------------------- +# Case 12: mixed delimiters (comma + whitespace) — same per-id missing-list +# ---------------------------------------------------------------------- +CURRENT_CASE="deps-mixed-delimiters-partial-missing" +CASES=$((CASES + 1)) +setup_fixture +write_archived "$TMP/.do-work/archive/REQ-001-a.md" "REQ-001" +# REQ-002 and REQ-003 not archived +write_req "$TMP/.do-work/REQ-010-target.md" "REQ-010" "REQ-001, REQ-002 REQ-003" +run_checker ".do-work/REQ-010-target.md" +assert_eq "0" "$CHK_RC" "$CURRENT_CASE rc" +assert_contains "REQ-002" "$CHK_STDOUT" "$CURRENT_CASE missing REQ-002" +assert_contains "REQ-003" "$CHK_STDOUT" "$CURRENT_CASE missing REQ-003" +assert_not_contains "REQ-001" "$CHK_STDOUT" "$CURRENT_CASE omits satisfied REQ-001" +assert_not_contains "malformed" "$CHK_STDERR" "$CURRENT_CASE no malformed for mixed valid ids" +teardown_fixture + +# ---------------------------------------------------------------------- +# Case 13: malformed tokens still rejected per delimiter style +# ---------------------------------------------------------------------- +CURRENT_CASE="malformed-space-separated" +CASES=$((CASES + 1)) +setup_fixture +write_archived "$TMP/.do-work/archive/REQ-005-a.md" "REQ-005" +write_req "$TMP/.do-work/REQ-070-target.md" "REQ-070" "REQ-005 REQ- foo" +run_checker ".do-work/REQ-070-target.md" +assert_eq "0" "$CHK_RC" "$CURRENT_CASE rc" +assert_eq "" "$CHK_STDOUT" "$CURRENT_CASE stdout empty (no missing valid deps)" +assert_contains "malformed" "$CHK_STDERR" "$CURRENT_CASE stderr flags malformed" +assert_contains "REQ-" "$CHK_STDERR" "$CURRENT_CASE stderr mentions REQ-" +assert_contains "foo" "$CHK_STDERR" "$CURRENT_CASE stderr mentions foo" +teardown_fixture + +CURRENT_CASE="malformed-mixed-delimiters" +CASES=$((CASES + 1)) +setup_fixture +write_archived "$TMP/.do-work/archive/REQ-005-a.md" "REQ-005" +write_req "$TMP/.do-work/REQ-071-target.md" "REQ-071" "REQ-005, REQ- foo" +run_checker ".do-work/REQ-071-target.md" +assert_eq "0" "$CHK_RC" "$CURRENT_CASE rc" +assert_eq "" "$CHK_STDOUT" "$CURRENT_CASE stdout empty" +assert_contains "malformed" "$CHK_STDERR" "$CURRENT_CASE stderr flags malformed" +teardown_fixture + # ---------------------------------------------------------------------- # Summary # ---------------------------------------------------------------------- diff --git a/lib/tests/claim-req.test.sh b/lib/tests/claim-req.test.sh index f1f78ee..7759960 100755 --- a/lib/tests/claim-req.test.sh +++ b/lib/tests/claim-req.test.sh @@ -281,6 +281,61 @@ assert_eq "untracked" "$PICK_STDOUT" "$CURRENT_CASE stdout is 'untracked'" assert_contains "Claim recorded" "$PICK_STDERR" "$CURRENT_CASE stderr explains untracked mode" teardown_fixture +# ---------------------------------------------------------------------- +# Case 7: session correlation — marker match stamps **Session:** into the block +# ---------------------------------------------------------------------- +CURRENT_CASE="session-stamped-on-marker-match" +CASES=$((CASES + 1)) +setup_tracked_fixture +write_req "$TMP/.do-work/REQ-007-foo.md" "REQ-007" +( + cd "$TMP" + git add .do-work/REQ-007-foo.md + git commit -q -m "add REQ-007" +) +# Seed a session.start carrying the terminal marker "mk7". +mkdir -p "$TMP/.do-work/state" +EMIT_EVENT_TS="2026-07-11T00:00:07Z" \ + bash "$LIB_DIR/emit-event.sh" "$TMP" session.start "sess-claim-7" '{"marker":"mk7"}' >/dev/null +# Claim with the matching marker exported (as the run orchestrator would). +claim_err="$TMP/.stderr.$$" +( cd "$TMP" && DO_WORK_UI_MARKER="mk7" "$CLAIMER" ".do-work/REQ-007-foo.md" "test-agent.7" >/dev/null 2>"$claim_err" ) +PICK_RC=$? +rm -f "$claim_err" +assert_eq "0" "$PICK_RC" "$CURRENT_CASE rc" +moved_content="$(cat "$TMP/.do-work/working/REQ-007-foo.md")" +assert_contains "**Session:** sess-claim-7" "$moved_content" "$CURRENT_CASE Session line stamped" +# The Session line must sit INSIDE the claim fences. +between="$(awk '//{f=1} f{print} //{f=0}' "$TMP/.do-work/working/REQ-007-foo.md")" +assert_contains "**Session:** sess-claim-7" "$between" "$CURRENT_CASE Session inside claim fences" +teardown_fixture + +# ---------------------------------------------------------------------- +# Case 8: no events.jsonl (pre-REQ-037 project) — Session omitted, claim succeeds +# ---------------------------------------------------------------------- +CURRENT_CASE="session-omitted-without-events" +CASES=$((CASES + 1)) +setup_tracked_fixture +write_req "$TMP/.do-work/REQ-008-foo.md" "REQ-008" +( + cd "$TMP" + git add .do-work/REQ-008-foo.md + git commit -q -m "add REQ-008" +) +# No events.jsonl exists in this fixture. +run_claim ".do-work/REQ-008-foo.md" "test-agent.8" +assert_eq "0" "$PICK_RC" "$CURRENT_CASE rc (claim still succeeds)" +assert_file_exists "$TMP/.do-work/working/REQ-008-foo.md" "$CURRENT_CASE moved into working/" +moved_content="$(cat "$TMP/.do-work/working/REQ-008-foo.md")" +case "$moved_content" in + *"**Session:**"*) fail "$CURRENT_CASE: Session line must be omitted when no session resolvable" ;; + *) : ;; +esac +# Sanity: the rest of the stamp is intact. +assert_contains "**Claimed by:** test-agent.8" "$moved_content" "$CURRENT_CASE Claimed by intact" +assert_contains "**Heartbeat:**" "$moved_content" "$CURRENT_CASE Heartbeat intact" +teardown_fixture + # ---------------------------------------------------------------------- # Summary # ---------------------------------------------------------------------- diff --git a/lib/tests/conformance-scan.test.sh b/lib/tests/conformance-scan.test.sh new file mode 100755 index 0000000..f482580 --- /dev/null +++ b/lib/tests/conformance-scan.test.sh @@ -0,0 +1,244 @@ +#!/usr/bin/env bash +# Tests for lib/conformance-scan.sh +# Plain bash (no bats dependency). Compatible with macOS bash 3.2. + +set -u + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +LIB_DIR="$( cd "$SCRIPT_DIR/.." && pwd )" +SCRIPT="$LIB_DIR/conformance-scan.sh" + +FAILED=0 +CASES=0 +CURRENT_CASE="" +TMP="" + +fail() { + echo "FAIL [$CURRENT_CASE]: $*" >&2 + FAILED=$((FAILED + 1)) +} + +assert_eq() { + local expected="$1" + local actual="$2" + local label="$3" + if [ "$expected" != "$actual" ]; then + fail "$label: expected '$expected', got '$actual'" + fi +} + +assert_contains() { + local needle="$1" + local haystack="$2" + local label="$3" + case "$haystack" in + *"$needle"*) : ;; + *) fail "$label: expected substring '$needle' in '$haystack'" ;; + esac +} + +setup_fixture() { + TMP="$(mktemp -d -t conformance-scan-test.XXXXXX)" +} + +teardown_fixture() { + if [ -n "${TMP:-}" ] && [ -d "$TMP" ]; then + rm -rf "$TMP" + fi +} + +run_scan() { + local project_root="$1" + local out_file="$TMP/.stdout.$$" + local err_file="$TMP/.stderr.$$" + bash "$SCRIPT" "$project_root" > "$out_file" 2> "$err_file" + SCAN_RC=$? + SCAN_STDOUT="$(cat "$out_file" 2>/dev/null || true)" + SCAN_STDERR="$(cat "$err_file" 2>/dev/null || true)" + rm -f "$out_file" "$err_file" +} + +CURRENT_CASE="conformant-tree" +CASES=$((CASES + 1)) +setup_fixture +mkdir -p "$TMP/project/.do-work/archive" +run_scan "$TMP/project" +assert_eq "0" "$SCAN_RC" "$CURRENT_CASE rc" +assert_eq "" "$SCAN_STDOUT" "$CURRENT_CASE stdout empty" +assert_eq "" "$SCAN_STDERR" "$CURRENT_CASE stderr empty" +teardown_fixture + +CURRENT_CASE="legacy-dir" +CASES=$((CASES + 1)) +setup_fixture +mkdir -p "$TMP/project/do-work" +run_scan "$TMP/project" +assert_eq "1" "$SCAN_RC" "$CURRENT_CASE rc" +assert_eq "legacy-dir safe-blocking do-work/ exists and .do-work/ does not" "$SCAN_STDOUT" "$CURRENT_CASE stdout" +assert_eq "" "$SCAN_STDERR" "$CURRENT_CASE stderr empty" +teardown_fixture + +CURRENT_CASE="dir-conflict" +CASES=$((CASES + 1)) +setup_fixture +mkdir -p "$TMP/project/do-work" "$TMP/project/.do-work" +run_scan "$TMP/project" +assert_eq "1" "$SCAN_RC" "$CURRENT_CASE rc" +assert_eq "dir-conflict blocking both do-work/ and .do-work/ exist" "$SCAN_STDOUT" "$CURRENT_CASE stdout" +assert_eq "" "$SCAN_STDERR" "$CURRENT_CASE stderr empty" +teardown_fixture + +CURRENT_CASE="pending-dir-empty" +CASES=$((CASES + 1)) +setup_fixture +mkdir -p "$TMP/project/.do-work/pending" +run_scan "$TMP/project" +assert_eq "1" "$SCAN_RC" "$CURRENT_CASE rc" +assert_eq "pending-dir destructive .do-work/pending/ exists (0 REQ files)" "$SCAN_STDOUT" "$CURRENT_CASE stdout" +assert_eq "" "$SCAN_STDERR" "$CURRENT_CASE stderr empty" +teardown_fixture + +CURRENT_CASE="pending-dir-non-empty" +CASES=$((CASES + 1)) +setup_fixture +mkdir -p "$TMP/project/.do-work/pending" +touch "$TMP/project/.do-work/pending/REQ-001-one.md" +touch "$TMP/project/.do-work/pending/REQ-002-two.md" +touch "$TMP/project/.do-work/pending/notes.txt" +run_scan "$TMP/project" +assert_eq "1" "$SCAN_RC" "$CURRENT_CASE rc" +assert_eq "pending-dir destructive .do-work/pending/ exists (2 REQ files)" "$SCAN_STDOUT" "$CURRENT_CASE stdout" +assert_eq "" "$SCAN_STDERR" "$CURRENT_CASE stderr empty" +teardown_fixture + +CURRENT_CASE="pending-dir-ignores-matching-directories" +CASES=$((CASES + 1)) +setup_fixture +mkdir -p "$TMP/project/.do-work/pending/REQ-999-dir.md" +run_scan "$TMP/project" +assert_eq "1" "$SCAN_RC" "$CURRENT_CASE rc" +assert_eq "pending-dir destructive .do-work/pending/ exists (0 REQ files)" "$SCAN_STDOUT" "$CURRENT_CASE stdout" +assert_eq "" "$SCAN_STDERR" "$CURRENT_CASE stderr empty" +teardown_fixture + +CURRENT_CASE="stale-config-key-present" +CASES=$((CASES + 1)) +setup_fixture +mkdir -p "$TMP/project/.do-work" +cat > "$TMP/project/.do-work/config.yml" <<'EOF' +notifications: + on_pending_validation: "" +EOF +run_scan "$TMP/project" +assert_eq "1" "$SCAN_RC" "$CURRENT_CASE rc" +assert_eq "stale-config-key destructive notifications.on_pending_validation" "$SCAN_STDOUT" "$CURRENT_CASE stdout" +assert_eq "" "$SCAN_STDERR" "$CURRENT_CASE stderr empty" +teardown_fixture + +CURRENT_CASE="stale-config-key-clean" +CASES=$((CASES + 1)) +setup_fixture +mkdir -p "$TMP/project/.do-work" +cat > "$TMP/project/.do-work/config.yml" <<'EOF' +worktree: + link_paths: [] + setup_command: "" + +routing: [] +EOF +run_scan "$TMP/project" +assert_eq "0" "$SCAN_RC" "$CURRENT_CASE rc" +assert_eq "" "$SCAN_STDOUT" "$CURRENT_CASE stdout empty" +assert_eq "" "$SCAN_STDERR" "$CURRENT_CASE stderr empty" +teardown_fixture + +CURRENT_CASE="stale-config-key-user-custom" +CASES=$((CASES + 1)) +setup_fixture +mkdir -p "$TMP/project/.do-work" +cat > "$TMP/project/.do-work/config.yml" <<'EOF' +notifications: + on_custom_hook: "echo hi" + +my_custom_section: + my_custom_key: true +EOF +run_scan "$TMP/project" +assert_eq "0" "$SCAN_RC" "$CURRENT_CASE rc" +assert_eq "" "$SCAN_STDOUT" "$CURRENT_CASE stdout empty" +assert_eq "" "$SCAN_STDERR" "$CURRENT_CASE stderr empty" +teardown_fixture + +CURRENT_CASE="stale-config-key-comment-only" +CASES=$((CASES + 1)) +setup_fixture +mkdir -p "$TMP/project/.do-work" +cat > "$TMP/project/.do-work/config.yml" <<'EOF' +# notifications.on_pending_validation was removed in UR-039; do not re-add it. +notifications: + on_new_hook: "" +EOF +run_scan "$TMP/project" +assert_eq "0" "$SCAN_RC" "$CURRENT_CASE rc" +assert_eq "" "$SCAN_STDOUT" "$CURRENT_CASE stdout empty" +assert_eq "" "$SCAN_STDERR" "$CURRENT_CASE stderr empty" +teardown_fixture + +CURRENT_CASE="usage-missing-arg" +CASES=$((CASES + 1)) +setup_fixture +out_file="$TMP/.stdout.$$" +err_file="$TMP/.stderr.$$" +bash "$SCRIPT" > "$out_file" 2> "$err_file" +SCAN_RC=$? +SCAN_STDOUT="$(cat "$out_file" 2>/dev/null || true)" +SCAN_STDERR="$(cat "$err_file" 2>/dev/null || true)" +rm -f "$out_file" "$err_file" +assert_eq "2" "$SCAN_RC" "$CURRENT_CASE rc" +assert_eq "" "$SCAN_STDOUT" "$CURRENT_CASE stdout empty" +assert_contains "Usage: conformance-scan.sh " "$SCAN_STDERR" "$CURRENT_CASE stderr usage" +teardown_fixture + +CURRENT_CASE="usage-invalid-root" +CASES=$((CASES + 1)) +setup_fixture +run_scan "$TMP/missing" +assert_eq "2" "$SCAN_RC" "$CURRENT_CASE rc" +assert_eq "" "$SCAN_STDOUT" "$CURRENT_CASE stdout empty" +assert_contains "Usage: conformance-scan.sh " "$SCAN_STDERR" "$CURRENT_CASE stderr usage" +teardown_fixture + +# REQ-301: migrate-linear is upgrade opt-in only — never a scanner drift row. +# Source documents the contract; runtime never emits migrate-linear. +CURRENT_CASE="migrate-linear-not-a-scanner-row" +CASES=$((CASES + 1)) +setup_fixture +SCAN_SRC="$(cat "$SCRIPT")" +assert_contains "migrate-linear" "$SCAN_SRC" "$CURRENT_CASE source documents migrate-linear" +assert_contains "/do-work upgrade migrate" "$SCAN_SRC" "$CURRENT_CASE source documents upgrade migrate surface" +mkdir -p "$TMP/project/.do-work/user-requests/UR-001" \ + "$TMP/project/.do-work/archive" \ + "$TMP/project/.do-work/working" +cat > "$TMP/project/.do-work/config.yml" <<'EOF' +tracker: + backend: linear + linear: + team_id: "team_example" +EOF +echo "historical brief" > "$TMP/project/.do-work/user-requests/UR-001/input.md" +echo "# REQ-001 historical" > "$TMP/project/.do-work/archive/REQ-001-done.md" +run_scan "$TMP/project" +assert_eq "0" "$SCAN_RC" "$CURRENT_CASE historical trees + linear backend still conformant" +assert_eq "" "$SCAN_STDOUT" "$CURRENT_CASE stdout empty (no migrate-linear drift)" +case "$SCAN_STDOUT" in + *migrate-linear*) fail "$CURRENT_CASE must never emit migrate-linear drift line" ;; +esac +assert_eq "" "$SCAN_STDERR" "$CURRENT_CASE stderr empty" +teardown_fixture + +echo "" +echo "conformance-scan tests: $CASES cases, $FAILED failure(s)" +if [ "$FAILED" -ne 0 ]; then + exit 1 +fi +exit 0 diff --git a/lib/coverage-rollup.test.sh b/lib/tests/coverage-rollup.test.sh similarity index 63% rename from lib/coverage-rollup.test.sh rename to lib/tests/coverage-rollup.test.sh index 39be565..3a127eb 100755 --- a/lib/coverage-rollup.test.sh +++ b/lib/tests/coverage-rollup.test.sh @@ -5,7 +5,8 @@ set -u SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -SCRIPT="$SCRIPT_DIR/coverage-rollup.sh" +LIB_DIR="$( cd "$SCRIPT_DIR/.." && pwd )" +SCRIPT="$LIB_DIR/coverage-rollup.sh" FAILED=0 CASES=0 @@ -26,6 +27,15 @@ assert_contains() { esac } +assert_not_contains() { + local needle="$1" + local haystack="$2" + local label="$3" + case "$haystack" in + *"$needle"*) fail "$label: unexpected substring '$needle' in '$haystack'" ;; + esac +} + setup_fixture() { TMP="$(mktemp -d -t coverage-rollup-test.XXXXXX)" mkdir -p "$TMP/.do-work/archive" "$TMP/.do-work/working" "$TMP/.do-work/pending" @@ -57,6 +67,31 @@ write_req() { EOF } +# write_req_with_suite adds a `**Suite:**` header line (REQ-263: guards +# against a future consumer duplicating derive-status.sh's marker logic +# instead of delegating to it — see decisions.md 2026-06-12 REQ-239/240). +write_req_with_suite() { + local path="$1" + local id="$2" + local ur="$3" + local status="$4" + local proof="$5" + local suite="$6" + local layer="${7:-agents}" + cat > "$path" < [closed-count] [gaps-count] # Writes a minimal UR-NNN/closure.md with the front-matter fields the rollup reads. write_closure() { @@ -99,7 +134,8 @@ write_req "$TMP/.do-work/archive/REQ-001-a.md" "REQ-001" "UR-001" "done" "checkp write_req "$TMP/.do-work/REQ-002-b.md" "REQ-002" "UR-001" "backlog" "" write_req "$TMP/.do-work/working/REQ-003-c.md" "REQ-003" "UR-001" "in-progress" "checkpoint:RUN-003 commit:def" run_script -assert_contains "UR-001 intended=3 proven=1 unproven=2 pending=0" "$OUT" "$CURRENT_CASE counts" +assert_contains "UR-001 intended=3 proven=1 unproven=2" "$OUT" "$CURRENT_CASE counts" +assert_not_contains "pending=" "$OUT" "$CURRENT_CASE no pending field" assert_contains "unproven_ids=REQ-002,REQ-003" "$OUT" "$CURRENT_CASE ids" # Additive: a UR with no path-unit REQs and no closure.md reports closed=n/a, # and the existing fields are unchanged. @@ -157,33 +193,45 @@ run_script "UR-040" assert_contains "closed=n/a" "$OUT" "$CURRENT_CASE closure" teardown_fixture -# --- Pending bucket (REQ-233): merged-but-unsigned-off REQs --- +# --- Legacy pending-validation data (UR-039): no derived pending bucket --- -# A UR with one merged-and-proven REQ plus one parked pending-validation REQ -# reads as pending=1 — the pending REQ is its own bucket, NOT counted as an -# unproven coverage gap. A UR with all code merged but sign-off outstanding -# therefore reads as pending, not as a gap or a completion. -CURRENT_CASE="pending-bucket" +# A legacy Status value in an active scanned directory falls through to +# unproven. The rollup has no pending bucket or output field. +CURRENT_CASE="legacy-pending-validation-is-unproven" CASES=$((CASES + 1)) setup_fixture write_req "$TMP/.do-work/archive/REQ-050-a.md" "REQ-050" "UR-050" "done" "checkpoint:RUN-050 commit:abc" -write_req "$TMP/.do-work/pending/REQ-051-park.md" "REQ-051" "UR-050" "pending-validation" "" +write_req "$TMP/.do-work/working/REQ-051-legacy.md" "REQ-051" "UR-050" "pending-validation" "" run_script "UR-050" -assert_contains "UR-050 intended=2 proven=1 unproven=0 pending=1" "$OUT" "$CURRENT_CASE counts" -# The parked REQ is NOT listed as an unproven gap. -case "$OUT" in - *unproven_ids=*) fail "$CURRENT_CASE: pending REQ wrongly listed in unproven_ids ($OUT)" ;; -esac +assert_contains "UR-050 intended=2 proven=1 unproven=1" "$OUT" "$CURRENT_CASE counts" +assert_contains "unproven_ids=REQ-051" "$OUT" "$CURRENT_CASE ids" +assert_not_contains "pending=" "$OUT" "$CURRENT_CASE no pending field" teardown_fixture -# A UR whose every REQ is pending-validation reads as all-pending: intended -# equals pending, proven and unproven both zero. -CURRENT_CASE="all-pending" +# A stale .do-work/pending/ directory is ignored by rollup. Upgrade owns +# migrating those files; status tooling must not scan that directory. +CURRENT_CASE="pending-directory-ignored" CASES=$((CASES + 1)) setup_fixture -write_req "$TMP/.do-work/pending/REQ-060-park.md" "REQ-060" "UR-060" "pending-validation" "" +write_req "$TMP/.do-work/archive/REQ-060-a.md" "REQ-060" "UR-060" "done" "checkpoint:RUN-060 commit:abc" +write_req "$TMP/.do-work/pending/REQ-061-stale.md" "REQ-061" "UR-060" "pending-validation" "" run_script "UR-060" -assert_contains "UR-060 intended=1 proven=0 unproven=0 pending=1" "$OUT" "$CURRENT_CASE counts" +assert_contains "UR-060 intended=1 proven=1 unproven=0" "$OUT" "$CURRENT_CASE counts" +assert_not_contains "REQ-061" "$OUT" "$CURRENT_CASE stale pending file ignored" +assert_not_contains "pending=" "$OUT" "$CURRENT_CASE no pending field" +teardown_fixture + +# A `**Suite:** not-run` REQ (REQ-263) is counted in unproven= and listed in +# unproven_ids= — rollup delegates to derive-status.sh for the marker logic, +# it does not duplicate it. +CURRENT_CASE="suite-not-run-counts-as-unproven" +CASES=$((CASES + 1)) +setup_fixture +write_req "$TMP/.do-work/archive/REQ-070-a.md" "REQ-070" "UR-070" "done" "checkpoint:RUN-070 commit:abc" +write_req_with_suite "$TMP/.do-work/archive/REQ-071-marker.md" "REQ-071" "UR-070" "done" "checkpoint:RUN-071 commit:def" "not-run" +run_script "UR-070" +assert_contains "UR-070 intended=2 proven=1 unproven=1" "$OUT" "$CURRENT_CASE counts" +assert_contains "unproven_ids=REQ-071" "$OUT" "$CURRENT_CASE ids" teardown_fixture echo "" @@ -192,4 +240,3 @@ if [ "$FAILED" -ne 0 ]; then exit 1 fi exit 0 - diff --git a/lib/tests/cycle-check.bats b/lib/tests/cycle-check.bats deleted file mode 100755 index 59d279f..0000000 --- a/lib/tests/cycle-check.bats +++ /dev/null @@ -1,134 +0,0 @@ -#!/usr/bin/env bats -# Tests for lib/cycle-check.sh -# Bats test suite (bats-core >= 1.x). -# Compatible with macOS bash 3.2 and Linux bash >= 4. - -SCRIPT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)" -CHECKER="$SCRIPT_DIR/cycle-check.sh" - -# --------------------------------------------------------------------------- -# Helpers -# --------------------------------------------------------------------------- - -# Write a REQ file to the backlog (root of .do-work/). -# Args: $1=path, $2=req-id, $3=ur, $4=depends-on -write_req() { - local path="$1" id="$2" ur="$3" deps="$4" - cat > "$path" <= 1.x). -# Compatible with macOS bash 3.2 and Linux bash >= 4. -# -# Acceptance criteria: -# - At most one condition reported (first match in trigger order). -# - Fingerprint is stable for same signal + same live-slot count + same hash. -# - Tests cover: no-deadlock (empty backlog + empty working/), -# no-deadlock (active commits), no-progress-stall, -# mass-stale-slots, runtime-cycle. - -SCRIPT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)" -DEADLOCK="$SCRIPT_DIR/deadlock-check.sh" -SCAN_STALE="$SCRIPT_DIR/scan-stale.sh" -CYCLE_CHECK="$SCRIPT_DIR/cycle-check.sh" - -# --------------------------------------------------------------------------- -# Helpers -# --------------------------------------------------------------------------- - -# Write a minimal backlog REQ file. -# Args: $1=path, $2=req-id, $3=ur -write_backlog_req() { - local path="$1" id="$2" ur="$3" - cat > "$path" < "$path" < -**Claimed by:** test-agent.1234 -**Claimed at:** 2026-05-21T00:00:00Z -**Heartbeat:** $hb - - -**UR:** UR-001 -**Status:** in-progress -**Depends on:** -EOF - else - cat > "$path" </dev/null 2>&1; then - # BSD date (macOS) - if [ "$offset" -lt 0 ]; then - local abs=$(( -offset )) - date -u -v-${abs}S +%Y-%m-%dT%H:%M:%SZ - else - date -u -v+${offset}S +%Y-%m-%dT%H:%M:%SZ - fi - else - # GNU date - date -u -d "@$(( $(date -u +%s) + offset ))" +%Y-%m-%dT%H:%M:%SZ - fi -} - -setup() { - TMP="$(mktemp -d -t deadlock-check-test.XXXXXX)" - mkdir -p "$TMP/.do-work/working" - mkdir -p "$TMP/.do-work/archive" - # Initialize a git repo so git log works; make a commit 1 second ago - git -C "$TMP" init -q - git -C "$TMP" config user.email "test@test.com" - git -C "$TMP" config user.name "Test" - # Create a placeholder file and commit it - touch "$TMP/.do-work/.gitkeep" - git -C "$TMP" add . - GIT_COMMITTER_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ - git -C "$TMP" commit -q --allow-empty -m "init" 2>/dev/null || true -} - -teardown() { - [ -n "${TMP:-}" ] && [ -d "$TMP" ] && rm -rf "$TMP" -} - -run_deadlock() { - # Run the script from $TMP so relative .do-work/ paths resolve correctly. - # Pass overrides via env vars: SCAN_STALE_CMD, CYCLE_CHECK_CMD - cd "$TMP" - run env \ - SCAN_STALE_CMD="$SCAN_STALE" \ - CYCLE_CHECK_CMD="$CYCLE_CHECK" \ - "$DEADLOCK" -} - -# --------------------------------------------------------------------------- -# Test 1: No deadlock — empty backlog and empty working/ -# --------------------------------------------------------------------------- -@test "no deadlock — empty backlog and empty working/" { - # No backlog REQs, no working slots → empty stdout - run_deadlock - [ "$status" -eq 0 ] - [ -z "$output" ] -} - -# --------------------------------------------------------------------------- -# Test 2: No deadlock — backlog has REQs but there is a recent commit -# --------------------------------------------------------------------------- -@test "no deadlock — backlog has REQs with recent git commit" { - write_backlog_req "$TMP/.do-work/REQ-001-a.md" "REQ-001" "UR-001" - # The git repo was initialized with a commit just now in setup() → recent - run_deadlock - [ "$status" -eq 0 ] - [ -z "$output" ] -} - -# --------------------------------------------------------------------------- -# Test 3: no-progress-stall — backlog non-empty, no recent commits -# --------------------------------------------------------------------------- -@test "no-progress-stall — backlog non-empty, no git commits in 5 min window" { - write_backlog_req "$TMP/.do-work/REQ-002-b.md" "REQ-002" "UR-001" - # Amend the commit date to be old (6 minutes ago) so git log --since=5m returns nothing. - # We do this by resetting the commit with an old date. - OLD_DATE="$(iso_at_offset -360)" # 6 minutes ago - cd "$TMP" - git -C "$TMP" commit -q --allow-empty --amend --no-edit \ - --date="$OLD_DATE" \ - -c "committer.date=$OLD_DATE" 2>/dev/null || \ - GIT_COMMITTER_DATE="$OLD_DATE" GIT_AUTHOR_DATE="$OLD_DATE" \ - git -C "$TMP" commit -q --allow-empty --amend --no-edit 2>/dev/null || true - run_deadlock - [ "$status" -eq 0 ] - echo "$output" | grep -q "deadlock-detected" - echo "$output" | grep -q "signal: no-progress-stall" - echo "$output" | grep -q "fingerprint: deadlock:no-progress-stall:" -} - -# --------------------------------------------------------------------------- -# Test 4: mass-stale-slots — all working/ slots are stale -# --------------------------------------------------------------------------- -@test "mass-stale-slots — all working slots are stale" { - # One stale working slot (heartbeat 1 hour ago) - STALE_ISO="$(iso_at_offset -3600)" - write_working_req "$TMP/.do-work/working/REQ-010-a.md" "REQ-010" "$STALE_ISO" - # Make git commit old so no-progress-stall is also triggered, but mass-stale should - # be the signal when stale count == slot count regardless. - # Actually: per REQ, first trigger wins. But mass-stale is trigger #2. - # We need no-progress-stall to NOT fire, so make a fresh commit. - # The setup() commit is fresh, so no-progress-stall won't fire. - # mass-stale is trigger 2: scan-stale count == working slot count. - run_deadlock - [ "$status" -eq 0 ] - echo "$output" | grep -q "deadlock-detected" - echo "$output" | grep -q "signal: mass-stale-slots" - echo "$output" | grep -q "fingerprint: deadlock:mass-stale-slots:" -} - -# --------------------------------------------------------------------------- -# Test 5: runtime-cycle — cycle-check exits 1 -# --------------------------------------------------------------------------- -@test "runtime-cycle — cycle-check.sh exits 1" { - # Create a cycle in the .do-work/ backlog to trigger cycle-check exit 1. - # REQ-020 depends on REQ-021, REQ-021 depends on REQ-020. - cat > "$TMP/.do-work/REQ-020-a.md" < "$TMP/.do-work/REQ-021-b.md" </dev/null || true - run_deadlock - [ "$status" -eq 0 ] - echo "$output" | grep -q "deadlock-detected" - echo "$output" | grep -q "signal: no-progress-stall" - # Should NOT contain mass-stale-slots since first trigger wins - ! echo "$output" | grep -q "mass-stale-slots" -} - -# --------------------------------------------------------------------------- -# Test 7: fingerprint stability — same inputs produce same fingerprint -# --------------------------------------------------------------------------- -@test "fingerprint is stable across two runs with same state" { - # Stale working slot to trigger mass-stale-slots - STALE_ISO="$(iso_at_offset -3600)" - write_working_req "$TMP/.do-work/working/REQ-040-a.md" "REQ-040" "$STALE_ISO" - # setup() has a fresh commit → no-progress-stall won't fire - - run_deadlock - FIRST_OUTPUT="$output" - fp1="$(echo "$FIRST_OUTPUT" | grep "^fingerprint:" | head -1)" - - run_deadlock - SECOND_OUTPUT="$output" - fp2="$(echo "$SECOND_OUTPUT" | grep "^fingerprint:" | head -1)" - - [ -n "$fp1" ] - [ "$fp1" = "$fp2" ] -} diff --git a/lib/tests/deadlock-check.test.sh b/lib/tests/deadlock-check.test.sh new file mode 100755 index 0000000..221f0a3 --- /dev/null +++ b/lib/tests/deadlock-check.test.sh @@ -0,0 +1,284 @@ +#!/usr/bin/env bash +# Tests for lib/deadlock-check.sh +# Plain bash (no bats dependency). Exit non-zero on first failure. +# Compatible with macOS bash 3.2 and Linux bash >= 4. +# +# Acceptance criteria: +# - At most one condition reported (first match in trigger order). +# - Fingerprint is stable for same signal + same live-slot count + same hash. +# - Cases cover: no-deadlock (empty backlog + empty working/), +# no-deadlock (recent commit), no-progress-stall, +# mass-stale-slots, runtime-cycle, first-trigger-wins, +# fingerprint stability. + +set -u + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +LIB_DIR="$( cd "$SCRIPT_DIR/.." && pwd )" +DEADLOCK="$LIB_DIR/deadlock-check.sh" +SCAN_STALE="$LIB_DIR/scan-stale.sh" +CYCLE_CHECK="$LIB_DIR/cycle-check.sh" + +FAILED=0 +CASES=0 +CURRENT_CASE="" + +fail() { + echo "FAIL [$CURRENT_CASE]: $*" >&2 + FAILED=$((FAILED + 1)) +} + +assert_eq() { + local expected="$1" actual="$2" label="$3" + if [ "$expected" != "$actual" ]; then + fail "$label: expected '$expected', got '$actual'" + fi +} + +assert_contains() { + local needle="$1" haystack="$2" label="$3" + case "$haystack" in + *"$needle"*) : ;; + *) fail "$label: expected substring '$needle' in '$haystack'" ;; + esac +} + +assert_not_contains() { + local needle="$1" haystack="$2" label="$3" + case "$haystack" in + *"$needle"*) fail "$label: unexpected substring '$needle' in '$haystack'" ;; + *) : ;; + esac +} + +# Compute an ISO-8601 UTC timestamp offset by N seconds from now (BSD + GNU). +iso_at_offset() { + local offset="$1" + if date -u -v+0S +%Y-%m-%dT%H:%M:%SZ >/dev/null 2>&1; then + if [ "$offset" -lt 0 ]; then + local abs=$(( -offset )) + date -u -v-${abs}S +%Y-%m-%dT%H:%M:%SZ + else + date -u -v+${offset}S +%Y-%m-%dT%H:%M:%SZ + fi + else + date -u -d "@$(( $(date -u +%s) + offset ))" +%Y-%m-%dT%H:%M:%SZ + fi +} + +# Write a minimal backlog REQ file. Args: $1=path, $2=req-id, $3=ur +write_backlog_req() { + local path="$1" id="$2" ur="$3" + cat > "$path" < "$path" < +**Claimed by:** test-agent.1234 +**Claimed at:** 2026-05-21T00:00:00Z +**Heartbeat:** $hb + + +**UR:** UR-001 +**Status:** in-progress +**Depends on:** +EOF + else + cat > "$path" < "$out_file" 2>/dev/null ) + RC=$? + OUTPUT="$(cat "$out_file" 2>/dev/null || true)" + rm -f "$out_file" +} + +# ---------------------------------------------------------------------- +# Case 1: no deadlock — empty backlog and empty working/ +# ---------------------------------------------------------------------- +CURRENT_CASE="no-deadlock-empty" +CASES=$((CASES + 1)) +setup_fixture +run_deadlock +assert_eq "0" "$RC" "$CURRENT_CASE rc=0" +assert_eq "" "$OUTPUT" "$CURRENT_CASE empty output" +teardown_fixture + +# ---------------------------------------------------------------------- +# Case 2: no deadlock — backlog has REQs but there is a recent commit +# ---------------------------------------------------------------------- +CURRENT_CASE="no-deadlock-recent-commit" +CASES=$((CASES + 1)) +setup_fixture +write_backlog_req "$TMP/.do-work/REQ-001-a.md" "REQ-001" "UR-001" +run_deadlock +assert_eq "0" "$RC" "$CURRENT_CASE rc=0" +assert_eq "" "$OUTPUT" "$CURRENT_CASE empty output" +teardown_fixture + +# ---------------------------------------------------------------------- +# Case 3: no-progress-stall — backlog non-empty, no commits in 5m window +# ---------------------------------------------------------------------- +CURRENT_CASE="no-progress-stall" +CASES=$((CASES + 1)) +setup_fixture +write_backlog_req "$TMP/.do-work/REQ-002-b.md" "REQ-002" "UR-001" +age_last_commit "$(iso_at_offset -360)" # 6 minutes ago +run_deadlock +assert_eq "0" "$RC" "$CURRENT_CASE rc=0" +assert_contains "deadlock-detected" "$OUTPUT" "$CURRENT_CASE detected" +assert_contains "signal: no-progress-stall" "$OUTPUT" "$CURRENT_CASE signal" +assert_contains "fingerprint: deadlock:no-progress-stall:" "$OUTPUT" "$CURRENT_CASE fingerprint" +teardown_fixture + +# ---------------------------------------------------------------------- +# Case 4: mass-stale-slots — all working/ slots are stale (recent commit +# keeps no-progress-stall from firing first). +# ---------------------------------------------------------------------- +CURRENT_CASE="mass-stale-slots" +CASES=$((CASES + 1)) +setup_fixture +write_working_req "$TMP/.do-work/working/REQ-010-a.md" "REQ-010" "$(iso_at_offset -3600)" +run_deadlock +assert_eq "0" "$RC" "$CURRENT_CASE rc=0" +assert_contains "deadlock-detected" "$OUTPUT" "$CURRENT_CASE detected" +assert_contains "signal: mass-stale-slots" "$OUTPUT" "$CURRENT_CASE signal" +assert_contains "fingerprint: deadlock:mass-stale-slots:" "$OUTPUT" "$CURRENT_CASE fingerprint" +teardown_fixture + +# ---------------------------------------------------------------------- +# Case 5: runtime-cycle — cycle-check.sh exits 1 on a backlog cycle. +# ---------------------------------------------------------------------- +CURRENT_CASE="runtime-cycle" +CASES=$((CASES + 1)) +setup_fixture +cat > "$TMP/.do-work/REQ-020-a.md" < "$TMP/.do-work/REQ-021-b.md" <&2 + FAILED=$((FAILED + 1)) +} + +assert_eq() { + local expected="$1" + local actual="$2" + local label="$3" + if [ "$expected" != "$actual" ]; then + fail "$label: expected '$expected', got '$actual'" + fi +} + +setup_fixture() { + TMP="$(mktemp -d -t derive-status-test.XXXXXX)" + mkdir -p "$TMP/.do-work/archive" "$TMP/.do-work/working" +} + +teardown_fixture() { + if [ -n "${TMP:-}" ] && [ -d "$TMP" ]; then + rm -rf "$TMP" + fi +} + +write_req() { + local path="$1" + local id="$2" + local status="$3" + local proof="$4" + cat > "$path" < "$path" < "$TMP/.do-work/archive/REQ-010-advisory.md" < exit 1 --- +CURRENT_CASE="bare-runtime-bash-lib-agents-exit-1" +CASES=$((CASES + 1)) +setup_fixture +printf '# Run\n\nbash lib/claim-req.sh "$REQ_PATH"\n' > "$TMP/agents/run.md" +run_lint +assert_eq "1" "$RC" "$CURRENT_CASE rc" +assert_contains "bare-runtime-bash-lib" "$OUT" "$CURRENT_CASE pattern name" +assert_contains "agents/run.md" "$OUT" "$CURRENT_CASE file" +teardown_fixture + +# --- bare runtime `bash lib/` under references/ -> exit 1 --- +CURRENT_CASE="bare-runtime-bash-lib-references-exit-1" +CASES=$((CASES + 1)) +setup_fixture +mkdir -p "$TMP/references" +printf '# Loop\n\nREQ_PATH=$(bash lib/pick-req.sh "$SCOPE" "$AGENT_ID")\n' > "$TMP/references/run-loop.md" +run_lint +assert_eq "1" "$RC" "$CURRENT_CASE rc" +assert_contains "bare-runtime-bash-lib" "$OUT" "$CURRENT_CASE pattern name" +assert_contains "references/run-loop.md" "$OUT" "$CURRENT_CASE file" +teardown_fixture + +# --- skill-dev allowlist: bash lib/tests/... under agents/ -> exit 0 --- +CURRENT_CASE="bare-runtime-bash-lib-allowlist-tests-exit-0" +CASES=$((CASES + 1)) +setup_fixture +mkdir -p "$TMP/agents/tracker" +printf '# Markdown\n\nRun `bash lib/tests/run-all.sh` as the regression gate.\n' > "$TMP/agents/tracker/markdown.md" +run_lint +assert_eq "0" "$RC" "$CURRENT_CASE rc" +teardown_fixture + +# --- skill-dev allowlist: bash lib/conformance-scan.sh under agents/ -> exit 0 --- +CURRENT_CASE="bare-runtime-bash-lib-allowlist-conformance-exit-0" +CASES=$((CASES + 1)) +setup_fixture +mkdir -p "$TMP/agents/tracker" +printf '# Port\n\n`bash lib/conformance-scan.sh` remains the regression gate.\n' > "$TMP/agents/tracker/port.md" +run_lint +assert_eq "0" "$RC" "$CURRENT_CASE rc" +teardown_fixture + +# --- correct skill-root form is allowed under agents/ -> exit 0 --- +CURRENT_CASE="skill-root-bash-lib-agents-exit-0" +CASES=$((CASES + 1)) +setup_fixture +printf '# Run\n\nbash {skill-root}/lib/claim-req.sh "$REQ_PATH"\n' > "$TMP/agents/run.md" +run_lint +assert_eq "0" "$RC" "$CURRENT_CASE rc" +teardown_fixture + +# --- bare bash lib/ outside agents/, references/, and SKILL.md is out of scope -> exit 0 --- +CURRENT_CASE="bare-bash-lib-docs-out-of-scope-exit-0" +CASES=$((CASES + 1)) +setup_fixture +printf '# How\n\nHistorically agents ran bash lib/claim-req.sh from CWD.\n' > "$TMP/docs/HOW-IT-WORKS.md" +run_lint +assert_eq "0" "$RC" "$CURRENT_CASE rc" +teardown_fixture + +# --- bare runtime `bash lib/conformance-scan.sh` in SKILL.md (entry) -> exit 1 --- +# Entry must use skill-root form; skill-dev allowlist does NOT apply on SKILL.md. +CURRENT_CASE="bare-runtime-bash-lib-skill-md-exit-1" +CASES=$((CASES + 1)) +setup_fixture +printf '# Skill\n\nbash lib/conformance-scan.sh "{project}"\n' > "$TMP/SKILL.md" +run_lint +assert_eq "1" "$RC" "$CURRENT_CASE rc" +assert_contains "bare-runtime-bash-lib" "$OUT" "$CURRENT_CASE pattern name" +assert_contains "SKILL.md" "$OUT" "$CURRENT_CASE file" +teardown_fixture + +# --- correct skill-root form in SKILL.md -> exit 0 --- +CURRENT_CASE="skill-root-bash-lib-skill-md-exit-0" +CASES=$((CASES + 1)) +setup_fixture +printf '# Skill\n\nbash {skill-root}/lib/conformance-scan.sh "{project}"\n' > "$TMP/SKILL.md" +run_lint +assert_eq "0" "$RC" "$CURRENT_CASE rc" +teardown_fixture + +# --- SKILL.md prohibition note documenting the bare form (contains "never") -> exit 0 --- +CURRENT_CASE="bare-runtime-bash-lib-skill-md-never-note-exit-0" +CASES=$((CASES + 1)) +setup_fixture +printf '# Skill\n\n(**never** bare `bash lib/conformance-scan.sh` — use skill-root form)\n' > "$TMP/SKILL.md" +run_lint +assert_eq "0" "$RC" "$CURRENT_CASE rc" +teardown_fixture + echo "" echo "doc-lint tests: $CASES cases, $FAILED failure(s)" if [ "$FAILED" -ne 0 ]; then diff --git a/lib/tests/emit-event.test.sh b/lib/tests/emit-event.test.sh new file mode 100755 index 0000000..c4ee1c9 --- /dev/null +++ b/lib/tests/emit-event.test.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash +# Tests for lib/emit-event.sh +# Plain bash (no bats dependency). Compatible with macOS bash 3.2. + +set -u + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +LIB_DIR="$( cd "$SCRIPT_DIR/.." && pwd )" +SCRIPT="$LIB_DIR/emit-event.sh" + +FAILED=0 +CURRENT_CASE="" + +fail() { + echo "FAIL [$CURRENT_CASE]: $*" >&2 + FAILED=$((FAILED + 1)) +} + +assert_eq() { + local expected="$1" actual="$2" label="$3" + if [ "$expected" != "$actual" ]; then + fail "$label: expected '$expected', got '$actual'" + fi +} + +setup() { TMP="$(mktemp -d -t emit-event-test.XXXXXX)"; } +teardown() { [ -n "${TMP:-}" ] && [ -d "$TMP" ] && rm -rf "$TMP"; } + +EVENTS() { echo "$TMP/.do-work/state/events.jsonl"; } + +# --- happy path: with data, deterministic ts ------------------------------ +CURRENT_CASE="happy path with data" +setup +mkdir -p "$TMP/.do-work" +EMIT_EVENT_TS="2026-07-11T00:00:00Z" bash "$SCRIPT" "$TMP" session.start "test-123" '{"marker":"m1"}' +rc=$? +assert_eq 0 "$rc" "exit code" +line="$(cat "$(EVENTS)")" +assert_eq '{"ts":"2026-07-11T00:00:00Z","session":"test-123","type":"session.start","data":{"marker":"m1"}}' "$line" "line content" +# exactly one line +assert_eq 1 "$(wc -l < "$(EVENTS)" | tr -d ' ')" "line count" +teardown + +# --- no data arg: omit the data field -------------------------------------- +CURRENT_CASE="no data field" +setup +mkdir -p "$TMP/.do-work" +EMIT_EVENT_TS="2026-07-11T00:00:00Z" bash "$SCRIPT" "$TMP" session.end "sess-w1" +line="$(cat "$(EVENTS)")" +assert_eq '{"ts":"2026-07-11T00:00:00Z","session":"sess-w1","type":"session.end"}' "$line" "line content" +case "$line" in + *data*) fail "data key should be absent" ;; +esac +teardown + +# --- defensive dir creation ------------------------------------------------ +CURRENT_CASE="defensive state dir creation" +setup +mkdir -p "$TMP/.do-work" # no state/ subdir yet +[ -d "$TMP/.do-work/state" ] && fail "precondition: state/ should not exist yet" +bash "$SCRIPT" "$TMP" session.start "s1" >/dev/null +[ -f "$(EVENTS)" ] || fail "events.jsonl not created" +teardown + +# --- append (not overwrite) ------------------------------------------------ +CURRENT_CASE="append two lines" +setup +mkdir -p "$TMP/.do-work" +bash "$SCRIPT" "$TMP" session.start "s1" >/dev/null +bash "$SCRIPT" "$TMP" session.end "s1" >/dev/null +assert_eq 2 "$(wc -l < "$(EVENTS)" | tr -d ' ')" "two appended lines" +teardown + +# --- missing args -> exit 1 ------------------------------------------------ +CURRENT_CASE="usage error" +setup +bash "$SCRIPT" "$TMP" session.start >/dev/null 2>&1 +assert_eq 1 "$?" "missing session exits 1" +teardown + +# --- session with special chars is escaped to valid JSON ------------------- +CURRENT_CASE="special-char session escaped" +setup +mkdir -p "$TMP/.do-work" +bash "$SCRIPT" "$TMP" session.start 'a"b\c' >/dev/null +# Must remain valid JSON — validate with python3 (present per repo precedent). +if command -v python3 >/dev/null 2>&1; then + python3 -c 'import json,sys; json.loads(open(sys.argv[1]).readline())' "$(EVENTS)" \ + || fail "escaped line is not valid JSON" +fi +teardown + +# --- every emitted line parses as valid JSON (parser-contract smoke) ------- +CURRENT_CASE="valid JSON contract" +setup +mkdir -p "$TMP/.do-work" +bash "$SCRIPT" "$TMP" session.start "s1" '{"marker":"x"}' >/dev/null +if command -v python3 >/dev/null 2>&1; then + python3 - "$(EVENTS)" <<'PY' || fail "line failed parser contract" +import json, sys +o = json.loads(open(sys.argv[1]).readline()) +assert isinstance(o.get("ts"), str), "ts must be string" +assert isinstance(o.get("session"), str), "session must be string" +assert isinstance(o.get("type"), str), "type must be string" +assert isinstance(o.get("data"), dict), "data must be object when present" +PY +fi +teardown + +# --- summary --------------------------------------------------------------- +if [ "$FAILED" -eq 0 ]; then + echo "emit-event.test.sh: all cases passed" + exit 0 +else + echo "emit-event.test.sh: $FAILED assertion(s) failed" >&2 + exit 1 +fi diff --git a/lib/tests/file-feedback.test.sh b/lib/tests/file-feedback.test.sh index 9894bac..4fccf53 100755 --- a/lib/tests/file-feedback.test.sh +++ b/lib/tests/file-feedback.test.sh @@ -142,7 +142,7 @@ run_script() { ( cd "$TMP" && \ - PATH="$MOCK_BIN:/usr/bin:/bin" \ + PATH="${FF_TEST_PATH:-$MOCK_BIN:/usr/bin:/bin}" \ FEEDBACK_LOCK_DIR="$TMP/.do-work/state" \ "$SCRIPT" "$event_type" "$fingerprint" "$context_json" "$title" "$body" \ > "$out_file" 2> "$err_file" @@ -229,7 +229,18 @@ CASES=$((CASES + 1)) setup_fixture write_config "true" "example/system-repo" rm -f "$MOCK_BIN/gh" -run_script "deadlock" "fp:1:2:3" "{}" "Title" "Body" +# Run against a sandbox PATH that has the coreutils the script needs but NO gh, +# so `command -v gh` fails regardless of host. Simply removing the mock and +# keeping /usr/bin on PATH is not enough on CI runners, which ship a real gh in +# /usr/bin alongside the coreutils. +NOGH_BIN="$TMP/nogh-bin" +mkdir -p "$NOGH_BIN" +for _t in bash sh env awk sed grep cat printf mktemp rm mkdir rmdir date head tail tr cut sort wc dirname basename flock; do + _p="$(command -v "$_t" 2>/dev/null || true)" + [ -n "$_p" ] && ln -sf "$_p" "$NOGH_BIN/$_t" +done +FF_TEST_PATH="$NOGH_BIN" run_script "deadlock" "fp:1:2:3" "{}" "Title" "Body" +unset NOGH_BIN _t _p assert_eq "0" "$RC_" "$CURRENT_CASE rc" assert_contains "gh" "$STDERR_" "$CURRENT_CASE warning mentions gh" teardown_fixture diff --git a/lib/tests/install-hooks.test.sh b/lib/tests/install-hooks.test.sh new file mode 100755 index 0000000..60ebf15 --- /dev/null +++ b/lib/tests/install-hooks.test.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +# Tests for lib/install-hooks.sh +# Plain bash (no bats dependency). Compatible with macOS bash 3.2. +# Requires python3; skips (passes) gracefully if it is unavailable. + +set -u + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +LIB_DIR="$( cd "$SCRIPT_DIR/.." && pwd )" +SCRIPT="$LIB_DIR/install-hooks.sh" +HOOK_SCRIPT="$LIB_DIR/session-hook.sh" + +if ! command -v python3 >/dev/null 2>&1; then + echo "install-hooks.test.sh: python3 unavailable — skipping" + exit 0 +fi + +FAILED=0 +CURRENT_CASE="" + +fail() { + echo "FAIL [$CURRENT_CASE]: $*" >&2 + FAILED=$((FAILED + 1)) +} + +assert_eq() { + local expected="$1" actual="$2" label="$3" + if [ "$expected" != "$actual" ]; then + fail "$label: expected '$expected', got '$actual'" + fi +} + +setup() { TMP="$(mktemp -d -t install-hooks-test.XXXXXX)"; } +teardown() { [ -n "${TMP:-}" ] && [ -d "$TMP" ] && rm -rf "$TMP"; } + +SETTINGS() { echo "$TMP/.claude/settings.json"; } + +# count occurrences of the hook command across the settings.json +count_cmd() { + grep -c "session-hook.sh $1" "$(SETTINGS)" 2>/dev/null || echo 0 +} + +# --- fresh project: installs both hooks ------------------------------------ +CURRENT_CASE="fresh install" +setup +out="$(bash "$SCRIPT" "$TMP")" +assert_eq "installed" "$out" "output" +[ -f "$(SETTINGS)" ] || fail "settings.json not created" +assert_eq 1 "$(count_cmd start | tr -d ' ')" "one SessionStart command" +assert_eq 1 "$(count_cmd end | tr -d ' ')" "one Stop command" +# valid JSON with expected structure +python3 - "$(SETTINGS)" "$HOOK_SCRIPT" <<'PY' || fail "structure check failed" +import json, sys +s = json.load(open(sys.argv[1])) +hook = sys.argv[2] +h = s["hooks"] +def has(ev, cmd): + return any( + e.get("command") == cmd + for g in h.get(ev, []) for e in g.get("hooks", []) + ) +assert has("SessionStart", hook + " start"), "SessionStart missing" +assert has("Stop", hook + " end"), "Stop missing" +PY +teardown + +# --- idempotence: second run makes no change (REQ acceptance criterion 5) --- +CURRENT_CASE="idempotent second run" +setup +bash "$SCRIPT" "$TMP" >/dev/null +out2="$(bash "$SCRIPT" "$TMP")" +assert_eq "already-conformant" "$out2" "second run output" +assert_eq 1 "$(count_cmd start | tr -d ' ')" "still one SessionStart after 2 runs" +assert_eq 1 "$(count_cmd end | tr -d ' ')" "still one Stop after 2 runs" +teardown + +# --- preserves unrelated existing settings --------------------------------- +CURRENT_CASE="preserves existing settings" +setup +mkdir -p "$TMP/.claude" +cat > "$(SETTINGS)" <<'JSON' +{ + "model": "opus", + "hooks": { + "PreToolUse": [ + { "hooks": [ { "type": "command", "command": "echo hi" } ] } + ] + } +} +JSON +bash "$SCRIPT" "$TMP" >/dev/null +python3 - "$(SETTINGS)" <<'PY' || fail "existing settings not preserved" +import json, sys +s = json.load(open(sys.argv[1])) +assert s.get("model") == "opus", "model key lost" +assert any( + e.get("command") == "echo hi" + for g in s["hooks"].get("PreToolUse", []) for e in g.get("hooks", []) +), "pre-existing PreToolUse hook lost" +assert "SessionStart" in s["hooks"], "SessionStart not added" +PY +teardown + +# --- --check reports absent then present ----------------------------------- +CURRENT_CASE="--check present/absent" +setup +assert_eq "absent" "$(bash "$SCRIPT" --check "$TMP")" "absent before install" +bash "$SCRIPT" "$TMP" >/dev/null +assert_eq "present" "$(bash "$SCRIPT" --check "$TMP")" "present after install" +teardown + +# --- invalid JSON settings -> exit 1, no clobber --------------------------- +CURRENT_CASE="invalid settings.json rejected" +setup +mkdir -p "$TMP/.claude" +printf '{ not json ' > "$(SETTINGS)" +bash "$SCRIPT" "$TMP" >/dev/null 2>&1 +assert_eq 1 "$?" "exit 1 on invalid JSON" +assert_eq '{ not json ' "$(cat "$(SETTINGS)")" "file left untouched" +teardown + +# --- summary --------------------------------------------------------------- +if [ "$FAILED" -eq 0 ]; then + echo "install-hooks.test.sh: all cases passed" + exit 0 +else + echo "install-hooks.test.sh: $FAILED assertion(s) failed" >&2 + exit 1 +fi diff --git a/lib/tests/install-target.test.sh b/lib/tests/install-target.test.sh index 547a3ee..1ee3fb9 100755 --- a/lib/tests/install-target.test.sh +++ b/lib/tests/install-target.test.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Tests for installer target resolution. +# Tests for hub-only installer target resolution. # Plain bash (no bats dependency). Compatible with macOS bash 3.2. set -u @@ -26,30 +26,56 @@ assert_eq() { fi } -run_resolve() { - local env_name="$1" - OUT="$(bash "$TARGET" --resolve "$env_name" "/tmp/home")" - RC=$? +assert_not_contains() { + local haystack="$1" + local needle="$2" + local label="$3" + case "$haystack" in + *"$needle"*) fail "$label: unexpectedly contains '$needle'" ;; + esac } -CURRENT_CASE="claude-target" +# --- hub default under synthetic HOME --- +CURRENT_CASE="hub-default" CASES=$((CASES + 1)) -run_resolve claude +unset AGENTS_SKILLS_HUB 2>/dev/null || true +OUT="$(bash "$TARGET" --resolve "/tmp/home")" +RC=$? assert_eq "0" "$RC" "$CURRENT_CASE rc" -assert_eq "/tmp/home/.claude/skills/do-work|/tmp/home/.claude/backups|Claude Code" "$OUT" "$CURRENT_CASE output" +assert_eq "/tmp/home/.agents/skills/do-work|/tmp/home/.agents/skills/.backups|skills hub" "$OUT" "$CURRENT_CASE output" +assert_not_contains "$OUT" ".claude/skills" "$CURRENT_CASE no-claude" +assert_not_contains "$OUT" ".codex/skills" "$CURRENT_CASE no-codex" -CURRENT_CASE="codex-target" +# --- AGENTS_SKILLS_HUB override wins over home_dir --- +CURRENT_CASE="hub-override" CASES=$((CASES + 1)) -run_resolve codex +OUT="$(AGENTS_SKILLS_HUB="/tmp/custom-hub" bash "$TARGET" --resolve "/tmp/home")" +RC=$? assert_eq "0" "$RC" "$CURRENT_CASE rc" -assert_eq "/tmp/home/.codex/skills/do-work|/tmp/home/.codex/backups|Codex" "$OUT" "$CURRENT_CASE output" +assert_eq "/tmp/custom-hub/do-work|/tmp/custom-hub/.backups|skills hub" "$OUT" "$CURRENT_CASE output" -CURRENT_CASE="invalid-env" +# --- no dual-env CLI: --resolve with env name is not required; bare resolve works --- +CURRENT_CASE="no-env-arg-required" CASES=$((CASES + 1)) -OUT="$(bash "$TARGET" --resolve invalid "/tmp/home" 2>/dev/null)" +unset AGENTS_SKILLS_HUB 2>/dev/null || true +OUT="$(bash "$TARGET" --resolve)" RC=$? -assert_eq "1" "$RC" "$CURRENT_CASE rc" -assert_eq "" "$OUT" "$CURRENT_CASE stdout" +assert_eq "0" "$RC" "$CURRENT_CASE rc" +# HOME-based path must end with .agents/skills/do-work|...|.backups|skills hub +case "$OUT" in + *"/.agents/skills/do-work|"*"/.agents/skills/.backups|skills hub") ;; + *) fail "$CURRENT_CASE output shape: got '$OUT'" ;; +esac + +# --- dual env names are not current behaviour --- +CURRENT_CASE="dual-env-not-current" +CASES=$((CASES + 1)) +# Invoking with a legacy env token must not resolve to claude/codex skill dirs. +# Hub API ignores the env name if a second positional is passed for home only; +# callers should use --resolve [home]. Document that claude/codex are not targets. +SRC="$(cat "$TARGET")" +assert_not_contains "$SRC" ".claude/skills/do-work" "$CURRENT_CASE source-no-claude-path" +assert_not_contains "$SRC" ".codex/skills/do-work" "$CURRENT_CASE source-no-codex-path" echo "" echo "install-target tests: $CASES cases, $FAILED failure(s)" @@ -57,4 +83,3 @@ if [ "$FAILED" -ne 0 ]; then exit 1 fi exit 0 - diff --git a/lib/tests/json-bash.test.sh b/lib/tests/json-bash.test.sh new file mode 100644 index 0000000..2a42886 --- /dev/null +++ b/lib/tests/json-bash.test.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +# Tests for lib/json-bash.sh — shared JSON string escape + field extract. +# Plain bash (no bats dependency). Compatible with macOS bash 3.2. + +set -u + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +LIB_DIR="$( cd "$SCRIPT_DIR/.." && pwd )" +HELPER="$LIB_DIR/json-bash.sh" + +FAILED=0 +CURRENT_CASE="" + +fail() { + echo "FAIL [$CURRENT_CASE]: $*" >&2 + FAILED=$((FAILED + 1)) +} + +assert_eq() { + local expected="$1" actual="$2" label="$3" + if [ "$expected" != "$actual" ]; then + fail "$label: expected '$expected', got '$actual'" + fi +} + +# --- source helper ---------------------------------------------------------- +CURRENT_CASE="source helper" +if [ ! -f "$HELPER" ]; then + fail "lib/json-bash.sh must exist" + echo "FAILED: $FAILED" + exit 1 +fi +# shellcheck disable=SC1090 +. "$HELPER" +# re-source must be safe +. "$HELPER" + +# --- json_escape: plain ----------------------------------------------------- +CURRENT_CASE="json_escape plain" +assert_eq 'hello' "$(json_escape 'hello')" "plain string" + +# --- json_escape: backslash then quote order -------------------------------- +# Input chars a \ " b → escape \ first → a \\ " b → escape " → a \\ \ " b +# Printed form: a\\\"b (valid JSON string content for a\"b). +CURRENT_CASE="json_escape backslash and quote" +assert_eq 'a\\\"b' "$(json_escape 'a\"b')" "backslash then quote" +assert_eq '\\\\' "$(json_escape '\\')" "single backslash doubles" +assert_eq '\"' "$(json_escape '"')" "lone quote" + +# --- json_escape: tab -> \\t ------------------------------------------------ +CURRENT_CASE="json_escape tab" +assert_eq 'a\tb' "$(json_escape $'a\tb')" "tab becomes \\t" + +# --- json_escape: strip CR/LF ----------------------------------------------- +CURRENT_CASE="json_escape strip CR LF" +assert_eq 'ab' "$(json_escape $'a\rb\n')" "CR and LF stripped" + +# --- json_escape: empty ----------------------------------------------------- +CURRENT_CASE="json_escape empty" +assert_eq '' "$(json_escape '')" "empty string" + +# --- json_string_field: basic compact --------------------------------------- +CURRENT_CASE="json_string_field basic" +line='{"ts":"t","session":"sess-1","type":"session.start"}' +assert_eq 'sess-1' "$(json_string_field "$line" session)" "session field" +assert_eq 'session.start' "$(json_string_field "$line" type)" "type field" + +# --- json_string_field: optional whitespace around colon -------------------- +CURRENT_CASE="json_string_field whitespace" +line='{ "session" : "sess-ws" }' +assert_eq 'sess-ws' "$(json_string_field "$line" session)" "spaced colon" + +# --- json_string_field: missing field --------------------------------------- +CURRENT_CASE="json_string_field missing" +assert_eq '' "$(json_string_field '{"a":"1"}' session)" "missing field empty" + +# --- json_string_field: empty payload --------------------------------------- +CURRENT_CASE="json_string_field empty payload" +assert_eq '' "$(json_string_field '' session_id)" "empty payload" + +# --- json_string_field: no trailing-newline glue (BSD sed safety) ----------- +# Without feeding sed a trailing newline, two successive extracts can glue. +CURRENT_CASE="json_string_field no glue without final newline" +a="$(json_string_field '{"session":"sess-X"}' session)" +b="$(json_string_field '{"session":"sess-Y"}' session)" +assert_eq 'sess-X' "$a" "first extract" +assert_eq 'sess-Y' "$b" "second extract" +assert_eq 'sess-Xsess-Y' "${a}${b}" "concat is two distinct ids not glued mid-token" + +# --- json_string_field: multi-key payload (session-hook style) -------------- +CURRENT_CASE="json_string_field multi key" +payload='{"session_id":"abc-123","cwd":"/tmp/proj","model":"opus-4"}' +assert_eq 'abc-123' "$(json_string_field "$payload" session_id)" "session_id" +assert_eq '/tmp/proj' "$(json_string_field "$payload" cwd)" "cwd" +assert_eq 'opus-4' "$(json_string_field "$payload" model)" "model" + +# --- functions are defined -------------------------------------------------- +CURRENT_CASE="functions exported" +type json_escape >/dev/null 2>&1 || fail "json_escape not a function" +type json_string_field >/dev/null 2>&1 || fail "json_string_field not a function" + +# --- summary ---------------------------------------------------------------- +if [ "$FAILED" -ne 0 ]; then + echo "FAILED: $FAILED" + exit 1 +fi +echo "OK: json-bash tests passed" +exit 0 diff --git a/lib/pick-req.test.sh b/lib/tests/pick-req-pending.test.sh similarity index 86% rename from lib/pick-req.test.sh rename to lib/tests/pick-req-pending.test.sh index 1d0e2ff..8c94d52 100755 --- a/lib/pick-req.test.sh +++ b/lib/tests/pick-req-pending.test.sh @@ -1,16 +1,17 @@ #!/usr/bin/env bash -# Tests for lib/pick-req.sh — pending-validation dep filter cases. +# Tests for lib/pick-req.sh — pending-only dep filter cases. # Plain bash (no bats dependency). Compatible with macOS bash 3.2. # # Covers: -# (a) pending-only dep → candidate is claimable (pick-req returns it) +# (a) pending-only dep → candidate is rejected # (b) archive-only dep → candidate is claimable (unchanged behaviour) -# (c) dep absent from both archive/ and pending/ → rejected (dep: on stderr) +# (c) dep absent from archive/ → rejected (dep: on stderr) set -u SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -PICKER="$SCRIPT_DIR/pick-req.sh" +LIB_DIR="$( cd "$SCRIPT_DIR/.." && pwd )" +PICKER="$LIB_DIR/pick-req.sh" FAILED=0 CASES=0 @@ -113,9 +114,9 @@ run_picker() { } # ----------------------------------------------------------------------- -# Case (a): pending-only dep → candidate is claimable +# Case (a): pending-only dep → candidate is rejected # ----------------------------------------------------------------------- -CURRENT_CASE="pending-only-dep-claimable" +CURRENT_CASE="pending-only-dep-rejected" CASES=$((CASES + 1)) setup_fixture # Dep parked in pending/ only — not in archive/ @@ -123,9 +124,9 @@ write_stub "$TMP/.do-work/pending/REQ-501-parked.md" "REQ-501" # Candidate depending on REQ-501 write_candidate "REQ-502" "REQ-501" run_picker "any" -assert_eq "0" "$PICK_RC" "$CURRENT_CASE exit code (0 = found claimable)" -assert_contains "REQ-502" "$PICK_STDOUT" "$CURRENT_CASE candidate path returned" -assert_not_contains "dep:REQ-501" "$PICK_STDERR" "$CURRENT_CASE no dep rejection on stderr" +assert_eq "1" "$PICK_RC" "$CURRENT_CASE exit code (1 = nothing claimable)" +assert_eq "" "$PICK_STDOUT" "$CURRENT_CASE no candidate returned" +assert_contains "dep:REQ-501" "$PICK_STDERR" "$CURRENT_CASE dep rejection on stderr" teardown_fixture # ----------------------------------------------------------------------- @@ -145,7 +146,7 @@ assert_not_contains "dep:REQ-503" "$PICK_STDERR" "$CURRENT_CASE no dep rejection teardown_fixture # ----------------------------------------------------------------------- -# Case (c): dep absent from both archive/ and pending/ → rejected +# Case (c): dep absent from archive/ → rejected # ----------------------------------------------------------------------- CURRENT_CASE="dep-absent-from-both-rejected" CASES=$((CASES + 1)) diff --git a/lib/tests/pick-req.test.sh b/lib/tests/pick-req.test.sh index a6dce8f..3cb6829 100755 --- a/lib/tests/pick-req.test.sh +++ b/lib/tests/pick-req.test.sh @@ -352,6 +352,88 @@ assert_eq "0" "$PICK_RC" "$CURRENT_CASE rc" assert_eq "$TMP/.do-work/REQ-112-explicit-high.md" "$PICK_STDOUT" "$CURRENT_CASE Priority 3 wins overall" teardown_fixture +# ---------------------------------------------------------------------- +# Case 15: space-separated Depends on — each id checked separately +# (UR-041 regression: "REQ-260 REQ-263 REQ-264" was treated as one id) +# ---------------------------------------------------------------------- +CURRENT_CASE="deps-space-separated-all-archived" +CASES=$((CASES + 1)) +setup_fixture +write_req "$TMP/.do-work/archive/REQ-260-a.md" "REQ-260" "src/a.ts" "" "UR-001" "done" +write_req "$TMP/.do-work/archive/REQ-263-b.md" "REQ-263" "src/b.ts" "" "UR-001" "done" +write_req "$TMP/.do-work/archive/REQ-264-c.md" "REQ-264" "src/c.ts" "" "UR-001" "done" +write_req "$TMP/.do-work/REQ-261-next.md" "REQ-261" "src/n.ts" "REQ-260 REQ-263 REQ-264" "UR-001" "backlog" +run_picker "any" "test-agent" +assert_eq "0" "$PICK_RC" "$CURRENT_CASE rc" +assert_eq "$TMP/.do-work/REQ-261-next.md" "$PICK_STDOUT" "$CURRENT_CASE space-separated multi-id deps pickable" +assert_not_contains "dep:" "$PICK_STDERR" "$CURRENT_CASE no dep rejection" +teardown_fixture + +# ---------------------------------------------------------------------- +# Case 16: space-separated deps — one missing → blocked on that id alone +# ---------------------------------------------------------------------- +CURRENT_CASE="deps-space-separated-one-missing" +CASES=$((CASES + 1)) +setup_fixture +write_req "$TMP/.do-work/archive/REQ-260-a.md" "REQ-260" "src/a.ts" "" "UR-001" "done" +write_req "$TMP/.do-work/archive/REQ-263-b.md" "REQ-263" "src/b.ts" "" "UR-001" "done" +# REQ-264 not archived +write_req "$TMP/.do-work/REQ-261-next.md" "REQ-261" "src/n.ts" "REQ-260 REQ-263 REQ-264" "UR-001" "backlog" +run_picker "any" "test-agent" +assert_eq "1" "$PICK_RC" "$CURRENT_CASE rc" +assert_eq "" "$PICK_STDOUT" "$CURRENT_CASE stdout empty" +assert_contains "dep:REQ-264" "$PICK_STDERR" "$CURRENT_CASE blocked on missing REQ-264" +assert_not_contains "dep:REQ-260 REQ-263 REQ-264" "$PICK_STDERR" "$CURRENT_CASE must not treat multi-id as one token" +teardown_fixture + +# ---------------------------------------------------------------------- +# Case 17: comma-separated multi-id still works (no regression) +# ---------------------------------------------------------------------- +CURRENT_CASE="deps-comma-separated-all-archived" +CASES=$((CASES + 1)) +setup_fixture +write_req "$TMP/.do-work/archive/REQ-144-a.md" "REQ-144" "src/a.ts" "" "UR-001" "done" +write_req "$TMP/.do-work/archive/REQ-145-b.md" "REQ-145" "src/b.ts" "" "UR-001" "done" +write_req "$TMP/.do-work/REQ-150-next.md" "REQ-150" "src/n.ts" "REQ-144, REQ-145" "UR-001" "backlog" +run_picker "any" "test-agent" +assert_eq "0" "$PICK_RC" "$CURRENT_CASE rc" +assert_eq "$TMP/.do-work/REQ-150-next.md" "$PICK_STDOUT" "$CURRENT_CASE comma form still pickable" +teardown_fixture + +# ---------------------------------------------------------------------- +# Case 18: mixed delimiters (comma + whitespace) — same claimable verdict +# ---------------------------------------------------------------------- +CURRENT_CASE="deps-mixed-delimiters-all-archived" +CASES=$((CASES + 1)) +setup_fixture +write_req "$TMP/.do-work/archive/REQ-001-a.md" "REQ-001" "src/a.ts" "" "UR-001" "done" +write_req "$TMP/.do-work/archive/REQ-002-b.md" "REQ-002" "src/b.ts" "" "UR-001" "done" +write_req "$TMP/.do-work/archive/REQ-003-c.md" "REQ-003" "src/c.ts" "" "UR-001" "done" +write_req "$TMP/.do-work/REQ-010-next.md" "REQ-010" "src/n.ts" "REQ-1, REQ-2 REQ-3" "UR-001" "backlog" +# Note: REQ-1 would not match REQ-001 — use exact ids matching archive stems +# Re-write with exact ids +write_req "$TMP/.do-work/REQ-010-next.md" "REQ-010" "src/n.ts" "REQ-001, REQ-002 REQ-003" "UR-001" "backlog" +run_picker "any" "test-agent" +assert_eq "0" "$PICK_RC" "$CURRENT_CASE rc" +assert_eq "$TMP/.do-work/REQ-010-next.md" "$PICK_STDOUT" "$CURRENT_CASE mixed delimiters pickable" +teardown_fixture + +# ---------------------------------------------------------------------- +# Case 19: mixed delimiters — one missing reports that single id +# ---------------------------------------------------------------------- +CURRENT_CASE="deps-mixed-delimiters-one-missing" +CASES=$((CASES + 1)) +setup_fixture +write_req "$TMP/.do-work/archive/REQ-001-a.md" "REQ-001" "src/a.ts" "" "UR-001" "done" +write_req "$TMP/.do-work/archive/REQ-002-b.md" "REQ-002" "src/b.ts" "" "UR-001" "done" +# REQ-003 not archived +write_req "$TMP/.do-work/REQ-010-next.md" "REQ-010" "src/n.ts" "REQ-001, REQ-002 REQ-003" "UR-001" "backlog" +run_picker "any" "test-agent" +assert_eq "1" "$PICK_RC" "$CURRENT_CASE rc" +assert_contains "dep:REQ-003" "$PICK_STDERR" "$CURRENT_CASE missing REQ-003 alone" +assert_not_contains "dep:REQ-001, REQ-002 REQ-003" "$PICK_STDERR" "$CURRENT_CASE not one blob" +teardown_fixture + # ---------------------------------------------------------------------- # Summary # ---------------------------------------------------------------------- diff --git a/lib/tests/provision-worktree.test.sh b/lib/tests/provision-worktree.test.sh new file mode 100644 index 0000000..8a4c3e8 --- /dev/null +++ b/lib/tests/provision-worktree.test.sh @@ -0,0 +1,226 @@ +#!/usr/bin/env bash +# Tests for lib/provision-worktree.sh +# Plain bash (no bats dependency). Exit non-zero on failure. +# Compatible with macOS bash 3.2. + +set -u + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +LIB_DIR="$( cd "$SCRIPT_DIR/.." && pwd )" +PROVISIONER="$LIB_DIR/provision-worktree.sh" + +FAILED=0 +CASES=0 +CURRENT_CASE="" + +fail() { + echo "FAIL [$CURRENT_CASE]: $*" >&2 + FAILED=$((FAILED + 1)) +} + +assert_eq() { + local expected="$1" + local actual="$2" + local label="$3" + if [ "$expected" != "$actual" ]; then + fail "$label: expected '$expected', got '$actual'" + fi +} + +assert_contains() { + local needle="$1" + local haystack="$2" + local label="$3" + case "$haystack" in + *"$needle"*) : ;; + *) fail "$label: expected substring '$needle' in '$haystack'" ;; + esac +} + +assert_not_contains() { + local needle="$1" + local haystack="$2" + local label="$3" + case "$haystack" in + *"$needle"*) fail "$label: did not expect substring '$needle' in '$haystack'" ;; + esac +} + +setup_fixture() { + TMP="$(mktemp -d -t provision-worktree-test.XXXXXX)" + MAIN_DIR="$TMP/main" + WT_DIR="$TMP/worktree" + mkdir -p "$MAIN_DIR" "$WT_DIR" +} + +teardown_fixture() { + if [ -n "${TMP:-}" ] && [ -d "$TMP" ]; then + rm -rf "$TMP" + fi +} + +run_provisioner() { + local main_root="$1" + local wt_root="$2" + local out_file="$TMP/.stdout.$$" + local err_file="$TMP/.stderr.$$" + bash "$PROVISIONER" "$main_root" "$wt_root" > "$out_file" 2> "$err_file" + PROV_RC=$? + PROV_STDOUT="$(cat "$out_file" 2>/dev/null || true)" + PROV_STDERR="$(cat "$err_file" 2>/dev/null || true)" + rm -f "$out_file" "$err_file" +} + +# ---------------------------------------------------------------------- +# Case 1: symlink-from-main — composer.json in worktree root, vendor in main +# ---------------------------------------------------------------------- +CURRENT_CASE="symlink-from-main" +CASES=$((CASES + 1)) +setup_fixture +# main has vendor/ +mkdir -p "$MAIN_DIR/vendor" +touch "$MAIN_DIR/vendor/autoload.php" +# worktree has composer.json but no vendor/ +touch "$WT_DIR/composer.json" +run_provisioner "$MAIN_DIR" "$WT_DIR" +assert_eq "0" "$PROV_RC" "$CURRENT_CASE rc" +assert_contains "linked: vendor" "$PROV_STDOUT" "$CURRENT_CASE stdout" +# symlink must exist and resolve into main +if [ ! -L "$WT_DIR/vendor" ]; then + fail "$CURRENT_CASE: $WT_DIR/vendor is not a symlink" +else + resolved="$(readlink "$WT_DIR/vendor")" + if [ "$resolved" != "$MAIN_DIR/vendor" ]; then + fail "$CURRENT_CASE: symlink target '$resolved' != '$MAIN_DIR/vendor'" + fi +fi +teardown_fixture + +# ---------------------------------------------------------------------- +# Case 2: depth-1 subdir detection — server/composer.json → server/vendor +# ---------------------------------------------------------------------- +CURRENT_CASE="depth-1-subdir-detection" +CASES=$((CASES + 1)) +setup_fixture +mkdir -p "$MAIN_DIR/server/vendor" +touch "$MAIN_DIR/server/vendor/autoload.php" +mkdir -p "$WT_DIR/server" +touch "$WT_DIR/server/composer.json" +run_provisioner "$MAIN_DIR" "$WT_DIR" +assert_eq "0" "$PROV_RC" "$CURRENT_CASE rc" +assert_contains "linked: server/vendor" "$PROV_STDOUT" "$CURRENT_CASE stdout" +if [ ! -L "$WT_DIR/server/vendor" ]; then + fail "$CURRENT_CASE: $WT_DIR/server/vendor is not a symlink" +fi +teardown_fixture + +# ---------------------------------------------------------------------- +# Case 3: config link_paths override — path listed in config.yml is linked +# ---------------------------------------------------------------------- +CURRENT_CASE="config-link-paths" +CASES=$((CASES + 1)) +setup_fixture +mkdir -p "$MAIN_DIR/.do-work" +mkdir -p "$MAIN_DIR/custom/deps" +touch "$MAIN_DIR/custom/deps/somefile" +cat > "$MAIN_DIR/.do-work/config.yml" < "$MAIN_DIR/.do-work/config.yml" < "$out_file" 2>&1 +rc=$? +if [ "$rc" -eq 0 ]; then + fail "$CURRENT_CASE: expected non-zero exit when called with no args, got 0" +fi +rm -f "$out_file" +teardown_fixture + +# ---------------------------------------------------------------------- +# Summary +# ---------------------------------------------------------------------- +echo "" +echo "Ran $CASES cases. Failures: $FAILED" +if [ "$FAILED" -gt 0 ]; then + exit 1 +fi +exit 0 diff --git a/lib/tests/resolve-session.test.sh b/lib/tests/resolve-session.test.sh new file mode 100644 index 0000000..9602394 --- /dev/null +++ b/lib/tests/resolve-session.test.sh @@ -0,0 +1,207 @@ +#!/usr/bin/env bash +# Tests for lib/resolve-session.sh +# Plain bash (no bats dependency). Exit non-zero on first failure. +# Compatible with macOS bash 3.2. + +set -u + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +LIB_DIR="$( cd "$SCRIPT_DIR/.." && pwd )" +RESOLVER="$LIB_DIR/resolve-session.sh" + +FAILED=0 +CASES=0 +CURRENT_CASE="" + +fail() { + echo "FAIL [$CURRENT_CASE]: $*" >&2 + FAILED=$((FAILED + 1)) +} + +assert_eq() { + local expected="$1" + local actual="$2" + local label="$3" + if [ "$expected" != "$actual" ]; then + fail "$label: expected '$expected', got '$actual'" + fi +} + +# Create an isolated project fixture with a .do-work/state/ dir. Sets TMP. +setup_fixture() { + TMP="$(mktemp -d -t resolve-session.XXXXXX)" + mkdir -p "$TMP/.do-work/state" +} + +teardown_fixture() { + if [ -n "${TMP:-}" ] && [ -d "$TMP" ]; then + rm -rf "$TMP" + fi +} + +# Append one event line to the fixture's events.jsonl via emit-event.sh so the +# format under test is exactly what the real emitter writes. +emit() { + local type="$1" session="$2" data="${3:-}" + EMIT_EVENT_TS="2026-07-11T00:00:0${CASES}Z" \ + bash "$LIB_DIR/emit-event.sh" "$TMP" "$type" "$session" "$data" >/dev/null +} + +# Run resolve-session.sh; store RC and STDOUT. +run_resolve() { + local marker_set="$1" # "1" to export DO_WORK_UI_MARKER, "0" to unset + local marker_val="$2" + local out_file="$TMP/.stdout.$$" + if [ "$marker_set" = "1" ]; then + DO_WORK_UI_MARKER="$marker_val" "$RESOLVER" "$TMP" > "$out_file" 2>/dev/null + else + ( unset DO_WORK_UI_MARKER; "$RESOLVER" "$TMP" > "$out_file" 2>/dev/null ) + fi + RC=$? + STDOUT="$(cat "$out_file" 2>/dev/null || true)" + rm -f "$out_file" +} + +# ---------------------------------------------------------------------- +# Case 1: marker match — stamps the session whose start carries the marker +# ---------------------------------------------------------------------- +CURRENT_CASE="marker-match" +CASES=$((CASES + 1)) +setup_fixture +emit "session.start" "sess-A" '{"marker":"m1"}' +emit "session.start" "sess-B" '{"marker":"m2"}' +run_resolve 1 "m1" +assert_eq "0" "$RC" "$CURRENT_CASE rc" +assert_eq "sess-A" "$STDOUT" "$CURRENT_CASE resolves marker owner" +teardown_fixture + +# ---------------------------------------------------------------------- +# Case 2: no marker, single un-ended session — fallback resolves it +# ---------------------------------------------------------------------- +CURRENT_CASE="no-marker-single-session" +CASES=$((CASES + 1)) +setup_fixture +emit "session.start" "sess-only" "" +run_resolve 0 "" +assert_eq "0" "$RC" "$CURRENT_CASE rc" +assert_eq "sess-only" "$STDOUT" "$CURRENT_CASE fallback resolves single session" +teardown_fixture + +# ---------------------------------------------------------------------- +# Case 3: multiple live sessions, no marker match — omit (never guess) +# ---------------------------------------------------------------------- +CURRENT_CASE="ambiguous-multi-session" +CASES=$((CASES + 1)) +setup_fixture +emit "session.start" "sess-X" "" +emit "session.start" "sess-Y" "" +run_resolve 0 "" +assert_eq "0" "$RC" "$CURRENT_CASE rc" +assert_eq "" "$STDOUT" "$CURRENT_CASE omits when ambiguous" +teardown_fixture + +# ---------------------------------------------------------------------- +# Case 4: missing events.jsonl — omit, exit 0 +# ---------------------------------------------------------------------- +CURRENT_CASE="missing-events" +CASES=$((CASES + 1)) +setup_fixture +# No emit — events.jsonl absent. +run_resolve 1 "m1" +assert_eq "0" "$RC" "$CURRENT_CASE rc" +assert_eq "" "$STDOUT" "$CURRENT_CASE omits when events.jsonl absent" +teardown_fixture + +# ---------------------------------------------------------------------- +# Case 5: marker set but no match — falls back to single un-ended session +# ---------------------------------------------------------------------- +CURRENT_CASE="marker-no-match-fallback" +CASES=$((CASES + 1)) +setup_fixture +emit "session.start" "sess-fb" "" +run_resolve 1 "does-not-exist" +assert_eq "0" "$RC" "$CURRENT_CASE rc" +assert_eq "sess-fb" "$STDOUT" "$CURRENT_CASE falls back when marker unmatched" +teardown_fixture + +# ---------------------------------------------------------------------- +# Case 6: ended session excluded from fallback candidates +# ---------------------------------------------------------------------- +CURRENT_CASE="ended-session-excluded" +CASES=$((CASES + 1)) +setup_fixture +emit "session.start" "sess-1" "" +emit "session.end" "sess-1" "" +emit "session.start" "sess-2" "" +run_resolve 0 "" +assert_eq "0" "$RC" "$CURRENT_CASE rc" +assert_eq "sess-2" "$STDOUT" "$CURRENT_CASE only the un-ended session resolves" +teardown_fixture + +# ---------------------------------------------------------------------- +# Case 7: marker match wins even when multiple sessions are live +# ---------------------------------------------------------------------- +CURRENT_CASE="marker-wins-over-ambiguity" +CASES=$((CASES + 1)) +setup_fixture +emit "session.start" "sess-P" '{"marker":"mk"}' +emit "session.start" "sess-Q" "" +run_resolve 1 "mk" +assert_eq "0" "$RC" "$CURRENT_CASE rc" +assert_eq "sess-P" "$STDOUT" "$CURRENT_CASE marker match beats ambiguity" +teardown_fixture + +# ---------------------------------------------------------------------- +# Case 8: marker prefix must not false-match (m1 vs m10) +# ---------------------------------------------------------------------- +CURRENT_CASE="marker-prefix-no-false-match" +CASES=$((CASES + 1)) +setup_fixture +emit "session.start" "sess-ten" '{"marker":"m10"}' +run_resolve 1 "m1" +assert_eq "0" "$RC" "$CURRENT_CASE rc" +# No exact marker match for m1; single un-ended session falls back to sess-ten. +assert_eq "sess-ten" "$STDOUT" "$CURRENT_CASE m1 does not match m10 (falls back)" +teardown_fixture + + +# ---------------------------------------------------------------------- +# Case 9: start→end→start same session id (no marker) — last event is start +# Documented invariant: un-ended means last event for that id is session.start +# ---------------------------------------------------------------------- +CURRENT_CASE="restart-same-session-last-event-start" +CASES=$((CASES + 1)) +setup_fixture +emit "session.start" "sess-R" "" +emit "session.end" "sess-R" "" +emit "session.start" "sess-R" "" +run_resolve 0 "" +assert_eq "0" "$RC" "$CURRENT_CASE rc" +assert_eq "sess-R" "$STDOUT" "$CURRENT_CASE resolves restarted un-ended session" +teardown_fixture + +# ---------------------------------------------------------------------- +# Case 10: two sessions both last-event start (multi live) — omit +# Reinforces last-event semantics do not change multi-candidate omit rule +# ---------------------------------------------------------------------- +CURRENT_CASE="multi-live-last-event-start-omit" +CASES=$((CASES + 1)) +setup_fixture +emit "session.start" "sess-L1" "" +emit "session.end" "sess-L1" "" +emit "session.start" "sess-L1" "" +emit "session.start" "sess-L2" "" +run_resolve 0 "" +assert_eq "0" "$RC" "$CURRENT_CASE rc" +assert_eq "" "$STDOUT" "$CURRENT_CASE omits when multiple last-event-start sessions" +teardown_fixture + +# ---------------------------------------------------------------------- +# Summary +# ---------------------------------------------------------------------- +if [ "$FAILED" -ne 0 ]; then + echo "resolve-session.test.sh: $FAILED assertion(s) failed across $CASES cases" >&2 + exit 1 +fi +echo "resolve-session.test.sh: all $CASES cases passed" +exit 0 diff --git a/lib/tests/run-all.sh b/lib/tests/run-all.sh new file mode 100755 index 0000000..10e2339 --- /dev/null +++ b/lib/tests/run-all.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# Aggregate test runner for do-work's lib/ coordination primitives. +# +# Runs every plain-bash `*.test.sh` in this directory, plus every `*.bats` +# suite when the `bats` binary is available (otherwise it reports them as +# skipped rather than failing). Prints a one-line-per-suite result and a +# final summary, and exits non-zero if any suite fails. +# +# This is the canonical "run the whole suite" command: +# +# bash lib/tests/run-all.sh +# +# Compatible with macOS bash 3.2. + +set -u + +TESTS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +pass=0 +fail=0 +skip=0 +failed_suites="" + +run_suite() { + local name="$1" + shift + local out + if out="$( "$@" 2>&1 )"; then + printf 'PASS %s\n' "$name" + pass=$(( pass + 1 )) + else + printf 'FAIL %s\n' "$name" + printf '%s\n' "$out" | sed 's/^/ /' + fail=$(( fail + 1 )) + failed_suites="$failed_suites $name" + fi +} + +# Plain-bash suites — the canonical style. `*.fixture.sh` and helpers are +# excluded by the `*.test.sh` glob. +for f in "$TESTS_DIR"/*.test.sh; do + [ -e "$f" ] || continue + run_suite "$( basename "$f" )" bash "$f" +done + +# Bats suites — run only if bats is installed; never fail the run for a +# missing optional dependency. +bats_files="" +for f in "$TESTS_DIR"/*.bats; do + [ -e "$f" ] || continue + bats_files="$bats_files $f" +done + +if [ -n "$bats_files" ]; then + if command -v bats >/dev/null 2>&1; then + for f in $bats_files; do + run_suite "$( basename "$f" )" bats "$f" + done + else + for f in $bats_files; do + printf 'SKIP %s (bats not installed)\n' "$( basename "$f" )" + skip=$(( skip + 1 )) + done + fi +fi + +echo "-----------------------------------------" +printf 'Suites: %d passed, %d failed, %d skipped\n' "$pass" "$fail" "$skip" +if [ "$fail" -ne 0 ]; then + printf 'Failed:%s\n' "$failed_suites" + exit 1 +fi +exit 0 diff --git a/lib/tests/session-hook.test.sh b/lib/tests/session-hook.test.sh new file mode 100755 index 0000000..cdc71ea --- /dev/null +++ b/lib/tests/session-hook.test.sh @@ -0,0 +1,252 @@ +#!/usr/bin/env bash +# Tests for lib/session-hook.sh +# Plain bash (no bats dependency). Compatible with macOS bash 3.2. + +set -u + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +LIB_DIR="$( cd "$SCRIPT_DIR/.." && pwd )" +SCRIPT="$LIB_DIR/session-hook.sh" + +FAILED=0 +CURRENT_CASE="" + +fail() { + echo "FAIL [$CURRENT_CASE]: $*" >&2 + FAILED=$((FAILED + 1)) +} + +assert_eq() { + local expected="$1" actual="$2" label="$3" + if [ "$expected" != "$actual" ]; then + fail "$label: expected '$expected', got '$actual'" + fi +} + +setup() { TMP="$(mktemp -d -t session-hook-test.XXXXXX)"; } +teardown() { [ -n "${TMP:-}" ] && [ -d "$TMP" ] && rm -rf "$TMP"; } + +EVENTS() { echo "$TMP/.do-work/state/events.jsonl"; } + +# --- start with .do-work + marker (mirrors REQ verification step 2) --------- +CURRENT_CASE="start emits session.start with marker" +setup +mkdir -p "$TMP/.do-work" +out=$( cd "$TMP" && echo '{"session_id":"test-123"}' | DO_WORK_UI_MARKER=m1 bash "$SCRIPT" start ) +rc=$? +assert_eq 0 "$rc" "exit code" +line="$(tail -n1 "$(EVENTS)")" +case "$line" in + *'"session":"test-123"'*) : ;; + *) fail "session missing: $line" ;; +esac +case "$line" in + *'"type":"session.start"'*) : ;; + *) fail "type wrong: $line" ;; +esac +case "$line" in + *'"data":{"marker":"m1"}'*) : ;; + *) fail "marker data missing: $line" ;; +esac +teardown + +# --- start without marker: no data field ----------------------------------- +CURRENT_CASE="start without marker omits data" +setup +mkdir -p "$TMP/.do-work" +# Explicitly clear the marker — it may be set in the ambient environment when +# this session was itself spawned by the extension. +( cd "$TMP" && echo '{"session_id":"s2"}' | env -u DO_WORK_UI_MARKER bash "$SCRIPT" start ) +line="$(tail -n1 "$(EVENTS)")" +case "$line" in + *data*) fail "data should be absent without marker: $line" ;; +esac +case "$line" in + *'"type":"session.start"'*) : ;; + *) fail "type wrong: $line" ;; +esac +teardown + +# --- end emits session.end ------------------------------------------------- +CURRENT_CASE="end emits session.end" +setup +mkdir -p "$TMP/.do-work" +( cd "$TMP" && echo '{"session_id":"s3"}' | bash "$SCRIPT" end ) +line="$(tail -n1 "$(EVENTS)")" +case "$line" in + *'"type":"session.end"'*) : ;; + *) fail "type wrong: $line" ;; +esac +case "$line" in + *'"session":"s3"'*) : ;; + *) fail "session wrong: $line" ;; +esac +teardown + +# --- no .do-work: exit 0, no file written (REQ verification step 3) --------- +CURRENT_CASE="no .do-work is a silent no-op" +setup +# TMP has NO .do-work/ directory. +out=$( cd "$TMP" && echo '{"session_id":"test-123"}' | DO_WORK_UI_MARKER=m1 bash "$SCRIPT" start ) +rc=$? +assert_eq 0 "$rc" "exit code without .do-work" +[ -e "$TMP/.do-work" ] && fail ".do-work must not be created" +[ -e "$(EVENTS)" ] && fail "events.jsonl must not be created" +teardown + +# --- missing session_id: no-op exit 0 -------------------------------------- +CURRENT_CASE="missing session_id is a no-op" +setup +mkdir -p "$TMP/.do-work" +( cd "$TMP" && echo '{"cwd":"'"$TMP"'"}' | bash "$SCRIPT" start ) +rc=$? +assert_eq 0 "$rc" "exit code" +[ -e "$(EVENTS)" ] && fail "no event should be written without session_id" +teardown + +# --- cwd from payload resolves the project --------------------------------- +CURRENT_CASE="cwd from payload resolves project" +setup +mkdir -p "$TMP/.do-work" +# Invoke from an unrelated dir; project comes from payload cwd. +( cd / && printf '{"session_id":"s4","cwd":"%s"}' "$TMP" | bash "$SCRIPT" start ) +[ -f "$(EVENTS)" ] || fail "event should be written to payload cwd project" +teardown + +# --- bad mode never fails the session -------------------------------------- +CURRENT_CASE="unknown mode exits 0" +setup +echo '{"session_id":"s5"}' | bash "$SCRIPT" bogus >/dev/null 2>&1 +assert_eq 0 "$?" "unknown mode still exits 0" +teardown + +# --- helper: write a fixture JSONL transcript whose LAST assistant model is $2 +write_transcript() { + # $1 = path, $2 = last assistant model id + cat > "$1" < no data.model key -------------------------- +CURRENT_CASE="start omits model when none available" +setup +mkdir -p "$TMP/.do-work" +( cd "$TMP" && printf '{"session_id":"t3","transcript_path":"/dev/null"}' | env -u DO_WORK_UI_MARKER bash "$SCRIPT" start ) +line="$(tail -n1 "$(EVENTS)")" +case "$line" in + *'"model"'*) fail "model should be absent: $line" ;; +esac +case "$line" in + *data*) fail "data should be absent with no marker and no model: $line" ;; +esac +teardown + +# --- model composes with marker ------------------------------------------- +CURRENT_CASE="start composes marker + model in data" +setup +mkdir -p "$TMP/.do-work" +( cd "$TMP" && echo '{"session_id":"t4","model":"claude-opus-4-8","transcript_path":"/dev/null"}' | DO_WORK_UI_MARKER=m9 bash "$SCRIPT" start ) +line="$(tail -n1 "$(EVENTS)")" +case "$line" in + *'"marker":"m9"'*) : ;; + *) fail "marker missing when composing: $line" ;; +esac +case "$line" in + *'"model":"claude-opus-4-8"'*) : ;; + *) fail "model missing when composing: $line" ;; +esac +teardown + +# --- AC3+AC4: Stop emits one model.change on diff, none when unchanged ------ +CURRENT_CASE="stop model.change once on diff, none when unchanged" +setup +mkdir -p "$TMP/.do-work" +write_transcript "$TMP/tr.jsonl" "claude-sonnet-5" +# session.start records opus +( cd "$TMP" && echo '{"session_id":"m1","model":"claude-opus-4-8","transcript_path":"/dev/null"}' | env -u DO_WORK_UI_MARKER bash "$SCRIPT" start ) +# stop 1: transcript says sonnet (differs) -> one model.change +( cd "$TMP" && printf '{"session_id":"m1","transcript_path":"%s"}' "$TMP/tr.jsonl" | bash "$SCRIPT" end ) +# stop 2: same transcript, model now matches recorded -> no new model.change +( cd "$TMP" && printf '{"session_id":"m1","transcript_path":"%s"}' "$TMP/tr.jsonl" | bash "$SCRIPT" end ) +count="$(grep -c 'model.change' "$(EVENTS)" | tr -d ' ')" +assert_eq 1 "$count" "exactly one model.change total" +mc="$(grep 'model.change' "$(EVENTS)")" +case "$mc" in + *'"model":"claude-sonnet-5"'*) : ;; + *) fail "model.change should carry new model: $mc" ;; +esac +ss="$(grep 'session.start' "$(EVENTS)")" +case "$ss" in + *'"model":"claude-opus-4-8"'*) : ;; + *) fail "session.start should carry opus: $ss" ;; +esac +# session.end still emitted per turn (regression guard) +case "$(grep -c 'session.end' "$(EVENTS)" | tr -d ' ')" in + 2) : ;; + *) fail "expected two session.end lines, one per stop" ;; +esac +teardown + +# --- no model determinable -> no model.change, session.end still emitted ---- +CURRENT_CASE="stop with no determinable model emits no model.change" +setup +mkdir -p "$TMP/.do-work" +( cd "$TMP" && printf '{"session_id":"n1","transcript_path":"/dev/null"}' | bash "$SCRIPT" end ) +[ -f "$(EVENTS)" ] || fail "session.end should still be written" +case "$(grep -c 'model.change' "$(EVENTS)" | tr -d ' ')" in + 0) : ;; + *) fail "no model.change when model undeterminable" ;; +esac +teardown + +# --- AC5: end mode with model payload but no .do-work -> exit 0, no writes --- +CURRENT_CASE="stop with model but no .do-work is a no-op" +setup +write_transcript "$TMP/tr.jsonl" "claude-sonnet-5" +out=$( cd "$TMP" && printf '{"session_id":"g1","transcript_path":"%s"}' "$TMP/tr.jsonl" | bash "$SCRIPT" end ) +rc=$? +assert_eq 0 "$rc" "exit 0 without .do-work" +[ -e "$TMP/.do-work" ] && fail ".do-work must not be created" +teardown + +# --- summary --------------------------------------------------------------- +if [ "$FAILED" -eq 0 ]; then + echo "session-hook.test.sh: all cases passed" + exit 0 +else + echo "session-hook.test.sh: $FAILED assertion(s) failed" >&2 + exit 1 +fi diff --git a/lib/tests/stamp-session.test.sh b/lib/tests/stamp-session.test.sh new file mode 100755 index 0000000..bad4740 --- /dev/null +++ b/lib/tests/stamp-session.test.sh @@ -0,0 +1,356 @@ +#!/usr/bin/env bash +# Tests for lib/stamp-session.sh +# Plain bash (no bats dependency). Exit non-zero on first failure. +# Compatible with macOS bash 3.2. +# +# IMPORTANT: All fixtures are created under mktemp -d. We never touch the +# real .do-work/working/ tree. + +set -u + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +LIB_DIR="$( cd "$SCRIPT_DIR/.." && pwd )" +STAMP_SESSION="$LIB_DIR/stamp-session.sh" + +FAILED=0 +CASES=0 +CURRENT_CASE="" + +fail() { + echo "FAIL [$CURRENT_CASE]: $*" >&2 + FAILED=$((FAILED + 1)) +} + +assert_eq() { + local expected="$1" + local actual="$2" + local label="$3" + if [ "$expected" != "$actual" ]; then + fail "$label: expected '$expected', got '$actual'" + fi +} + +assert_contains() { + local needle="$1" + local haystack="$2" + local label="$3" + case "$haystack" in + *"$needle"*) : ;; + *) fail "$label: expected substring '$needle' in '$haystack'" ;; + esac +} + +setup_fixture() { + TMP="$(mktemp -d -t stamp-session-test.XXXXXX)" + mkdir -p "$TMP/.do-work/working" "$TMP/.do-work/archive" "$TMP/.do-work/state" +} + +teardown_fixture() { + if [ -n "${TMP:-}" ] && [ -d "$TMP" ]; then + rm -rf "$TMP" + fi + TMP="" +} + +# Claim stamp WITH Heartbeat, WITHOUT Session. +write_req_no_session() { + local path="$1" + cat > "$path" <<'EOF' +# REQ-999: Sample REQ + + +**Claimed by:** test-agent.1234 +**Claimed at:** 2026-05-21T01:00:00Z +**Heartbeat:** 2026-05-21T01:00:00Z + + +**UR:** UR-001 +**Status:** in-progress +**Created:** 2026-05-21 +**Layer:** agents +**Files:** lib/stamp-session.sh +**Depends on:** + +## Task + +Do the thing. +EOF +} + +# Claim stamp WITH Heartbeat and Session. +write_req_with_session() { + local path="$1" + cat > "$path" <<'EOF' +# REQ-999: Sample REQ + + +**Claimed by:** test-agent.1234 +**Claimed at:** 2026-05-21T01:00:00Z +**Heartbeat:** 2026-05-21T01:00:00Z +**Session:** sess-old + + +**UR:** UR-001 +**Status:** in-progress +**Created:** 2026-05-21 +**Layer:** agents +**Files:** lib/stamp-session.sh +**Depends on:** + +## Task + +Do the thing. +EOF +} + +# Claim stamp markers absent entirely. +write_req_no_claim() { + local path="$1" + cat > "$path" <<'EOF' +# REQ-999: Sample REQ + +**UR:** UR-001 +**Status:** in-progress +**Created:** 2026-05-21 +**Layer:** agents +**Files:** lib/stamp-session.sh +**Depends on:** + +## Task + +Do the thing. +EOF +} + +# Run stamp-session.sh. Stores SS_RC, SS_STDOUT, SS_STDERR. +# Args after req path are forwarded (optional session-id). +run_stamp() { + local req_path="$1" + shift + local err_file="$TMP/.stderr.$$" + local out_file="$TMP/.stdout.$$" + "$STAMP_SESSION" "$req_path" "$@" > "$out_file" 2> "$err_file" + SS_RC=$? + SS_STDOUT="$(cat "$out_file" 2>/dev/null || true)" + SS_STDERR="$(cat "$err_file" 2>/dev/null || true)" + rm -f "$err_file" "$out_file" +} + +read_session() { + grep -m1 '^\*\*Session:\*\*' "$1" | sed 's/^\*\*Session:\*\* //' +} + +count_session() { + grep -c '^\*\*Session:\*\*' "$1" 2>/dev/null || echo 0 +} + +# Extract content between claim fences (exclusive of markers). +claim_between() { + awk ' + /^$/ { inblock=1; next } + /^$/ { inblock=0 } + inblock { print } + ' "$1" +} + +# ---------------------------------------------------------------------- +# Case 1: insert Session when absent (explicit session-id) +# ---------------------------------------------------------------------- +CURRENT_CASE="insert-when-absent" +CASES=$((CASES + 1)) +setup_fixture +REQ="$TMP/.do-work/working/REQ-999-foo.md" +write_req_no_session "$REQ" + +run_stamp "$REQ" "sess-new-1" +assert_eq "0" "$SS_RC" "$CURRENT_CASE rc" + +cnt="$(count_session "$REQ")" +assert_eq "1" "$cnt" "$CURRENT_CASE session count" + +sid="$(read_session "$REQ")" +assert_eq "sess-new-1" "$sid" "$CURRENT_CASE session value" + +content="$(cat "$REQ")" +case "$content" in + *"**Session:** sess-new-1"*""*) : ;; + *) fail "$CURRENT_CASE: Session not positioned before claimed-end" ;; +esac + +between="$(claim_between "$REQ")" +assert_contains "**Session:** sess-new-1" "$between" "$CURRENT_CASE Session inside claim fences" +assert_contains "**Claimed by:** test-agent.1234" "$content" "$CURRENT_CASE Claimed by preserved" +assert_contains "**Heartbeat:** 2026-05-21T01:00:00Z" "$content" "$CURRENT_CASE Heartbeat preserved" + +teardown_fixture + +# ---------------------------------------------------------------------- +# Case 2: replace existing Session (explicit session-id) +# ---------------------------------------------------------------------- +CURRENT_CASE="replace-existing" +CASES=$((CASES + 1)) +setup_fixture +REQ="$TMP/.do-work/working/REQ-999-foo.md" +write_req_with_session "$REQ" + +original_sid="$(read_session "$REQ")" +assert_eq "sess-old" "$original_sid" "$CURRENT_CASE original session" + +run_stamp "$REQ" "sess-new-2" +assert_eq "0" "$SS_RC" "$CURRENT_CASE rc" + +cnt="$(count_session "$REQ")" +assert_eq "1" "$cnt" "$CURRENT_CASE session still single" + +new_sid="$(read_session "$REQ")" +assert_eq "sess-new-2" "$new_sid" "$CURRENT_CASE session replaced" + +# Ownership / heartbeat untouched. +claimed_by="$(grep -m1 '^\*\*Claimed by:\*\*' "$REQ" | sed 's/^\*\*Claimed by:\*\* //')" +assert_eq "test-agent.1234" "$claimed_by" "$CURRENT_CASE Claimed by unchanged" +hb="$(grep -m1 '^\*\*Heartbeat:\*\*' "$REQ" | sed 's/^\*\*Heartbeat:\*\* //')" +assert_eq "2026-05-21T01:00:00Z" "$hb" "$CURRENT_CASE Heartbeat unchanged" + +teardown_fixture + +# ---------------------------------------------------------------------- +# Case 3: empty resolve leaves existing Session line untouched +# ---------------------------------------------------------------------- +CURRENT_CASE="empty-resolve-preserve" +CASES=$((CASES + 1)) +setup_fixture +REQ="$TMP/.do-work/working/REQ-999-foo.md" +write_req_with_session "$REQ" + +# No events.jsonl → resolve-session prints nothing. Omit session-id so the +# script must call resolve-session; empty result must leave sess-old alone. +run_stamp "$REQ" +assert_eq "0" "$SS_RC" "$CURRENT_CASE rc" + +cnt="$(count_session "$REQ")" +assert_eq "1" "$cnt" "$CURRENT_CASE session still single" + +sid="$(read_session "$REQ")" +assert_eq "sess-old" "$sid" "$CURRENT_CASE session preserved when resolve empty" + +teardown_fixture + +# ---------------------------------------------------------------------- +# Case 4: omit session-id + resolvable session inserts/replaces via resolve +# ---------------------------------------------------------------------- +CURRENT_CASE="resolve-and-insert" +CASES=$((CASES + 1)) +setup_fixture +REQ="$TMP/.do-work/working/REQ-999-foo.md" +write_req_no_session "$REQ" + +# Single un-ended session in events.jsonl so resolve-session prints it. +cat > "$TMP/.do-work/state/events.jsonl" <<'EOF' +{"type":"session.start","session":"sess-resolved","ts":"2026-05-21T01:00:00Z"} +EOF + +run_stamp "$REQ" +assert_eq "0" "$SS_RC" "$CURRENT_CASE rc" + +sid="$(read_session "$REQ")" +assert_eq "sess-resolved" "$sid" "$CURRENT_CASE resolved session stamped" + +teardown_fixture + +# ---------------------------------------------------------------------- +# Case 5: missing claim stamp → exit non-zero +# ---------------------------------------------------------------------- +CURRENT_CASE="missing-claim-stamp-fail" +CASES=$((CASES + 1)) +setup_fixture +REQ="$TMP/.do-work/working/REQ-999-foo.md" +write_req_no_claim "$REQ" + +run_stamp "$REQ" "sess-x" +case "$SS_RC" in + 0) fail "$CURRENT_CASE: expected non-zero exit, got 0" ;; + *) : ;; +esac +case "$SS_STDERR" in + *claim*|*stamp*|*claimed*) + : ;; + *) + fail "$CURRENT_CASE: stderr should mention missing claim stamp, got: $SS_STDERR" ;; +esac +# File must not gain a Session line or claim block. +if grep -q '^\*\*Session:\*\*' "$REQ" 2>/dev/null; then + fail "$CURRENT_CASE: must not invent Session without claim stamp" +fi +if grep -q 'claimed-start' "$REQ" 2>/dev/null; then + fail "$CURRENT_CASE: must not create claim stamp" +fi + +teardown_fixture + +# ---------------------------------------------------------------------- +# Case 6: path outside working/ → exit non-zero +# ---------------------------------------------------------------------- +CURRENT_CASE="outside-working-fail" +CASES=$((CASES + 1)) +setup_fixture +REQ="$TMP/.do-work/REQ-999-stray.md" +write_req_no_session "$REQ" + +run_stamp "$REQ" "sess-x" +case "$SS_RC" in + 0) fail "$CURRENT_CASE: expected non-zero exit, got 0" ;; + *) : ;; +esac +case "$SS_STDERR" in + *working*|*outside*) + : ;; + *) + fail "$CURRENT_CASE: stderr should warn about non-working path, got: $SS_STDERR" ;; +esac + +teardown_fixture + +# ---------------------------------------------------------------------- +# Case 7: missing file → exit non-zero +# ---------------------------------------------------------------------- +CURRENT_CASE="missing-file-fail" +CASES=$((CASES + 1)) +setup_fixture +MISSING="$TMP/.do-work/working/REQ-404-not-here.md" + +run_stamp "$MISSING" "sess-x" +assert_eq "1" "$SS_RC" "$CURRENT_CASE rc=1" +case "$SS_STDERR" in + *not\ found*|*does\ not\ exist*|*missing*|*REQ-404*) + : ;; + *) + fail "$CURRENT_CASE: stderr should mention missing file, got: $SS_STDERR" ;; +esac + +teardown_fixture + +# ---------------------------------------------------------------------- +# Case 8: empty explicit session-id same as omit — leave untouched when resolve empty +# ---------------------------------------------------------------------- +CURRENT_CASE="empty-arg-preserve" +CASES=$((CASES + 1)) +setup_fixture +REQ="$TMP/.do-work/working/REQ-999-foo.md" +write_req_with_session "$REQ" + +run_stamp "$REQ" "" +assert_eq "0" "$SS_RC" "$CURRENT_CASE rc" +sid="$(read_session "$REQ")" +assert_eq "sess-old" "$sid" "$CURRENT_CASE empty arg leaves session" + +teardown_fixture + +# ---------------------------------------------------------------------- +# Summary +# ---------------------------------------------------------------------- +echo "" +echo "Ran $CASES cases. Failures: $FAILED" +if [ "$FAILED" -gt 0 ]; then + exit 1 +fi +exit 0 diff --git a/lib/tests/synth-status.test.sh b/lib/tests/synth-status.test.sh index 2a033e6..283444f 100755 --- a/lib/tests/synth-status.test.sh +++ b/lib/tests/synth-status.test.sh @@ -282,6 +282,70 @@ if [ "$ELAPSED" -gt 10 ]; then fi teardown_fixture +# ---------------------------------------------------------------------- +# Case 8: unscoped archive is capped; totals still count all archived. +# ---------------------------------------------------------------------- +CURRENT_CASE="archive-cap" +CASES=$((CASES + 1)) +setup_fixture +i=1 +while [ "$i" -le 20 ]; do + # Zero-pad so glob order matches numeric order (REQ-01 … REQ-20). + id="$(printf 'REQ-%03d' "$i")" + write_archive_req "$TMP/.do-work/archive/${id}-a.md" "$id" "UR-020" + i=$((i + 1)) +done +run_synth +assert_eq "0" "$RC" "$CURRENT_CASE rc=0" +assert_contains "archived=20" "$STDOUT" "$CURRENT_CASE totals count all 20" +assert_contains "No live work" "$STDOUT" "$CURRENT_CASE idle cue" +assert_contains "and 5 more archived" "$STDOUT" "$CURRENT_CASE cap note" +assert_contains "REQ-020" "$STDOUT" "$CURRENT_CASE newest archive row shown" +assert_not_contains "REQ-001" "$STDOUT" "$CURRENT_CASE oldest archive row hidden" +teardown_fixture + +# ---------------------------------------------------------------------- +# Case 9: archive bucket always shows status done (stale header ignored). +# ---------------------------------------------------------------------- +CURRENT_CASE="archive-status-done" +CASES=$((CASES + 1)) +setup_fixture +cat > "$TMP/.do-work/archive/REQ-900-stale-header.md" <] + +Execute the backlog autonomously — until empty or a stopper is hit. The optional `UR-NNN` argument scopes execution to that UR's REQs only, ignoring all other backlog entries. The orchestrator dispatches a fresh worker subagent per REQ (see [agents/run-worker.md](../agents/run-worker.md)) and reads its structured return report. + +By default the orchestrator runs **serially** — one REQ at a time. The optional `--parallel N` flag enables **single-session parallel mode**: one orchestrator dispatches up to `N` concurrent workers from a single terminal, then serializes their integration through a merge queue. See [Parallel Execution](concepts.md#parallel-execution) (single-session parallel mode). + +- `N` is the maximum number of concurrently dispatched workers (the window width). Effective `N = min(flag-or-config, 10)`. +- **Default `N = 1`** (absent flag, `--parallel 1`, or `parallel.max_workers: 1`) ⇒ the serial loop runs byte-for-byte unchanged. +- When `--parallel` is absent, the default comes from **`parallel.max_workers`** in `.do-work/config.yml` (default `1`). The flag overrides config per-run; config sets the project default. +- A request above the cap (e.g. `--parallel 20`) is clamped to `10` with a one-line notice. + +**Budget (`--budget `).** `--budget` caps the run's cumulative **estimated** model spend (a tier-weighted dollar estimate per worker attempt, recorded in the ledger's `cost_estimate_num` field — not a metered bill). It overrides `cost.budget` from `.do-work/config.yml` for this invocation only. + +- **Stop semantics.** After each worker attempt's ledger write, the orchestrator sums estimated spend (`lib/run-ledger.sh --sum-run`) and compares it to the budget. When estimated spend reaches the budget, it **finishes the in-flight REQ's integration first** (never abandons a mid-merge), then stops gracefully at the next REQ boundary with a **budget-stop report** (estimated spend vs budget, REQs completed vs remaining). It never silently exceeds an explicit budget. +- **Empty / unset budget ⇒ unlimited** (default behaviour, unchanged). The gate is inert and adds no per-run cost. +- Under `--parallel N`, the same gate rides the merge queue: once the budget is reached the orchestrator stops refilling the window and lets live workers drain, then emits the budget-stop report. + +1. Detect `{project}`. +2. Determine UR scope: + - If `UR-NNN` was provided, record it — the run agent will filter the backlog to that UR. + - If not provided, the full backlog is in scope. +3. Resolve the parallel window width: `--parallel N` if given, else `parallel.max_workers` (default 1), clamped to 10. `N == 1` runs serial; `N > 1` runs `agents/run.md`'s `## Parallel Run Mode`. Resolve the effective budget: `--budget ` if given, else `cost.budget` (empty = unlimited). +4. Pre-flight checks: + - Working/ files are classified by agents/run.md's pre-flight (mine/sibling/stale buckets); stale slots are surfaced only when the backlog has no claimable REQ — do not prompt merely because working/ is non-empty. + - If no `REQ-NNN-*.md` files exist in `{project}/.do-work/` (backlog root) within scope, report "Backlog is empty." and stop. +5. Read [agents/run.md](../agents/run.md) in full. +6. Follow the run agent instructions exactly, passing through the UR scope, the resolved parallel window width, and the effective budget. + +--- + +### status [UR-NNN] + +Render a read-only live situation room: all in-flight REQs, their claimers, heartbeat ages, any deadlock warnings, and a Coverage section showing intended/proven/unproven REQs. + +1. Detect `{project}`. +2. Determine UR scope: + - If `UR-NNN` was provided, pass it through to scope the report to that UR's REQs. + - If not provided, all in-flight REQs are reported. +3. Confirm `{project}/.do-work/` exists. If not, report "do-work not installed." and stop. +4. Read [agents/status.md](../agents/status.md) in full. +5. Follow the status agent instructions exactly. No state changes, no commits, no prompts. + +--- + +### close UR-NNN + +Validate the integrated result of a UR against its verbatim brief — walking every path-unit's entry point to its terminal state in the merged app — and write a per-path-unit closure report. + +1. Detect `{project}`. +2. Confirm `UR-NNN` was provided. If not, report "close requires a UR id (e.g. /do-work close UR-042)." and stop. +3. Confirm `{project}/.do-work/user-requests/UR-NNN/input.md` exists. If not, report "UR-NNN not found at {project}/.do-work/user-requests/UR-NNN/. Check the UR number and try again." and stop. +4. Read [agents/close.md](../agents/close.md) in full. +5. Follow the close agent instructions exactly. The close agent is dispatched as a fresh subagent — pass only the project do-work path, the UR reference, and the merged branch. + +--- + +### unblock REQ-NNN + +Force a stuck in-flight REQ out of `working/` and back into the backlog. Use when a worker died, a concurrent-conflict won't resolve, or scope creep needs human triage. + +1. Detect `{project}`. +2. Confirm `REQ-NNN` was provided. If not, report "unblock requires a REQ id (e.g. /do-work unblock REQ-042)." and stop. +3. Confirm `{project}/.do-work/working/REQ-NNN-*.md` exists. If not, report "REQ-NNN is not in working/ — nothing to unblock." and stop. +4. Read [agents/unblock.md](../agents/unblock.md) in full. +5. Follow the unblock agent instructions exactly, including the judgment gate on partial commits (Step 3 in the agent file). + +--- + +### resume REQ-NNN + +Re-dispatch a fresh worker for a stopped REQ without sending it back through the backlog. Preserves the existing claim stamp; only the heartbeat is refreshed. + +1. Detect `{project}`. +2. Confirm `REQ-NNN` was provided. If not, report "resume requires a REQ id (e.g. /do-work resume REQ-042)." and stop. +3. Confirm `{project}/.do-work/working/REQ-NNN-*.md` exists and its `**Status:**` is `stopped`. If not in `working/`, report "REQ-NNN is not in working/ — nothing to resume." If status is not `stopped`, report the actual status and stop. +4. Read [agents/resume.md](../agents/resume.md) in full. +5. Follow the resume agent instructions exactly. Resume is a one-shot — do not loop back to the backlog after dispatch. + +--- + +### log + +Generate build-in-public draft posts for configured social media platforms. + +1. Detect `{project}`. +2. Read [agents/log.md](../agents/log.md) in full. +3. Follow the log agent instructions exactly. + +--- + +### retro + +Mine the run ledger and feedback fingerprints; produce a human report; regenerate `.do-work/state/calibration.md` as advisory capture guidance. + +1. Detect `{project}`. +2. Confirm `{project}/.do-work/` exists. If not, report "do-work not installed." and stop. +3. Read [agents/retro.md](../agents/retro.md) in full. +4. Follow the retro agent instructions exactly. Pass the resolved `{project}/.do-work/` path as the project do-work path. diff --git a/references/concepts.md b/references/concepts.md new file mode 100644 index 0000000..27c33ad --- /dev/null +++ b/references/concepts.md @@ -0,0 +1,287 @@ +# Concepts reference + +On-demand detail for naming, milestones, parallel execution, layers, path-units, decisions, REQ schema, commits, and checkpointed verification. Read from `SKILL.md` when needed; do not load unless the active subcommand requires it. + +## File Naming + +- User requests: `UR-001`, `UR-002`, ... (zero-padded to 3 digits) +- Feature requests: `REQ-001-short-slug.md`, `REQ-002-short-slug.md`, ... +- Slugs are lowercase kebab-case, max 5 words + +## Milestone Mode + +When a UR file contains both: + +1. The marker `source: /saas-thesis handoff` (in frontmatter or body) +2. A `### Milestones` heading with `#### M1` (or higher) subheadings + +`/do-work` enters **milestone mode**. The differences from normal flow: + +- Capture decomposes ONE milestone at a time, not the whole UR. +- REQ files are prefixed: `REQ-M1-001-.md`, `REQ-M2-001-.md`. +- Run loop halts at the end of each milestone's REQs and prompts for the deploy gate. +- Deploy-gate sign-off is non-delegable human confirmation. +- State files in `{project}/.do-work/state/`: + - `active-milestone.md` — single line, current milestone identifier (e.g. `M1`). + - `milestones.md` — checklist of all milestones with status: `pending` / `captured` / `running` / `deployed`. + +Milestone mode is **implicit** — triggered by UR shape, not a flag. URs that do not match the trigger continue to behave as before. The `/saas-thesis` skill produces UR files with the correct shape for handoff. + +## Parallel Execution + +do-work offers parallelism two complementary ways. **Multi-terminal mode** (below) needs no flag — launch `/do-work run` from several terminals and the coordination layer keeps them from colliding. **Single-session parallel mode** (`--parallel N`) lets one terminal fan out N concurrent workers itself. The two compose: one `--parallel 3` orchestrator and two plain `/do-work run` terminals are just three agent-ids in the same claim arbitration. + +`/do-work run [UR-NNN]` is safe to launch from multiple terminals simultaneously. Up to 10 orchestrators can run in parallel — each claims a different REQ from the backlog. Coordination is handled by a dedicated library layer; no flag, no daemon, no in-memory state is required for multi-terminal mode. + +### Single-session parallel mode (`--parallel N`) + +`/do-work run [UR-NNN] --parallel N` makes **one** orchestrator dispatch up to `N` concurrent workers from a single terminal, integrating their results through a serialized merge queue. It does not replace multi-terminal mode — it adds one-terminal parallelism on top of the same coordination primitives. + +- **Window width `N`.** The maximum number of concurrently dispatched workers. Effective `N = min(flag-or-config, 10)`. +- **Default `N = 1`** (absent flag, `--parallel 1`, or `parallel.max_workers: 1`) ⇒ the serial loop runs byte-for-byte unchanged; the parallel code path is entered only when `N > 1`. +- **Config default.** `parallel.max_workers` in `.do-work/config.yml` (default `1`, under the existing `parallel:` section alongside `parallel.stale_threshold_seconds`) sets the project default. The `--parallel` flag overrides it per-run. +- **Cap `10`.** A request above 10 is clamped to 10 with a one-line notice, matching the 10-orchestrator design bound and protecting the shared main working tree, git object store, and the single `feedback.lock`. + +**How it works** (full spec: `agents/run.md` `## Parallel Run Mode`; design: `docs/design/single-session-parallel.md`): + +- **Fan-out** is N concurrent `Agent`-tool dispatches in one turn — the same dispatch surface serial mode uses, not a separate scheduler. +- **Claim-as-slot-frees.** The orchestrator claims one REQ immediately before each dispatch (never a batch up front) so `pick-req.sh`'s footprint exclusion sees each claim before the next pick. The window refills one REQ each time a slot frees. +- **Serialized merge queue.** Workers return on `req/REQ-NNN` branches in any order. Integration runs in two stages: **Stage A** (acceptance-evidence → policy → independent review) is read-only and may run N-wide; **Stage B** (ledger → merge → archive → teardown → metadata commit) is serial, single-writer — at most one merge/archive touches the main working tree and `.do-work/` at any instant, exactly as serial mode. +- **Failure isolation.** One stopped worker (or a failed gate / a 5-retry merge exhaustion → `concurrent-conflict`) frees its slot and surfaces per-REQ in arrival order; the other workers and queued reports proceed. No new stopper reasons. +- **Deploy gates stay single-flow.** Milestone deploy gates are not parallelised — the existing first-to-detect drain check and single y/n prompt are unchanged; fan-out pauses new claims while a gate is open. +- **Coordination lib untouched.** `pick-req.sh`, `claim-req.sh`, `check-footprint.sh`, `scan-stale.sh` and the rest keep their contracts; this mode is a run-loop shape change, not a primitive change. + +### Coordination Layer + +**Footprint-aware claiming.** Before claiming a REQ, each orchestrator calls `lib/pick-req.sh` to identify the next safe candidate. `lib/pick-req.sh` uses `lib/check-footprint.sh` to detect file-level overlap between the candidate and every REQ currently in `working/`. If overlap exists, the candidate is skipped and the next backlog entry is evaluated. This eliminates the primary source of cross-agent file conflicts. + +**Dependency-aware ordering.** `lib/pick-req.sh` also calls `lib/check-deps.sh` to verify that all `Depends on:` REQs listed in the candidate's header have been archived (status: done) before the candidate is eligible. Circular dependency chains are gated at capture time by `lib/cycle-check.sh` — a dependency graph with a cycle will be rejected during capture, not at run time. + +**Atomic claim.** Once a safe, dep-satisfied candidate is selected, `lib/claim-req.sh` writes the ownership stamp atomically: + +```markdown + +**Claimed by:** hostname.pid +**Claimed at:** 2026-05-21T11:42:08Z +**Heartbeat:** 2026-05-21T11:42:08Z +**Session:** 9f3c1a20-1b2c-4d5e-8f90-a1b2c3d4e5f6 + +``` + +**`**Session:**`** is an **optional** claim-block field (last line before ``) correlating the REQ with the live do-work session, so the extension can re-adopt a session after a restart (see the event-stream telemetry, `lib/session-hook.sh`). `lib/claim-req.sh` resolves it via `lib/resolve-session.sh`: the `session.start` whose `data.marker` matches `$DO_WORK_UI_MARKER`, else the single un-ended session for the project. When no session can be determined without guessing — no marker match with multiple live sessions, or no `events.jsonl` at all (older projects) — the line is **omitted entirely**, and its absence is valid everywhere. Heartbeat refreshes leave it untouched; `unblock` strips it with the rest of the stamp; a `resume` that re-resolves a session updates it. + +**Checkpoint-based liveness.** Each worker stamps the `**Heartbeat:**` timestamp in its REQ file via `lib/heartbeat.sh` at natural progress checkpoints — after reading the REQ, after each TDD cycle, after each verification step, and before commit — rather than from a background timer (a backgrounded loop cannot survive a fresh-shell-per-call harness). `lib/scan-stale.sh` (called during pre-flight and by `/do-work status`) flags REQs whose heartbeat is older than `parallel.stale_threshold_seconds` (default 900 s / 15 minutes — sized to span the gap between checkpoints) as potentially dead. Stale REQs surface in the status report for human triage — they are not automatically unblocked. + +**Deadlock detection.** `lib/deadlock-check.sh` checks for circular wait chains across the `working/` set: does REQ-A depend on REQ-B which depends on REQ-A (both in-flight)? Any cycle found is reported immediately by `/do-work status` under a `DEADLOCK DETECTED` banner. Recovery is manual: use `/do-work unblock REQ-NNN` to break the cycle. + +**Visible differences from single-agent mode:** + +- The per-REQ announce line is prefixed with `[]` (where `agent-id` is `hostname.pid`) so you can attribute output across terminals. +- Multiple REQs appear in `working/` simultaneously, each carrying the ownership stamp above. +- The final cross-REQ test suite runs once, from whichever orchestrator drains last (gated by `.do-work/state/final-suite-running.md` lockfile). +- On a commit or merge conflict, the losing worker waits up to ~110 seconds (5 retries: 5s / 15s / 30s / 60s backoff) before exiting with `status: stopped`, `reason: concurrent-conflict`. Use `/do-work resume REQ-NNN` to re-dispatch. + +### Isolation per REQ + +Workers always run in isolated git worktrees at `{project}/.worktrees/req-NNN` on a `req/REQ-NNN` branch. The orchestrator merges the branch back into the base branch and tears down the worktree after integration. See [agents/run-worker.md](../agents/run-worker.md) `## Isolation Mode` and `## Worktree Workflow` for the canonical procedure. + +### Recovery Commands + +| Situation | Command | +|---|---| +| REQ is stuck / worker died / heartbeat stale | `/do-work unblock REQ-NNN` — strips claim, returns REQ to backlog | +| REQ stopped (concurrent-conflict / transient error) | `/do-work resume REQ-NNN` — refreshes heartbeat, re-dispatches worker | +| Deadlock or unclear state | `/do-work status [UR-NNN]` — renders live situation room, deadlock banner | + +See `agents/status.md`, `agents/unblock.md`, `agents/resume.md` for agent-level instructions. + +### Constraints That Stay Single-Agent + +- Milestone deploy gates remain non-delegable — the first orchestrator to detect milestone-complete owns the gate; siblings idle (logging `Idle — waiting on milestone M deploy gate`) and resume when `.do-work/state/active-milestone.md` advances. `.do-work/state/gate-owner.md` records the gate owner. +- The stale-slot prompt in pre-flight runs in whichever orchestrator finds the stale slot first. + +### State Files + +All coordination state lives under `.do-work/state/`: + +- `gate-owner.md` — agent-id currently handling a milestone deploy gate (deleted on resolve). +- `final-suite-running.md` (or `final-suite-M-running.md` in milestone mode) — lockfile for the final cross-REQ test suite. + +### Implementation Reference + +- Claim: `lib/pick-req.sh`, `lib/check-footprint.sh`, `lib/claim-req.sh` +- Dependencies: `lib/check-deps.sh`, `lib/cycle-check.sh` +- Liveness: `lib/heartbeat.sh`, `lib/scan-stale.sh` +- Deadlock: `lib/deadlock-check.sh` +- Archive integrity: `lib/check-archive-integrity.sh` — pre-archive gate enforcing Status `done` + non-empty Closure proof + zero unchecked acceptance criteria (`agents/run.md` Step 4b/4-pr.4) +- Orchestrator: `agents/run.md` §§ Agent Identity, Pre-flight Check, Step 1: Claim the next REQ, When the Backlog is Empty, Step 7b +- Worker: `agents/run-worker.md` §§ Isolation Mode, Worktree Workflow, Concurrent-Conflict Retry + +## Layers + +do-work uses project-declared layers to gap-check feature briefs. Declare your project's layers once in `.do-work/config.yml`: + +```yaml +layers: [frontend, backend] # web app +# layers: [commands, core, output] # CLI tool +# layers: [public_api, internal] # library / SDK +# layers: [agents, commands, templates] # do-work itself +``` + +Capture and verify use this list to enforce that REQs cover every declared layer for `feature`-class briefs (or surface explicit "no" decisions). Empty `layers:` opts out — feature briefs will halt until layers are declared or `--no-layers` is passed. + +Every REQ written by capture carries a `**Layer:**` field naming one of the declared layers, or `none` for bug-fix / pure-refactor / test-only REQs. + +Feature REQs that add new surface (anything callable or visible from outside their own code) include an `## Integration` section answering three sub-questions: + +- **Reachability** — How does the user (or caller) reach this? +- **Data dependencies** — What existing data does this read or write? +- **Service dependencies** — What existing services or modules does this extend? + +Capture inspects the codebase to draft answers and verifies each cited file/symbol exists before claiming high confidence. Verify enforces the Integration block on every non-`none` feature REQ. + +## Path Units + +For feature-class briefs, capture decomposes by reachable path first. A path-unit is a top-level REQ that names: + +- `**Entry point:**` — how a user, caller, command, or system reaches the path. +- `**Terminal state:**` — the observable end state that proves the path closed. + +Layer-specific work is captured as child REQs underneath the path-unit. Child REQs carry the normal `**Layer:**` value and point back to the path-unit with `**Parent:** REQ-NNN`. Layers therefore operate inside path-units: they still prevent frontend/backend/command/template gaps, but the closure unit is the reachable path. + +Migration is additive. Legacy REQs without `**Entry point:**`, `**Terminal state:**`, or `**Parent:**` remain valid. New path-units must have both entry point and terminal state before they can verify or archive as complete. + +## Decisions Memory + +`.do-work/decisions.md` is an append-only, cross-UR record of standing decisions (ADR-lite). It gives capture, ideate, and workers a shared institutional memory so a call made in one UR ("validation lives server-side") is not re-litigated or contradicted in the next. + +**Format** — one line per decision, no paragraphs (this is a memory, not documentation; anything needing prose belongs in a design doc): + +``` +YYYY-MM-DD | UR/REQ ref | decision | rationale +``` + +- `YYYY-MM-DD` — the date the decision was recorded. +- `UR/REQ ref` — the UR or REQ the decision was made under (e.g. `UR-035` or `REQ-224`). +- `decision` — the standing choice, stated as a constraint. +- `rationale` — one phrase explaining why. + +**Discipline:** + +- **Append-only.** Never rewrite or delete an existing line. +- **Supersede with a new line.** To reverse or change a decision, append a fresh line that references the superseded one (e.g. `... | supersedes 2026-06-01 entry | ...`). The old line stays as history. +- **The file is optional.** No agent creates it; it comes into being when the first decision is appended. An absent file is silently fine everywhere it is read. + +**Writers:** capture appends a line at judgment points where a choice shapes the decomposition (a layer opt-out, a split-vs-merge call, a layer-coverage user answer). **Readers:** capture (Step 1), ideate (Step 2 project context), run-worker (Step 2 — treats standing decisions as constraints), and question all read it when present. + +## REQ Header Schema + +Every REQ file carries a structured header immediately below the title. The canonical field list is: + +| Field | Required | Description | +|---|---|---| +| `**UR:**` | yes | Parent UR identifier (e.g. `UR-030`) | +| `**Status:**` | yes | `backlog` / `in-progress` / `stopped` / `done` | +| `**Created:**` | yes | ISO date (YYYY-MM-DD) | +| `**Layer:**` | yes | Declared project layer, or `none` for bug-fix/refactor/test-only REQs | +| `**Entry point:**` | optional | How a user, caller, command, or system reaches this path-unit. Required to be non-empty for top-level path-unit REQs. | +| `**Terminal state:**` | optional | The observable end state that proves this path-unit is complete. Required to be non-empty for top-level path-unit REQs. | +| `**Parent:**` | optional | Parent path-unit REQ id for child layer-tasks. Empty or absent on top-level path-units and legacy REQs. | +| `**Closure proof:**` | optional | Evidence reference proving verification passed, such as `checkpoint:.do-work/runs/RUN-001.yml#REQ-123` or `commit:abc123 tests:passed`; empty until proven. | +| `**Suite:**` | optional | Written by the run orchestrator during advisory-check consolidation when the worker's own test/build suite could not be provisioned; the only value is `not-run`. Consumed by `lib/derive-status.sh`, which derives such a REQ `unproven` regardless of an otherwise-passing closure proof. Absent on normal REQs. | +| `**Criteria approved:**` | optional | Acceptance-criteria provenance: `agent-drafted` when capture generated it, or `human ` when a human previously reviewed it. This field does not block run. | +| `**Priority:**` | optional | Backlog urgency `1`–`3` (3 = most urgent), derived by capture from dependency-graph depth. Read by `lib/pick-req.sh` to order claimable candidates (Priority desc, then REQ number asc). Absent or out-of-range sorts as `2`, so legacy REQs are unaffected. | +| `**Size:**` | optional | Effort estimate `S` / `M` / `L`, derived by capture from file count, layer span, and criteria count. `Size: L` is a primary opus-escalation signal in `agents/run.md` Model Selection. Absent falls back to the lexical heuristics. | +| `**Files:**` | yes | Space-separated list of primary output files — used by `lib/check-footprint.sh` for overlap detection | +| `**Depends on:**` | optional | REQ ids this REQ must not start before, separated by commas and/or whitespace (e.g. `REQ-144, REQ-145` or `REQ-144 REQ-145`) — tokenized by `lib/pick-req.sh` / `lib/check-deps.sh` and checked against `archive/` | + +A **path-unit** is a REQ whose `**Entry point:**` and `**Terminal state:**` are both non-empty. Path-units describe a vertical, reachable slice of intent. Child layer-tasks point back to a path-unit with `**Parent:**`; legacy REQs without these fields remain valid because the migration is additive. + +`**Status:**` remains writable and authoritative for coordination (`backlog`, `working/`, dependency gating, stale checks, and archive flow). `**Closure proof:**` is a separate evidence signal used to derive whether a done REQ is proven; it does not replace the coordination status field. + +### `## Manual checks (advisory)` section + +An optional REQ body section that holds human, device, or environment checks that cannot be executed by a worker in an isolated worktree. + +**Written by:** `agents/capture.md` on path-unit REQs (or the single REQ for legacy-style decompositions) when the brief includes checks that require a human, a physical device, or an environment the worker cannot provision. Capture writes this section — and its executability self-correction scan (Step 4b) moves any mis-classified `## Verification Steps` entries here automatically before committing REQ files. + +**Advisory only:** Workers never execute `## Manual checks (advisory)` items. The section is explicitly outside the checkpoint loop, never blocks archive, and is not part of the worker's checkpoint log. + +**Archived by run:** `/do-work run` consolidates worker-reported `deferred_checks:` and any existing `## Manual checks (advisory)` items into the archived REQ, then completes the normal `done` archive path once automated gates pass. + +**Advisory record only:** `## Manual checks (advisory)` items are preserved in the archived REQ as an advisory record for humans. They sit outside the system's validation gate and are not surfaced by any command automatically. + +**One exception — the un-run suite:** human and device advisory items never affect proven-ness. An un-run test/build suite is different: alongside its advisory bullet, the run orchestrator also stamps `**Suite:** not-run` on the archived REQ, which `lib/derive-status.sh` reads to derive the REQ `unproven`. + +**Format (each item):** a checklist line stating what a person should do and what observable outcome confirms it: + +```markdown +## Manual checks (advisory) + +- [ ] [Action: what a person should do] — Observable outcome: [what they should see or confirm] +``` + +`**Criteria approved:** agent-drafted` means capture generated the acceptance criteria. It is informational provenance, not a run gate. Existing backlog REQs should run unless dependencies, footprint, policy, tests, verification, review, or genuinely ambiguous criteria stop them. + +When a REQ is claimed by a worker, a claim block is inserted between the title and the first header field: + +```markdown + +**Claimed by:** hostname.pid +**Claimed at:** 2026-05-21T11:42:08Z +**Heartbeat:** 2026-05-21T11:42:08Z +**Session:** 9f3c1a20-1b2c-4d5e-8f90-a1b2c3d4e5f6 + +``` + +The heartbeat timestamp is refreshed in-place by `lib/heartbeat.sh` — this is a filesystem-only operation, never a git commit. The optional `**Session:**` line (see the **Atomic claim** description above) correlates the REQ with the live session and is omitted when no session can be resolved. Canonical documentation: `.do-work/archive/REQ-144-extend-req-template-schema.md`. + +## Commit Convention + +**Markdown backend** (`tracker.backend` unset / `markdown`): + +``` +feat(REQ-NNN): short title + +REQ: .do-work/archive/REQ-NNN-slug.md +UR: .do-work/user-requests/UR-NNN/input.md +Output: path/to/primary/output +``` + +**Linear backend** (`tracker.backend: linear`) — Linear issue id only (design §6.5): + +``` +feat(ENG-123): short title + +Issue: ENG-123 +UR: UR-007 +Output: path/to/primary/output +``` + +Commits are created per-REQ (or per Linear issue) on completion. Under markdown, the claim/heartbeat update path (`lib/heartbeat.sh`) is filesystem-only — it writes directly to the REQ file and does **not** produce a git commit. Under Linear, heartbeat is a claim-protocol comment refresh (see Tracker backends). Unblock operations use `chore(REQ-NNN): unblock — return to backlog` (markdown) or the Linear-id equivalent when `backend: linear`. + +## Checkpointed Verification + +REQ `## Verification Steps` are ordered checkpoints. Workers execute them in sequence and record a checkpoint log that localizes both success and failure: + +```yaml +req: REQ-NNN +status: passed | failed +checkpoints: + - step: 1 + total: 3 + type: test + command: "npm test -- --filter settings" + status: passed + - step: 2 + total: 3 + type: runtime + command: "curl http://localhost:3000/settings" + status: failed + handoff: "route -> render" +last_good_step: 1 +failed_step: 2 +``` + +On failure, the log must answer: which step failed, at which handoff, and what the last good step was. On success, the full passed checkpoint log becomes the natural target for `**Closure proof:**`. + diff --git a/references/linear-ops.md b/references/linear-ops.md new file mode 100644 index 0000000..11df705 --- /dev/null +++ b/references/linear-ops.md @@ -0,0 +1,1091 @@ +# Linear port op sequences (reference) + +One hop from [`agents/tracker/linear.md`](../agents/tracker/linear.md). Load when executing a named port op under `tracker.backend: linear`. Hard-stop template + status_map + hierarchy lock stay in the agent file. + +**Shared agent protocol for every sequence:** + +```text +1. search_tool "" +2. If zero Linear tools / no matching capability → HARD STOP (linear.md setup block; no dual-write) +3. use_tool with qualified name + exact input_schema from search +4. On tool error / team unresolved → HARD STOP; do not invent data +``` + +**Search query hints (not proven tool names):** `"linear team"`, `"linear project"`, `"linear milestone"`, `"linear create issue"`, `"linear list issues"`, `"linear update issue"`, `"linear label"`, `"linear status"`, `"linear comments"`, `"linear document"`. + +## Hierarchy (apply to every op) + +``` +Team (config) +└── Project product_project — one shared product Project per local product (not per UR) + ├── Project Milestone (UR) — §9.1 ; brief, ideate, verify, close + └── Issue (REQ) — on product Project, attached to UR milestone + └── Sub-issue (layer child) +``` + +**Product Project naming:** `tracker.linear.product_project` defaults to **empty** (not skill name `do-work`). Resolve order is documented under `ensure_product_container` / `agents/config.md` Load Config step 8. Example for the do-work skill repo itself may still use name `do-work`. + +**No Initiative-as-UR.** Path-milestone mode (M1/M2) is a *cursor + Issue markers* on the UR milestone — see [linear-path-milestones.md](linear-path-milestones.md). + +--- + +## Templates (design §9) + +Bodies are **markdown conventions** in Linear description fields — not custom Linear fields. Prefer description appends; fall back to UR Project Milestone/Issue **comments** if description size limits require it (record a one-line pointer in the section when spilling). + +**Machine markers (required):** + +| Entity | Marker (first non-empty line of structured body) | Op consumers | +|--------|--------------------------------------------------|--------------| +| UR Project Milestone | `` | `create_ur`, `read_ur`, `list_urs`, `append_ideate`, `append_clarifications`, verify/close writers | +| Issue (REQ) | `` | `create_req`, `update_req`, `read_req`, `set_files`, `set_blocked_by`, `claim_req` / `heartbeat_req` / `unblock_req` / `set_req_status`, archive later | + +On **read/update**: if the marker is missing, treat as template parse failure → **stop the op**; do not invent headers or rewrite the body into template form without an explicit migrate path. + +### §9.1 UR Project Milestone description template + +```markdown + +**UR-id:** UR-007 +**Class:** feature +**Created:** YYYY-MM-DD +**Product-project:** do-work +**Product-project-id:** {linear-project-uuid} +**Milestone-id:** {linear-milestone-uuid} + +## Brief +{verbatim intake} + +## Clarifications + +## Ideate + +## Open gaps + +## Capture summary + +## Verify + +## Closure +``` + +#### §9.1 field semantics + +| Field / section | Write rules | Readers | +|-----------------|-------------|---------| +| `` | Must be present at create; never strip | All UR ops | +| `**UR-id:**` | Sequential `UR-NNN` slug only (not a Linear entity id) | Resolve UR; `list_urs` | +| `**Class:**` | Intake classification (feature / …) | Capture, status | +| `**Created:**` | ISO date `YYYY-MM-DD` at create | Display | +| `**Product-project:**` | Shared product Project **display name** after ensure (from bound Project; not a hard-coded skill default) | Display; prefer id for resolve | +| `**Product-project-id:**` / `**Milestone-id:**` | Linear UUIDs after ensure + milestone create | Prefer ids over names | + +| `## Brief` | **Verbatim** intake — never overwrite on ideate/question | `read_ur` | +| `## Clarifications` | `append_clarifications` appends Q&A; does not create REQs | Question, capture | +| `## Ideate` | `append_ideate` writes/appends ideate body | Ideate, capture | +| `## Open gaps` / `## Capture summary` | Capture phase | Capture, verify | +| `## Verify` / `## Closure` | `write_verify_report` / `write_close_report` (REQ-296) | Verify, close, go | + +### §9.2 Issue (REQ) description template + +```markdown + +**UR:** UR-007 +**Layer:** agents | none | … +**Parent:** ENG-100 | none +**Entry point:** … # path-unit parents only +**Terminal state:** … # path-unit parents only +**Milestone:** M1 # milestone mode only; omit or `none` otherwise +**Files:** path1 path2 +**Depends on:** ENG-101 ENG-102 +**Size:** S|M|L +**Priority:** 1-3 +**Criteria approved:** agent-drafted +**Closure proof:** +**Suite:** + +## Task + +## Acceptance Criteria +- [ ] … + +## Verification Steps +1. … + +## Integration + +## Manual checks (advisory) +- [ ] … + +## Outputs +``` + +#### §9.2 field semantics + +| Field / section | Write rules | Readers | +|-----------------|-------------|---------| +| `` | Required at create; never strip | All REQ ops | +| `**UR:**` | Owning UR slug | `list_reqs_for_ur` cross-check; display | +| `**Layer:**` | Layer name or `none`; also label `Layer/{name}` when labels available | Capture, footprint | +| `**Parent:**` | Parent **Linear issue id** or `none`; children also set native `parentId` | Path-units | +| `**Entry point:**` / `**Terminal state:**` | Path-unit **parents only**; leave empty on leaves | Capture path-units | +| `**Milestone:**` | Milestone mode only: `M` (e.g. `M1`); omit or `none` otherwise; also label `M` when labels available (REQ-298) | `list_milestone_reqs` | +| `**Files:**` | Space-separated paths/globs; sole write intent of `set_files` | Footprint / pick | +| `**Depends on:**` | Space-separated **Linear issue ids** — **mirror only**; authoritative graph is native `blocks` relations via `set_blocked_by` | Display; eligibility uses relations when present | +| `**Size:**` | `S` \| `M` \| `L`; also label `Size/{S\|M\|L}` when labels available | Capture; optional estimate map | +| `**Priority:**` | `1`–`3` (or empty) | Capture / pick display | +| `**Criteria approved:**` | Provenance only (`agent-drafted` / human…) | Workers | +| `**Closure proof:**` / `**Suite:**` | Set by archive/orchestrator path | Archive integrity | +| `## Task` … `## Outputs` | Capture / worker sections; preserve unknown sections on update | Workers, review | + +### Labels (`tracker.linear.labels.*`) + +When label tools are discoverable (create/list/attach), agents **must** keep labels aligned with body headers on create/update: + +| Config key | Default | Applied as | When | +|------------|---------|------------|------| +| `labels.layer_prefix` | `Layer/` | `Layer/{name}` e.g. `Layer/agents` | Every Issue with a non-empty `**Layer:**` (skip or omit for `none` if team convention prefers no label) | +| `labels.size_prefix` | `Size/` | `Size/S`, `Size/M`, `Size/L` | Every Issue with `**Size:**` set | +| `labels.path_unit` | `path-unit` | Exact label name `path-unit` | Path-unit **parent** Issues only (not layer children) | + +**Rules:** + +1. Resolve or create labels via live tools only; never invent label UUIDs. +2. Body headers remain the parse source if labels are missing tools — still write headers. +3. `ensure_product_container` may pre-create common labels when create-label tools exist. +4. Estimate: if the team uses T-shirt estimates and tools allow, map Size → estimate **after** body/label write; estimate is optional display, not the footprint source. + +### States (`tracker.linear.status_map`) + +| do-work status | Config key | Default Linear state name | +|----------------|------------|---------------------------| +| backlog | `status_map.backlog` | `Todo` | +| in_progress | `status_map.in_progress` | `In Progress` | +| stopped | `status_map.stopped` | `Canceled` | +| done | `status_map.done` | `Done` | + +**Hard-fail validation (when `backend: linear`):** + +1. At preflight (before first CRUD op in a session), list team workflow states via discovered tools. +2. For **every** key in `status_map` (defaults filled if omitted), the Linear state **name** must exist on the team. +3. If any mapped name is missing → **hard-stop** with rename-or-override instructions (setup block). **Never** invent states; **never** pick a “close enough” name; **never** fall back to markdown. +4. Create/update ops that set status use the **validated** state id for the mapped name only. + +### Deps dual-write (template + relations) + +| Concern | Rule | +|---------|------| +| Authoritative graph | Native Linear **`blocks` relations** (this issue is blocked by dependency issues) | +| Body mirror | `**Depends on:** ENG-101 ENG-102` (Linear issue ids only — never markdown `REQ-NNN`) | +| Writer | Prefer `set_blocked_by` for sole intent; `create_req` may set deps at create the same way | +| Diverge | Relations win for `list_claimable_reqs` / deps checks | +| Relations tools missing | Body-only deps + **one-time** warning; still no markdown dual-store; document GraphQL fallback if spike later marks relations **missing** | + +### Path-units + +- **Parent Issue:** §9.2 with `**Entry point:**` / `**Terminal state:**`; label `path-unit` when available; no required `parentId`. +- **Layer children:** Linear `parentId` (or schema field from live create-issue tool) = parent Linear id; body `**Parent:**` = same id; layer label when available; leave entry/terminal empty. + +--- + +## UR/REQ CRUD sequences + +**Shared agent protocol for every step below:** + +```text +1. search_tool "" +2. If zero Linear tools / no matching capability → HARD STOP (setup block; no dual-write) +3. use_tool with qualified name + exact input_schema from search +4. On tool error / team unresolved → HARD STOP; do not invent data +``` + +**Search query hints (not proven tool names):** use queries such as `"linear team"`, `"linear initiative"`, `"linear project"`, `"linear create issue"`, `"linear list issues"`, `"linear update issue"`, `"linear label"`, `"linear status"`. Map hits to the step’s need. Skill “typical tools” tables are **candidates to search for**, never hard-coded as proven. + +**Id rules:** + +| Entity | Id form | +|--------|---------| +| UR slug | Sequential `UR-NNN` (UR Project Milestone metadata only) | +| REQ | **Linear issue identifier only** (e.g. `ENG-123`) — never allocate `REQ-NNN` under Linear backend | +| Product Project | `tracker.linear.product_project` — shared for all URs on this local product; **name or UUID**; empty default; resolve chain + ensure persist UUID (never invent skill name `do-work`) | +| UR milestone name | `tracker.linear.ur_milestone_name_pattern` (default `{ur_id}: {title}`) | + + +### Preflight (before first CRUD op in a session) + +1. Config effective backend is `linear` (else do not use this file). +2. `search_tool "linear"` (or `"linear team"`) — must return Linear MCP tools; else hard-stop. +3. Resolve team: config `tracker.linear.team_id` and/or `team_key` via discovered team list/get tools. Unresolved → hard-stop (do not guess). +4. Validate every `status_map` value exists on the team workflow (discovered status-list tool). Missing name → hard-stop with rename/override instructions. +5. Cache team id, status ids for mapped states, and (optionally) label ids for the session. + +### `ensure_product_container` + +| | | +|---|---| +| **Intent** | Team resolvable; ensure the shared **product Project** for this local product (not per UR); optional labels ready. Create/bind when missing; **always persist** Project UUID to `tracker.linear.product_project`. | +| **Preconditions** | Preflight steps 2–4 done (MCP tools, team resolved, `status_map` validated). Config loaded (`agents/config.md`). | +| **Failure** | Hard-stop on empty-name, multi-match, tool missing, create/get failure. **Never** create markdown `.do-work/` as substitute product container. **Never** invent Initiatives as UR containers. **Never** fall through to skill name `do-work` for empty `product_project`. | + +**Agent sequence (executable):** + +1. **Resolve target name/id (lookup key)** — same chain as `agents/config.md` Load Config step 8; do **not** invent a different order: + 1. Let `pp` = `tracker.linear.product_project` (missing / null / whitespace-only → empty). + 2. If `pp` is **non-empty** (name **or** UUID) → **lookup key = `pp`**. Explicit config wins; do not replace with `project.name`, basename, or skill name `do-work`. + 3. If `pp` is **empty** → lookup key = `project.name` when non-empty; else **git-root directory basename**. + 4. If the final lookup key is still empty/whitespace → **hard-stop** (**empty-name**): instruct operator to set `project.name` or `tracker.linear.product_project` in `{project}/.do-work/config.yml`. Do **not** invent a name. Do **not** markdown-fallback. +2. **Rediscover project tools** — `search_tool` for Linear project surfaces (`"linear project"`, `"linear list projects"`, `"linear save project"` / create-project). Map hits to list/get/create. If list/get (and, for name create path, create/`save_project`) tools are undiscoverable → **hard-stop** (setup block). Never hard-code tool names; use qualified names + `input_schema` from search. +3. **Resolve or create on the team** + - **UUID path** (lookup key is already a Project UUID / id form): `use_tool` get-project (or list filtered by id). Found → use that Project. Not found → **hard-stop** (do not invent; do not create a Project whose name is the UUID string). + - **Name path** (lookup key is a display name): + 1. `use_tool` list-projects scoped to the **resolved team** (team id/key from preflight). Prefer `query` / name filter when the schema supports it; otherwise list and filter client-side. + 2. Keep **exact name matches** (case-sensitive unless the live tool documents otherwise) on that team. + 3. **Match count:** + - **0** → **create** via rediscovered create/save-project surface (`save_project` when discovered): `name` = lookup key; attach team with `addTeams` **or** `setTeams` = resolved team (schema requires at least one team). Capture returned Project UUID. + - **1** → **use** that Project (id + name). + - **>1** → **hard-stop** (**multi-match**): list matching ids/names; require operator to set `tracker.linear.product_project` to the desired **UUID**. Do not pick “first”; do not invent; no markdown fallback. +4. **Persist UUID** — **always** write the resolved Project **UUID** to `tracker.linear.product_project` in `{project}/.do-work/config.yml` and in-memory config. If the file already stores that same UUID, skip the write (idempotent). Prefer UUID over name for all subsequent ops in the session. +5. **Optional labels** — when create/list label tools exist, pre-create common labels (`labels.layer_prefix`, `size_prefix`, `path_unit`) for the team. Label failure is non-fatal for container ensure (body headers remain source of truth); project ensure itself must already have succeeded. +6. **Return** product Project **id (UUID)** + **name**. Cache for the session. + +**Does not:** create a UR or REQ; create a per-UR Linear Project; create Initiatives; write local UR/REQ markdown as the store. + +### `create_ur` + +| | | +|---|---| +| **Intent** | Record intake brief as a **UR Project Milestone** on the shared **product Project**. Does **not** create REQs. **Not** Initiative-as-UR. **Not** a new Linear Project per UR. | +| **Preconditions** | Preflight passed; `ensure_product_container` done; next `UR-NNN` slug allocatable. | +| **Atomicity** | Product Project resolvable + Project Milestone create must succeed as one logical unit. **No partial UR.** | + +**Agent sequence:** + +1. **Ensure product Project** — call **`ensure_product_container`** first (resolve chain + list/create/bind + persist UUID). Do **not** restate a hard-coded product name here; do **not** create a per-UR Project. +2. **Allocate next `UR-NNN` slug** + - `search_tool` for project milestones list tools (`"linear milestones"`, `"linear project milestones"`). + - List milestones on the product Project; scan names / descriptions for `UR-*` / `**UR-id:** UR-*` / ``. + - Pick next free sequential `UR-NNN`. +3. **Build body** + - Milestone name: apply `tracker.linear.ur_milestone_name_pattern` (default `{ur_id}: {title}`, e.g. `UR-007: Add SSO`). + - Description: §9.1 template with verbatim brief; `**Product-project:**` + product project name; `**Product-project-id:**` from ensure; leave `**Milestone-id:**` empty until create returns it. +4. **Create Project Milestone** on the product Project + - `search_tool "linear milestone"` / create-milestone surface. + - If **no** milestone create tool is discovered → **hard-stop** (do **not** invent Initiative-as-UR; do **not** create a per-UR Project as a fake UR). + - `use_tool` create with discovered schema (project id + name + description as required). + - Record milestone id; patch `**Milestone-id:**` if update tools allow. +5. **Return** UR slug, product project id/name, milestone id/name. **Do not** write `.do-work/user-requests/UR-NNN/`. **Do not** create Linear Initiatives. **Do not** create a Linear Project per UR. + +### `read_ur` + +| | | +|---|---| +| **Intent** | Load brief + attached sections (ideate, clarifications, verify, closure if present). | +| **Sequence** | 1) Resolve product Project (`product_project`). 2) List Project Milestones; match `UR-NNN` via name pattern / `**UR-id:**` / marker. 3) Get/read milestone description (and comments if sections spilled). 4) Parse §9.1 markers. | +| **Failure** | Unknown UR → error to caller; MCP missing → hard-stop. | + +### `list_urs` + +| | | +|---|---| +| **Intent** | Enumerate URs (ids + titles) for prompts/status. | +| **Sequence** | `search_tool` → list Project Milestones on product Project; keep those with `` / `**UR-id:**` / matching `ur_milestone_name_pattern`. Return `UR-NNN` + title; use `read_ur` for full body. | +| **Failure** | MCP missing → hard-stop. | + +### `create_req` + +| | | +|---|---| +| **Intent** | Create one backlog REQ (Issue) on the product Project, attached to the UR Project Milestone. Optional path-unit parent + layer children as sub-issues. | +| **Preconditions** | UR Project Milestone exists on product Project (from `create_ur` or resolve); preflight passed. | +| **Id rule** | Resulting id is the **Linear issue id only** (e.g. `ENG-123`). **Never** allocate `REQ-NNN`. | + +**Agent sequence:** + +1. Resolve **product Project id** + **UR Project Milestone id** (`search_tool` + list/get). Missing either → hard-stop or fail create (UR incomplete). +2. Resolve **backlog** workflow state id from `status_map.backlog` (default `"Todo"`) via discovered status tools. +3. Build Issue **description** from §9.2 with capture fields (`**UR:**`, layer, files, depends-on Linear ids, size, priority, task, AC, verification, …). Titles short and actionable. +4. **Path-unit parent** (if this REQ is a path-unit): + - Create parent Issue first: team + project + title + §9.2 body (`**Entry point:**` / `**Terminal state:**` filled); labels include `path-unit` when label tools exist. + - For each layer child: create Issue with `parentId` (or schema field returned by live create-issue tool) set to parent Linear id; body `**Parent:** ENG-…`; layer label when available. +5. **Standalone / leaf REQ:** + - `search_tool "linear create issue"` (or `"linear issues"`). + - If create-issue undiscoverable → **hard-stop** (no markdown dual-write). + - `use_tool` create: team, project=product Project, milestone=UR Project Milestone, title, description, state=backlog map, optional assignee=`default_assignee_id`, labels, `parentId` when child. +6. **Deps at create (optional):** if dependency Linear ids are known, run the same dual-write as **`set_blocked_by`** (native `blocks` relations when tools exist **and** body `**Depends on:**` mirror). If relations missing → body-only + one-time warning (port rule). +7. **Labels:** attach `Layer/{name}`, `Size/{S|M|L}`, and `path-unit` (parents only) per **Labels** table when label tools exist. +8. **State:** create in `status_map.backlog` only (validated id from preflight) — never invent a state name. +9. Return Linear issue id(s). Human assignee only from config — agents do not steal assignee for claim (see **Claim protocol**). + +### `update_req` + +| | | +|---|---| +| **Intent** | Edit Issue body/fields without claim/archive lifecycle. Prefer dedicated ops for status, deps, footprint, claim when those are the sole intent. | +| **Sequence** | 1) `search_tool` + get issue by Linear id. 2) Require ``; merge structured header / section edits into §9.2 description (preserve unknown sections). 3) `search_tool` + update issue with only changed fields (title, description, labels, project, parent). 4) **Deps sole intent → use `set_blocked_by`** (do not half-update relations). 5) **Footprint sole intent → use `set_files`**. 6) If a broader body edit also changes deps/files, after description update run the same dual-write / header rules as those ops. | +| **Failure** | Issue missing → error; missing machine marker / unparsable required fields → stop op (do not invent); MCP missing → hard-stop. | + +### `read_req` + +| | | +|---|---| +| **Intent** | Load full REQ (headers + body sections). | +| **Sequence** | `search_tool` → get issue by Linear id (e.g. `ENG-123`). Parse `` headers and sections. Optionally list children if path-unit parent. Map Linear workflow state name back through `status_map` for do-work status display. | +| **Failure** | Unknown id → error; MCP missing → hard-stop. | + +### `list_reqs_for_ur` + +| | | +|---|---| +| **Intent** | All REQs for a UR, any status — Issues on product Project with that UR Project Milestone. | +| **Sequence** | 1) Resolve product Project + UR Project Milestone. 2) `search_tool "linear list issues"`. 3) List Issues filtered by **product project** and **UR milestone** (not global team backlog alone). 4) Return Linear ids + titles + states (+ parentId if present). | +| **Notes** | UR Project Milestone membership is the scope. Do not scan local `.do-work/REQ-*`. | +| **Failure** | UR milestone missing → empty or error; MCP missing → hard-stop. | + +### `append_ideate` + +| | | +|---|---| +| **Intent** | Append or write ideate content onto an existing UR Project Milestone — **without** overwriting `## Brief`. | +| **Preconditions** | Preflight passed; UR exists (Project Milestone with §9.1 marker + `**UR-id:**`). | +| **Does not** | Create REQs, Projects, or local `ideate.md` files. | + +**Agent sequence:** + +1. **Rediscover** — `search_tool "linear milestone"` / `"linear project milestones"` (and/or get/update milestone). Zero tools → hard-stop (setup block). +2. **Resolve Initiative** for `UR-NNN` (same as `read_ur`: scan Initiatives for `**UR-id:**` / Project `do-work/{UR-id}` → linked initiative). +3. **Read** current description (and comments if sections spilled). Require ``. +4. **Locate `## Ideate`** section: + - If present and empty → replace section body with ideate markdown. + - If present and non-empty → **append** new ideate content (prefer dated subheading or clear separator); do not delete prior ideate unless the phase explicitly replaces. + - If missing → insert `## Ideate` after `## Clarifications` (or after `## Brief` if clarifications absent), preserving order of other §9.1 sections. +5. **Never** modify `## Brief` verbatim intake. +6. **Write** — `use_tool` update milestone description with the merged markdown. If description hits size limits → post overflow as Initiative comment titled/tagged for ideate and leave a one-line pointer under `## Ideate`. +7. ****Return** UR slug + milestone id + product project id. No `.do-work/user-requests/` write. + +| Failure | Behavior | +|---------|----------| +| UR / milestone not found | Error to caller | +| Marker missing / unparsable | Stop op; do not invent template | +| MCP / update tool missing | Hard-stop | + +### `append_clarifications` + +| | | +|---|---| +| **Intent** | Append question-phase Q&A onto the UR under `## Clarifications`. Does **not** create REQs. | +| **Preconditions** | Preflight passed; UR exists. | +| **Does not** | Overwrite `## Brief`; replace prior Q&A wholesale (append only). | + +**Agent sequence:** + +1. **Rediscover** — `search_tool` for milestone get/update (same surface as `append_ideate`). +2. ****Resolve + read** UR Project Milestone; require ``. +3. **Locate `## Clarifications`**: + - Append each Q&A as: + + ```markdown + **Q:** {question} + **A:** {answer} + ``` + + - Keep prior entries. If section missing, insert after `## Brief` before `## Ideate`. +4. **Write** updated description via discovered update tool (comment spill same as ideate if needed). +5. ****Return** UR slug + milestone id. + +| Failure | Behavior | +|---------|----------| +| UR missing | Error to caller | +| Marker missing | Stop op | +| MCP missing | Hard-stop | + +### `set_blocked_by` + +| | | +|---|---| +| **Intent** | Write the depends-on graph for a REQ: **authoritative** native `blocks` relations **and** body `**Depends on:**` mirror. | +| **Preconditions** | Preflight passed; target Issue exists; dependency ids are Linear issue ids (or empty list to clear). | +| **Ids** | Linear identifiers only (e.g. `ENG-101`). **Never** markdown `REQ-NNN`. | +| **Authority** | Relations win on diverge (port **Deps authority**). Eligibility consumers use relations when present. | + +**Agent sequence:** + +1. **Rediscover** — `search_tool` for: get/update issue; issue **relations** create/list/delete (queries such as `"linear issue relations"`, `"linear blocks"`, `"linear dependencies"`). Map hits to create/remove `blocks` edges only with **observed** tool names + schemas. +2. **Read issue** by Linear id. Require ``. Parse current `**Depends on:**` and existing relations if list tools exist. +3. **Normalize target set** — caller supplies ordered/unordered list of blocker issue ids (issues that **block** this issue / this issue depends on). Empty list = clear all deps. +4. **Relations path (when create/list/delete relation tools discovered):** + - List existing `blocks` relations involving this issue (schema-dependent: type `blocks` / blockedBy — use fields from live schema). + - **Remove** relations whose other end is not in the target set (only deps edges this op owns; do not delete unrelated relation types). + - **Add** `blocks` relations for each target id missing an edge. Direction: dependency **blocks** the current issue (current issue is blocked by deps) — match Linear’s relation model from live schema docs on the tool; if ambiguous after schema read, hard-stop with gap note rather than guessing both directions. + - On partial relation write failure → hard-stop; do not leave body claiming success without relations if tools were supposed to run. +5. **Body mirror (always when description is writable):** + - Set header `**Depends on:**` to space-separated target Linear ids (or empty / omit value when cleared). + - Preserve all other §9.2 headers and sections. + - `search_tool` + update issue description. +6. **Relations tools missing after live probe:** + - Write body mirror only. + - Emit **one-time warning** to the caller/session: relations unavailable; body is sole store until tools appear; eligibility must treat body as fallback (port rule). Still **no** markdown dual-write. + - Prefer documenting GraphQL/API fallback in this file when spike marks the cell **missing** (not **unknown**). +7. **Return** issue id + final depends-on id list + whether relations were written. + +| Failure | Behavior | +|---------|----------| +| Issue missing | Error to caller | +| Invalid / unresolvable dependency id | Error; do not write partial graph | +| Marker missing | Stop op | +| MCP missing | Hard-stop | +| Relation tool error mid-write | Hard-stop; operator may re-run op to reconcile | + +### `set_files` + +| | | +|---|---| +| **Intent** | Set the footprint list (`**Files:**`) on a REQ Issue. Does **not** claim, unclaim, or change workflow status. | +| **Preconditions** | Preflight passed; Issue exists. | +| **Notes** | Overlap vs other in-flight REQs is evaluated later by `list_claimable_reqs` / claim consumers — this op only writes the declaration. | + +**Agent sequence:** + +1. **Rediscover** — `search_tool "linear update issue"` / `"linear issues"`; get + update tools required. +2. **Read issue** by Linear id. Require ``. +3. **Set header** `**Files:**` to the caller’s space-separated path list (empty clears footprint). Do not invent paths. Preserve all other headers/sections and the machine marker. +4. **Write** description via `use_tool` update. Labels/status/assignee unchanged unless a future combined op says otherwise. +5. **Return** issue id + files list. + +| Failure | Behavior | +|---------|----------| +| Issue missing | Error to caller | +| Marker missing / unparsable | Stop op | +| MCP / update missing | Hard-stop | + +### Hard-stop at create/update time (CRUD-specific) + +| Condition | Behavior | +|-----------|----------| +| Linear MCP tools undiscoverable at `create_ur` / `create_req` / append / `set_*` | Hard-stop + setup instructions; **no** Initiative-as-UR, **no** Issue invent, **no** markdown dual-write | +| `team_id` / `team_key` unresolved | Hard-stop; do not guess | +| Product Project ok, milestone create fail | Hard-stop; no partial UR; operator recovery for orphan milestone if any | +| Create-issue tools missing | Hard-stop; do not write `.do-work/REQ-*` | +| Template required fields unparsable on update/read | Stop the op; do not invent fields (port / design §14) | +| Missing `` / `` on structured write | Stop the op; do not auto-rewrap without explicit migrate | +| Any `status_map` state name missing on team workflow | Hard-stop + rename/override instructions; never invent states | + +--- + + + +--- + +## Non-ticket artifact homes (design §10 — REQ-296) + +Agents **must not invent** homes. Use only the rows below (plus local gate locks). Config titles are authoritative when set. + +| Artifact | Linear home | Format | Writers / readers | Port op / sequence | +|----------|-------------|--------|-------------------|--------------------| +| Decisions | Team Doc title = `tracker.linear.decisions_doc_title` (default **`do-work/decisions`**) | One line per decision: `YYYY-MM-DD \| UR/REQ ref \| decision \| rationale` | capture write; capture / ideate / question / worker read | **`append_decision`**; **Read decisions** helper | +| Calibration | Team Doc title = `tracker.linear.calibration_doc_title` (default **`do-work/calibration`**) | Full calibration body (same shape as markdown `state/calibration.md`) | retro write (full replace); capture read | **Write / read calibration Doc** | +| Run / cost notes | Comment on Issue after attempt; optional Project update for run rollup | YAML fenced block + `` | run | **`append_run_note`** (REQ-294) | +| Verify report | UR Project Milestone description `## Verify` + milestone comment | Full report markdown | verify, go | **`write_verify_report`** | +| Close report | UR Project Milestone description `## Closure` + milestone comment | Per path-unit results (closure schema) | close | **`write_close_report`** | +| Path-milestone cursor (M1/M2) | UR Project Milestone description `` | active M + checklist | capture, run | **`read_active_milestone`** / **`set_active_milestone`** / **`list_milestone_reqs`** (REQ-298 path; **REQ-299** ops) | +| Gate locks | **Local** `{project}/.do-work/state/gate-owner.md`, `final-suite-*.md` | unchanged | run | **`write_gate_state`** (local only; REQ-299 concurrent serialize) | + +**Create-if-missing (Team Docs):** on first write, if no Doc with the configured title exists for the configured team, create it (title exact match to config), then write. Readers: if missing, treat as empty (no decisions / no calibration) — never invent content. + +**Hard-stop (REQ-296 / REQ-297):** if Docs tools (for decisions/calibration) or UR Project Milestone update/comment tools (for verify/close) are undiscoverable after `search_tool`, **or** Team Doc create/update fails (permission, size, MCP error), **or** milestone description append/update fails **and** the §10 milestone-comment path also fails — hard-stop that op with Linear setup / permission instructions. Do **not**: + +- fall back to local `.do-work/decisions.md` / `state/calibration.md` / `closure.md` as the work-item store +- invent alternate Doc titles outside `decisions_doc_title` / `calibration_doc_title` +- invent ad-hoc Issue comments (or Project updates) as a substitute home for decisions, calibration, verify, or close reports + +§10-allowed UR Project Milestone comment for the full verify/close body (with a section pointer) remains valid when description size alone fails. + +--- + + + +--- + +## Claim protocol (design §8 — Linear representation) + +Semantics: `port.md` **Claim / Mid-flight MCP failure**. Linear has **no** filesystem atomic rename — atomicity is **optimistic re-read + comment protocol + timestamps** (intentional; same multi-agent recovery story as markdown concurrent-conflict). + +### Config keys (consumers) + +| Key | Default | Role | +|-----|---------|------| +| `tracker.linear.agent_claim_marker` | `` | First line of every claim-protocol comment | +| `tracker.linear.heartbeat_max_age_seconds` | `null` | Max age of latest **active** heartbeat before stale; **`null` → use `parallel.stale_threshold_seconds`** | +| `parallel.stale_threshold_seconds` | `900` | Fallback stale threshold (seconds) | +| `tracker.linear.status_map.backlog` | `Todo` | Unclaimed / unblocked | +| `tracker.linear.status_map.in_progress` | `In Progress` | Claimed / running / resumed | +| `tracker.linear.status_map.stopped` | `Canceled` | Stopped (claim retained until unblock) | +| `tracker.linear.default_assignee_id` | `""` | Human operator; set on issue **create** only — claim ops never overwrite | + +**Effective stale max age:** + +``` +stale_max = tracker.linear.heartbeat_max_age_seconds +if stale_max is null or missing: + stale_max = parallel.stale_threshold_seconds # default 900 +``` + +A claim is **stale** when the latest **active** claim block’s `heartbeat` ISO timestamp is older than `stale_max` seconds relative to now (UTC). + +### Human assignee vs agent claim + +| Field | Owner | Rule | +|-------|-------|------| +| Linear **assignee** | Human operator | Set from `default_assignee_id` on `create_req` when configured. **Agents never change assignee** for claim, heartbeat, unblock, resume, or status. | +| Workflow **state** | Agent claim lifecycle | Maps via `status_map` (backlog / in_progress / stopped / done). | +| Claim **comment** | Agent | `agent_claim_marker` block with `agent_id`, timestamps, `status: active\|released`. | + +Warn operators (status / docs): **do not clear agent claim comments while a run is live** — clearing them breaks multi-agent coordination the same way deleting a markdown claim stamp would. + +### Claim comment body (canonical) + +Marker text must equal config `agent_claim_marker` (default shown): + +```markdown + +agent_id: hostname.pid +claimed_at: 2026-07-31T12:00:00Z +heartbeat: 2026-07-31T12:05:00Z +session: optional-uuid +status: active +``` + +| Field | Required | Notes | +|-------|----------|-------| +| marker line | yes | Exactly `tracker.linear.agent_claim_marker` | +| `agent_id` | yes | Stable per worker (e.g. `hostname.pid` or orchestrator session id) | +| `claimed_at` | yes on first claim | ISO-8601 UTC; preserve on heartbeat/resume | +| `heartbeat` | yes | ISO-8601 UTC; consumers take the **latest** active block | +| `session` | optional | UUID or run id for triage | +| `status` | yes | `active` (held) or `released` (unblocked / voluntarily dropped) | + +**Parse rules:** + +1. List issue comments (discovered tools). Consider only comments whose body **starts with** (or whose first non-empty line is) `agent_claim_marker`. +2. Parse key: value lines case-sensitively for keys above. +3. **Latest active claim** = among comments with `status: active` (or missing status treated as active only if `agent_id` + `heartbeat` present — prefer explicit `status:`), the one with the newest `heartbeat` (tie-break: newest comment created_at). +4. A claim with `status: released` is **not** active. +5. If multiple agents have concurrent `active` comments, the one with the newest **fresh** heartbeat wins for “who holds”; a second agent attempting claim while another is fresh → **concurrent-conflict**. + +### Concept → Linear mapping + +| Concept | Linear rule | +|---------|-------------| +| **Unclaimed** | Workflow maps to `status_map.backlog` **and** no **active** claim comment (or latest claim is `released`) | +| **Claim** | Re-read issue + comments; if another agent has active claim with **fresh** heartbeat → fail; else set state → `in_progress`; post claim comment (`status: active`) | +| **Heartbeat** | New claim-protocol comment **or** append/update path that writes updated `heartbeat` (prefer new comment if update-comment tools missing); consumers take latest active block | +| **Stale** | Latest active `heartbeat` older than effective `stale_max` — eligible for takeover / reclaim under multi-agent rules | +| **Unblock** | State → `backlog`; post/update claim comment `status: released` (assignee unchanged) | +| **Resume** | `stopped` → `in_progress`; refresh heartbeat on **same** `agent_id` / claim ownership; assignee unchanged | +| **Concurrent conflict** | Same stopper as markdown multi-agent: stop with `concurrent-conflict`; `/do-work resume` allowed when claim still held | +| **Mid-flight MCP death** | **Leave claimed** — do not force backlog or invent cleanup; resume/unblock after MCP recovers | + +### Helper: read active claim (shared) + +Used by claim, heartbeat, list_claimable, status, unblock, resume: + +1. `search_tool` for issue get + list comments (e.g. `"linear issue comments"`, `"linear comments"`). +2. Get issue by Linear id; read workflow state name → map through inverted `status_map`. +3. List comments; filter + parse claim blocks (above). +4. Return: `{ agent_id, claimed_at, heartbeat, session, status, fresh: bool, stale: bool }` for the latest active claim, or empty if none. +5. `fresh` = active and age(heartbeat) ≤ `stale_max`. `stale` = active and age > `stale_max`. + +If comment tools are undiscoverable → **hard-stop** (claim protocol cannot run); never invent comments or fall back to markdown working/. + +--- + +### `list_claimable_reqs` + +| | | +|---|---| +| **Intent** | Return REQs that are backlog, deps-satisfied, footprint-free, and unclaimed (or stale-eligible) — in pick order. **Does not claim.** | +| **Preconditions** | Preflight passed; Product Project + optional UR milestone scope known (optional `UR-NNN` / milestone id). | +| **Authoritative deps** | Native **`blocks` relations** (port). Body `**Depends on:**` is mirror only. | +| **Ids** | Linear issue ids only. | +| **v1 lib** | Implemented as agent/MCP steps only — **not** `lib/pick-req.sh` (markdown). No Linear-aware bash required. | + +**Pick order (REQ-295 — deterministic first-survivor):** + +Sort candidates **before** filtering, then walk in order and return the first survivor (orchestrator typically takes head of the ordered claimable list). Tie-break ladder: + +| Rank | Key | Direction | Source | +|------|-----|-----------|--------| +| 1 | `**Priority:**` | **descending** numeric (`3` most urgent before `1`); missing/empty/malformed → treat as **`2`** (same default as `lib/pick-req.sh` / capture) | Issue body header | +| 2 | `created_at` | ascending (older first) | Linear issue create timestamp | +| 3 | Linear identifier | ascending lexicographic (`ENG-12` before `ENG-100` only if string sort; prefer natural numeric suffix when practical) | e.g. `ENG-123` | + +Milestone / scope filters (when caller passes them) apply **before** the walk: only issues on the scoped UR Project Milestone / path-milestone marker are candidates. + +**Skip reasons (emit one line per rejected candidate — drain-classify parity):** + +| Reason token | When | Run-loop mapping (`drain-classify` intent) | +|--------------|------|---------------------------------------------| +| `scope:` | Caller scope (UR Project / milestone) excludes the issue | `scope-blocked` | +| `claim:` | Active **fresh** foreign claim holds the issue (not reclaimable) | not claimable; re-pick later | +| `dep:` | Authoritative **blocks** (or body fallback) has at least one undones dependency | `deps-blocked` | +| `overlap:` | Footprint path set intersects an in-flight claim’s `**Files:**` | `overlap-blocked` | + +When the ordered walk yields **zero** claimable issues, the orchestrator classifies from the skip multiset with precedence **`overlap-blocked` > `deps-blocked` > `scope-blocked` > `truly-empty`** (same as `lib/drain-classify.sh`). Empty candidate set with no skip lines → `truly-empty`. + +**Footprint algorithm (REQ-295 — parity with `lib/check-footprint.sh` intent):** + +1. Parse candidate Issue body `**Files:**` into a path/glob list (comma- and/or whitespace-separated tokens; trim each). +2. **Empty or missing `**Files:**`** → candidate is **footprint-free** against every peer (empty set intersects nothing). Do not invent paths. +3. Expand each token against the **local** project working tree (runtime stays local): + - Simple globs (`*`, `?`) expand with **nullglob** semantics — patterns that match nothing contribute **no** paths (two unmatched globs do **not** collide with each other). + - `**` (globstar) forms expand by walking descendants under the prefix (same intent as markdown `check-footprint.sh`). + - Literal paths that exist are included as-is; missing literals contribute nothing (nullglob-equivalent). +4. Build the **in-flight peer set**: every other issue whose workflow maps to `in_progress` **or** `stopped` **and** whose latest claim is `status: active` (fresh **or** stale-but-not-yet-unblocked). **Exclude** `done` + `released` (post-`archive_req`) and pure backlog unclaimed issues. +5. For each peer, parse + expand `**Files:**` the same way. If the intersection of expanded path sets is non-empty → reject candidate with `overlap:` (optionally list intersecting paths in detail for status). +6. Do **not** call `lib/check-footprint.sh` as the Linear store — that script reads `.do-work/working/`. Reimplement the **semantics** here via Issue bodies + local path expansion. + +**Agent sequence:** + +1. **Rediscover** — `search_tool` for: list issues by project; get issue; list relations; list comments; list workflow states (already validated at load). +2. **Enumerate candidates** — issues on product Project (filtered by UR milestone when scoped) whose workflow state maps to **`status_map.backlog`**. Exclude `done` / `in_progress` / `stopped` unless a stale active claim is being recovered under explicit reclaim policy (default pick: **backlog + unclaimed only**). Apply scope filter; emit `scope:` for excluded-by-scope backlog issues when useful for classify. +3. **Sort** candidates by the pick-order ladder above. +4. **For each candidate** in sorted order: + - **Claim check** — run **Helper: read active claim**. Skip with `claim:` if active claim is **fresh** (another agent holds it). If active claim is **stale**, treat as reclaimable (eligible) unless caller policy forbids takeover. + - **Deps check** — list `blocks` relations (deps that block this issue). Every dependency issue must be in workflow state mapping to **`status_map.done`** (archived-equivalent). If any dep unsatisfied → `dep:` and continue. If relations tools missing → fall back to body `**Depends on:**` with the one-time warning (port); still no markdown store. + - **Footprint check** — apply the footprint algorithm above; on overlap → `overlap:` and continue. + - **Survivor** — append to claimable ordered list. +5. **Return** ordered list of claimable Linear issue ids (and optional titles) **plus** the skip-reason lines for rejected candidates. Empty claimable list is valid. + +| Failure | Behavior | +|---------|----------| +| MCP / list tools missing | Hard-stop | +| Project missing | Empty list or error to caller | + +--- + +### `claim_req` + +| | | +|---|---| +| **Intent** | Optimistically claim a REQ and move it to in-progress. | +| **Preconditions** | Issue appears claimable under port rules at **re-read** time; caller supplies `agent_id`. | +| **Does not** | Change Linear **assignee**. Does not write local `.do-work/working/`. | + +**Agent sequence:** + +1. **Rediscover** — get issue, update issue (state), list/create comments, list relations (for optional re-check). +2. **Optimistic re-read** (mandatory before any write): + - Get issue by Linear id. + - Map workflow state. Prefer candidate still backlog-equivalent **or** stopped/in_progress only if latest active claim is **stale** and takeover is allowed. + - Read active claim via helper. + - If another `agent_id` holds an **active + fresh** claim → **stop** with reason **`concurrent-conflict`** (do not write). Resume of *that* claimer’s work is for the claim owner / operator, not this agent. + - If **this** `agent_id` already holds active fresh claim → treat as idempotent success (refresh heartbeat optional) or no-op claim. +3. **Write claim** (only after re-read succeeds): + - Set workflow state → `status_map.in_progress` (resolved state id from preflight). **Do not** modify assignee. + - Post a new comment (preferred) with body: + + ```markdown + + agent_id: {agent_id} + claimed_at: {now_iso} + heartbeat: {now_iso} + session: {optional} + status: active + ``` + + Use config `agent_claim_marker` as the first line (default ``). +4. **Post-write re-read (recommended):** re-list claim comments; if another agent’s newer active claim appeared, treat as lost race → **`concurrent-conflict`**; do not fight by overwriting assignee or deleting their comment. Leave both comments; operator/status sees conflict; loser stops. +5. **Return** issue id + claim fields. On conflict: empty commit hash N/A; caller exits stopped with `concurrent-conflict`. + +| Failure | Behavior | +|---------|----------| +| Fresh foreign claim | `concurrent-conflict` — stop; resume allowed later for owner | +| Issue missing | Error to caller | +| Comment or state tools missing | Hard-stop | +| MCP dies after state→in_progress but before comment | **Leave claimed** as far as written; operator resume/unblock; do not invent rollback that races siblings | +| MCP dies after full claim | **Leave claimed** (port mid-flight rule) | + +--- + +### `heartbeat_req` + +| | | +|---|---| +| **Intent** | Refresh liveness on an active claim so siblings do not treat the slot as stale. | +| **Preconditions** | Issue has an active claim owned by this `agent_id` (or orchestrator acting as claim owner). | +| **Does not** | Change workflow state, assignee, or body fields. **No git commit** — comment-only (parity with markdown FS-only heartbeat). | + +**Agent sequence:** + +1. **Rediscover** — get issue + list/create comments. +2. **Read active claim** — must be `status: active` and `agent_id` match (or explicit owner handoff policy). If no active claim → error (nothing to heartbeat). If foreign active fresh claim → error / concurrent-conflict (do not stamp over). +3. **Write heartbeat** — post a new claim-protocol comment (or update the existing comment if update-comment tools exist and schema allows) with: + - same `agent_id`, same `claimed_at` (preserve original claim time) + - `heartbeat: {now_iso}` + - `status: active` + - same `session` if known +4. Consumers always take the **latest** active block by `heartbeat` timestamp. +5. **Return** issue id + new heartbeat time. + +| Failure | Behavior | +|---------|----------| +| Not claim owner / no active claim | Error; do not create a new claim (use `claim_req`) | +| MCP missing mid-heartbeat | Hard-stop; **leave** prior claim/heartbeat as last written | + +**Checkpoint usage (run-worker):** stamp at the same logical checkpoints as markdown (`heartbeat.sh`): after read REQ, after red, after each green cycle, after each verification step, immediately before commit — via this op against the Linear issue id. + +--- + +### `set_req_status` + +| | | +|---|---| +| **Intent** | Set workflow status (e.g. `stopped`, `in-progress`) **without** full archive and **without** clearing claim (unless target is backlog — then prefer `unblock_req`). | +| **Preconditions** | Issue exists; target status key is in `status_map` and validated on team. | +| **Does not** | Steal assignee; archive; strip claim when moving to `stopped`. | + +**Agent sequence:** + +1. **Rediscover** — get/update issue; resolve target Linear state id from `status_map.`. +2. **Map intent:** + - `stopped` — set state → `status_map.stopped`. **Keep** active claim comment (`status: active`); refresh heartbeat optional. Record stopper reason via `append_run_note` or issue comment (not by deleting claim). + - `in_progress` — set state → `status_map.in_progress` (usually via `claim_req` or **resume**, not bare status). + - `backlog` — **do not** use this op alone to clear a claim; call **`unblock_req`**. + - `done` — **do not** use this op; call **`archive_req`** (this file, REQ-294). +3. **Write** state only (+ optional reason comment). Preserve assignee and claim comments. +4. **Return** issue id + new do-work status key. + +| Failure | Behavior | +|---------|----------| +| Unknown status key / missing state on team | Hard-stop (status_map validation) | +| MCP missing | Hard-stop; if already claimed → leave claimed | + +--- + +### `archive_req` + +| | | +|---|---| +| **Intent** | Mark REQ **done** with closure proof and outputs; release the in-flight claim/footprint. Linear is the sole archive store. | +| **Preconditions** | Worker returned `status: done` with non-empty `closure_proof` and AC evidence; **when `review.required: true` (default), post-build review must have returned `status: passed`**; claim owned by the orchestrating flow (or operator-approved). | +| **Does not** | Steal assignee; delete the Issue; write local `.do-work/archive/REQ-*` as source of truth; auto-merge git (merge/PR stay local in `agents/run.md`); run after a failed review or failed acceptance-evidence gate. | + +**Orchestrator gates (REQ-295 — must pass before this op is invoked):** + +| Gate | On failure | Call `archive_req`? | Claim / workflow | +|------|------------|---------------------|------------------| +| Acceptance evidence (`check-acceptance-evidence` / report AC map) | `stopped` / `verification-failing` | **No** | Leave `in_progress` or set `stopped` via `set_req_status`; **claim stays active** | +| Policy blocked (`check-policy` exit 1) | `stopped` / policy-blocked path | **No** | Same — claim intact | +| Review (`agents/review.md`) when `review.required: true` | `stopped` / `review-failed` | **No** | Same — claim intact; optional `append_run_note` with `result: stopped:review-failed` | +| Review when `review.required: false` | Review may be skipped | Yes (if other gates pass) | — | +| Missing / empty `closure_proof` | Do not archive | **No** | Leave claimed | + +Failed review or failed acceptance-evidence **never** transitions to `status_map.done` and **never** posts claim `status: released` via this op. Resume/unblock remain the recovery paths. + +**Agent sequence:** + +1. **Rediscover** — `search_tool` for: get/update issue; list/create comments; list workflow states (already validated at load). Map hits to **observed** tool names + schemas. +2. **Pre-archive re-read** — get issue by Linear id. Confirm: + - Workflow is `in_progress` or `stopped` (not already `done` unless idempotent re-archive policy is explicit). + - Latest claim is `status: active` (preferred) owned by this run, **or** operator override documented in the call. + - Caller asserts review/evidence gates already passed (this op does not re-run review; it trusts the orchestrator). + - If MCP fails here after a prior claim → **leave claimed**; stop; never silent-release and never markdown-archive. +3. **Write body fields** (update Issue description; preserve machine marker `` and other headers): + - Set / replace `**Closure proof:**` with the worker’s non-empty proof string (may cite checkpoint log + commit short hash). + - Ensure `## Outputs` exists; replace or append the orchestrator’s outputs list from the worker YAML (`path` + one-line description per item). Prefer a full section rewrite from the report so the archived Issue matches the attempt. + - Tick ACs already checked by the worker when the body still has `- [ ]` that the report marked passed — do not invent new AC text. + - Optional: set `**Suite:** not-run` when the worker deferred with `category: suite-not-run` (parity with markdown archive header). +4. **State → done** — set workflow to `status_map.done` (resolved state id from preflight). **Assignee unchanged.** +5. **Release claim** — post claim-protocol comment with `status: released` (same shape as `unblock_req` release). Latest released block means the issue is no longer in-flight for footprint purposes. Prefer preserving prior `agent_id` / `claimed_at`. +6. **Optional** — `append_run_note` for the successful attempt (result `done`, cost, model, commit) if the orchestrator has not already written one for this attempt. +7. **Return** issue id + done confirmation. Do **not** create or move local REQ markdown files. + +| Failure | Behavior | +|---------|----------| +| Missing / empty closure proof | Do not archive; leave in_progress/stopped + **leave claimed**; surface to orchestrator (parity with missing-closure-proof) | +| Review / acceptance gate failed | **Do not call** this op; issue stays claimed | +| MCP dies mid-archive (partial body or state write) | **Hard-stop; leave claimed** if claim not yet released; operator re-runs archive or resume after recovery — never silent markdown fallback | +| State tools / comment tools missing | Hard-stop | + +**Parity with markdown `archive_req`:** done status + closure proof + outputs + footprint released. Representation differs (Issue body + workflow + claim comment vs `working/` → `archive/` move). + +**Footprint after archive:** other agents’ `list_claimable_reqs` no longer treat this issue as in-flight (done + released claim), so its `**Files:**` no longer blocks siblings. + +--- + +### `append_run_note` + +| | | +|---|---| +| **Intent** | Append a ledger-ish / cost / run note for a REQ attempt. **Authoritative** work-item note in Linear mode. | +| **Preconditions** | Target Issue (Linear id) exists; attempt context known (agent, model, result, timestamps, optional cost). | +| **Does not** | Replace `archive_req`; change workflow state or assignee; require local `.do-work/runs/` as the store. | + +**Agent sequence:** + +1. **Rediscover** — `search_tool` for issue comments create (and optionally project updates for run rollup). Queries such as `"linear issue comments"`, `"linear create comment"`. +2. **Build note body** — Issue comment with a YAML fenced block carrying ledger fields (same conceptual fields as `lib/run-ledger.sh` / `RUN-NNN.yml`): + + ````markdown + + ```yaml + req: ENG-123 + agent: hostname.pid + model: sonnet + branch: req/ENG-123 + started: 2026-07-31T12:00:00Z + ended: 2026-07-31T12:20:00Z + result: done + review: passed + cost_estimate: "" + commit: abcdef1 + pr_url: "" + commands: [] + tests: [] + changed_files: [] + ``` + ```` + + Adjust fields to what the orchestrator collected; `result` may be `done`, `stopped:`, or `failed`. Marker line `` is stable for readers/retro. +3. **Post comment** on the Issue via discovered create-comment tool. +4. **Optional Project update** — if project-update tools exist and the caller wants a run rollup, post a short summary on product Project or UR milestone (non-authoritative) (non-authoritative convenience; Issue comment remains the home per design §10). +5. **Return** comment id / success. + +| Failure | Behavior | +|---------|----------| +| Comment tools missing | Hard-stop for this op; do not invent a local markdown “note store” as work-item substitute | +| MCP dies after claim, during note | **Leave claimed**; stop; retry note later — never silent-release | + +#### Local ledger telemetry (optional; not a second store) + +When `ledger.enabled: true`, the orchestrator **may also** append `{project}/.do-work/runs/RUN-NNN.yml` via `lib/run-ledger.sh` for offline retro tooling (design §7). + +| Store | Role when `backend: linear` | +|-------|------------------------------| +| **Issue comment via `append_run_note`** | **Authoritative** run/cost note | +| **Local `RUN-NNN.yml`** | **Telemetry only** — offline sum/budget/retro convenience | +| **Local UR/REQ markdown** | **Not** a work-item store; do not dual-write REQs | + +Rules: + +1. Local ledger **must not** become the system of record for work items or claim state. +2. Retro prefers Linear run notes when `backend: linear`; falls back to local runs if comments are unavailable. +3. If `ledger.enabled` is false, skip local file; still prefer `append_run_note` for Linear run history when the attempt warrants a note. +4. Budget gate may sum local telemetry when present; if only Linear notes exist, sum from those comments or skip numeric gate with an explicit note — never invent spend. + +#### List run notes (helper — retro / budget; not a separate port op name) + +Readers (primarily **`agents/retro.md`**, optionally budget/status) collect authoritative Linear run history when `backend: linear`: + +1. **Rediscover** issue list/get + comment list tools (`search_tool`). +2. **Scope** — Issues under the product Project for the configured team (or a single UR milestone when scoped). Prefer Issues that have been attempted (in_progress / stopped / done), not pure backlog with zero comments. +3. **List comments** per Issue; keep bodies whose first marker line is `` (same marker as `append_run_note`). +4. **Parse** the YAML fenced block (fields: `req`, `agent`, `model`, `result`, timestamps, cost, commit, …). Treat parse failures as skip-with-warning (do not invent stats). +5. **Prefer** these notes for retro interpretation when present. If comment tools fail or zero notes found → fall back to local `{project}/.do-work/runs/RUN-NNN.yml` telemetry (if any). Never dual-write a fabricated local ledger from partial Linear data. +6. Do **not** invent spend, stop rates, or shapes from narrative Issue comments that lack the run-note marker. + +--- + +### `append_decision` + +| | | +|---|---| +| **Intent** | Append one standing decision line to the team's decisions memory (append-only). | +| **Preconditions** | `tracker.backend: linear`; team resolvable; decisions Doc title from config known. | +| **Home** | Team Doc titled `tracker.linear.decisions_doc_title` (default **`do-work/decisions`**). **Never** invent a different title or a local `.do-work/decisions.md` store while backend is linear. | +| **Does not** | Rewrite prior lines; change Issues; write calibration. | + +**Line format** — **identical** one-line grammar to markdown `.do-work/decisions.md` / SKILL.md § Decisions Memory (four pipe-separated fields; no paragraphs): + +``` +YYYY-MM-DD | UR/REQ ref | decision | rationale +``` + +| Field | Rule | +|-------|------| +| `YYYY-MM-DD` | UTC date the decision was recorded | +| `UR/REQ ref` | UR slug (`UR-035`) and/or Linear issue id (`ENG-123`) — same slot as markdown `REQ-NNN` | +| `decision` | Standing choice, stated as a constraint | +| `rationale` | One phrase explaining why | + +**Discipline (parity with markdown):** append-only; never rewrite or delete a prior line; supersede with a new line that references the old; absent Doc on **read** = empty set (do not create on read). + +**Agent sequence:** + +1. **Rediscover** — `search_tool` for Linear **Team Docs** (list/get/create/update). Queries such as `"linear team docs"`, `"linear document"`, `"linear create document"`. Use only qualified names + schemas returned. +2. **Resolve title** — `title = tracker.linear.decisions_doc_title` if non-empty, else `do-work/decisions`. **Never** invent another title. +3. **Find or create** — list/search Docs on the configured team for exact title match. + - If found → load body. + - If missing → **create-if-missing** with that exact title and empty or header-only body (e.g. `# do-work decisions\n\n` plus append-only lines below). +4. **Append one line** — build `YYYY-MM-DD | | | ` (UTC date). Append as a new trailing line; preserve all existing lines. Do not reorder or edit prior decisions. +5. **Update Doc** — write the full new body via discovered update tool. +6. **Return** Doc id + success. Do **not** also append to local `.do-work/decisions.md`. + +| Failure | Behavior | +|---------|----------| +| Docs tools missing / unauthenticated | Hard-stop; Linear setup instructions; **no** local decisions file as substitute store | +| Team unresolved | Hard-stop | +| Create fails (permission / size / MCP) | Hard-stop; **do not** invent Issue comments, alternate Doc titles, or local `decisions.md` | +| Update fails (permission / size / MCP) | Hard-stop; leave Doc as-is; **do not** invent alternate homes; retry later | + +#### Read decisions (helper — not a separate port op name) + +Readers (**capture, ideate, question, run-worker** — REQ-297) load standing decisions as **constraints**: + +1. Same rediscovery + title resolution as `append_decision` (`decisions_doc_title` / default `do-work/decisions`). +2. If Doc missing → empty set (continue; never create on read-only path). +3. If present → parse body lines matching the four-field decision grammar; hold in context. Same discipline as markdown `.do-work/decisions.md` readers (worker treats lines as hard constraints; ideate/question use them as evidence / contradiction flags). +4. Do **not** also read local `.do-work/decisions.md` when `backend: linear`. + +--- + +### Write / read calibration Doc + +Calibration is **not** a separate port op name in `port.md`; representation under Linear is fixed here so retro/capture do not invent homes. Full body shape matches markdown `state/calibration.md` (header + `## Capture guidance` bullets + ``). + +| | | +|---|---| +| **Intent** | Persist (retro) or load (capture) capture-facing calibration guidance. | +| **Home** | Team Doc titled `tracker.linear.calibration_doc_title` (default **`do-work/calibration`**). | +| **Write semantics** | **Full replace** every retro run (truncate-write equivalent) — never append-merge with prior bullets. | +| **Read semantics** | Advisory only; absence is silent no-op. | + +**Write sequence (retro):** + +1. **Rediscover** Team Docs tools (`search_tool`). +2. **Resolve title** — `tracker.linear.calibration_doc_title` or default `do-work/calibration`. +3. **Find or create-if-missing** Doc with that exact title on the configured team. +4. **Build body** — same markdown format as retro Step 5 (≤8 guidance bullets, retro-meta footer). +5. **Replace entire body** (not append). +6. **Return** Doc id. Do **not** also write `{project}/.do-work/state/calibration.md` as the store when `backend: linear`. + +**Read sequence (capture):** + +1. Rediscover + resolve title. +2. If Doc missing → continue without calibration. +3. If present → load body; keep guidance bullets as advisory input (brief always wins). + +| Failure | Behavior | +|---------|----------| +| Docs tools missing on write | Hard-stop retro calibration write; do not invent local calibration store | +| Create/update fails (permission / size / MCP) | Hard-stop; **do not** invent alternate Doc titles, Issue comments, or local `state/calibration.md` | +| Docs tools missing on read | Treat as absent calibration (advisory path); do not hard-stop capture solely for missing Docs on read if the rest of capture can proceed without it — prefer hard-stop only when backend is linear **and** the agent was required to read remote work-items that also failed | + +**Empty retro (`runs=0` and no Linear run notes to interpret):** do **not** create or replace the calibration Doc (parity with markdown: write no file). + +--- + +### `write_verify_report` + +| | | +|---|---| +| **Intent** | Persist verify-phase coverage report for a UR. | +| **Preconditions** | UR Project Milestone exists (`read_ur` / product Project + milestone); verify agent has produced the report body. | +| **Home** | UR Project Milestone description section **`## Verify`** + **milestone comment** with the full report. **Not** a local file under `user-requests/` as source of truth. | +| **Does not** | Create REQs; change claim state; invent alternate section names. | + +**Agent sequence:** + +1. **Rediscover** — tools to get/update Project Milestone description and create comments. Queries such as `"linear milestone"`, `"linear update milestone"`, `"linear create comment"`. +2. **Resolve UR** — load UR Project Milestone for `UR-NNN` (`read_ur`). Confirm `` body. +3. **Build report** — full markdown verify report (confidence score, coverage, gaps, issues, summary — same console shape as `agents/verify.md` Step 5c). +4. **Update `## Verify` section** — replace or insert content under `## Verify` in the UR Project Milestone description (prefer description append/replace of that section only; do not overwrite `## Brief`). Include at least: confidence score, recommendation, and a short summary. If the full report exceeds size limits, put a one-line pointer in `## Verify` (e.g. `Full report: see Initiative comment `) and put the **full** body in the comment. +5. **Post milestone comment** — full report body, optionally prefixed with `` for stable readers. +6. **Return** Initiative id + comment id / success. Do **not** write a durable local verify path as the work-item store. + +| Failure | Behavior | +|---------|----------| +| Milestone tools missing | Hard-stop | +| UR / milestone not found | Hard-stop; do not invent Initiative | +| Size limit on description only | Section pointer + full **milestone** comment (required §10 path above) — **not** inventing a home | +| Description **and** Initiative comment both fail (permission / size / MCP) | Hard-stop; **do not** invent Issue comments, alternate Docs, or local verify files as the store | + +**Markdown backend note:** `markdown.md` remains console-primary for verify (no fixed durable path). Linear makes verify durable via this op. + +--- + +### `write_close_report` + +| | | +|---|---| +| **Intent** | Persist close-phase path-unit closure report for a UR. | +| **Preconditions** | UR Project Milestone exists; close agent has produced the closure document (YAML front matter + per path-unit rows). | +| **Home** | UR Project Milestone description section **`## Closure`** + **milestone comment** with the full closure report. **Not** `{project}/.do-work/user-requests/UR-NNN/closure.md` as source of truth under Linear. | +| **Does not** | Edit REQs/source; reopen Issues; put gate locks in Linear. | + +**Agent sequence:** + +1. **Rediscover** milestone get/update + comment create tools. +2. **Resolve UR** — UR Project Milestone for `UR-NNN` via `read_ur`. +3. **Build report** — same schema as `agents/close.md` Step 5 (`ur`, `closed_at`, `branch`, `path_units`, `verdict_summary`, `overall`, plus per-path-unit rows). Empty path-unit case still writes a valid `overall: no-path-units` report. +4. **Update `## Closure` section** — replace/insert under `## Closure` only. Short summary in description is fine; full YAML+rows may live in the comment if size-constrained (pointer line in section required when spilling). +5. **Post milestone comment** — full closure markdown, optionally prefixed with ``. +6. **Evidence artifacts** — screenshots / command captures remain **local** under a UR-scoped path only if the operator needs files on disk (optional); `evidence_ref` may point at local paths or inline snippets. Local evidence files are **not** a second work-item store for the report itself. +7. ****Return** milestone id + success. Do **not** dual-write authoritative `closure.md` under `user-requests/` when `backend: linear`. + +| Failure | Behavior | +|---------|----------| +| Milestone tools missing | Hard-stop | +| UR missing | Hard-stop | +| Size limit on description only | Section pointer + full **milestone** comment (§10) | +| Description **and** Initiative comment both fail (permission / size / MCP) | Hard-stop; **do not** invent Issue comments, alternate Docs, or local `closure.md` as the store | + +#### Close path-unit collection (Linear — REQ-297) + +When `agents/close.md` walks a UR under `backend: linear`, path-units come from Linear Issues — not from local `archive/REQ-*.md`: + +1. Resolve product Project + UR milestone and call **`list_reqs_for_ur`** (all Issues in that Project; include done/archived-equivalent). +2. For each Issue body, treat as a **path-unit** when `**Layer:**` is `none` **and** both `**Entry point:**` and `**Terminal state:**` are present and non-empty after trim. +3. Extract: `req` = **Linear issue identifier** (e.g. `ENG-123`); `entry_point` / `terminal_state` = verbatim header values. +4. Do **not** read `**Closure proof:**` (same cold-dispatch rule as markdown). +5. Walk still runs against the **merged local app** (git). Persist results only via **`write_close_report`**. +6. Closure row `req:` fields and report headings use Linear ids (`## ENG-123 — closed`), never parallel `REQ-NNN` allocation. + +Brief load under Linear: **`read_ur`** (UR Project Milestone description `## Brief` / machine sections) — do not require local `user-requests/UR-NNN/input.md` as the store. + +--- + + + +--- + +## Footprint and deps in the run loop (REQ-294 / REQ-295) + +| Concern | Linear rule | Markdown parity | +|---------|-------------|-----------------| +| **Deps satisfied?** | Every issue on the authoritative **`blocks`** graph (deps that block this issue) is in `status_map.done` | `**Depends on:**` ids in `archive/` | +| **Deps diverge** | Relations **win**; body `**Depends on:**` is display/mirror | File header is the store | +| **Footprint free?** | Footprint algorithm under `list_claimable_reqs` (empty Files = free; nullglob; in-flight = active claim on in_progress/stopped) | `lib/check-footprint.sh` vs `working/` | +| **After `archive_req`** | Done + released claim → no longer in-flight; footprint frees for siblings | File left `working/` | +| **Pick order** | Priority **DESC** (3 before 1; missing→2) → created_at ASC → identifier ASC (REQ-295) | Priority DESC (missing→2) then numeric REQ id in `pick-req.sh` | +| **Skip reasons** | `dep:` / `overlap:` / `scope:` / `claim:` lines (REQ-295) | pick-req stderr `dep` / `overlap` / `scope` | + +`list_claimable_reqs` (above) implements both checks. Run Step 1 must not re-implement with local REQ files while `backend: linear`. + +--- + +## No Linear-aware bash in `lib/` (v1 — REQ-295) + +| Surface | v1 home | +|---------|---------| +| Pick / claim / deps / footprint / heartbeat / unblock / archive integrity (Linear) | **Agent sequences in this file** via Linear MCP (`search_tool` → `use_tool`) | +| Markdown store of the same ops | Existing `lib/pick-req.sh`, `claim-req.sh`, `check-deps.sh`, `check-footprint.sh`, `heartbeat.sh`, `check-archive-integrity.sh`, … | +| Local runtime (both backends) | `provision-worktree.sh`, worktrees, merges, `state/*` locks, events, optional `run-ledger.sh` telemetry | + +**Do not** add Linear API clients, tokens, or GraphQL shells under `lib/` for v1. If a future REQ introduces Linear-aware bash, it must be explicit and tested — out of scope here. + +--- + +## Deps authority (Linear) + +Native **`blocks` relations** are authoritative for `list_claimable_reqs` / deps checks. Issue body `**Depends on:**` is a **mirror**. **`set_blocked_by`** (REQ-291 sequence) always: + +1. Updates relations when relation tools are discoverable (add/remove to match the target set). +2. Updates the body mirror in the same op. +3. If relations tools are **missing** after live probe → body-only + one-time warning (port rule); never markdown dual-store. +4. If spike later marks relations **missing** (not merely **unknown**), document GraphQL/other fallback in this section before production claim depends on it. + +Dependency ids are **Linear issue identifiers only**. + +--- + + diff --git a/references/linear-path-milestones.md b/references/linear-path-milestones.md new file mode 100644 index 0000000..1d6fea6 --- /dev/null +++ b/references/linear-path-milestones.md @@ -0,0 +1,343 @@ +# Linear path-milestone mode sequences (reference) + +One hop from [`agents/tracker/linear.md`](../agents/tracker/linear.md). Load for deploy-gate / M1–Mn delivery mode only. + +## Disambiguation (read first) + +| | **Milestone-as-UR** | **Path-milestone mode (M1/M2)** | +|--|---------------------|--------------------------------| +| Purpose | Hierarchy: the UR itself | Delivery bridges *within* one UR | +| Linear entity | **Project Milestone** named via `ur_milestone_name_pattern` | Not a separate UR entity | +| Cursor | n/a (the milestone *is* the UR) | `` block on the **UR Project Milestone description** | +| Issues | All REQs for the UR attach to the UR milestone | Additionally tagged `M1` / `**Milestone:** M1` for listing | +| Gate | n/a | Local `state/gate-owner.md` only | + +**Never** invent Initiative-as-UR. **Never** put gate ownership in Linear. + +--- + +## Milestone mode (design §11 — REQ-298 path; REQ-299 ops) + +### Trigger (unchanged) + +Identical to markdown capture / run: + +1. UR brief frontmatter or body contains `source: /saas-thesis handoff`. +2. Body contains a `### Milestones` heading with at least one `#### M1` (or higher) subheading. + +Both required → **milestone mode**. Neither Linear labels nor Project cursor alone turn milestone mode on. Brief load under Linear: **`read_ur`** (`## Brief` / machine sections); do not invent a different trigger. + +### Project description cursor block (marker format) + +Authoritative work-item cursor under `backend: linear`. Lives on the **UR Project Milestone** description (Milestone-as-UR entity on `product_project`), not a separate per-UR Project and not local `active-milestone.md`. + +```markdown + +**Active:** M1 + +# Milestones + +- [x] M1 — — captured +- [ ] M2 — — pending +- [ ] M3 — — pending +``` + +| Field | Rules | +|-------|--------| +| `` | Required first line of the machine block. Absent on Project ⇒ **not** in milestone mode (same as missing `active-milestone.md`). | +| `**Active:**` | Single token `M` (e.g. `M1`) or empty / `none` when cursor cleared after all deployed or gate stop. | +| `# Milestones` checklist | One line per bridge milestone. Status suffix: `pending` \| `captured` \| `running` \| `deployed` (parity with markdown `milestones.md`). Checked box when status is `captured` or later; agents may keep `[x]` only for `deployed` if they prefer — **status word is authoritative**. | + +**Statuses (same vocabulary as markdown capture):** + +| Status | Meaning | +|--------|---------| +| `pending` | Not yet captured | +| `captured` | REQs written for this M | +| `running` | Run loop active for this M (optional stamp) | +| `deployed` | Deploy gate passed for this M | + +#### Parse algorithm (REQ-299) + +Given Project description text `D`: + +1. Locate the first line that is exactly (or trims to) ``. +2. **If not found** → marker absent → return `{ active: null, checklist: [] }` — **does not invent a milestone id**. +3. Collect the machine block from that marker through the end of the `# Milestones` checklist (until blank line before next top-level machine marker, next `` that is not checklist content, or EOF). +4. Within the block, find the first line matching `**Active:**\s*(.*)$`. Trim the capture group: + - empty, missing, or case-insensitive `none` → `active: null` (still **does not invent a milestone id**). + - else require token shape `M` + digits (e.g. `M1`, `M12`); malformed → treat as `active: null` (do not invent / coerce). +5. Parse checklist lines under `# Milestones` matching `- [ |x|X] M — … — ` into `{ id, name, status, checked }` rows (best-effort; active id does not require checklist parse success). +6. Return `{ active, checklist }`. Never read local `state/active-milestone.md` as the store under Linear. + +### Issue milestone markers (for `list_milestone_reqs`) + +When capture creates Issues under milestone mode, mark membership so listing does not depend on markdown `REQ-M1-NNN` filenames: + +1. **Prefer** Linear Project **milestone entity** / issue–milestone link when live `search_tool` finds such tools — attach the issue to milestone `M` (or the entity named `M` / matching title). +2. **Else (v1 default):** apply a **label** whose name is exactly the milestone id (`M1`, `M2`, …) when label tools exist, **and** set body header `**Milestone:** M1` (same id) next to other `` headers. +3. **Parse order for filters:** (entity attachment if present) → label `M` → body `**Milestone:** M`. Any one match includes the issue. Missing all three → issue is **not** in that milestone (do not invent). + +Path-unit parents and layer children for the same unit share the same milestone marker. + +### `read_active_milestone` + +| | | +|---|---| +| **Intent** | Read the active milestone cursor (if any). | +| ****Home** | UR Project Milestone description block `` (path-milestone mode cursor — not the UR entity itself). | +| **Preconditions** | None beyond readable Project; missing / empty block ⇒ not in milestone mode. | +| **Returns** | `{ active: "M1" \| null, checklist: [...] }` — `active` null when marker missing, `**Active:**` empty/`none`/malformed, or Project unresolved. **Does not invent a milestone id.** | + +**Agent sequence:** + +1. **Rediscover** Project get/list tools (`search_tool` → `use_tool`). +2. **Resolve Project** — name `do-work/{UR-id}` (or Project id from `read_ur` / `**Project-id:**`). Caller may pass Project id or UR id. +3. **Read description.** Apply **Parse algorithm** above. +4. **Return** structured result. Do **not** read local `state/active-milestone.md` as the store. Do **not** default missing cursor to `M1` inside this op. Do **not** confuse path-milestone cursor with the UR Project Milestone entity. + +| Failure | Behavior | +|---------|----------| +| Milestone tools missing | Hard-stop — Linear setup; do **not** fall back to local `active-milestone.md` as work-item store | +| UR Project Milestone missing | Hard-stop (UR not provisioned) | +| Marker missing | Return `active: null` (not-in-milestone) — **does not invent a milestone id**; not an error | +| `**Active:**` empty / `none` / malformed | Return `active: null` — **does not invent a milestone id** | + +**Caller defaults (not part of this op):** capture Step 1b may use `M1` as the first-decompose target when `active` is null and the brief trigger is true. That policy lives in `agents/capture.md` and must call **`set_active_milestone`** to persist — it is not a fabricated return from `read_active_milestone`. + +### `set_active_milestone` + +| | | +|---|---| +| **Intent** | Set, advance, or clear the active milestone cursor; maintain checklist status. | +| **Home** | Same Project description block as `read_active_milestone`. | +| **Preconditions** | Milestone mode applicable (trigger was true at capture, or block already exists); target id is `M` or clear. | +| **Does not** | Own the deploy-gate y/n prompt; write `gate-owner.md` (use **`write_gate_state`**); create Issues. | + +**Agent sequence:** + +1. ****Rediscover** Project Milestone get/update tools. +2. **Resolve UR Project Milestone** for the UR. +3. **Read** current description + existing path-milestone cursor block (create block if capture is writing first cursor). +4. **Apply caller intent:** + - **Set / advance** to `M`: set `**Active:** M`; update checklist line for prior M to `deployed` (or caller-supplied status); set target line to `captured` / `running` / as requested. + - **Capture stamp:** after capture writes REQs for `M`, set `**Active:** M` and mark that line `captured` (create full checklist from brief `### Milestones` on first write). + - **Clear** (all deployed, or gate `n` stop): set `**Active:**` empty or remove the active value; mark remaining lines per caller; or strip the whole block when the run stops with no next M. Prefer leaving checklist history with `deployed` marks when useful for humans. +5. ****Write** UR Project Milestone description — replace **only** the `` path-milestone machine block; preserve §9.1 sections outside the block. +6. **Return** new `active` value (or null if cleared). + +| Failure | Behavior | +|---------|----------| +| Milestone tools missing / update fails | Hard-stop; do **not** write local `active-milestone.md` as substitute store | +| Invalid target id | Hard-stop / refuse | + +**Deploy-gate consumers (run Step 7b):** on human **y**, call `set_active_milestone` with next pending id (or clear if none). On human **n**, clear active. Gate file lifecycle stays on **`write_gate_state`**. + +### `list_milestone_reqs` + +| | | +|---|---| +| **Intent** | List REQs (Linear Issues) belonging to the active or named milestone. | +| **Preconditions** | Milestone id known (`M`) or active cursor set via `read_active_milestone`. | +| ****Scope** | Issues on product Project attached to this UR Project Milestone only. | + +**Agent sequence:** + +1. **Resolve milestone id** — argument `M`, else `read_active_milestone` → if `active` null, return empty list (not milestone mode). **Do not invent** an id to list against. +2. **Rediscover** issue list tools; optionally milestone-entity tools. +3. **`list_reqs_for_ur`** (or equivalent Project-scoped issue list) for the UR Project. +4. **Filter** to issues whose milestone marker matches `M` (entity / label / `**Milestone:**` — see above). +5. **Optional status filter** (caller): + - `backlog` — workflow maps to `status_map.backlog` (claimable candidates for this M). + - `in_flight` — `in_progress` or `stopped` with active claim. + - `done` — `status_map.done`. + - `any` (default) — all membership matches. +6. **Return** ordered list of Linear issue ids (+ optional titles/status). Sort: Priority DESC (missing→2), created_at ASC, id ASC (same as `list_claimable_reqs` when used for pick). + +**Used by:** + +| Consumer | How | +|----------|-----| +| Run Step 1.0 | Constrain claim pool to active M (`list_milestone_reqs` ∩ `list_claimable_reqs`, or pass milestone scope into claimable walk) | +| Run Step 7b drain | Backlog for M must be empty; no foreign in-flight claims for M | +| Worker milestone_complete | No remaining non-done issues for active M in Project (or no backlog + no foreign in-flight) | +| Capture numbering | Count existing issues for M when assigning sequence metadata (Linear ids remain authoritative identifiers) | + +| Failure | Behavior | +|---------|----------| +| Issue list tools missing | Hard-stop | +| Active unknown and no id arg | Empty list | + +**No fallback to other milestones** — same rule as markdown: empty list means this M is drained for that filter; do not widen to M2 while active is M1. + +### `write_gate_state` + +| | | +|---|---| +| **Intent** | Coordinate deploy-gate ownership / final-suite locks. | +| **Home** | **Local only** — `{project}/.do-work/state/gate-owner.md` (and related `state/final-suite-*.md` locks). **Never** Linear Docs, Issues, Project description, or Initiative fields. **Remains local-allowed** under Linear backend (REQ-299). | +| **Preconditions** | Milestone / gate flow active; project filesystem writable. | + +**Agent sequence (backend-agnostic; same under markdown and linear):** + +1. Ensure `{project}/.do-work/state/` exists (`mkdir -p`). +2. To **claim gate ownership** (concurrent serialize — REQ-299): + - **Read** `gate-owner.md` if present. + - If present and content (trimmed) is a **different** `AGENT_ID` → **do not overwrite**; return `{ owned: false, owner: }` so the caller enters sibling idle-wait (run Step 1.0a). Concurrent gate ownership serializes via this **local** file even when milestone cursor content is remote. + - If absent, or content is self / malformed-as-absent: write single-line local `AGENT_ID`. + - **Re-read** after write. If contents ≠ local `AGENT_ID` → lost race; return `{ owned: false, owner: }` (do not show the deploy-gate prompt). + - If contents = local `AGENT_ID` → return `{ owned: true, owner: }`. +3. To **release**: delete `gate-owner.md` when the gate resolves (y or n). +4. Final-suite coordination files under `state/` follow existing run-agent rules. +5. **Return** path written/deleted and ownership result. + +| Failure | Behavior | +|---------|----------| +| Cannot write `state/` | Hard-stop gate coordination; do not invent a Linear lock substitute | +| Foreign owner already present | Yield — do not clobber; siblings idle | + +This op is **not** a dual-write of work items — it is the intentional local runtime lock allowed by design §5.5 / §10 / §11 / port.md. Under Linear milestone mode, **cursor** changes go through `set_active_milestone` (Project description); **gate ownership** always goes through this local file. **Siblings idle on deploy gate the same as markdown mode** (run Step 1.0a): foreign `gate-owner.md` → poll `read_active_milestone` + local gate file until cursor advances or clears. + +--- + +### Commits and PRs (Linear mode — design §6.5) + +Runtime/git stay local. Message format uses the **Linear issue id**: + +``` +feat(ENG-123): short title + +Issue: ENG-123 +UR: UR-007 +Output: path/to/primary/output +``` + +| Rule | Detail | +|------|--------| +| Subject scope | `feat(ENG-123):` / `fix(ENG-123):` / `chore(ENG-123):` — Linear identifier, not `REQ-NNN` | +| Footer | `Issue: ENG-123` (required); `UR: UR-NNN` when known; `Output:` primary path | +| Archive path | **No** `.do-work/archive/REQ-…` line required | +| Branch | **`req/`** after **Branch sanitize** (below) | +| Worktree dir | `{project}/.worktrees/req-` (hard default lowercase; see sanitize) | +| PR title/body | Same id convention when `delivery.mode: pr` | +| Markdown backend | Unchanged: `feat(REQ-NNN):` + `REQ:` / `UR:` / `Output:` paths | + +Workers and orchestrators under `backend: linear` use this convention for implementation commits and PR metadata. See `agents/run-worker.md` W2 / Step 8 and `agents/run.md` merge/archive/PR steps. + +#### Branch sanitize (REQ-295) + +Git refs disallow some characters. Derive branch and worktree names from the Linear issue id: + +| Step | Rule | Example (`ENG-123`) | +|------|------|---------------------| +| 1. Start | Linear issue identifier as returned by Linear | `ENG-123` | +| 2. Allowed set | Keep `[A-Za-z0-9._-]` only | `ENG-123` | +| 3. Replace | Map every other character (spaces, `/`, `:`, etc.) to `-` | — | +| 4. Collapse | Collapse consecutive `-` / `.` runs; strip leading/trailing `-` and `.` | — | +| 5. Branch | `req/` (preserve identifier case as sanitized) | `req/ENG-123` | +| 6. Worktree path | **Hard default:** `{project}/.worktrees/req-` — always lowercase the sanitized id for the directory name (FS consistency across case-sensitive/insensitive hosts). Do not keep mixed-case worktree dirs. | `.worktrees/req-eng-123` | +| 7. Empty guard | If sanitize yields empty, hard-stop (do not invent a branch name) | — | + +Orchestrator merge / PR / teardown **must** use the same branch string the worker created (pass it through the worker report or reconstruct via the same sanitize function). Never mix `req/REQ-NNN` markdown naming with Linear issue ids on the same run. + +--- + +### `unblock_req` + +| | | +|---|---| +| **Intent** | Return a REQ to backlog and **release** the agent claim (markdown: strip stamp + move out of `working/`). | +| **Preconditions** | Issue is in-flight or stopped with a claim, or explicitly targeted by operator `/do-work unblock`. | +| **Does not** | Change human assignee; delete issue; auto-revert git commits (git recovery stays local/operator, same as `agents/unblock.md` judgment). | + +**Agent sequence:** + +1. **Rediscover** — get/update issue, list/create comments. +2. **Read** current state + active claim (for status report / audit). +3. **Release claim** — post claim-protocol comment: + + ```markdown + + agent_id: {prior_or_operator} + claimed_at: {prior_claimed_at_or_now} + heartbeat: {now_iso} + session: {optional} + status: released + ``` + + Prefer preserving prior `agent_id` / `claimed_at` when known so history remains readable. Latest block with `status: released` means **unclaimed**. +4. **State → backlog** — set workflow to `status_map.backlog`. **Assignee unchanged.** +5. **Do not** write local backlog files. Optional: `append_run_note` that unblock occurred. +6. **Return** issue id + released. + +| Failure | Behavior | +|---------|----------| +| Issue missing | Error (“nothing to unblock”) | +| MCP missing after partial write | Hard-stop; operator re-runs unblock when healthy — do not silent-markdown | +| Comment posted but state update fails | Hard-stop with recovery: re-run unblock to set backlog | + +**Parity with markdown `agents/unblock.md`:** claim cleared + status backlog + available for `list_claimable_reqs`. Git partial-commit judgment remains outside the tracker port (local). + +--- + +### Resume (Linear — `agents/resume.md` consumer) + +Resume is **not** a separate port op name; it composes `set_req_status` + `heartbeat_req` (and preserves claim ownership). Match markdown resume semantics: + +| | | +|---|---| +| **Intent** | Re-dispatch work for a **stopped** REQ without unclaim / backlog round-trip. | +| **Preserves** | Active claim (`agent_id`, `claimed_at`); human assignee. | +| **Changes** | Workflow `stopped` → `in_progress`; heartbeat refreshed. | + +**Agent sequence:** + +1. **Rediscover** + get issue by Linear id (caller passes e.g. `ENG-123`). +2. **Confirm stopped** — workflow maps to `status_map.stopped`. If not stopped → refuse (same as markdown: only stopped REQs resume). +3. **Confirm claim** — latest claim is `status: active` (prefer same agent / operator-approved). If claim is `released` or missing → refuse; tell operator to use run/claim or unblock path, not resume. +4. **Set state** → `status_map.in_progress` (**assignee unchanged**). +5. **`heartbeat_req`** — refresh `heartbeat` now; keep `agent_id` / `claimed_at`. +6. **Return** issue id; orchestrator re-dispatches worker (worktree/branch rules stay local). + +| Failure | Behavior | +|---------|----------| +| Not stopped | Refuse | +| No active claim | Refuse — not a resume candidate | +| Fresh foreign claim | `concurrent-conflict` / refuse | +| MCP missing | Hard-stop; **leave claimed** (still stopped or partial in_progress) | + +--- + +### Status reporting (claimers / heartbeats) + +**Consumer:** `agents/status.md` Step **1L** when `/do-work status` runs with `backend: linear`. + +Do **not** glob `.do-work/working/` or run `lib/synth-status.sh` as the work-item store. Instead: + +1. **Rediscover** list issues (scope: product Project + optional UR Project Milestone, or all UR milestones on the product Project). Prefer `list_reqs_for_ur` / list-by-project sequences already documented above. +2. For each issue with workflow in `in_progress` or `stopped` (and optionally recent `released` for audit): + - Run **Helper: read active claim** — parse latest claim-protocol comment (`agent_claim_marker` / ``) → show **claimer** (`agent_id`), **claimed_at**, **heartbeat**, **fresh/stale** vs effective `stale_max`, claim `status`. +3. Surface **stale** active claims as warnings (parity with `lib/scan-stale.sh` / deadlock banner intent). +4. Surface **deps** from authoritative **`blocks` relations** when tools exist (body `**Depends on:**` is mirror only). +5. Never invent local REQ paths; identify rows by Linear issue id. +6. Read-only — status never posts claim comments or changes workflow state. + +--- + +### Concurrent-conflict and mid-flight (summary) + +| Event | Behavior | +|-------|----------| +| Claim re-read sees foreign **fresh** active claim | Stop `concurrent-conflict`; no assignee change; resume allowed for claim owner | +| Lost race on post-write re-read | Same stopper; do not delete the other agent’s comment | +| MCP dies after successful claim, before archive/unblock | **Leave claimed** (in_progress + active claim comment + last heartbeat); worker/orchestrator **stops**; resume or unblock after recovery | +| MCP dies mid-`archive_req` before claim release | **Leave claimed** if still active; re-run archive when healthy | +| MCP dies before claim completes | Hard-stop; no markdown substitute store | +| Silent-release or markdown fallback after claim | **Forbidden** — never auto-release claim; never switch to markdown work-item ops while `backend: linear` | +| Operator clears claim comments in Linear UI mid-run | Protocol broken — status should warn; treat as unclaimed/ambiguous and stop rather than invent state | + +**Mid-flight policy (run path — REQ-294 / port):** after a successful `claim_req`, any Linear MCP failure leaves the Issue **claimed** (`status_map.in_progress` + latest claim `status: active`). The failing agent exits stopped (appropriate stopper reason). Operator recovers with `/do-work resume` or `/do-work unblock` once MCP is healthy. Same multi-agent recovery story as markdown concurrent-conflict / stale slots. + +--- + + diff --git a/references/linear-paths.md b/references/linear-paths.md new file mode 100644 index 0000000..ef078d0 --- /dev/null +++ b/references/linear-paths.md @@ -0,0 +1,608 @@ +# Linear path narratives + capability matrix (reference) + +One hop from [`agents/tracker/linear.md`](../agents/tracker/linear.md). Load when implementing or auditing a path-unit (REQ-288…301) or re-filling the capability matrix. **Not** the day-to-day op index — sequences live in [linear-ops.md](linear-ops.md). + +**Hierarchy lock (authoritative):** UR = **Project Milestone** on shared `product_project` (default `do-work`). **Not** Initiative-as-UR. Path narratives below may still mention historical Initiative wording in child-work tables; prefer the lock + [linear-ops.md](linear-ops.md) sequences. + +## Disambiguation: Milestone-as-UR vs path-milestone mode (M1/M2) + +| Concept | What it is | Where it lives | +|---------|------------|----------------| +| **Milestone-as-UR** | The Linear **Project Milestone** entity that *is* the User Request (`UR-NNN`) | On shared **product Project** (`product_project`) | +| **Path-milestone mode (M1/M2)** | Optional *delivery* mode inside one UR when the brief has `source: /saas-thesis handoff` + `### Milestones` | Cursor block `` on the **UR Project Milestone description**; Issues tagged `M1`/`M2` | + +Do **not** create Linear Initiatives for URs. Do **not** treat M1/M2 path-milestones as separate URs. + +--- + +## Path: Linear MCP capability spike (REQ-288) + +| | | +|---|---| +| **Entry point** | Operator sets a **sandbox** Linear team (`tracker.linear.team_id` / `team_key`); agent rediscovers MCP tools live before any full CRUD wiring | +| **Terminal state** | Capability matrix present; live probe records **available**/**missing**/**partial** **or** documents **matrix unavailable** + hard-stop when MCP is down; **no production work-item migration** on this path; CRUD REQs unblocked only after a future MCP-connected fill marks required cells | + +This path answers design risk §17 #1 (**MCP thin / offline tools**) and the clarification **spike first, then implement**. Full port op sequences, templates, claim, and migration live in later path-units — **not** here. + +**Child work under this path:** + +| Area | Responsibility | REQ | +|------|----------------|-----| +| Live tool rediscovery on sandbox team | `search_tool` → `use_tool` probes; fill matrix cells from **observed** tools only | REQ-289 ran — **matrix unavailable** (no Linear MCP) | +| Hard-stop / setup copy | Verbatim operator instructions when MCP missing (this file + Linear skill) | REQ-288 skeleton → REQ-289 confirmed | +| `status_map` vs real team states | Document defaults + hard-fail; validate names on sandbox workflow | Defaults documented; live names **not validated** (MCP missing) | +| Full op sequences / templates / claim | Deferred — other path-units after matrix is known | REQ-290 documents UR/REQ CRUD sequences (still `search_tool` live; claim/run later) | + +**Do not** invent Linear tool names as if proven. Until a **later** live probe (post-REQ-289, with Linear MCP connected) records a row as **available**, treat tool names as **unknown**. CRUD sequences below still call `search_tool` first and hard-stop if undiscoverable — they do **not** treat skill “typical tools” tables as proven. + +--- + +## Path: Linear UR/REQ CRUD (REQ-290) + +| | | +|---|---| +| **Entry point** | `/do-work` intake or start with `tracker.backend: linear` and valid team config (Load Config step 7) | +| **Terminal state** | product Project + UR Project Milestone + Issues on that milestone exist with §9 templates; `create_ur` / `create_req` / `update_req` / `read_req` / `list_reqs_for_ur` (+ `read_ur` / `list_urs`) sequences are documented as agent steps that rediscover tools live | + +This path-unit wires **work-item create/read/update/list** only (design §6 hierarchy, §9 templates). Claim/heartbeat/pick/status/unblock/resume are REQ-292; archive, non-ticket Docs, milestone, and migration remain later path-units. + +**Hard rules for every CRUD op in this path:** + +1. **Rediscover, never invent** — each op begins with `search_tool` for the needed Linear surface; call `use_tool` only with a qualified name + `input_schema` from that search. +2. **Hard-stop if undiscoverable** — if Linear MCP tools are missing, unauthenticated, or the needed capability has no discovered tool, **stop** with the setup block in this file. Do not invent issues/initiatives; do not write local UR/REQ markdown as a substitute store. +3. **No dual-write** — Linear is the sole work-item store while `backend: linear`. No parallel `.do-work/user-requests/` or `.do-work/REQ-*` as source of truth. +4. **Linear issue ids only** — REQs are identified by Linear identifiers (e.g. `ENG-123`). **No** parallel `REQ-NNN` allocation in Linear mode. `UR-NNN` remains a Project/Initiative slug only. +5. **Atomic `create_ur`** — never leave Issues without a resolvable product Project + UR Project Milestone. If milestone create fails after product Project ensure, hard-stop; do not continue intake as if the UR exists. + +**Child work under this path:** + +| Area | Responsibility | REQ | +|------|----------------|-----| +| UR create/read/list sequences | product Project (`product_project`) + UR Project Milestone (`ur_milestone_name_pattern`) | REQ-290 (this section) | +| REQ create/update/read/list | Issues in that Project; §9.2 body; path-unit `parentId` sub-issues | REQ-290 (this section) | +| Templates + append/deps/footprint ops | §9 field semantics; `append_ideate` / `append_clarifications` / `set_blocked_by` / `set_files` | REQ-291 | +| Claim / heartbeat / pick / status / unblock / resume | Optimistic claim comment protocol (§8); human assignee preserved | REQ-292 | +| Archive / non-ticket homes | Deferred → REQ-294–297 | later REQs | +| Idle markdown→Linear migration | Deferred → **REQ-300** | upgrade + this file | + +--- + +## Path: Linear templates + append/deps/footprint (REQ-291) + +| | | +|---|---| +| **Entry point** | Any phase that writes UR sections (ideate/question) or REQ deps/footprint under `tracker.backend: linear` | +| **Terminal state** | §9.1 / §9.2 templates (machine markers `` / ``), labels (`Layer/*`, `Size/*`, `path-unit`), `status_map` hard-fail rules, and full agent sequences for `append_ideate`, `append_clarifications`, `set_blocked_by` (blocks relations + `**Depends on:**` mirror), and `set_files` are documented with live rediscovery | + +This path-unit **extends** REQ-290 CRUD: templates become the field contract, and the remaining create/update surface for intake→capture without claim is complete. + +**Hard rules (in addition to REQ-290 CRUD rules):** + +1. **Machine markers are mandatory** on every UR Project Milestone description (``) and Issue description (``). Parse/stop if missing on read/update — do not invent fields. +2. **`set_blocked_by` dual-write** — when relation tools exist: native `blocks` relations **and** body `**Depends on:**` mirror in one op. Relations are authoritative for eligibility (port rule). +3. **Labels from config prefixes** — `tracker.linear.labels.layer_prefix` (default `Layer/`), `size_prefix` (default `Size/`), `path_unit` (default `path-unit`). Apply on create/update when label tools are discoverable; body headers still hold the same values for parse. +4. **`status_map` hard-fail** — every mapped workflow state name must exist on the team; missing → hard-stop (never invent a close-enough state). +5. **Prefer section append** on Initiative for ideate/clarifications; never overwrite `## Brief` verbatim intake. + +--- + +## Path: Linear claim / status / unblock / resume (REQ-292) + +| | | +|---|---| +| **Entry point** | `/do-work run` \| `status` \| `unblock` \| `resume` with `tracker.backend: linear` | +| **Terminal state** | Optimistic claim comment protocol works; status reports claimers/heartbeats; unblock/resume match markdown semantics; mid-flight failure leaves claimed | + +This path-unit implements design **§8 Claim protocol** as Linear agent sequences for `list_claimable_reqs`, `claim_req`, `heartbeat_req`, `set_req_status`, `unblock_req`, plus **resume** and **status** consumers. Semantics stay in `port.md`; representation is workflow state + claim **comments** (not a local claim stamp file). + +**Hard rules (in addition to prior Linear path rules):** + +1. **Human assignee is sacred** — `default_assignee_id` on create; agents **never** set/clear/steal Linear **assignee** for claim, heartbeat, unblock, or resume. +2. **Claim = comment + workflow**, not assignee — `status_map.in_progress` + comment starting with `tracker.linear.agent_claim_marker` (default ``). +3. **Optimistic re-read** — every `claim_req` re-reads issue + claim comments before write; race lost → `concurrent-conflict` stop; resume allowed. +4. **Stale age** — `tracker.linear.heartbeat_max_age_seconds` when set; else `parallel.stale_threshold_seconds` (default `900`). +5. **Mid-flight MCP death** — **leave claimed** (in_progress + last active claim/heartbeat); do not auto-release. Operator uses resume or unblock after MCP recovers. +6. **No dual-write** — no local `.do-work/working/` claim stamps while `backend: linear`. +7. **Rediscover tools** — comments, issue get/update, list issues, workflow states, relations — always `search_tool` first; invent nothing. + +**Child work under this path:** + +| Area | Responsibility | REQ | +|------|----------------|-----| +| Claim comment protocol + claim/heartbeat/unblock/resume/status/list_claimable | Full sequences in this section | REQ-292 | +| Phase playbooks that *call* these ops | `status` / `unblock` / `resume` / `run` Linear op callouts | REQ-293 | +| `archive_req` + `append_run_note` + run commit convention | Done + proof + outputs; run notes; §6.5 commits | REQ-294 | + +--- + +## Path: Linear run coordination (REQ-294) + +| | | +|---|---| +| **Entry point** | `/do-work run` with `tracker.backend: linear` (after claim path) | +| **Terminal state** | Worker/orchestrator can pick → claim → deps/footprint checks → archive a REQ using Linear as sole work-item store; worktrees/git remain local; commit messages use Linear issue ids; mid-flight MCP failure leaves the issue claimed | + +This path-unit closes the **run loop** on Linear (design phasing step 5 + §5.5 runtime split + §6.5 commits + §7 ledger note + clarification leave-claimed). Claim/pick sequences are REQ-292/293; this path adds **`archive_req`**, **`append_run_note`**, commit/PR message convention, and the ledger telemetry rule. + +**Hard rules (in addition to claim-path rules):** + +1. **`archive_req` is the only done transition** — set `status_map.done`, write **`Closure proof:`** + **`## Outputs`** on the Issue, release claim (`status: released`). Do **not** use bare `set_req_status` for done. Do **not** write local `.do-work/archive/REQ-*` as the work-item store. +2. **Footprint overlap** — `list_claimable_reqs` / claim eligibility compare candidate `**Files:**` against `**Files:**` parsed from Issue bodies of **in-flight claims** (workflow `in_progress` or `stopped` with active claim). Same intent as `lib/check-footprint.sh`. +3. **Deps satisfaction** — authoritative graph is native Linear **`blocks` relations**. A dep is satisfied only when that issue’s workflow maps to `status_map.done`. Body `**Depends on:**` is mirror only. +4. **Commits/PRs (§6.5)** — messages reference the Linear issue id (`feat(ENG-123): …` + `Issue:` / `UR:` / `Output:` footer). No `.do-work/archive/REQ-…` path required. Branch may be `req/ENG-123` (sanitize for git refs). +5. **`append_run_note` is authoritative** for run/cost notes in Linear mode (Issue comment, YAML fenced). When `ledger.enabled: true`, orchestrator **may also** write local `.do-work/runs/RUN-NNN.yml` — **telemetry only**, not a second work-item store. Retro prefers Linear run notes; falls back to local runs if comments unavailable. +6. **Mid-flight MCP failure after claim** — **leave claimed** (active claim comment + `in_progress`); worker/orchestrator **stops** for resume/unblock. **Never** silent-release. **Never** fall back to markdown store. +7. **Runtime stays local** — worktrees, merges, PRs, `state/*` locks, events, config.yml unchanged (§5.5). + +**Child work under this path:** + +| Area | Responsibility | REQ | +|------|----------------|-----| +| `archive_req` + `append_run_note` sequences + §6.5 + ledger telemetry rule | Documented in this file; run/run-worker callouts | REQ-294 (this section) | +| Deeper pick ordering / review-gate / branch sanitize wiring | Further run-agent refinements | REQ-295 | + +--- + +## Path: Linear run pick ordering / footprint / review-gate / branch sanitize (REQ-295) + +| | | +|---|---| +| **Entry point** | `/do-work run` with `tracker.backend: linear` after REQ-294 archive/notes/commits path | +| **Terminal state** | `list_claimable_reqs` has deterministic pick order + skip reasons + footprint algorithm parity; `archive_req` / `append_run_note` stay the only Linear archive/note ops; worktree branches use `req/` (sanitized); review gate still blocks archive when `review.required`; failed review/evidence never calls `archive_req`; claim loss → `concurrent-conflict` with resume; **no** Linear-aware bash in `lib/` for v1 | + +This path-unit **refines** the REQ-294 run loop for production pick/integrate edge cases. It does **not** re-open claim protocol (REQ-292) or invent new port op names. + +**Hard rules (REQ-295):** + +1. **Pick order is deterministic** — Priority **descending** (3 most urgent before 1; missing/malformed defaults to **2**), then created_at ascending, then Linear identifier ascending. First survivor wins (parity with `lib/pick-req.sh` priority + first-survivor model). +2. **Skip reasons are emitted** for every rejected candidate (`dep:`, `overlap:`, `scope:`, `claim:`) so the run loop can map to `overlap-blocked` / `deps-blocked` / `scope-blocked` / `truly-empty` without calling `pick-req.sh`. +3. **Footprint algorithm** matches `lib/check-footprint.sh` intent: parse `**Files:**`, treat empty/missing as free (no overlap), expand globs with nullglob semantics (unmatched globs do not collide), compare expanded path sets against in-flight claims only. +4. **Review gate before archive** — when `review.required: true` (config default), orchestrator must pass post-build review **before** calling `archive_req`. Failed review or failed acceptance-evidence gate **must not** call `archive_req`; issue stays `in_progress`/`stopped` with claim protocol intact. +5. **Branch sanitize** — worktree branch may be `req/` after sanitizing for git ref rules (see **Branch sanitize** below). Worktree directory mirrors the sanitized slug under `.worktrees/`. +6. **Concurrent claim loss** — same stopper as markdown multi-agent: `concurrent-conflict`; `/do-work resume` allowed when the claim is still held by the owner. Never invent a different stopper enum value. +7. **No Linear-aware bash in `lib/` for v1** — pick/claim/deps/footprint/heartbeat/archive-integrity **semantics** for Linear live as agent sequences in this file (MCP). `lib/*.sh` remain markdown-backend implementations. Runtime helpers that are backend-agnostic (`provision-worktree.sh`, local locks, optional local ledger telemetry) stay local and do **not** call Linear APIs. + +**Child work under this path:** + +| Area | Responsibility | REQ | +|------|----------------|-----| +| Deeper `list_claimable_reqs` order + skip reasons + footprint algorithm | This file | REQ-295 (this section) | +| Review-gate / failed-gate → no `archive_req`; branch sanitize wiring | `agents/run.md`, `agents/run-worker.md`, `agents/review.md` + this file | REQ-295 | +| `archive_req` + `append_run_note` (YAML-fenced Issue comment) | Remain as REQ-294 sequences; preconditions tightened here | REQ-294/295 | + +--- + +## Path: Linear non-ticket artifacts (REQ-296) + +| | | +|---|---| +| **Entry point** | capture `append_decision`; verify/close write reports; retro calibration; run notes; gate coordination — with `tracker.backend: linear` | +| **Terminal state** | Artifacts live **only** in fixed Linear homes (design §10); agents never invent ad-hoc locations; gate locks stay local `state/*` | + +This path-unit maps **non-ticket** work-item artifacts to Linear homes and documents write/read sequences. Ticket lifecycle (UR/REQ/claim/archive) is prior path-units; this path freezes **where** decisions, calibration, verify, close, and run notes live. + +**Hard rules (REQ-296):** + +1. **Fixed homes only** — use the §10 table below. Do **not** invent alternate Docs titles, Initiative sections, comment markers, or local markdown dual-stores for these artifacts while `backend: linear`. +2. **Decisions + calibration = Team Docs** — titles from config: `tracker.linear.decisions_doc_title` (default `do-work/decisions`) and `tracker.linear.calibration_doc_title` (default `do-work/calibration`). **Create-if-missing** when Docs tools are discoverable. +3. **Verify / close = UR Project Milestone** — `write_verify_report` → milestone description `## Verify` (+ comment with full report). `write_close_report` → milestone `## Closure` (+ comment). Prefer description section update; fall back to comment-only if size limits require it. +4. **Run notes = Issue comments** — `append_run_note` (REQ-294) remains authoritative; optional Project update is non-authoritative rollup only. +5. **Gate locks stay local** — `write_gate_state` writes/deletes `{project}/.do-work/state/gate-owner.md` (and final-suite locks under `state/*`). **Never** put gate ownership in Linear. +6. **No dual-write** — do not also write `.do-work/decisions.md`, `state/calibration.md`, or `user-requests/UR-NNN/closure.md` as the work-item store when `backend: linear`. Optional local ledger telemetry for run notes only when `ledger.enabled` (REQ-294). +7. **Rediscover Docs tools** — Team Docs are unproven until live MCP marks them available; each op still begins with `search_tool`. Missing Docs/milestone tools → hard-stop for that op (never invent a local substitute store). + +**Child work under this path:** + +| Area | Responsibility | REQ | +|------|----------------|-----| +| §10 home map + `append_decision` / calibration Doc / `write_verify_report` / `write_close_report` / `write_gate_state` sequences | This file | REQ-296 (this section) | +| Phase agents call those homes | `agents/capture.md`, `agents/verify.md`, `agents/close.md`, `agents/retro.md` | REQ-296 | +| Full consumer wiring + hard-stop invent ban + close Linear path-unit walk + retro prefer run notes | This file + capture/ideate/question/verify/close/retro/run-worker | REQ-297 | +| `append_run_note` Issue comments | Remain as REQ-294 sequences | REQ-294 | + +--- + +## Path: Linear artifact home consumers (REQ-297) + +| | | +|---|---| +| **Entry point** | capture / ideate / question / verify / close / retro / run-worker after load path with `tracker.backend: linear` | +| **Terminal state** | All §10 readers and writers use port sequences in this file; Doc titles from config; decisions one-line grammar identical to markdown; close walks **Linear issue ids**; retro prefers Linear run notes; create/update failures hard-stop with **no invented homes** | + +REQ-296 documented the homes and write sequences. **REQ-297** finishes the consumer surface: + +| Consumer | Linear port ops / helpers (this file) | +|----------|----------------------------------------| +| `agents/capture.md` | **Read decisions**; **`append_decision`**; **Read calibration Doc** | +| `agents/ideate.md` | **Read decisions** (constraints for Connector / contradiction flags) | +| `agents/question.md` | **Read decisions** (self-answer pass evidence) | +| `agents/run-worker.md` | **Read decisions** (standing constraints; conflict → stop) | +| `agents/verify.md` | **`write_verify_report`** (and `read_ur` / `list_reqs_for_ur` for brief + REQs) | +| `agents/close.md` | Path-unit walk via **Linear issue ids** + **`write_close_report`** | +| `agents/retro.md` | **List run notes** (prefer) → local `RUN-NNN.yml` fallback; **Write calibration Doc** | + +**Hard rules (REQ-297):** + +1. **Config titles only** — decisions Doc = `tracker.linear.decisions_doc_title` (default `do-work/decisions`); calibration Doc = `tracker.linear.calibration_doc_title` (default `do-work/calibration`). Never invent alternate titles. +2. **Same decisions grammar as markdown** — every line is exactly `YYYY-MM-DD | UR/REQ ref | decision | rationale` (SKILL.md § Decisions Memory). Linear issue ids may appear in the ref slot (e.g. `ENG-123`); pipe-separated four fields; one line per decision; append-only; supersede by new line. +3. **Close walks Linear issue ids** — under `backend: linear`, path-units are Issues in Project `do-work/{UR-id}` with path-unit semantics (`Layer: none` + non-empty Entry point + Terminal state). The `req` field in closure rows is the **Linear identifier** (e.g. `ENG-123`), not `REQ-NNN`. +4. **Retro prefers Linear run notes** — when `backend: linear`, collect `` Issue comments via **List run notes** before treating local `.do-work/runs/` as the only history. Fall back to local telemetry only when comments are unavailable. +5. **Hard-stop on Doc / Initiative write failure — no invent** — if Team Doc **create** or **update** fails (permission, size, MCP error), or UR Project Milestone description section update **and** milestone comment both fail for verify/close, **hard-stop**. Agents must **not** invent ad-hoc Issue comments for decisions/calibration, alternate Doc titles, local `.do-work/decisions.md` / `state/calibration.md` / `closure.md` as substitute stores, or any home outside the §10 table. +6. **§10-allowed spill only** — for verify/close, putting the full report in a **UR Project Milestone comment** while leaving a one-line pointer under `## Verify` / `## Closure` is the documented size path (still §10). That is **not** inventing a home. Putting the report on a random Issue, a different milestone, or a new Doc title **is** inventing — forbidden. + +--- + +## Path: Linear milestone mode (REQ-298) + +| | | +|---|---| +| **Entry point** | Milestone-shaped UR (`source: /saas-thesis handoff` + `### Milestones`) with `tracker.backend: linear` — capture, run claim loop, deploy gate | +| **Terminal state** | Active milestone cursor lives on **Project description** ``; `list_milestone_reqs` / `set_active_milestone` / `read_active_milestone` work via this file; deploy gate remains **local** `state/gate-owner.md` with human y/n; **trigger shape unchanged** | + +This path-unit implements design **§11 Milestone mode (Linear)**. Trigger and gate ownership match markdown; only the **cursor store** and **REQ listing** move to Linear. + +**Hard rules (REQ-298):** + +1. **Trigger unchanged** — Milestone mode activates only when the UR brief has **both** (a) `source: /saas-thesis handoff` and (b) a `### Milestones` heading with at least one `#### M1` (or higher) subheading. Same as markdown capture Step 1b. Do **not** invent a Linear-only trigger. +2. **Cursor home = UR Project Milestone description** — machine block starting with `` on the UR’s Project (`do-work/{UR-id}`). **Not** local `state/active-milestone.md` as the work-item store under Linear. **Not** Initiative description. **Not** Team Docs. +3. **Checklist lives with the cursor** — active id + full milestone checklist (parity with markdown `active-milestone.md` + `milestones.md`) inside that Project description block. +4. **Deploy gate stays local** — first orchestrator claims via **`write_gate_state`** → `{project}/.do-work/state/gate-owner.md`; human y/n; siblings idle-wait on gate-owner + cursor changes via **`read_active_milestone`**. **Never** put gate ownership in Linear. +5. **Issue membership** — REQs for a milestone are Issues in the UR Project, filterable by milestone marker: prefer Linear Project milestone entity when MCP tools support it after live rediscovery; else **label** equal to the milestone id (e.g. `M1`) and/or body header `**Milestone:** M1`. `list_milestone_reqs` uses those markers. +6. **No dual-write** — do not treat local `active-milestone.md` / `milestones.md` as authoritative while `backend: linear`. Local files remain allowed only for **gate locks** (`gate-owner.md`, final-suite locks). +7. **Rediscover Project tools** — every cursor read/write begins with `search_tool` for Project get/update. Missing tools → hard-stop (never invent a local cursor substitute store). + +**Child work under this path:** + +| Area | Responsibility | REQ | +|------|----------------|-----| +| Path narrative + trigger/cursor home/gate locality hard rules | This file (above) | REQ-298 | +| `read_active_milestone` / `set_active_milestone` / `list_milestone_reqs` full sequences + marker parse + empty→null | This file | **REQ-299** | +| Capture Linear branches call port ops after decompose | `agents/capture.md` | REQ-298 path; **REQ-299** ops | +| Run filter / idle-wait / deploy-gate drain call port ops; local gate-owner serialize | `agents/run.md` | REQ-298 path; **REQ-299** ops | +| `write_gate_state` (local-only + concurrent serialize) | This file + run.md | REQ-296 home; **REQ-299** concurrent rules | + +--- + +## Path: Linear milestone cursor ops (REQ-299) + +| | | +|---|---| +| **Entry point** | Capture milestone decompose; run Step 1.0 / 1.0a / 7b under `tracker.backend: linear` | +| **Terminal state** | Milestone cursor ops complete: marker format documented + parsed; empty marker → `active: null` (does **not** invent a milestone id); siblings idle on deploy gate same as markdown; concurrent gate ownership serializes via **local** `state/gate-owner.md`; `write_gate_state` remains local-allowed; capture/run call port ops only | + +REQ-298 documented the §11 path (trigger, cursor home, local gate). **REQ-299** finishes the **port op surface** and acceptance rules: + +| Op / rule | Where | Notes | +|-----------|-------|--------| +| Marker format + parse algorithm | This file — **Project description cursor block** + **Parse algorithm** | `` + `**Active:**` + `# Milestones` checklist | +| `read_active_milestone` | This file | Empty / missing marker → `active: null`; **does not invent a milestone id** | +| `set_active_milestone` | This file | Set / advance / clear on Project description only | +| `list_milestone_reqs` | This file | Filter by Issue milestone markers; no widen to other M | +| Sibling idle on deploy gate | `agents/run.md` Step 1.0a | Same idle loop as markdown; Linear polls `read_active_milestone` + **local** `gate-owner.md` | +| Concurrent gate ownership | `write_gate_state` (this file) + run Step 7b.2 | Serializes via **local** `state/gate-owner.md` even when cursor content is remote | +| Capture / run Linear branches | `agents/capture.md`, `agents/run.md` | Call port ops; never treat local `active-milestone.md` as Linear store | + +**Hard rules (REQ-299):** + +1. **Marker format is authoritative** — Project description machine block must start with `` then `**Active:**` then `# Milestones` checklist (see block template below). Parse only that format; do not invent alternate markers (YAML frontmatter, Initiative fields, Team Docs). +2. **Empty marker → null active (does not invent a milestone id)** — when the Project description has **no** `` marker, or the block is present but `**Active:**` is empty / `none` / missing, `read_active_milestone` returns `active: null` (not-in-milestone / not-active). It **must not** invent `M1` or any other id on read. Capture may *choose* `M1` as first-decompose default **after** observing null — that default is capture policy, not a return value of `read_active_milestone`. +3. **`write_gate_state` remains local-allowed** — gate ownership and final-suite locks stay under `{project}/.do-work/state/` (design §5.5 / §10 / §11). Never Linear Issues as gate locks; never Team Docs for gate ownership. Not dual-write of work items. +4. **Concurrent gate ownership serializes via local `gate-owner.md`** — even when milestone **cursor** content is remote (Project description), gate ownership is **only** the local file. First successful claim (absent→write own `AGENT_ID`, re-read confirms self) owns the human y/n prompt; losers idle on Step 1.0a. Do **not** invent a Linear lock or Project-description gate field. +5. **Siblings idle same as markdown** — empty active-M backlog + foreign `gate-owner.md` → idle-wait; wake on cursor advance (`set_active_milestone` / `read_active_milestone`) or cursor clear + gate release. Poll interval and 30-minute stuck prompt parity with markdown Step 1.0a. +6. **Capture and run call port ops** — Linear milestone branches must use `read_active_milestone` / `set_active_milestone` / `list_milestone_reqs` / `write_gate_state` from this file; no silent markdown cursor fallback. + +--- + +## Path: Idle markdown→Linear migration (REQ-300 path + REQ-301 upgrade wiring) + +| | | +|---|---| +| **Entry point** | `/do-work upgrade migrate` (or upgrade **Step 9** migrate path) when the project still uses the **markdown** work-item store and wants a one-shot cutover to Linear — design §12 | +| **Terminal state** | All URs/REQs from markdown backlog + archive exist in Linear (product Project + UR Project Milestones / Issues on those milestones); Team Docs for decisions (+ empty calibration if missing); `tracker.backend: linear` + resolved team ids written to config; local `user-requests/` + `archive/` (and backlog REQ files) left as **read-only historical** trees; **post-cutover work-item ops ignore historical markdown trees**; **no dual-write**; dry-run lists planned creates without write; re-run when already linear **refuses without rewriting Issues** | + +This path-unit implements design **§12 Migration (markdown → Linear)**. It is **idle-only**, **operator-confirmed** (destructive apply gate) or **dry-run**, and **all-or-nothing** on preflight / MCP failure (no partial cutover). + +**Hard rules (REQ-300 + REQ-301):** + +1. **Preflight is absolute** — migration runs only when **all** of: + - `{project}/.do-work/working/` has **zero** `REQ-*.md` files (empty of in-flight work). + - **No active claims** (no claim stamps with live heartbeats in working/ — redundant if working empty; still verify no stranded claim protocol elsewhere the agent knows about for markdown). + - Effective `tracker.backend` is still **`markdown`** (or unset → markdown). + - Operator **confirms** cutover via the **destructive/confirm gate** **or** the invocation is **dry-run** (report only). +2. **Already linear → refuse without rewriting Issues (idempotent refuse, REQ-301)** — if effective `tracker.backend` is already **`linear`**, report **already-migrated / `already-linear`** and **stop**. **Do not** create, update, rewrite, or re-sync Linear Issues (or product Project milestones / Docs from historical markdown). **Do not** re-run M2–M6 write phases. Config left unchanged. Re-running migrate after cutover is therefore safe: clear refuse, zero remote writes. +3. **Refuse entirely on failed preflight** — if `working/` is non-empty **or** active claims exist, **refuse the whole migration**. Do **not** create any Linear entities. Do **not** change `tracker.backend`. Config and markdown trees left unchanged. Message: idle required; finish or unblock in-flight work first. +4. **Hard-stop on unusable Linear MCP** — before any write (and if MCP dies mid-migration), **hard-stop** with Linear skill setup instructions. Leave markdown trees **and** `tracker.backend` **unchanged**. **No partial cutover** (do not flip config after only some URs/REQs landed; do not dual-write). Prefer operator cleanup of any orphan Linear entities created mid-flight only when a write phase already started — document orphans in the stop report; never flip backend mid-orphan. +5. **No dual-write after cutover + ignore historical trees (REQ-301)** — once `tracker.backend: linear` is set, work-item ops use **only** this file. Local `.do-work/user-requests/`, backlog `REQ-*.md`, and `archive/` become **historical read-only** (do not delete). **Post-cutover work-item ops must ignore historical markdown trees** — never list/read/parse them as the work-item store (no silent fallthrough to markdown paths). Runtime/git/`state/*` stay local. +6. **Dry-run** — when flag/mode is dry-run: run preflight + inventory + **planned-create list** (Initiatives / Projects / Issues / Docs / config flip); **zero** Linear writes; **zero** config changes. Exit after the report. +7. **Destructive confirm for apply** — apply mode requires affirmative operator confirmation (upgrade Step 9b). Without confirm and without dry-run → refuse (no write). +8. **Rediscover tools** — every Linear create/list uses `search_tool` → `use_tool` with live schemas. Never invent tool names. Missing create tools → hard-stop (same as CRUD preflight). +9. **Map, do not invent** — preserve UR ids, REQ task text, AC checkboxes, deps, parents, status (backlog vs done), closure proof / outputs when present. Linear REQs get **Linear issue ids** only after create (markdown `REQ-NNN` may be noted in body for historical trace, not as the Linear identifier). + +**Surfacing (upgrade / conformance — REQ-301 wiring):** + +| Surface | Role | +|---------|------| +| `agents/upgrade.md` Step **9** / `/do-work upgrade migrate` | Operator-facing UX: preflight, **destructive confirm** or dry-run, invoke this sequence, report; already-linear refuse | +| `lib/conformance-scan.sh` | Documents that `migrate-linear` is **not** a drift row; historical trees after cutover are not drift; never auto-flags markdown backend | +| Port op `migrate_markdown_to_linear` | Shared contract (preconditions, refuse / hard-stop, dry-run) — `agents/tracker/port.md` | +| This section | Full agent sequence + status/relation/parent mapping + post-cutover ignore rules | + +**Child work under this path:** + +| Area | Responsibility | REQ | +|------|----------------|-----| +| Path narrative + hard rules + agent sequence | This file | **REQ-300** | +| Port op contract + shared refuse/hard-stop rules | `agents/tracker/port.md` | **REQ-300** | +| Upgrade migrate step + dry-run flag UX (initial) | `agents/upgrade.md` | **REQ-300** | +| Upgrade/conformance wiring: destructive confirm, dry-run list, already-linear no-rewrite, post-cutover ignore, scan header | `agents/upgrade.md`, `lib/conformance-scan.sh`, this file | **REQ-301** | + +--- + +### `migrate_markdown_to_linear` (agent sequence) + +| | | +|---|---| +| **Intent** | One-shot idle markdown → Linear cutover (design §12). | +| **Preconditions** | See hard rules. Team id/key intended for Linear must be known (config `tracker.linear.team_id` / `team_key` or operator-supplied before write). | +| **Modes** | `dry-run` (report planned creates only) \| `apply` (writes + config flip after full success; requires destructive confirm). | +| **Does not** | Delete markdown trees; dual-write after cutover; migrate mid-flight working/ REQs; flip config on partial failure; rewrite Issues when already linear. | + +#### Step M0 — Invocation flags + +| Flag | Meaning | +|------|---------| +| `--dry-run` / dry-run mode | Inventory + **list planned creates** only; no Linear write; no config write | +| apply (default when operator confirmed) | Full sequence after **destructive confirm**; config flip only at M6 after successful creates | + +Upgrade agent passes the mode after confirm / dry-run selection (`agents/upgrade.md` Step 9). + +#### Step M1 — Preflight (refuse = entire abort) + +1. Resolve `{project}` (`git rev-parse --show-toplevel` or CWD). +2. Load config (`agents/config.md`). Effective backend must be **`markdown`**. If effective backend is **`linear`**, **refuse** with already-migrated / `already-linear`: + - **Do not re-run production migration.** + - **Do not create, update, or rewrite Linear Issues** (nor Initiatives / Projects / Docs from historical markdown). + - **Do not** proceed to M2–M6. + - Config and Linear store unchanged. This is the **idempotent re-run** path. +3. **Working empty:** + ```bash + # Non-zero count → refuse + find "{project}/.do-work/working" -maxdepth 1 -name 'REQ-*.md' 2>/dev/null | wc -l + ``` + Any `REQ-*.md` in `working/` → **refuse entirely** (message: drain or unblock working/ first). Config unchanged. +4. **No active claims:** with working empty of REQ files, markdown claims are absent. If any claim stamp protocol file is found outside the empty working/ contract, treat as refuse (do not invent partial cleanup). +5. **Linear readiness (write modes and dry-run):** + - `search_tool "linear"` (or `"linear team"`) — must return Linear MCP tools. Zero tools → **hard-stop** with setup block (same as this file's **Hard-stop** section). **Config backend left markdown.** Markdown trees unchanged. + - Resolve team via `tracker.linear.team_id` and/or `team_key`. Unresolved → **hard-stop** (do not guess). Config unchanged. + - Validate every `status_map` state exists on the team workflow. Missing → **hard-stop** with rename/override instructions. Config unchanged. +6. **Destructive/confirm gate** (apply mode only): upgrade agent must have an affirmative confirm (`AskUserQuestion` or equivalent). Without confirm and without dry-run → **refuse** (do not write). Dry-run does not require this gate. +7. On any refuse/hard-stop in M1: **stop**. No Linear creates. No config edit. + +#### Step M2 — Inventory (read markdown store only) + +Build a plan from the **markdown** store (allowed because backend is still markdown): + +| Source | Collect | +|--------|---------| +| `{project}/.do-work/user-requests/UR-*/` | Each `UR-NNN`: `input.md` brief, ideate, clarifications, verify/close artifacts if present | +| `{project}/.do-work/REQ-*.md` (backlog root) | Open REQs (not working, not archive) | +| `{project}/.do-work/archive/REQ-*.md` | Done REQs | +| `{project}/.do-work/decisions.md` | Standing decision lines (if present) | +| `{project}/.do-work/state/calibration.md` | Calibration body (if present) — else plan empty calibration Doc | + +For each REQ file parse: `**UR:**`, `**Status:**`, `**Parent:**`, `**Depends on:**`, `**Files:**`, `**Layer:**`, `**Entry point:**` / `**Terminal state:**` (path-unit), `## Task`, `## Acceptance Criteria` (preserve `- [ ]` / `- [x]`), `## Verification Steps`, `## Outputs`, `**Closure proof:**`, size/priority/criteria-approved headers. + +Group REQs by UR. Skip any REQ whose UR directory is missing only after recording a plan warning (still attempt create under that UR slug if inventable from REQ header). + +**In-flight forbidden:** working/ was empty at M1 — do not invent migration of in-progress slots. + +#### Step M3 — Dry-run report (always build; exit here if dry-run) + +Emit a planned-create report, for example: + +```text +markdown→Linear migration plan (dry-run|apply) +Team: +backend after cutover: linear + +Team Docs: + - create-or-update: do-work/decisions (N lines from decisions.md | empty) + - create-if-missing: do-work/calibration (body | empty stub) + +URs (Initiatives + Projects): + - UR-007: Initiative title "…" + Project do-work/UR-007 + link + - … + +REQs (Issues): + - REQ-100 → Project do-work/UR-007 | status=done | parent=none | deps=REQ-99 + - REQ-101 → Project do-work/UR-007 | status=backlog | parent=REQ-100 (path-unit child) + - … + +Config flip (apply only): tracker.backend: linear; team_id: … +Post-cutover: user-requests/ + archive/ + backlog REQ-*.md remain on disk as historical read-only; ops stop reading them as store. +``` + +If mode is **dry-run**: **stop here**. Zero Linear writes. Zero config changes. Return report to operator. + +#### Step M4 — Team Docs (apply only) + +1. Rediscover Team Docs tools (`search_tool`). +2. **Decisions** — title `tracker.linear.decisions_doc_title` (default `do-work/decisions`). Find or create-if-missing. If local `decisions.md` has lines, write them into the Doc body (preserve one-line grammar). If local empty/missing, create empty/header Doc. +3. **Calibration** — title `tracker.linear.calibration_doc_title` (default `do-work/calibration`). Create-if-missing; if local `state/calibration.md` exists, full-replace Doc body with it; else empty stub. +4. Failure (permission/MCP) → **hard-stop**. Do **not** flip `tracker.backend`. Prefer not to continue Issues if Docs failed at the start; if any Doc was created, list it in the stop report for operator cleanup. **No partial cutover of config.** + +#### Step M5 — URs then REQs (apply only) + +For each inventoried UR (stable order: ascending `UR-NNN`): + +1. **Create UR Project Milestone** — name from `ur_milestone_name_pattern` / brief title; description = §9.1 template filled from `input.md` + ideate + clarifications + verify/closure when present (``, `**UR-id:** UR-NNN`, `**Product-project:**` from config). +2. **Ensure product Project** (`product_project`, default `do-work`) on the resolved team. +3. **Attach** nothing else for UR create — Issues later attach to the UR Project Milestone. Record product project id + milestone id on the §9.1 body. +4. Atomicity: same as `create_ur` — no partial UR without product Project + milestone. Failure → **hard-stop**; list created entity ids for cleanup; **do not flip config**. + +Then for each REQ belonging to that UR (parents before children; backlog + archive): + +5. **Map status** via `status_map`: + - archive / `**Status:** done` → `status_map.done` (default `"Done"`) + - backlog / open / missing done → `status_map.backlog` (default `"Todo"`) + - **Never** migrate as `in_progress` (preflight forbids working/). If a file claims stopped in archive-like state, map to `status_map.done` only when archive path or explicit done; otherwise backlog or stopped map per `**Status:**` (`stopped` → `status_map.stopped`). +6. **Build Issue body** from §9.2: copy headers/sections; preserve AC checkboxes literally. Optional historical line: `**Migrated-from:** REQ-NNN` (display only; **not** the Linear id). +7. **Create Issue** in the UR Project with mapped workflow state; labels Layer/Size/path-unit when tools exist; assignee from `default_assignee_id` when set. +8. **Parents / path-units:** if `**Parent:** REQ-X` (markdown id), resolve to the Linear issue id created earlier in this run for that markdown id (maintain a `REQ-NNN → ENG-…` map). Set Linear `parentId` + body `**Parent:** ENG-…`. Create path-unit parents before children. +9. **Deps:** after all Issues for the UR (or globally once all Issues exist), for each REQ with `**Depends on:**`, map markdown ids through the same map and run **`set_blocked_by`** dual-write (native `blocks` + body mirror) using **Linear** ids. If relation tools missing → body-only + one-time warning (port rule). +10. Mid-sequence MCP failure → **hard-stop**. Do **not** set `tracker.backend: linear`. Report orphan milestone/Issue ids. Markdown trees unchanged. Operator may clean Linear side and re-run after idle preflight (re-run should be safe to plan; apply may create duplicates if orphans left — operator cleans first). + +#### Step M6 — Config flip (apply only; only after M4–M5 full success) + +Write `{project}/.do-work/config.yml`: + +- `tracker.backend: linear` +- `tracker.linear.team_id` / `team_key` as resolved (persist the id used) +- Leave other `tracker.linear.*` keys as already migrated defaults + +**Only after** this write is the cutover complete. Until then, effective backend remains markdown. + +If config write fails after Linear creates succeeded: **hard-stop** with: Linear entities exist; config still markdown; operator must set `tracker.backend: linear` manually **or** delete Linear orphans and retry. Do not dual-write; do not invent a half-mode. + +#### Step M7 — Post-cutover (historical trees; ops ignore them) + +1. **Do not delete** `.do-work/user-requests/`, `.do-work/archive/`, backlog `REQ-*.md`, or `decisions.md`. +2. Treat them as **read-only historical**. Phase agents with `backend: linear` **must ignore historical markdown trees** as the work-item store: + - **Forbidden as store** after cutover: reading/listing/parsing `.do-work/user-requests/`, `.do-work/REQ-*.md` (backlog root), `.do-work/archive/REQ-*.md`, local `decisions.md` / `state/calibration.md` as authoritative work-item data. + - **Required store:** Linear only via named port ops in this file (load path → `port.md` + this file). + - Historical trees may remain on disk for human audit; agents never dual-read them “for safety.” +3. Runtime locals unchanged: worktrees, `state/*` locks, events, gate-owner, optional ledger telemetry. +4. Report success: counts created, id map summary (`REQ-NNN → Linear id`), config backend now linear, pointer to Linear skill if further setup needed. +5. **Re-run after cutover:** M1 step 2 refuses with already-linear — **without rewriting Issues**. + +#### Failure matrix (no partial cutover) + +| Failure | Behavior | +|---------|----------| +| Already `tracker.backend: linear` | **Refuse** `already-linear` / already-migrated — **no Issue rewrites**; config unchanged | +| `working/` non-empty or active claims | **Refuse entirely** — no Linear writes; config unchanged | +| Operator declines confirm (apply) | **Refuse** — no writes | +| Linear MCP missing / unauthenticated / team unresolved / status_map missing | **Hard-stop** with setup instructions — markdown trees + config unchanged | +| MCP dies during M4–M5 | **Hard-stop** — config **not** flipped; list orphans; markdown unchanged | +| Config write fails after creates | **Hard-stop** — report manual flip or orphan cleanup; no dual-write mode | +| Dry-run | **List planned creates** only — always safe; zero writes | + +#### Mapping summary + +| Markdown | Linear | +|----------|--------| +| `user-requests/UR-NNN/` + brief | UR Project Milestone (``) on product Project | +| Backlog `REQ-*.md` | Issue in Project; state `status_map.backlog` | +| `archive/REQ-*.md` | Issue in Project; state `status_map.done` (+ closure/outputs in body) | +| `**Parent:** REQ-X` | `parentId` + `**Parent:** ` after id map | +| `**Depends on:** REQ-A REQ-B` | `blocks` relations + body mirror with Linear ids | +| AC `- [ ]` / `- [x]` | Same checkbox markdown in Issue description | +| `decisions.md` | Team Doc `do-work/decisions` (or config title) | +| `state/calibration.md` | Team Doc `do-work/calibration` (or config title); empty if missing | +| `tracker.backend` after success | `linear` + team ids | + +--- + +## Path: Linear claim phase-agent wiring (REQ-293) + +| | | +|---|---| +| **Entry point** | `/do-work status` \| `unblock` \| `resume` \| `run` after load path with `tracker.backend: linear` | +| **Terminal state** | Those phase agents call **only** the named port ops in this file for claim/pick/status/unblock/resume (no `.do-work/working/` claim stamps, no `pick-req.sh` / `claim-req.sh` / `synth-status.sh` as the work-item store) | + +REQ-292 documents the op sequences. **REQ-293** wires the consumers: + +| Phase agent | Linear port ops / sections (this file) | +|-------------|----------------------------------------| +| `agents/status.md` | **Status reporting (claimers / heartbeats)**; Helper: read active claim; optional `list_reqs_for_ur` scope | +| `agents/unblock.md` | **`unblock_req`** (release claim + backlog state); git partial-commit judgment stays local | +| `agents/resume.md` | **Resume** (compose `set_req_status` + `heartbeat_req`); worktree/branch stay local | +| `agents/run.md` | **`list_claimable_reqs`** → **`claim_req`**; **`archive_req`** + **`append_run_note`**; worker **`heartbeat_req`** checkpoints; mid-flight **leave claimed**; §6.5 commits | +| `agents/run-worker.md` | §6.5 commit/PR format; mid-flight **leave claimed**; Linear **`heartbeat_req`** when issue-id claim | + +**Hard rules for wired consumers:** + +1. Resolve backend first (load path). **Markdown** keeps existing `lib/*.sh` + file steps. **Linear** uses this file only for work-item claim/status/unblock/resume/pick/**archive/run notes**. +2. REQ identifiers under Linear are **Linear issue ids** (e.g. `ENG-123`), not `REQ-NNN` paths under `.do-work/`. +3. Human **assignee** is never stolen. Claim is comment + workflow. +4. Mid-flight MCP failure after `claim_req`: **leave claimed**; operator uses resume or unblock (port rule). Never silent-release; never markdown fallback. +5. Run loop (REQ-294): deps via **blocks**; footprint via Issue `**Files:**` of in-flight claims; archive via **`archive_req`**; commits use Linear issue ids. + +--- + + + +--- + +## Capability matrix (spike) + +**Status legend** + +| Status | Meaning | +|--------|---------| +| **unknown** | Not proven in a live session; do not wire production ops on this cell | +| **available** | Live `search_tool` / `use_tool` confirmed (record qualified name + date in Notes) | +| **missing** | Live probe ran; no tool for this need — document fallback or hard gap | +| **partial** | Related tools exist but not full create/link/read needed by port | + +### Matrix availability (REQ-289 live probe) + +| | | +|---|---| +| **Probe date** | 2026-07-31 | +| **Protocol** | `search_tool` queries: `"linear"`, `"linear issues initiative project document"`, `"server:linear mcp.linear"` | +| **Result** | **Matrix unavailable** — Linear MCP server not connected; zero `linear__*` tools discovered | +| **Connected MCP servers observed** | `github`, `gmail`, `google_calendar`, `google_drive`, `notion`, `skill-seekers`, `tasks` (no `linear`) | +| **use_tool probes** | **Not run** — no qualified Linear tool names returned; inventing calls is forbidden | +| **Sandbox team** | Not reachable (no team list/get tools); `tracker.linear.team_id` / `team_key` not validated this session | +| **Operator action** | Hard-stop applies when `tracker.backend: linear` — follow setup block below (API key / OAuth / `mcp.linear.app`), restart agent, re-run discovery, then fill rows as **available** / **missing** / **partial** from live tools only | +| **Secrets** | None used or recorded | + +**Session note (REQ-288 path skeleton, 2026-07-31):** earlier worker also lacked Linear MCP; all rows left **unknown**. + +**Session note (REQ-289 live rediscovery, 2026-07-31):** re-ran `search_tool` for Linear. Confirmed **no Linear MCP handshake** in this session — semantic hits only mentioned Linear as a Notion connected source or GitHub project tools, not a `linear` MCP server. Capability matrix remains **unavailable**; every design-need row stays **unknown**. Do **not** treat skill “typical tools” tables as proven. No secrets in this file. + +### Required capabilities vs port needs + +| Capability (design need) | Port / design use | Live status | Qualified tool name(s) | Notes / fallback | +|--------------------------|-------------------|-------------|------------------------|------------------| +| **Team resolve** | `ensure_product_container`; config validation | unknown | — | REQ-289: MCP missing — unproven | +| **Workflow states** | `status_map` validation; claim/status/archive | unknown | — | REQ-289: cannot list team states without MCP | +| **Project Milestones** (UR) | `create_ur`, `read_ur`, `list_urs`, verify/close homes | unknown | — | REQ-289: **unproven** (MCP missing); hierarchy = Milestone-as-UR on `product_project` | +| **Product Project** (`product_project`) | Shared container; Issues scoped by UR milestone | unknown | — | REQ-289: unproven | +| **UR Project Milestone attach** | Issues attached to UR milestone on product Project | unknown | — | No Initiative create; MCP has no Initiative create tools | +| **Issues** (REQ) | `create_req`, `read_req`, `update_req`, list | unknown | — | REQ-289: unproven; Linear issue ids only once available | +| **Sub-issues / parent** | Path-unit parent + layer children (`parentId`) | unknown | — | REQ-289: unproven | +| **Issue relations `blocks`** | `set_blocked_by`; deps **authoritative** | unknown | — | REQ-289: **unproven**; if later **missing** → description-only deps + one-time warning (port rule) or GraphQL fallback | +| **Comments** | Claim/heartbeat protocol; `append_run_note` | unknown | — | REQ-289: unproven | +| **Team Docs** | `append_decision`, calibration | unknown | — | REQ-289: **unproven** (MCP missing); titles stay config-driven when proven | +| **Labels** | Layer / Size / path-unit | unknown | — | REQ-289: unproven | +| **Assignee** | Human `default_assignee_id` on create | unknown | — | REQ-289: unproven | + +### Port op readiness + +| Port op | Depends on capability rows | Sequence status | +|---------|----------------------------|-----------------| +| `ensure_product_container` | Team resolve, labels (optional) | Documented (CRUD preflight) | +| `create_ur` / `read_ur` / `list_urs` | Product Project + Project Milestones | **Documented** (REQ-290) — live `search_tool` required; hard-stop if undiscoverable | +| `append_ideate` / `append_clarifications` | UR Project Milestone (description/comments) | **Documented** (REQ-291) — section append under §9.1; rediscover update tools | +| `create_req` / `update_req` / `read_req` | Issues, Projects, labels, statuses | **Documented** (REQ-290) | +| `list_reqs_for_ur` | Issues by Project | **Documented** (REQ-290) | +| `list_claimable_reqs` | Issues + relations + comments + statuses | **Documented** (REQ-292/294/295) — Priority DESC (missing→2) → created_at ASC → id ASC; skip reasons; deps via **blocks**; footprint algorithm; no claim side-effect | +| `claim_req` / `heartbeat_req` / `unblock_req` | Issues status + comments | **Documented** (REQ-292) — optimistic claim comment protocol | +| `set_req_status` | Workflow states, issues | **Documented** (REQ-292) — stopped / in-progress without archive or unclaim | +| `archive_req` | Workflow states, issues, claim release, body proof/outputs | **Documented** (REQ-294/295) — done + proof + outputs + claim released; **not** called after failed review/evidence | +| `set_blocked_by` | Issue relations `blocks` (+ body mirror) | **Documented** (REQ-291) — dual-write; if relations **missing** → body-only + one-time warning (port rule) | +| `set_files` | Issue description headers | **Documented** (REQ-291) — updates `**Files:**` only; no claim side-effect | +| `append_decision` | Team Doc `decisions_doc_title` | **Documented** (REQ-296 ops; REQ-297 consumers) — create-if-missing; same one-line grammar; hard-stop on create/update fail | +| Calibration (retro write / capture read) | Team Doc `calibration_doc_title` | **Documented** (REQ-296/297) — create-if-missing; full replace body; hard-stop invent ban | +| `write_verify_report` | UR Project Milestone `## Verify` + milestone comment | **Documented** (REQ-296/297) — dual-fail hard-stop | +| `write_close_report` | UR Project Milestone `## Closure` + milestone comment | **Documented** (REQ-296/297) — close path-unit walk uses Linear issue ids | +| `append_run_note` | Issue comments (+ optional project update) | **Documented** (REQ-294) — authoritative run/cost notes; local ledger optional telemetry | +| List run notes (helper) | Issue comments `` | **Documented** (REQ-297) — retro prefers Linear notes, falls back to local telemetry | +| `read_active_milestone` / `set_active_milestone` / `list_milestone_reqs` | UR Project Milestone description `` + Issue path-milestone markers (M1/M2) | **Documented** (REQ-298 path; **REQ-299** ops) — empty marker → null; does not invent milestone id | +| `write_gate_state` | **Local** `state/gate-owner.md` (not Linear) | **Documented** (REQ-296 home; **REQ-299** concurrent serialize) — local only; never Linear | + +--- + + diff --git a/references/run-loop.md b/references/run-loop.md new file mode 100644 index 0000000..2cf5856 --- /dev/null +++ b/references/run-loop.md @@ -0,0 +1,1098 @@ +# Run loop sequences (reference) + +One hop from [`agents/run.md`](../agents/run.md). Load when executing the serial run loop (claim → dispatch → gates → integrate → recover). Hard rules and step outline stay in the agent file. + +--- + +## Agent Identity + +Each `/do-work run` process derives a stable `hostname.pid` identifier once at startup and reuses it for the lifetime of that run loop. + +### ID derivation + +```bash +AGENT_ID="$(hostname).$$" +# Example result: mbp-tom.42137 +``` + +- `hostname` — machine name, distinguishes agents on different machines sharing a repo +- `$$` — the shell PID of the current `/do-work run` process, unique per process on the same machine +- The combined string is computed **once** when the orchestrator starts and stored in the shell variable `AGENT_ID` + +### Ownership stamp format + +When the orchestrator claims a REQ into `working/`, it inserts the following block at the top of the REQ file, immediately under the `# REQ-NNN:` heading and before the existing `**UR:** ...` field: + +```markdown + +**Claimed by:** +**Claimed at:** +**Heartbeat:** + +``` + +Example of a claimed REQ header: + +```markdown +# REQ-115: Pre-flight concurrent-slot check + + +**Claimed by:** mbp-tom.42137 +**Claimed at:** 2026-05-15T14:03:22Z +**Heartbeat:** 2026-05-15T14:03:22Z + + +**UR:** UR-025 +**Status:** in-progress +``` + +### Stamp lifecycle + +| Phase | Actor | Action | +|---|---|---| +| Claim time | Orchestrator (REQ-114) | Inserts `` block after claiming the file into `working/` | +| Pre-flight | Sibling orchestrators (REQ-115) | Read `working/REQ-*.md` files; parse the block to attribute each slot to its owning agent | +| Archive time | Worker (this file) | Strips the `` block before moving the file to `archive/` | + +The stamp is a filesystem-visible, human-readable contract. Archived REQs do not retain ownership metadata — only the git commit message records which agent committed the change. + +--- + + +--- + +## Pre-flight Check + +> **Default behaviour:** By default the orchestrator claims unblocked backlog work; stale-slot triage is a fallback that fires only when the backlog is empty for this agent. The `working/` scan at §3 is informational — it populates buckets used by the picker's overlap exclusion and, if the backlog is drained, the fallback prompt. It is NOT a gate on starting work. + +Before starting the loop: + +### 1. Branch and working-directory checks + +- Confirm you are on the correct git branch. +- Confirm your working directory is `{project}` (the user's repo), NOT the skill clone at `~/.claude/skills/do-work/`. All file edits and git commits must happen in `{project}`. If you are in the skills directory, `cd` to `{project}` before proceeding. +- **Ensure `.do-work/state/` exists.** Run `mkdir -p {project}/.do-work/state` defensively. Subsequent steps (stale reclaim, milestone mode, deadlock surfacing, gate-owner writes, final-suite lockfile) write here; installs from before REQ-170 may not have created the directory. + +### 2. Resolve agent id + +Compute `AGENT_ID` per `## Agent Identity`: + +```bash +AGENT_ID="$(hostname).$$" +``` + +### 2a. Resolve `{skill-root}` to a concrete absolute path + +**Single home:** resolve once via **Load Config step 8** in [`agents/config.md`](../agents/config.md) (walk-up from loaded instruction file with marker requirements + inherit of a valid `$SKILL_ROOT`; hard-stop if the path is unknown). Keep `$SKILL_ROOT` in context for this run. Do **not** re-implement a second full recipe here — no env/hub/CWD fallback. + +`{skill-root}` is the absolute skill install root (directory containing `agents/`, `lib/`, `SKILL.md`). Lib invocations throughout this file (`{skill-root}/lib/scan-stale.sh`, etc.) and in `agents/run-worker.md` (heartbeat, file-feedback) only resolve when `{skill-root}` is a real absolute path. A worker `cd`'d into a consumer project's worktree has no `lib/` of its own, so the orchestrator substitutes `$SKILL_ROOT` into every `{skill-root}/lib/...` call it makes and passes that same absolute value as the worker **Skill root** input (Step 2 dispatch). + +### 2b. Generate or refresh the project context pack + +Workers run context-starved by design (their "When Invoked" rule). To raise implementation quality without making each worker re-explore the repo, the orchestrator maintains **one** project context pack at `{project}/.do-work/state/context-pack.md` and passes its path to every worker. One orchestrator-level scan amortises across every worker in every run. + +**Staleness rule (documented, pick-one): the pack is stale when it is older than 14 days OR more than 50 commits behind `HEAD`.** Regenerate only when stale or absent — a fresh pack costs no per-run scan. + +```bash +PACK="{project}/.do-work/state/context-pack.md" +REGEN=0 +if [ ! -f "$PACK" ]; then + REGEN=1 # absent → must generate +else + PACK_MTIME=$(stat -f %m "$PACK" 2>/dev/null || stat -c %Y "$PACK") + AGE_DAYS=$(( ($(date +%s) - PACK_MTIME) / 86400 )) + # Commits landed on HEAD since the pack was last written. + COMMITS_SINCE=$(git rev-list --count --since="@$PACK_MTIME" HEAD 2>/dev/null || echo 0) + if [ "$AGE_DAYS" -ge 14 ] || [ "$COMMITS_SINCE" -ge 50 ]; then + REGEN=1 # stale → refresh + fi +fi +``` + +**If `REGEN=0` (pack is fresh): skip generation entirely.** Do not scan, do not rewrite the file. This is the common case and it must carry zero per-run scan cost. + +**If `REGEN=1` (absent or stale): scan the project once and write a ~200-line pack.** Keep it to roughly 200 lines — a map, not a copy of the codebase. Cover: + +- **Architecture** — the top-level shape of the system (layers, services, entry points) in a few sentences. +- **Directory roles** — one line per significant top-level directory (what lives there, what it is for). +- **Key services / modules** — the handful of files or modules a worker is most likely to touch or extend, with a one-line role each. +- **Naming & test conventions** — how files, tests, and symbols are named; where tests live; the dominant test idiom. +- **How to run the suite** — the exact command(s) to run the project's tests (mirror `config.test.suite_command` when set). + +Write the result to `$PACK` (filesystem only — `.do-work/state/` is orchestrator-owned; do not commit it from here). The pack is project-level state, regenerated on the staleness cadence above, and read by every dispatched worker. + +### 3. Scan and classify working/ slots (informational — hold all buckets in memory, do not prompt) + +**Staleness detection — delegate to `lib/scan-stale.sh`:** + +```bash +STALE_SLOTS=$(bash {skill-root}/lib/scan-stale.sh) +``` + +`scan-stale.sh` (REQ-149, extended in REQ-172) reads `parallel.stale_threshold_seconds` from `.do-work/config.yml` (default 300 s) and prints one line per stale slot in the form ` age=`. Slots with a missing or malformed `**Heartbeat:**` are treated as stale by the script and emit `age=unknown`. The orchestrator does not re-implement this logic inline. + +**Ownership classification — inline (cheap deterministic read):** + +Glob `{project}/.do-work/working/REQ-*.md`. For each file found, read its ownership stamp (the `` block) and classify the slot into one of three buckets. **Retain all three buckets in memory. Do not prompt at this stage regardless of what the stale bucket contains.** + +| Bucket | Condition | Action | +|---|---|---| +| **`mine`** | `**Claimed by:**` in the stamp matches `AGENT_ID` | Resume this REQ — skip the claim step and jump directly to worker dispatch for it | +| **`sibling`** | `**Claimed by:**` is set, differs from `AGENT_ID`, AND the slot path is NOT in `$STALE_SLOTS` | Leave alone — another live orchestrator owns it | +| **`out-of-milestone`** | Milestone mode is active (`.do-work/state/active-milestone.md` exists) AND the slot's milestone id (parsed from the filename: `REQ-M-NNN-slug.md` → `M`) differs from the active milestone | Silently ignore — treat the same as `sibling` (a previous-milestone REQ still in flight during a milestone transition is informational only) | +| **`stale`** | Slot path appears in `$STALE_SLOTS` output | Hold in memory — surface only as fallback when backlog has no claimable REQ | + +### 3a. Timestamp reasoning rule + +All timestamps in REQ files (`**Claimed at:**`, `**Heartbeat:**`, and any +`` value) are UTC with a `Z` suffix. The local wall-clock +date may differ from the UTC date by ±1 day based on the host's +timezone. Do NOT decide whether a slot is fresh by comparing the +heartbeat's calendar date to "today" — that reasoning will misclassify +recent slots as stale across the UTC/local date boundary. + +Slot staleness is determined solely by `$STALE_SLOTS` (the output of +`lib/scan-stale.sh`, which compares UTC epochs deterministically). +When you need to surface "how long ago" to the user, use the `age=` +token from `scan-stale.sh`'s output — not the raw ISO timestamp. + +### 3b. Legacy stranded REQ triage (advisory — no automatic state change) + +While classifying `working/` slots in §3, also identify **legacy stranded REQs**: files whose `**Status:**` is `stopped` and whose `**Reason:**` value is not in the documented stopper enum (`tests-failing`, `verification-failing`, `missing-creds`, `ambiguous-criteria`, `scope-creep`, `dependency-missing`, `concurrent-conflict`, `unknown-error`). The canonical example is `awaiting-human-verification`, an improvised reason from an older human-wait flow. + +**Detection:** for each `working/REQ-*.md` file, read `**Status:**` and `**Reason:**`. If `**Status:** stopped` AND `**Reason:**` is non-empty AND the reason does not match any enum value above, record the file as a **legacy stranded slot**. + +**Advisory output (emit once per run, immediately after §3 classification — do NOT block the run or prompt):** + +If any legacy stranded slots were found, print a triage notice before proceeding to §4: + +``` +⚠ Legacy stranded REQ(s) detected in working/: + - REQ-NNN reason: () + ... +These REQs stopped with an unrecognized reason and were never migrated to the +current delivery flow. Triage guidance (advisory — take the appropriate action manually): + • If the req/ branch exists and still needs work: + → Resume it: /do-work resume REQ-NNN + • If the code was not delivered and no usable branch remains: + → Unblock it: /do-work unblock REQ-NNN (returns it to backlog for re-dispatch). +Run continues — no automatic state change was made. +``` + +This triage report is informational only. The orchestrator does NOT automatically move files, rewrite status fields, or modify any REQ. The human (or a subsequent operator) takes the appropriate action based on the guidance. The legacy slots are classified into the `stale` bucket for footprint exclusion purposes (same as any stopped slot). + +### 4. Resume any `mine` slot + +If the `mine` bucket is non-empty, resume that REQ — skip the claim step and jump directly to worker dispatch for it. + +### 5. Try the backlog (primary path) + +No `mine` slot is present. Immediately attempt `lib/pick-req.sh`: + +```bash +PICK_STDERR=$(mktemp) +REQ_PATH=$(bash {skill-root}/lib/pick-req.sh "$SCOPE" "$AGENT_ID" 2>"$PICK_STDERR") +``` + +`pick-req.sh` already excludes any REQ whose `**Files:**` overlaps with a slot in `working/` — **both `sibling` and `stale` slots are treated as in-flight** for the purpose of footprint exclusion. You do not need to communicate the stale list to the picker separately; it reads `working/` directly. + +- **If `pick-req.sh` returns a path:** claim it (proceed to The Loop, Step 1 claim sequence). **Do not surface any stale-slot prompt**, regardless of what `$STALE_SLOTS` contains. +- **If `pick-req.sh` returns nothing:** continue to §6. + +### 6. Fallback: backlog drained — evaluate working set + +Reached only when `pick-req.sh` returned no candidate AND the `mine` bucket is empty. Now the stale bucket matters: + +- **`stale` is non-empty:** prompt the user once (batch all stale slots into a single message — do NOT prompt per slot): + + ``` + N stale REQ(s) found in working/: + - REQ-NNN (claimed by , last activity ago) + - ... + These appear abandoned. Reclaim into this run, return to backlog, or abort? + ``` + + Where `` is derived from the `age=` token in `$STALE_SLOTS` output: convert seconds to the coarsest human unit that is non-zero (e.g. `42s`, `7m`, `2h`, `3d`). When `age=unknown`, render `unknown` in place of a duration. Do NOT use the raw ISO heartbeat timestamp to fill this field. + + - **Reclaim into this run:** For each stale REQ, rewrite its stamp to the local `AGENT_ID` and a fresh `**Claimed at:**` (ISO-8601 UTC). These REQs become the first ones this orchestrator processes in the loop — treat them as `mine`. + + Before rewriting the stamp, classify *why* the slot went stale and emit feedback (best-effort, non-blocking) iff there has been **no commit activity** touching any path under the REQ's `**Files:**` declaration in the last hour: + + ```bash + LAST_COMMIT_AGE_SEC=$(($(date +%s) - $(git log -1 --format=%ct -- 2>/dev/null || echo 0))) + if [ "$LAST_COMMIT_AGE_SEC" -gt 3600 ] || [ "$LAST_COMMIT_AGE_SEC" -eq "$(date +%s)" ]; then + # Classify the reason using the age= token from $STALE_SLOTS — not the raw + # ISO heartbeat. One of: + # no-heartbeat — age=unknown AND heartbeat field absent or malformed in the REQ file + # heartbeat-frozen — age= present (numeric) AND older than the stale threshold + # no-progress — age= present and under threshold but no commits on REQ's files + # Do NOT decide reason class by comparing **Heartbeat:** calendar dates to "today". + REASON_CLASS="" + FINGERPRINT="stale-slot:${REASON_CLASS}" + bash {skill-root}/lib/file-feedback.sh stale-slot \ + "$FINGERPRINT" \ + '{"req":"REQ-NNN","prior_owner":"","reason_class":"'"$REASON_CLASS"'","last_commit_age_sec":'"$LAST_COMMIT_AGE_SEC"'}' \ + "Stale-slot reclaim: REQ-NNN (${REASON_CLASS})" \ + "REQ-NNN sat in working/ with no commit progress in over an hour before reclaim. Prior owner appears abandoned; this orchestrator is taking the slot." \ + || true + fi + ``` + + > **JUDGMENT:** The title carries the REQ id and the reason class so an inbox skim tells you whether agents are dying silently (no-heartbeat) versus making progress without committing (no-progress). The body is one sentence — a single stale reclaim is routine; the inbox's fingerprint dedup surfaces the recurrence pattern. + + - **Return to backlog:** For each stale REQ, `git mv` it back to the backlog root, strip its ownership stamp, reset `**Status:**` to `backlog`, and commit per REQ. Stage **only** that REQ's file path — do not sweep `.do-work/`. Example: + ```bash + git mv {project}/.do-work/working/REQ-NNN-slug.md {project}/.do-work/REQ-NNN-slug.md + # edit the file to strip the claim block and reset Status + git add {project}/.do-work/REQ-NNN-slug.md + git commit -m "chore(REQ-NNN): return stale claim to backlog" + ``` + - **Abort:** Exit pre-flight and halt this orchestrator. + +- **`stale` is empty AND `sibling` is non-empty:** fall through to `## When the Backlog is Empty` — siblings are still doing the remaining work. + +- **`stale` is empty AND `sibling` is empty:** fall through to `## When the Backlog is Empty`. + +### 7. Backlog emptiness check + +If both `pick-req.sh` returned nothing (§5) AND the stale set is empty (§6 fallback was not triggered or returned to backlog), fall through to `## When the Backlog is Empty`. + +--- + +## REQ Classification + +Before dispatching a worker for a REQ, classify the REQ to pick the most appropriate `subagent_type` for the `Agent` tool. Classification is config-driven: the routing rules live in `config.routing` (see `agents/config.md`), not hard-coded here, so the stock skill ships portable and each user routes specialist work to whatever subagents exist on their own machine. + +### Apply the routing config + +Read `config.routing` — the ordered list of `{match, agent}` rules loaded at startup (see `## Load Config` in `agents/config.md`). Then: + +1. Scan the REQ's `## Task`, `## Context`, `## Acceptance Criteria`, and `## Verification Steps`. +2. Walk the `routing` rules **top to bottom, first match wins**. Each rule's `match` is a signal description or keyword list; if the REQ's content fits it, the chosen `subagent_type` is that rule's `agent`, and you stop scanning. +3. If no rule matches — or `routing` is empty (the shipped default) — the `subagent_type` is `general-purpose`. + +There are no hard-coded specialist agents in this section. Portable agents (`Explore` for pure exploration, `feature-dev:*` for architecture/review) are routed only when a `routing` rule names them — they are not assumed present. `agents/config.md` ships a commented example `routing` block reproducing the original specialist table; a user restores that behaviour by uncommenting it and confirming each named agent exists locally. + +### Fallback rule + +When no `routing` rule matches with confidence — or none is configured — **fall back to `general-purpose` silently**. Never block, never ask the user, never stop the loop on classification ambiguity. The cost of picking `general-purpose` for a specialist task is small; the cost of stalling the loop is large. + +### Logging + +Include the chosen `subagent_type` in the per-REQ progress line so the user can see routing decisions: + +``` +Starting REQ-NNN [type=general-purpose]: [title] +``` + +This is the only "progress" signal the orchestrator emits before the worker returns — the worker runs in a separate session and its output does not stream back. Plan accordingly. + +--- + +## Model Selection + +After classifying `subagent_type`, pick a `model` for the dispatch. Default to `sonnet` to save tokens. Escalate to `opus` only when the REQ shows signals of genuine difficulty. + +### Primary signals → model + +Two structural signals are read directly from the REQ header and take precedence over everything below — check them first, in order: + +| Primary signal | model | +|---|---| +| REQ has a previous `status: stopped` attempt recorded in its body (retry after Sonnet failed) | `opus` | +| REQ header carries `**Size:** L` (capture sized this REQ large from its file count / layer span / criteria count) | `opus` | + +If either primary signal fires, select `opus` and skip the lexical scan. The `**Size:**` field, when present, is capture's own up-front difficulty estimate — trust it over re-deriving difficulty from prose. + +### Fallback signals → model (REQs without `**Size:**`) + +When the REQ has **no `**Size:**` field** (legacy REQs, or capture left it off because the shape was ambiguous), fall back to scanning the REQ's `## Task`, `## Context`, and `## Acceptance Criteria` (top to bottom; first match wins). When `**Size:** S` or `**Size:** M` is present, these lexical rules still apply as a secondary check but never downgrade a `Size: L`: + +| Fallback signal in REQ | model | +|---|---| +| Task touches 4+ distinct files, OR spans 3+ layers (e.g. controller + model + view + test) | `opus` | +| Task introduces new architecture: new service, new abstraction, new module boundary, schema design, or "design X" | `opus` | +| Task involves debugging across layers, race conditions, concurrency, or performance investigation | `opus` | +| `subagent_type` is `feature-dev:code-architect` or `feature-dev:code-reviewer` | `opus` | +| Anything else: single-file edits, doc/markdown updates, agent/skill/config edits, mechanical refactors, scoped bug fixes, test additions, exploration | `sonnet` | + +### Fallback rule + +When in doubt, **default to `sonnet`**. The worker's stopping-rules already catch failures: if Sonnet can't make tests pass after 3 attempts, it returns `status: stopped` and the orchestrator's retry path picks `opus` automatically (signal #1 above). + +### Logging + +The chosen `model` appears in the per-REQ announce line alongside `subagent_type` (see Step 1). + +--- + + +--- + +## The Loop + +Repeat until the backlog is empty: + +### Step 1: Claim the next REQ + +#### Step 1.0 — Milestone filter (milestone mode only) + +Resolve whether milestone mode is active via the tracker backend (REQ-298 Linear path; REQ-299 ops). + +**Markdown backend:** + +- Check whether `{project}/.do-work/state/active-milestone.md` exists. +- **File absent (non-milestone mode):** skip this step entirely — proceed to the backlog glob as written below, behaviour unchanged from REQ-114. +- **File present (milestone mode):** + 1. Read the file. Its contents are a single line such as `M1` or `M2`. Trim whitespace to obtain ``. + 2. **Constrain the candidate glob** to `{project}/.do-work/REQ-M-*.md` instead of `{project}/.do-work/REQ-*.md`. Sort ascending and iterate exactly as the steps below describe. + 3. **No fallback to other milestones.** If the constrained glob returns no files, the active milestone's backlog is drained — fall through to **Step 1.0a: Sibling idle-waiting** below. The orchestrator MUST NOT silently widen the glob to pick up REQs from other milestones. The deploy gate (Step 7b) is the only mechanism that advances `active-milestone.md` to the next milestone. + +**Linear backend (REQ-298 path; REQ-299 ops):** + +1. Call port op **`read_active_milestone`** (`agents/tracker/linear.md`) for the scoped UR Project (`do-work/{UR-id}` when `/do-work run UR-NNN`, else each active Project the run scopes). Cursor lives on Project description `` — **not** local `active-milestone.md`. When the Project description has **no milestone marker**, the op returns `active: null` and **does not invent a milestone id**. +2. **`active` null (non-milestone mode / empty marker):** skip this step; proceed with unconstrained `list_claimable_reqs`. +3. **`active` set (e.g. `M1`):** constrain claim pool to that milestone — pass milestone scope into **`list_claimable_reqs`** and/or intersect with **`list_milestone_reqs`** for `M` (status backlog / claimable). Issue markers: label `M` and/or body `**Milestone:** M` (see linear.md). +4. **No fallback to other milestones.** If the constrained list is empty, fall through to **Step 1.0a**. Deploy gate (Step 7b) + **`set_active_milestone`** are the only advances of the cursor. + +#### Step 1.0a — Sibling idle-waiting (milestone mode, empty active-milestone backlog) + +Reached only when Step 1.0 found the active milestone's backlog empty. The local orchestrator may be a *sibling* — another orchestrator could already be handling the deploy gate. Do not fall through to `## When the Backlog is Empty` yet; first check whether a gate is in progress. + +1. Re-read the active cursor and capture as ``: + - **Markdown:** re-read `{project}/.do-work/state/active-milestone.md`. + - **Linear:** **`read_active_milestone`** again (Project description). +2. Check gate ownership via **local** `{project}/.do-work/state/gate-owner.md` (port **`write_gate_state`** home — **both backends**; never Linear). Concurrent gate ownership **serializes via this local file** even when milestone cursor content is remote (REQ-299): + - **File absent:** No sibling has claimed the gate. This orchestrator has finished its in-flight REQ and the milestone backlog is empty, but no one has surfaced the gate yet. Fall through to `## When the Backlog is Empty` — this is the genuine drain path for a single-orchestrator run, or the loser of a race where the gate-owner will detect milestone completion on its own next worker return. + - **File present:** Read the single line — the ``. If it equals the local `AGENT_ID`, this orchestrator already owns the gate (re-entry after a restart mid-prompt) — jump to Step 7b. Otherwise enter **idle-waiting** mode (**siblings idle on deploy gate same as markdown mode**). +3. **Idle-waiting loop.** Log exactly once: + + ``` + [] Idle — waiting on milestone M deploy gate (handled by ). + ``` + + Then poll every 30 seconds: + + - **Markdown —** poll `{project}/.do-work/state/active-milestone.md`: + - **File contents changed** (new milestone id, e.g. `M`): the gate-owner advanced. Exit idle-waiting and restart the loop at Step 1. + - **File deleted:** the gate-owner stopped the run (user answered `n`). Exit idle-waiting → `## When the Backlog is Empty`. + - **File unchanged AND `gate-owner.md` deleted while `active-milestone.md` is also gone:** treat as stop → empty-backlog path. + - **File unchanged after 30 minutes:** surface stuck-owner prompt (same text as before). + - **Otherwise:** continue polling. + - **Linear —** poll **`read_active_milestone`** (+ still read local `gate-owner.md` — never a Linear lock): + - **`active` changed** to a new id: gate-owner advanced. Exit idle-waiting → Step 1. + - **`active` null / cleared** while gate-owner released: stop → empty-backlog path. + - **Unchanged after 30 minutes:** same stuck-owner user prompt. + - **Otherwise:** continue polling. + +No commits are made while idle-waiting — the orchestrator is reading cursor + local gate state only. + +**Compute your agent-id** using the rule in `## Agent Identity`: + +```bash +AGENT_ID="$(hostname).$$" +``` + +**Scope argument:** `SCOPE` is derived from the optional `UR-NNN` argument at startup (see `## When Invoked`). Default is `any`. When `/do-work run UR-NNN` is invoked, `SCOPE=UR-NNN` and the picker filters out REQs whose `**UR:**` field does not match. The picker is also milestone-aware: + +- **Markdown:** when `state/active-milestone.md` exists it constrains its glob to `REQ-M-*.md` regardless of `SCOPE`. +- **Linear:** when **`read_active_milestone`** returns a non-null `active`, constrain via **`list_milestone_reqs`** / claimable scope to that `M` (Issue markers), regardless of `SCOPE`. + +**Pick the next claimable REQ — port op `list_claimable_reqs`:** + +- **Markdown backend:** implement via `lib/pick-req.sh` (below). +- **Linear backend:** implement via **`list_claimable_reqs`** in `agents/tracker/linear.md` (project filter + backlog + **blocks** deps + footprint algorithm + Priority **DESC** (missing→2) → created_at ASC → identifier ASC + skip reasons `dep:`/`overlap:`/`scope:`/`claim:`). When milestone mode is active, apply port op **`list_milestone_reqs`** membership filter for the active M (REQ-298 path; REQ-299 ops). Do **not** run `pick-req.sh` as the Linear store. On empty claimable list, map the op’s skip-reason lines with the same precedence as `drain-classify.sh`: **`overlap-blocked` > `deps-blocked` > `scope-blocked` > `truly-empty`**. No Linear-aware bash required. + +```bash +# markdown only — linear: call list_claimable_reqs (linear.md) instead +PICK_STDERR=$(mktemp) +REQ_PATH=$(bash {skill-root}/lib/pick-req.sh "$SCOPE" "$AGENT_ID" 2>"$PICK_STDERR") +``` + +`pick-req.sh` (REQ-145) applies the full scope / dependency / footprint-overlap filter in one pass and prints the absolute path of the first claimable REQ to stdout (exit 0), or nothing (exit 1) if no candidate survives. Its stderr carries one `:` line per rejected candidate. + +**If `pick-req.sh` returns empty (exit 1) — classify and branch:** + +```bash +CLASSIFICATION=$(cat "$PICK_STDERR" | bash {skill-root}/lib/drain-classify.sh) +rm -f "$PICK_STDERR" +``` + +`drain-classify.sh` (REQ-152) reads the stderr lines and emits one of four labels, precedence `overlap-blocked > deps-blocked > scope-blocked > truly-empty`: + +| Classification | Meaning | Action | +|---|---|---| +| `overlap-blocked` | At least one candidate blocked by footprint overlap with a sibling slot | Idle-wait (see below) | +| `deps-blocked` | All survivors blocked on unsatisfied dependencies | Idle-wait (see below) | +| `scope-blocked` | All candidates excluded by the `` filter | Idle-wait (see below) — a new capture or a scope change can add eligible REQs | +| `truly-empty` | No candidates considered at all (backlog drained for this picker view) | Fall through to `## When the Backlog is Empty` | + +**Idle-wait loop** (entered on `overlap-blocked`, `deps-blocked`, or `scope-blocked`). Log the entry classification once, then poll every **30 seconds**, max **30 minutes**: + +```bash +ELAPSED=0 +while [ "$ELAPSED" -lt 1800 ]; do + sleep 30 + ELAPSED=$((ELAPSED + 30)) + # Refresh heartbeat on a still-owned slot, if any. No-op when CURRENT_SLOT is unset. + if [ -n "${CURRENT_SLOT:-}" ] && [ -e "$CURRENT_SLOT" ]; then + bash {skill-root}/lib/heartbeat.sh "$CURRENT_SLOT" >/dev/null 2>&1 || true + fi + # Re-pick. + REQ_PATH=$(bash {skill-root}/lib/pick-req.sh "$SCOPE" "$AGENT_ID" 2>"$PICK_STDERR") + if [ -n "$REQ_PATH" ]; then + break # back to the claim step + fi +done +``` + +On 30-minute timeout, **run deadlock detection before falling back to the generic prompt**: + +```bash +DEADLOCK_OUT=$(bash {skill-root}/lib/deadlock-check.sh) +``` + +`deadlock-check.sh` (REQ-156) prints empty stdout when no deadlock is detected and a structured report otherwise. Branch on its output: + +**If `DEADLOCK_OUT` is empty (no deadlock):** Surface the generic prompt to the user: `Claim blocked () — still no claimable REQ after 30 min. Continue waiting, or abort?` Act on user response. + +**If `DEADLOCK_OUT` is non-empty (deadlock detected):** + +1. Parse the report. Extract `signal`, `fingerprint`, `diagnosis`, `live-slots`, `stale-slots`, `backlog-size`, `last-commit-age`. +2. **Ensure `state/` exists.** Run `mkdir -p {project}/.do-work/state` before any lock acquisition or state write. This is defensive — installs created before REQ-170 may not have `state/`, and orchestrators must not crash on a missing directory. +3. **Acquire the surfacing lock** via `flock -n` on `.do-work/state/feedback.lock` so only one orchestrator writes `deadlock.md` and surfaces to the user. Siblings that fail to acquire the lock skip steps 4–6 and exit the idle-wait loop quietly (they will pick up via their own timeout if the deadlock persists). +4. **Lock-holder only:** write `{project}/.do-work/state/deadlock.md` containing the full `deadlock-check.sh` output plus a timestamp. This file is the cross-process signal that the deadlock has been surfaced. +5. **Lock-holder only:** emit feedback by calling `bash {skill-root}/lib/file-feedback.sh deadlock "" ''` where `` is a single-line JSON object with `signal`, `live-slots`, `stale-slots`, `backlog-size`, `last-commit-age`, `classification` (the idle-wait entry classification). The script handles its own enable/disable, deduplication, and lock-on-feedback.lock — call it best-effort and continue regardless of exit code. +6. **Lock-holder only — surface to the user**, gated on standalone mode only (recovery prompts are workflow-critical and must not depend on `config.next_steps.enabled`): + - **If standalone** (not running as a delegate inside go): use the `AskUserQuestion` tool with options: + 1. **"Reset stale slots"** — return any slots listed in `stale-slots` to the backlog (per the Pre-flight stale-slot return path). + 2. **"Show situation room"** — print the suggestion `Run /do-work status` and exit cleanly. + 3. **"Unblock a REQ"** — ask which REQ id; print `Run /do-work unblock REQ-NNN` and exit cleanly. + 4. **"Abort"** — exit this orchestrator cleanly. + - **If delegate** (running inside go): print the diagnosis block (the `deadlock-check.sh` output plus a one-line summary) and exit cleanly. Do not prompt. + +> **JUDGMENT:** The deadlock diagnosis must distinguish a stuck deadlock from a slow-but-live backlog. `deadlock-check.sh` returning a report is strong evidence (no commits in 5 min OR all slots stale OR runtime cycle) — trust it and surface. Empty output means heartbeats are still advancing or commits are landing; in that case the generic "continue waiting?" prompt is correct. Never silently keep idling past the 30-minute mark — either the deadlock path or the user prompt must fire. + +**If pick returns a candidate — claim via port op `claim_req`:** + +- **Markdown backend:** `lib/claim-req.sh` (below). +- **Linear backend:** **`claim_req`** in `agents/tracker/linear.md` — optimistic re-read; set `status_map.in_progress`; post `` (config `agent_claim_marker`) comment with `agent_id` / timestamps / `status: active`; **never** change assignee. Race lost → `concurrent-conflict` (retry list/claim or stop; resume allowed for owner). Mid-flight MCP death after claim → **leave claimed**. + +```bash +# markdown only — linear: call claim_req (linear.md) with issue id + AGENT_ID +COMMIT_HASH=$(bash {skill-root}/lib/claim-req.sh "$REQ_PATH" "$AGENT_ID") +``` + +`claim-req.sh` (REQ-146) performs the `git mv` → stamp insertion → `Status: in-progress` update → stage → commit sequence atomically and prints the commit short hash to stdout. On failure it writes a diagnostic to stderr and exits non-zero: + +- **Exit 2 (`Claim lost: REQ-NNN`)** — a sibling won the race on this exact file. Re-run `pick-req.sh` from the top of Step 1 (the lost candidate is now in `working/` and will be excluded by the overlap filter). Linear equivalent: re-run **`list_claimable_reqs`** then **`claim_req`**. +- **Any other non-zero exit** — log the stderr diagnostic and re-run pick after a 2 s backoff. After 3 consecutive non-race failures, stop and report to the user. + +After a successful claim (`claim-req.sh` or Linear **`claim_req`**): + +**Announce:** + +``` +[] [scope=] Starting REQ-NNN [type=, model=, isolation=]: [title] +``` + +### Step 2: Dispatch the worker subagent + +Read all of [agents/run-worker.md](run-worker.md) — that is the worker's full instruction set. You will pass it inline to the dispatched subagent. + +Determine `subagent_type` using the rules in `## REQ Classification` above. Default to `general-purpose`. +Determine `model` using the rules in `## Model Selection` above. Default to `sonnet`. + +#### Step 2a: Criteria provenance note + +Read the REQ header's `**Criteria approved:**` value when present, but do not block worker dispatch based on it. `agent-drafted` is provenance, not a pre-run approval requirement. If a REQ exists in the backlog and its dependencies, footprint, scope, and policy gates allow it to run, dispatch the worker. + +Unexpected ambiguity still stops the run: if the acceptance criteria are missing, contradictory, impossible to verify, or become invalid during implementation, the worker must return `status: stopped` with `reason: ambiguous-criteria` or `verification-failing`. Do not ask for approval merely because criteria were generated by capture. + +Identify the **prior-REQ archived paths** for the same UR — these provide the worker context about what has already been built: + +1. Read the REQ's `**UR:**` field +2. Glob `{project}/.do-work/archive/REQ-*.md` +3. For each archived REQ, read its `**UR:**` field and keep only those matching the current UR +4. Pass the resulting absolute paths to the worker + +Dispatch via the `Agent` tool. Pass the worker **five named inputs** — REQ path, UR path, prior-REQ paths, the project context-pack path (from Pre-flight Step 2b), and the resolved skill-root (from Pre-flight Step 2a) — plus the run-worker.md instructions inline. Substitute the concrete `$SKILL_ROOT` value for `{skill-root}` in the instructions you paste so the worker's `{skill-root}/lib/...` calls resolve to a real path: + +``` +Agent( + description: "Run worker for REQ-NNN", + subagent_type: , + model: , + prompt: """ +You are the Run Worker. Follow the instructions below exactly. Prefer the inputs given; bounded exploration of files your implementation genuinely touches is allowed (see your When Invoked rule). Do not load other REQs or URs. + + +REQ: {absolute path to working/REQ-NNN-slug.md} +UR: {absolute path to user-requests/UR-NNN/input.md} +Prior REQs from this UR (may be empty): + - {absolute path} + - {absolute path} +Context pack: {absolute path to .do-work/state/context-pack.md} +Skill root: {resolved absolute $SKILL_ROOT — the directory containing lib/; your {skill-root}/lib/... calls use this value} + + + +{full contents of agents/run-worker.md verbatim, with {skill-root} replaced by the resolved $SKILL_ROOT} + + +Return your structured YAML report as your final message. Nothing else. +""" +) +``` + +The worker performs: create worktree → read REQ → read context → TDD red → implement → verify green → run affected tests → check acceptance criteria → execute verification steps → commit on feature branch → return YAML, all in its own session. **The worker does NOT merge, archive, or tear down its worktree** — those are the orchestrator's Step 4 (Integrate) responsibilities. + +The worker's stdout does not stream back to the orchestrator — only its final structured report is visible. Do not poll, do not babysit. Wait for the dispatch to return. + +### Step 3: Process the worker report + +The worker's final message is a fenced YAML block matching the schema defined in [agents/run-worker.md](run-worker.md) `## Return Report`. Parse it. Branch on `status`: + +| `status` | Action | +|---|---| +| `done` | Capture `commit` hash and `outputs`. Continue to Step 4 (Integrate). | +| `stopped` | The worker hit a stopper (`reason` enum: `tests-failing`, `verification-failing`, `missing-creds`, `ambiguous-criteria`, `scope-creep`, `dependency-missing`, `concurrent-conflict`, `unknown-error`). Continue to Step 5 (Recover) — handle per `## Stopping Rules`. Skip Step 4. **Workers never report a human-wait stopper** — there is no `awaiting-human-verification` reason. Inherently non-executable verification steps are *deferred* by the worker (returned in `deferred_checks:`) and are recorded as advisory manual checks during the normal archive path. | +| `failed` | The worker crashed before completing. Treat as `stopped` with `reason: unknown-error`. | + +If the worker's report is missing or unparseable, treat as `status: failed` with `reason: unknown-error` and surface the raw output to the user. + +If the worker reports `status: stopped` with `reason: verification-failing`, parse `last_good_step`, `failed_step`, and `checkpoint_log` from the report. Include the localized failure in the user-facing stopper report, e.g. `Verification failed at step ; last good step was ; handoff: .` + +If the worker reports `status: done`, validate acceptance evidence before Step 4 integration: + +```bash +# markdown: path is working/REQ file. linear: pass issue id / exported body via port read_req — same evidence rules; do not invent a second store. +bash {skill-root}/lib/check-acceptance-evidence.sh {project}/.do-work/working/REQ-NNN-slug.md +``` + +If validation fails, treat the result as `status: stopped`, `reason: verification-failing`, surface the validator diagnostics, and do not merge, write closure proof, review, or archive. **Under Linear: do not call `archive_req`** — issue stays `in_progress`/`stopped` with claim protocol intact (optional `set_req_status` → stopped + `append_run_note`). This gate extends the checkpoint/closure-proof model; it does not replace `closure_proof`. + +**Review gate (`review.required` — REQ-295):** + +1. Read `review.required` from config (default **`true`**). +2. When **`review.required: true`**: after acceptance evidence validation passes, run the post-build review gate **before** Step 4 integration. Worker says done is not final until evidence + review both pass. **Failed review must not call `archive_req`** (Linear) and must not move/archive the markdown REQ. +3. When **`review.required: false`**: skip review dispatch; proceed to Step 4 only if evidence (and policy) gates passed. Still never archive on failed evidence. + +**Review is dispatched as a fresh, independent subagent — never followed inline in the orchestrator's own context.** The orchestrator that wants the run to finish must not grade its own work; the reviewer runs cold, with no run history, seeing only the artifacts you hand it. + +Before dispatching review, run deterministic policy checks using changed files, command evidence, and REQ metadata: + +```bash +bash {skill-root}/lib/check-policy.sh \ + --project {project} \ + --files \ + --commands \ + --req {project}/.do-work/working/REQ-NNN-slug.md +``` + +Capture both the exit code and stdout/stderr — they are an input to the review dispatch. + +- **Exit `1`:** treat the result as `status: stopped`, `reason: policy-blocked`, surface the blocked path or blocked command diagnostics, leave the REQ in `working/`, and do not review, merge, archive, or write completion state. +- **Exit `2`:** a `risk.require_review` signal fired. Continue into review and pass the `review_required` diagnostics as mandatory review context. This exit code is also the trigger for **adversarial mode** (below). +- **Exit `0`:** continue into review normally. + +The helper reads `security.blocked_paths`, `security.blocked_commands`, and `risk.require_review` from `.do-work/config.yml`. + +#### 3a. Dispatch the review subagent + +Read all of [agents/review.md](review.md) — that is the reviewer's full instruction set. Pass it inline to the dispatched subagent, exactly as Step 2 does for the worker. The reviewer receives **five named inputs and nothing else** — no run narrative, no prior-REQ context, no memory of the worker's reasoning: + +``` +Agent( + description: "Post-build review for REQ-NNN", # or ENG-123 under Linear + subagent_type: general-purpose, + model: , + prompt: """ +You are the Review agent. Follow the instructions below exactly. You run as an independent subagent with no run history — judge only the artifacts handed to you. + + +# markdown: +Working REQ: {absolute path to working/REQ-NNN-slug.md} +UR: {absolute path to user-requests/UR-NNN/input.md} +# linear (instead of Working REQ path): +# Issue id: {ENG-123 — load via read_req; no .do-work/working/ as store} +# UR context: {UR-NNN / Project do-work/UR-NNN when known} +Worker report: {the worker's returned YAML report, inline} +Diff / commit: {the implementation diff, or the feature-branch commit reference} +Policy check: {the captured check-policy.sh output and exit code} + + + +{full contents of agents/review.md verbatim} + + +Return your structured YAML review report as your final message. Nothing else. +""" +) +``` + +Parse the reviewer's returned YAML (schema in [agents/review.md](review.md) `## Output`). Branch on its `status`: + +- **`status: passed`:** continue to Step 4 (Integrate). +- **`status: failed`:** treat the result as `status: stopped`, `reason: review-failed`, surface the review `findings`, leave the REQ in `working/` (markdown) **or** leave the Linear issue claimed (`in_progress`/`stopped` + active claim — **do not call `archive_req`**), and do not merge, write closure proof, archive, or record completion. Optional Linear: `set_req_status` → stopped + `append_run_note` with `result: stopped:review-failed` / `review: failed`. + +#### 3b. Adversarial mode (config-gated, risk-triggered) + +Read `review.adversarial` (loaded at startup; default `false`). + +- **`review.adversarial` is `false` (default), OR `check-policy.sh` exited `0`:** dispatch exactly **one** reviewer as in §3a. This is the shipped path. +- **`review.adversarial` is `true` AND `check-policy.sh` exited `2`:** dispatch **three** reviewers in parallel, each scoped to a distinct lens — **correctness**, **security**, **regression**. Use the same §3a dispatch shape per reviewer, adding a line to the prompt naming the lens (e.g. `Review lens: security — weight your findings toward this lens; still report blockers you see outside it.`). Aggregate the three returned reports into one verdict: + 1. **Majority gate:** the gate passes only when at least **2 of 3** reviewers return `status: passed`. + 2. **Blocker override:** any `severity: blocker` finding from **any** reviewer fails the gate regardless of the majority outcome. Blockers are never out-voted. + 3. On failure (majority not met OR any blocker present), apply the same handling as a single failed review: `status: stopped`, `reason: review-failed`, surface the union of all three reviewers' `findings`, leave the REQ in `working/`. + + Default stays single-reviewer to contain token cost until run-level budget enforcement (REQ-226) exists. + +### Step 3b: Run Ledger + +Collect ledger inputs while the run progresses: REQ id (or Linear issue id), agent id, selected model, branch, started and ended timestamps, command evidence, test evidence, changed files, result, cost estimate or budget note, review outcome, and derived proof status. + +**Backend branch for run notes (REQ-294):** + +| Backend | Authoritative note | Optional local file | +|---------|--------------------|---------------------| +| **markdown** | When `ledger.enabled`: `lib/run-ledger.sh` → `.do-work/runs/RUN-NNN.yml` (`append_run_note` in `markdown.md`) | same file is the store | +| **linear** | **`append_run_note`** on the Issue (YAML-fenced comment per `linear.md`) | If `ledger.enabled: true`, **may also** write `RUN-NNN.yml` via `lib/run-ledger.sh` — **telemetry only**, not a second work-item store. Retro prefers Linear comments; falls back to local runs if comments unavailable | + +When `ledger.enabled` is true (either backend), record one append-only local run ledger entry per worker attempt under `{project}/.do-work/runs/RUN-NNN.yml` using `lib/run-ledger.sh` — under Linear this is the optional telemetry path above, **in addition to** `append_run_note`. + +Finalize the (local) ledger after the attempt reaches a terminal outcome: + +```bash +bash {skill-root}/lib/run-ledger.sh \ + --project {project} \ + --req \ + --agent \ + --model \ + --branch \ + --started \ + --ended \ + --result \ + --review \ + --cost \ + --cost-estimate \ + --pr \ + --commands \ + --tests \ + --changed-files +``` + +For stopped workers, write the ledger (and Linear **`append_run_note`** when backend is linear) before returning control to the user, with `result: stopped:` and the best available evidence lists. For policy-blocked or acceptance-evidence failures before review, use `review: not-run`. If `ledger.enabled` is false, skip **local** ledger creation; under Linear still prefer **`append_run_note`** when the attempt warrants a durable note. + +When `deferred_checks:` is non-empty, still write `result: done` with the normal review and evidence fields. Delivery happened and all automated gates passed; any human/device follow-up is advisory data in the archived REQ, not a distinct ledger result. + +The worker also reports `milestone_complete` (boolean) and `milestone` (id when true). Step 7b uses these. + +#### Step 3b.1: Budget gate (enforcement hook) + +Run this **immediately after** the ledger write above, on every worker attempt — serial mode here, and at the same point inside the merge queue's Stage B for parallel mode (P3 reuses Step 3b verbatim; the gate rides along). + +**Inert unless armed.** If the effective budget (resolved at startup) is empty/unset, **skip this gate entirely** — never sum, never stop. This preserves today's unlimited behaviour with zero overhead. Likewise skip when `ledger.enabled` is false (no ledger to sum). + +When the budget is non-empty: + +1. Sum cumulative estimated spend for this run from the ledger: + ```bash + SPENT="$(bash {skill-root}/lib/run-ledger.sh --sum-run {project}/.do-work/runs)" + ``` +2. Compare `SPENT` against the effective `BUDGET` (numeric, same dollar unit): + - **`SPENT < BUDGET` ⇒ under budget.** Continue normally to Step 4 (Integrate) and loop. + - **`SPENT >= BUDGET` ⇒ budget exhausted.** Do **not** abandon the current attempt. **Finish the current REQ's integration first** (complete Step 4 fully — merge/archive/teardown/commit, or the PR delivery sequence — so the loop never stops mid-merge or mid-archive). Then, at the REQ boundary (where Step 8 would normally claim the next REQ), **stop gracefully** instead of looping: emit the **budget-stop report** and end the run. + +> **JUDGMENT:** The gate trips *after* the attempt that crossed the line, never mid-attempt. An in-flight integration always completes — abandoning a half-merged REQ would corrupt state, which is a worse failure than a small budget overshoot. The estimate is tier-weighted (see budget unit above), so the report names spend as an estimate, not a metered total. + +**Budget-stop report** (print before ending; under `next_steps.enabled` + standalone, surface via `AskUserQuestion` like a stopper, else print and stop): + +``` +Budget reached — stopping at REQ boundary. + +Estimated spend: $ / budget $ (tier-weighted estimate, not a metered bill) +REQs completed this run: +REQs remaining in backlog: +Last integrated: REQ-NNN + +Re-run with a higher --budget (or raise cost.budget) to continue. +``` + +The in-parallel variant is identical: when the gate trips inside Stage B, finish that report's Step 4 integration, then **stop admitting new reports to Stage B and stop refilling the window (P2)** — let live workers drain naturally (their integrations still complete), then emit the budget-stop report. No worker is killed mid-attempt; the window simply stops being refilled past the budget boundary. + +### Step 4: Integrate (worker = code, orchestrator = state) + +> **JUDGMENT:** The integration sequence below is the orchestrator's responsibility BECAUSE workers run in isolated worktrees. The worker has committed implementation files to `req/REQ-NNN`; the orchestrator now merges that branch into the base branch, archives the REQ, tears down the worktree, and commits the metadata change. This is the only place where `.do-work/` lifecycle writes happen. + +Reached only when `status: done` and both acceptance evidence validation and post-build review passed. + +**Delivery mode dispatch.** Read `config.delivery.mode` (default `merge`): + +- **`merge`** (default) — execute substeps **4a → 4b → 4c → 4d** below, in order; each must succeed before the next. This is the historical local-merge behaviour, unchanged. +- **`pr`** — skip 4a–4d entirely and execute the **PR delivery** sequence (`#### 4-pr`) instead. PR mode never runs the local merge. + +The guards in 4b and 4-pr.4 (path-unit closure and non-empty closure proof) and the closure-proof model are identical in both delivery modes — only the delivery vehicle differs. Whichever path runs, proceed to Step 7 when it completes. + +#### 4a. Merge the feature branch + +From the orchestrator's checkout (the main working tree, NOT the worktree). Branch name is backend-specific: + +| Backend | Feature branch | Merge subject | +|---------|----------------|---------------| +| **markdown** | `req/REQ-NNN` | `merge(REQ-NNN): integrate` | +| **linear** | `req/` (e.g. `req/ENG-123` — same string worker created via linear.md Branch sanitize) | `merge(ENG-123): integrate` | + +```bash +# markdown: +git merge --no-ff req/REQ-NNN -m "merge(REQ-NNN): integrate" +# linear (example): +# git merge --no-ff req/ENG-123 -m "merge(ENG-123): integrate" +``` + +On text-level conflict (any file contains `<<<<<<<`): + +1. `git merge --abort`. +2. Apply the 5-retry exponential-backoff policy (5s / 15s / 30s / 60s waits): + - `git pull --rebase origin ` (if remote exists; otherwise local fetch). + - Re-attempt the merge. +3. On the 5th failure, leave the feature branch alive (do NOT delete it), transition the REQ to `**Status:** stopped`, `**Reason:** concurrent-conflict` (handled in the Recover step below), and surface to the user. The branch can be resumed via `/do-work resume REQ-NNN` (markdown) or `/do-work resume ENG-123` (linear) which checks out the worktree and re-runs the worker on the same branch. **Same stopper enum; resume allowed.** + +#### 4b. Archive the REQ file + +Read the worker's YAML report's `outputs:` list and `closure_proof` value. + +**Linear backend (`tracker.backend: linear` — REQ-294/295):** do **not** rewrite/move local `.do-work/working/` or `.do-work/archive/` REQ files as the work-item store. Execute **`archive_req`** from `agents/tracker/linear.md` on the Linear issue id **only when every pre-archive gate passed**: + +1. **Hard gates (any failure → do not call `archive_req`):** path-unit Entry/Terminal when present; non-empty `closure_proof`; acceptance-evidence passed; when `review.required: true`, review `status: passed`. Failed review or failed acceptance-evidence leaves the issue `in_progress`/`stopped` with **claim protocol intact** (no `status: released`, no `status_map.done`). +2. When gates pass: `archive_req` sets workflow → `status_map.done`, writes `**Closure proof:**` + `## Outputs` on the Issue, posts claim `status: released`. +3. On Linear MCP failure mid-archive: **leave claimed** if claim not yet released; stop for resume/unblock; never silent markdown archive. +4. Optional: `append_run_note` for the done attempt if not already written in Step 3b (YAML-fenced ledger fields as Issue comment). +5. Skip the markdown file rewrite/move/integrity-script steps below. Continue to 4c (worktree teardown using the **Linear** branch/worktree paths from 4a/W2) and any local git metadata commit that does not invent a second work-item store. + +**Markdown backend** (default): rewrite the REQ file in place under `.do-work/working/REQ-NNN-slug.md`: + +0. **Path-unit closure guard.** Before any archive mutation, read `**Entry point:**` and `**Terminal state:**` from the REQ file. If either field is present, both must be present and non-empty. If a path-unit is missing either value, do not archive it. Transition the REQ to `**Status:** stopped`, add `**Reason:** path-unit-incomplete`, and surface: `REQ-NNN cannot close: path-unit requires non-empty Entry point and Terminal state.` Non-path REQs with both fields absent are unaffected. +1. Require non-empty `closure_proof` when the worker returned `status: done`. If it is missing or empty, transition the REQ to `**Status:** stopped`, add `**Reason:** missing-closure-proof`, and do not archive. +2. Strip the ownership stamp (``). +3. Update `**Status:**` to `done`. +4. Write the worker's `closure_proof` value into `**Closure proof:**`. If the header is absent, insert it before `**Files:**`. +5. Append a `## Outputs` section based on the `outputs:` array from the worker's YAML report. One bullet per entry: `- `. +5a. **Manual checks (advisory).** If the worker report's `deferred_checks:` list is non-empty OR the REQ already carries a `## Manual checks (advisory)` section, consolidate all deferred items into that section before archiving. Create the section if absent. Keep existing bullets, and add one unchecked bullet per worker item: `- [ ] (: )`. This section is advisory only; it never blocks archive. If any consolidated item carries `category: suite-not-run`, additionally write a `**Suite:** not-run` header field on the archived REQ (placed with the other header fields, below `**Closure proof:**`). This marker makes `lib/derive-status.sh` derive the REQ `unproven` even though it archives as `done` — archive and merge are unaffected; only the derived proof view changes. Human/device/environment deferrals never carry `category: suite-not-run` and never produce this marker. +5b. **Archive-integrity gate.** With the working file now fully rewritten, run the deterministic guardrail on it before the move: + ```bash + bash {skill-root}/lib/check-archive-integrity.sh {project}/.do-work/working/REQ-NNN-slug.md + ``` + It asserts the final on-disk state is internally consistent: `**Status:** done`, a non-empty `**Closure proof:**`, and zero unchecked `- [ ]` items inside `## Acceptance Criteria`. **Exit non-zero ⇒ do not archive:** transition the REQ to `**Status:** stopped`, add `**Reason:** archive-integrity`, surface the script's stderr diagnostics, and leave the file in `working/`. This is the persistence-boundary enforcement of the invariants steps 3–4 and the worker's acceptance-criteria ticking (`agents/run-worker.md` Step "Mark each `- [x]`") are supposed to satisfy — those are prose an LLM can silently skip; this gate cannot be skipped. (`archive-integrity` is an orchestrator-assigned reason like `path-unit-incomplete` and `missing-closure-proof`; it is not a worker reason.) +6. Move the file to `archive/`: + ```bash + mv {project}/.do-work/working/REQ-NNN-slug.md {project}/.do-work/archive/REQ-NNN-slug.md + ``` + +#### 4c. Tear down the worktree + +Use the same branch and worktree paths the worker created: + +```bash +# markdown: +git worktree remove {project}/.worktrees/req-NNN +git branch -d req/REQ-NNN # safe delete; refuses if not fully merged +# linear (example ENG-123 → sanitized slug eng-123): +# git worktree remove {project}/.worktrees/req-eng-123 +# git branch -d req/ENG-123 +``` + +If `git branch -d` refuses (the merge somehow incomplete), surface to the user; leave the branch alive for manual investigation. Never use `-D`. + +#### 4d. Commit the metadata change + +If `.do-work/` is tracked in this project, stage only the archive move and commit. + +For the **archive** path (4b): + +```bash +git add {project}/.do-work/archive/REQ-NNN-slug.md +git add {project}/.do-work/working/REQ-NNN-slug.md # stages the removal +git commit -m "chore(REQ-NNN): archive + +REQ: {project}/.do-work/archive/REQ-NNN-slug.md +UR: {project}/.do-work/user-requests/UR-NNN/input.md" +``` + +If `.do-work/` is gitignored: skip this commit silently. The move is filesystem-only, and the worker's `feat(REQ-NNN): ...` commit (now on the base branch via the merge) is the authoritative record. + +Proceed to Step 7. + +#### 4-pr. PR delivery (delivery.mode: pr) + +Runs *instead of* 4a–4d when `config.delivery.mode` is `pr`. The closure-proof model is unchanged — evidence still gates archive; the PR is the delivery vehicle, not the proof. Execute these substeps in order; each must succeed before the next. + +**4-pr.0 Precondition — remote + `gh` (never a silent merge fallback).** Before any push, verify both: + +```bash +git remote get-url origin # a remote must be configured +gh auth status # the gh CLI must be installed and authenticated +``` + +If a remote is missing **or** `gh` is absent/unauthenticated, **stop**: do NOT merge, do NOT push, do NOT archive. Leave the REQ in `working/` (and the branch alive), transition it to `**Status:** stopped`, `**Reason:** missing-creds`, and surface to the user per `## Stopping Rules`. PR mode must **never** silently fall back to `merge` mode. + +**4-pr.1 Push the REQ branch.** With the precondition met, push the worker's branch to the remote: + +```bash +git push -u origin req/REQ-NNN +``` + +**4-pr.2 Determine the PR target by granularity.** Read `config.delivery.pr.granularity` (default `req`): + +- **`req`** (default) — open the PR immediately, from `req/REQ-NNN` into the base branch. Continue to 4-pr.3. +- **`ur`** — do NOT open a per-REQ PR. Instead accumulate this REQ onto the UR's shared integration branch: + 1. Resolve the UR id from the REQ's `**UR:**` field → integration branch `ur/UR-NNN`. + 2. If `ur/UR-NNN` does not yet exist on the remote, create it from the base branch and push it. + 3. Merge `req/REQ-NNN` into `ur/UR-NNN` (`git merge --no-ff`, applying the same conflict/retry policy as 4a) and push `ur/UR-NNN`. + 4. Archive this REQ now (4-pr.4) recording the `ur/UR-NNN` branch, but **defer PR creation**: the single PR opens at UR drain. After the last REQ for this UR archives and the UR's backlog is empty (see `## When the Backlog is Empty` drain check), open one PR from `ur/UR-NNN` into the base branch using the same title/body shape as 4-pr.3 (title/body keyed to the UR rather than a single REQ; the body links the UR and lists each integrated REQ). Record that PR's URL on the UR. Then continue past 4-pr.5 to Step 7. + +**4-pr.3 Open the PR (`req` granularity, or the single UR-drain PR).** + +```bash +gh pr create \ + --base \ + --head req/REQ-NNN \ + --title "" \ + --body "" +``` + +PR body mirrors the commit convention (see SKILL.md / README `## Commit Convention`) and ends with the standard generated-with footer: + +``` +REQ: .do-work/archive/REQ-NNN-slug.md +UR: .do-work/user-requests/UR-NNN/input.md +Output: + +🤖 Generated with [Claude Code](https://claude.com/claude-code) +``` + +Capture the PR URL printed by `gh pr create`. + +**4-pr.4 Archive the REQ.** Apply the **same** archive logic as 4b (path-unit closure guard, non-empty closure-proof requirement, strip ownership stamp, set `**Status:** done`, write `**Closure proof:**`, append `## Outputs`, consolidate `deferred_checks:` or an existing `## Manual checks (advisory)` section into advisory bullets — including the `**Suite:** not-run` header write from 4b step 5a when a consolidated item carries `category: suite-not-run` —, **archive-integrity gate (4b step 5b — `bash {skill-root}/lib/check-archive-integrity.sh` on the rewritten file; non-zero ⇒ stop with `**Reason:** archive-integrity`, do not archive)**, `mv` to `archive/`) — with one addition: append the PR URL to `## Outputs` as a bullet, e.g. `- PR — `. For `ur` granularity where the PR opens later, record the integration branch in `## Outputs` now and append the PR URL bullet when the UR-drain PR opens. + +**4-pr.5 Tear down the worktree — but keep the branch.** Remove the worktree; do **not** delete the branch (the PR owns it): + +```bash +git worktree remove {project}/.worktrees/req-NNN +# NO `git branch -d` — the open PR owns req/REQ-NNN (or it lives on in ur/UR-NNN). +``` + +**4-pr.6 Record the PR URL in the ledger.** When `ledger.enabled` is true, pass the captured URL to the ledger via `--pr` (see Step 3b) so the run record's `pr_url` field carries it. If the metadata commit (4d-equivalent) runs for a tracked `.do-work/`, stage and commit the archive move per 4d — `chore(REQ-NNN): archive`. + +Proceed to Step 7. + +### Step 5: Recover (on stopper) + +Reached only when `status: stopped` or `failed`. The REQ file is still in `working/` (worker didn't move it). Handle per `## Stopping Rules`. + +For `reason: concurrent-conflict` after Step 4a's 5-retry exhaustion: leave the feature branch alive; update the REQ to `**Status:** stopped` and `**Reason:** concurrent-conflict`. `/do-work resume REQ-NNN` is the recovery path. + +For other stoppers: surface to the user via `AskUserQuestion` (existing stopping-rules behaviour). + +Do not proceed to Step 7. + +### Step 6 — (reserved; removed in an earlier revision) + +### Step 7: Report progress + +``` +✅ REQ-NNN complete: [title] + Output: [path] + Commit: [short hash] + +Remaining in backlog: N +``` + +### Step 7b: Milestone deploy-gate check (milestone mode only) + +The deploy-gate prompt is **owned by the orchestrator, not the worker**. The worker has no user-interaction surface and is explicitly forbidden from auto-confirming any gate. Under parallelism, only **one** orchestrator surfaces the prompt to the user — the first to detect milestone completion *and* observe a fully drained milestone backlog. + +**Is milestone mode active?** + +- **Markdown:** `{project}/.do-work/state/active-milestone.md` exists. +- **Linear (REQ-298/299):** **`read_active_milestone`** returns non-null `active` (Project description ``). Empty / missing marker → null (not-in-milestone; does not invent a milestone id). Do **not** require local `active-milestone.md`. + +If not in milestone mode, the worker typically reports `milestone_complete: false` and the orchestrator simply continues until the backlog is empty. Skip the rest of this step. + +If milestone mode is active: + +1. Read `milestone_complete` from the worker's most recent return report. +2. **Markdown:** if `milestone_complete` is `false`, continue the loop normally — claim the next REQ. If `true`, run the **first-to-detect drain check** before showing any prompt. +3. **Linear:** if `milestone_complete` is `true`, **or** after a successful archive **`list_milestone_reqs`** for active M with status `backlog` is empty (and claimable for that M is empty), run the drain check. Worker `milestone_complete` alone is not required when the orchestrator can prove the M backlog is empty via port ops. First-to-detect still means *first whose drain check passes* and who claims the local gate. + +#### Step 7b.1 — Drain confirmation + +Let `` be: + +- **Markdown:** trimmed contents of `{project}/.do-work/state/active-milestone.md`. +- **Linear:** `active` from **`read_active_milestone`**. + +**Markdown drain:** + +1. Glob `{project}/.do-work/REQ-M-*.md` (backlog root). **Must return zero files.** If non-zero, a sibling can still claim more work in this milestone — abort the gate detection, continue the loop normally (Step 8). Some other return-report will trigger the gate later. +2. Glob `{project}/.do-work/working/REQ-M-*.md`. For each file, read its `` ownership stamp: + - Slots whose `**Claimed by:**` equals the local `AGENT_ID` are expected — at most one (the just-archived REQ's transient state) and not a blocker. + - Any slot owned by a **different** agent-id is a sibling's in-flight REQ for the same milestone. The milestone is not yet drained. +3. **If sibling slots are present**, poll every 30 seconds, up to 30 minutes: + - Re-run the working/ glob and re-classify on each tick. + - When no sibling-owned slots remain, the milestone is drained — proceed to Step 7b.2. + - On 30-minute timeout, surface to the user: `Milestone M appears stuck — sibling slot(s) have not drained after 30 minutes. Continue waiting, or abort?` Act on the user's response (continue → resume polling; abort → exit this orchestrator cleanly without writing `gate-owner.md`). +4. **If both globs come back clean on the first check (or after polling completes)**, this orchestrator owns the gate. Proceed to Step 7b.2. + +**Linear drain (REQ-298 path; REQ-299 ops):** + +1. Port op **`list_milestone_reqs`** for `M` with status `backlog` (or claimable intersection). **Must return zero issues.** If non-zero, abort gate detection → Step 8. +2. Port op **`list_milestone_reqs`** for `M` with status `in_flight` (active claim). For each issue, read active claim comment: + - Claims by local `AGENT_ID` are expected (just-archived / releasing) and not a blocker once archive completed. + - Any **foreign** active claim means the milestone is not drained. +3. **If foreign in-flight issues exist**, poll every 30 seconds, up to 30 minutes (re-list + re-classify). Timeout → same stuck-sibling user prompt (list Linear issue ids + agent ids). Abort without writing `gate-owner.md` if user aborts. +4. **If clean**, this orchestrator owns the gate → Step 7b.2. + +#### Step 7b.2 — Claim the gate + +1. **Write local gate ownership** via port op **`write_gate_state`** (claim) → `{project}/.do-work/state/gate-owner.md` containing a single line: the local `AGENT_ID`. (**Both backends** — concurrent gate ownership serializes via this **local** file only, even when milestone cursor content is remote — REQ-299; never Linear. Use the op’s re-read / lost-race rules: if another agent already owns the file, **do not** show the prompt; enter Step 1.0a idle-wait instead. Siblings in Step 1.0a read the file to attribute the wait.) +2. Read the deploy gate text for the active milestone: + - **Markdown:** from `{project}/.do-work/user-requests/UR-NNN/input.md` — line beginning `**Deploy gate:**` under `#### M`. + - **Linear:** from **`read_ur`** brief / Initiative description — same `**Deploy gate:**` line under `#### M` in the milestone-shaped brief. +3. Halt the loop and print: + + ``` + Milestone M REQs complete. + + Deploy gate: + + Has the deploy gate been satisfied? (y/n) + ``` + +4. Wait for user input. + +#### Step 7b.3 — Advance on `y` + +**Markdown:** + +- Update `{project}/.do-work/state/milestones.md` to mark M as `deployed`. +- Identify the next pending milestone (lowest M with status `pending` in milestones.md). + - **If one exists:** update `{project}/.do-work/state/active-milestone.md` to that milestone id. **This file change is the signal that wakes idle siblings** (see Step 1.0a). + - **If none exists** (all milestones deployed): delete `{project}/.do-work/state/active-milestone.md` so idle siblings fall through to `## When the Backlog is Empty`. +- Delete `{project}/.do-work/state/gate-owner.md` (or **`write_gate_state`** release). + +**Linear (REQ-298/299):** + +- Call port op **`set_active_milestone`**: mark M checklist line `deployed`; set `**Active:**` to next pending `M` **or clear** if none remain. **This Project description change wakes idle siblings** polling `read_active_milestone` (Step 1.0a). +- Do **not** require local `active-milestone.md` / `milestones.md` as the store. +- Release local gate via **`write_gate_state`** / delete `{project}/.do-work/state/gate-owner.md` (local-only). + +Then (both backends): + +- Ask: "Begin capture for the next milestone? (y/n)" + - On **y**: print: "Run `/do-work capture UR-NNN` to decompose milestone M." Exit. + - On **n**: exit cleanly. The user can return later. + +#### Step 7b.4 — Stop on `n` + +- Ask: "What needs to change? Describe the gap." Capture the user's description. +- Delete `{project}/.do-work/state/gate-owner.md` (local release — both backends). +- **Markdown:** Delete `{project}/.do-work/state/active-milestone.md`. **This deletion wakes idle siblings** into the empty-backlog path (see Step 1.0a). +- **Linear:** **`set_active_milestone`** clear (`active` null). **This cursor clear wakes idle siblings** polling `read_active_milestone`. +- Print: "Run `/do-work capture UR-NNN` to add new REQs for the gap, or edit the UR's milestone definition. Idle siblings will exit when the active milestone cursor is cleared." +- Exit. + +#### State file: `gate-owner.md` (local — both backends) + +| Action | Actor | When | +|---|---|---| +| **Write** | Gate-owning orchestrator (Step 7b.2) via **`write_gate_state`** | After drain confirmation passes, before printing the gate prompt | +| **Read** | Sibling orchestrators (Step 1.0a) | When their active-milestone backlog is empty, to attribute the idle log line | +| **Delete** | Gate-owning orchestrator (Step 7b.3 or Step 7b.4) | After the user answers y or n, before exit | + +Contents: a single line — the gate-owner's `AGENT_ID`. No header, no trailing data. If the file is ever found with malformed contents, treat as absent and continue. **Never** store gate ownership in Linear (design §11 / REQ-298 path; REQ-299 concurrent serialize). Concurrent claims use **`write_gate_state`** re-read rules so ownership serializes via this local file even when the milestone cursor is remote. + +#### Non-delegation + +- **Sign-off is non-delegable.** The orchestrator must NOT auto-confirm the deploy gate. The orchestrator must NOT attempt to deploy or test deployment itself. The worker is also forbidden from these actions (see [agents/run-worker.md](run-worker.md)). +- Only the *which orchestrator owns showing the prompt* changes under parallelism. The prompt text and the requirement for an explicit human y/n answer are unchanged. + +### Step 8: Loop + +If the Step 3b.1 budget gate tripped on the REQ just integrated, **do not loop** — the budget-stop report has already been emitted and the run ends here. Otherwise, go back to Step 1 and claim the next REQ. + +A REQ with `deferred_checks:` is not a stopper — its code merged, its advisory checks were recorded in the archive, and its worktree was torn down. Continue looping exactly as after any done REQ. + +**Dependency note.** Deferred manual checks do not change dependency flow. The REQ lands in `archive/`, so `lib/check-deps.sh` and `lib/pick-req.sh` treat it as satisfied through the normal archive-only path. + +--- + + +--- + +## Stopping Rules + +Workers cannot pause and ask the user — they have no interaction surface. Every stopper must surface to the user **through the orchestrator**, never inline from the worker. The worker emits `status: stopped` with a structured `reason`; the orchestrator decides what to show the user. + +### Stopper category → worker `reason` enum + +| Situation | Worker emits `reason` | +|-----------|----------------------| +| Tests cannot be made to pass after 3 attempts | `tests-failing` | +| Verification steps fail after 3 attempts | `verification-failing` | +| A REQ has unmet dependencies on another REQ not yet complete | `dependency-missing` | +| Task requires external credentials or access not available | `missing-creds` | +| Acceptance criteria are ambiguous and cannot be interpreted | `ambiguous-criteria` | +| A change would affect files outside the REQ's stated scope | `scope-creep` | +| Commit or merge conflict unresolved after 5 retries (see run-worker.md `## Concurrent-Conflict Retry`) | `concurrent-conflict` | +| Any other unrecoverable error | `unknown-error` | + +The worker captures relevant details in the report's `details` field. The worker does not retry beyond what's defined in [agents/run-worker.md](run-worker.md) and never asks the user a question — it exits with the structured report. + +### Orchestrator handles user interaction + +When the worker returns `status: stopped`, the orchestrator surfaces the stopper to the user. Recover the REQ from `working/` if it was not archived, then: + +If this agent is running **standalone** (not as a delegate inside the go agent): + +**Use the `AskUserQuestion` tool** (do NOT just print the options as text) with these options: + +1. **"Show blocker details"** — Display the worker's `details` field and any captured output +2. **"Retry current REQ"** — Re-dispatch the worker for the same REQ (fresh subagent session) +3. **"Skip"** — End the interaction + +If this agent is running as a **delegate** inside go: print the stopper and the worker's `details` field, then stop. Do not loop, do not silently retry, do not auto-resolve. + +### Per-REQ retry counter (ambiguous-criteria recurrence) + +The orchestrator tracks per-REQ stopped-reason occurrences so a *second* `ambiguous-criteria` stop on the same REQ can surface as feedback (a single ambiguity is normal; a second on the same REQ means the user-facing clarification did not stick or the REQ wording is genuinely defective). + +Counter store: `{project}/.do-work/state/retry-counters.md`. Format — one Markdown table row per (REQ, reason) pair: + +```markdown +| REQ-NNN | ambiguous-criteria | 2 | 2026-05-21T02:14:22Z | +``` + +Columns: REQ id, reason, count, last-seen ISO-8601 UTC. The orchestrator keeps this in memory for the lifetime of the loop and flushes to the file after each update. If the file is missing on startup, treat all counters as zero. + +When the worker returns `status: stopped`, `reason: ambiguous-criteria`: + +1. Increment the (REQ-NNN, ambiguous-criteria) counter in memory and persist to `retry-counters.md`. +2. If the new count is **≥ 2**, emit feedback (best-effort, non-blocking) before surfacing the stopper to the user: + + ```bash + FINGERPRINT="ambiguous-req:REQ-NNN" + bash {skill-root}/lib/file-feedback.sh ambiguous-criteria \ + "$FINGERPRINT" \ + '{"req":"REQ-NNN","occurrence":'"$COUNT"',"first_seen":"","last_seen":""}' \ + "Ambiguous-criteria recurrence: REQ-NNN (occurrence #$COUNT)" \ + "Worker has now stopped on REQ-NNN with reason ambiguous-criteria $COUNT times. The acceptance criteria likely need a rewrite, not another retry." \ + || true + ``` + +3. Proceed to the existing user-interaction step above (AskUserQuestion or stop-and-print). + +> **JUDGMENT:** Fire feedback only on the 2nd+ occurrence — the first stop is the worker doing its job; the second is the signal. Title states the REQ id and occurrence count so the human inbox immediately knows which REQ needs editing. The body must point at the *criteria* as the problem (not the worker, not the model) so the human reaches for the REQ file rather than a retry button. + +--- + diff --git a/references/run-parallel.md b/references/run-parallel.md new file mode 100644 index 0000000..297d1d5 --- /dev/null +++ b/references/run-parallel.md @@ -0,0 +1,205 @@ +# Parallel run mode + empty-backlog drain (reference) + +One hop from [`agents/run.md`](../agents/run.md). Load only when effective window width `N > 1`, or when the serial loop reaches empty-backlog drain. + +--- + +## Parallel Run Mode + +> **Entered only when the effective window width `N > 1`** (see `## When Invoked → Parallel window width`). When `N == 1` this entire section is skipped and `## The Loop` runs serially, byte-for-byte unchanged. +> +> **Authority:** this section transcribes `docs/design/single-session-parallel.md` (REQ-220). Each subsection cites its design decision. If any decision proves unimplementable as written, stop with `ambiguous-criteria` naming the design section — do not improvise coordination semantics. + +Single-session parallel mode is **one orchestrator dispatching up to `N` concurrent workers from one terminal**, then integrating their results through a **serialized merge queue**. Every safety primitive (`pick-req.sh` overlap exclusion, `claim-req.sh` atomicity, dependency ordering, worktree isolation, heartbeat staleness, the final-suite lockfile) is reused unchanged. What changes is only the shape of the hot path: from serial `claim → dispatch → wait → integrate` to windowed `claim K → dispatch K → integrate as each returns → refill`. + +Pre-flight (`## Pre-flight Check`) runs exactly as in serial mode — branch/dir checks, `mkdir -p state/`, `AGENT_ID`, context pack, the informational `working/` scan. A non-empty `mine` bucket is resumed first (those REQs occupy window slots before any new claim). + +### P1. Fan-out mechanism — concurrent `Agent` dispatches (design §1) + +Fan out via **N concurrent `Agent`-tool dispatches in one turn** — the same worker-dispatch surface serial mode uses at `## The Loop` Step 2. The harness runs concurrent tool calls in a single turn in parallel. Do **not** delegate fan-out to a Workflow/scheduler primitive: the `Agent` tool is the only dispatch surface guaranteed wherever serial mode works, it keeps the announce/return checkpoints that logging, the ledger (Step 3b), and stopper-surfacing all hang off, and it keeps resume granularity at one REQ. Each dispatch carries the identical five-input worker contract from Step 2 (REQ path, UR path, prior-REQ paths, context-pack path, resolved `$SKILL_ROOT`, plus `run-worker.md` inline). Announce each at claim time exactly as Step 1's announce line. + +### P2. Window fill — claim-as-slot-frees (design §2) + +**Claim one REQ immediately before each dispatch — never a batch up front.** `pick-req.sh` reads `working/` directly to build its footprint-exclusion set, so a claim must be *visible in `working/`* before the next pick or two picked candidates could overlap each other. + +**Fill loop** (run at start, and again on every refill): + +``` +while live workers < N: + REQ_PATH = pick-req.sh "$SCOPE" "$AGENT_ID" # Step 1 picker, unchanged + if REQ_PATH is empty: break # nothing claimable right now + claim-req.sh "$REQ_PATH" "$AGENT_ID" # Step 1 claim, atomic, lands in working/ + classify + select model (## REQ Classification, ## Model Selection) + dispatch worker (P1) and count it as a live worker +``` + +- Each `claim-req.sh` updates `working/` before the next `pick-req.sh`, so the next pick automatically skips overlapping and now-claimed REQs. **Never `pick-req.sh × K` then claim.** +- **Overlapping candidates:** the first claimed wins the slot; the rest are excluded by the overlap filter on the next pick and stay in the backlog. They become claimable again only when the winning slot drains (its REQ is integrated and leaves `working/`). Identical to multi-terminal behaviour — no new arbitration. +- A claimed-but-not-dispatched gap is impossible: each claim is immediately followed by its dispatch in the same fan-out turn. +- **Claim races / errors** are handled exactly as `## The Loop` Step 1 (`claim-req.sh` exit 2 ⇒ re-pick; other non-zero ⇒ backoff/re-pick, stop after 3 consecutive non-race failures). +- **Picker returns empty while slots are free:** do not idle-wait the way serial Step 1 does — live workers are still running and will free footprints as they integrate. Break the fill loop and go drain the merge queue (P3); refill again after each slot frees. Only when the window is fully empty **and** `pick-req.sh` returns empty do you fall through to `## When the Backlog is Empty`. + +> **JUDGMENT:** J8 — when the window has free slots but the picker returns empty, prefer draining ready workers over blocking. A footprint freed by an integrating peer may unblock the next pick. Surface to the user only via the existing empty-backlog / deadlock paths once **no** workers are live. + +### P3. The merge queue (design §3) + +Workers return on `req/REQ-NNN` branches concurrently and **in any order** (a fast REQ dispatched second can return before a slow one dispatched first). Every gate after dispatch must serialize the single-writer tail. The merge queue is one in-orchestrator **FIFO of returned worker reports awaiting integration, ordered by arrival** — not by REQ number. + +Arrival order is correct because footprints are disjoint by construction (no two queued REQs touch the same files) and dependency ordering is already enforced upstream at claim time (`pick-req.sh` will not hand out a REQ whose `**Depends on:**` are unarchived). The queue never needs to reorder for deps, and arrival order avoids head-of-line blocking. + +Each dequeued report runs **exactly the existing serial Steps 3–4, internals unchanged**, split into two stages: + +**Stage A — concurrent, read-only (safe N-wide).** May run across multiple queued reports in parallel; writes nothing to the base branch or `.do-work/` lifecycle state: +1. Step 3 — acceptance-evidence gate (`check-acceptance-evidence.sh`) +2. Step 3 — policy gate (`check-policy.sh`) +3. Step 3a/3b — **independent review dispatch** (`review.md` as a fresh subagent; adversarial mode per Step 3b when `review.adversarial` + policy exit 2). Review reads only `(REQ, diff, evidence)` with no run context, so its dispatches may be fanned out concurrently — the same `Agent`-tool concurrency used for workers — keeping review latency off the critical path. + +**Stage B — serial, single-writer (one REQ at a time).** A report enters Stage B only after passing **every** Stage A gate. Run the existing Step 3b ledger + Step 4 substeps in order: +4. Step 3b — ledger entry (`run-ledger.sh`) +5. Step 4a — `git merge --no-ff req/REQ-NNN` from the **main working tree** (never a worktree) +6. Step 4b — archive the REQ file (closure-proof + path-unit guards unchanged) +7. Step 4c — tear down the worktree + `git branch -d` +8. Step 4d — commit the metadata change + +**Serialization invariant:** at most one Stage B sequence touches the main working tree and `.do-work/` at any instant — exactly as serial mode. Only after an entry finishes Step 4d (or diverts to Recover) do you admit the next report to Stage B. After each Stage B completion the freed slot triggers a P2 refill. + +**Conflict handling mid-queue — reuse Step 4a verbatim, do not invent a new retry path.** On text-level conflict (`<<<<<<<`): `git merge --abort`, then the existing **5-retry exponential backoff** (5s / 15s / 30s / 60s), each attempt re-syncing the base branch and re-merging. On the 5th failure: leave the `req/REQ-NNN` branch alive, transition the REQ to `**Status:** stopped`, `**Reason:** concurrent-conflict`, surface to the user, and **continue draining the rest of the queue** — a conflict on one queued REQ must not abort the others. Resumable via `/do-work resume REQ-NNN`. Because footprints are disjoint, a content conflict between two queued REQs should not occur; the retry path absorbs conflicts against concurrent multi-terminal siblings or a remote (P5). + +> **JUDGMENT:** J9 — admit reports to Stage B in arrival order, one at a time; never hold a ready report waiting for a slower lower-numbered REQ. A Stage A failure or a Stage B 5-retry exhaustion on one report diverts only that report to Recover (P4) and never blocks its siblings. + +### P4. Failure isolation (design §5) + +**One worker (or one queued integration) stopping must never abort its siblings.** + +- A worker returning `status: stopped` / `failed`, or a Stage A gate failure on its report, is handled **exactly** as serial Step 5 (Recover) and `## Stopping Rules`: the REQ stays in `working/` with `**Status:** stopped` + `**Reason:**`; the orchestrator surfaces the stopper. The difference under parallelism: **do not halt the loop** — record the stopper, **free that window slot**, and (if backlog remains) claim a refill (P2). The other workers and any queued ready reports proceed untouched. +- **Stoppers are queued per-REQ and surfaced in arrival order**, one decision at a time. When `next_steps.enabled` is true **and** standalone, each stopper surfaces via `AskUserQuestion` (Show details / Retry / Skip) as in serial mode. When the gate is closed (delegate mode or `next_steps.enabled=false`), each stopper prints its `details` and the loop continues — no auto-retry. The per-REQ retry counter and ambiguous-criteria feedback (`## Stopping Rules`) apply per REQ, unchanged. +- **No new stopper reasons.** The `## Stopping Rules` enum is complete; `concurrent-conflict` (P3 5-retry exhaustion) is already in it. +- **Drain accounting.** A stopped REQ left in `working/` is, for the empty-backlog drain check (`## When the Backlog is Empty` Step B), a slot owned by *this* `AGENT_ID` — `mine`, tolerated, not a blocker. The single-session orchestrator finishes its loop when the backlog is empty **and** its window has no live workers, then runs the final-suite path (P5). + +### P5. Coexistence with multi-terminal orchestrators (design §6) + +Single-session mode is **just one more agent-id in the existing claim arbitration — no special-casing.** + +- **Claim arbitration.** This orchestrator has one `AGENT_ID = hostname.pid`; its N claimed slots all carry it. A multi-terminal sibling's `pick-req.sh` excludes those slots by footprint just as it excludes any terminal's slots, and vice-versa. N-wide claiming from one process is indistinguishable, to the picker, from N processes each claiming once. +- **Heartbeat / staleness.** Each dispatched worker keeps its own slot's heartbeat fresh (`run-worker.md` checkpoint-stamping). A sibling's stale scan treats a single-session worker's slot like any other. This mode does **not** change the heartbeat mechanism. +- **Merge contention.** Both this orchestrator and a multi-terminal sibling merge into the same base branch. The Step 4a / P3 5-retry backoff is precisely what absorbs a merge that collides with a sibling's just-landed commit — a sync conflict resolved by rebase-and-retry. +- **Final-suite lockfile.** `## When the Backlog is Empty` (Steps B–E) is reused unchanged. The single-session orchestrator runs its drain check (backlog empty + no `other`-owned slots) only after its own N workers have all returned and drained, then races for the committed `final-suite-running.md` lockfile like any other contender. Its internal N-way fan-out is invisible at the lockfile layer. + +### P6. Out of scope — deploy gates stay single-flow (design §7) + +- **Milestone deploy gates are NOT parallelised.** Step 7b (the deploy-gate y/n prompt) is non-delegable and owned by exactly one orchestrator. When a worker reports `milestone_complete: true`, run the existing first-to-detect drain check (Step 7b) and surface the single y/n prompt. The N-way fan-out **pauses new claims while the gate is open** (the active-milestone backlog is, by definition, drained when the gate fires). No change to gate semantics. +- **The coordination lib is untouched.** `pick-req.sh`, `claim-req.sh`, `check-footprint.sh`, `scan-stale.sh`, `deadlock-check.sh`, `run-ledger.sh` keep their current contracts. This mode is a run-loop shape change, not a primitive change. No new state files, no new stopper reasons. + +--- + +## When the Backlog is Empty + +The final cross-REQ test suite must run exactly once per drained backlog — fired by the **last orchestrator to finish**, not whichever orchestrator happens to observe the empty backlog first. Under N-way parallelism, this section guarantees that property via an explicit drain check and a committed lockfile. + +### Step A — Trigger + +Reached when the claim step (Step 1, REQ-114) returns no claimable REQ **and** this orchestrator has just archived its previous REQ. (Pre-flight empty-backlog also lands here — see `## Pre-flight Check` Step 5.) + +### Step B — Drain check (am I the last?) + +Before running the suite, classify the live state by reading ownership stamps (per `## Agent Identity` and REQ-113): + +1. **Backlog root:** glob `{project}/.do-work/REQ-*.md`. Must be empty. + - In milestone mode (`{project}/.do-work/state/active-milestone.md` exists), glob `{project}/.do-work/REQ-M-*.md` instead. +2. **Working slots:** glob `{project}/.do-work/working/REQ-*.md` (milestone mode: `working/REQ-M-*.md`). For each slot file, read its `` block and classify by `**Claimed by:**`: + + | Classification | Condition | + |---|---| + | `mine` | Stamp's `**Claimed by:**` equals local `AGENT_ID`. Tolerated — at most one, the just-archived REQ's transient state. Not a blocker. | + | `other` | Stamp's `**Claimed by:**` differs from local `AGENT_ID`. A sibling is still in flight — drain check **fails**. | + | `other` (defensive) | No stamp present (legacy / malformed slot). Treat as `other` — the local agent must not run the suite without checking with siblings. | + +3. **Drain check passes** iff backlog glob is empty AND no slot is classified `other`. Proceed to Step C. +4. **Drain check fails** (one or more `other` slots): proceed to Step E (sibling idle exit). + +### Step C — Lockfile acquisition (sibling-also-drained race) + +Two orchestrators can both pass the drain check at near-the-same instant (each just archived its own REQ, neither sees the other's slot). The lockfile is the tiebreaker. + +Lockfile path: +- Non-milestone mode: `{project}/.do-work/state/final-suite-running.md` +- Milestone mode: `{project}/.do-work/state/final-suite-M-running.md` + +Acquisition sequence (first-to-commit wins): + +1. Check whether the lockfile already exists. If yes, another orchestrator already holds the suite — proceed to Step E (sibling idle exit), substituting "Sibling is running the final suite" framing. +2. Write the lockfile with a single block: + + ```markdown + **Held by:** + **Started at:** + ``` + +3. Stage and commit atomically: + + ```bash + git add {project}/.do-work/state/final-suite-running.md # or final-suite-M-running.md + git commit -m "chore: final-suite lock" + ``` + + - **Commit succeeds:** this orchestrator holds the lock. Proceed to Step D. + - **Commit fails** (e.g. sibling won the race, working tree shows their lockfile already committed, or merge conflict on the lockfile): treat as lost race. Discard local lockfile changes (`git checkout -- ` then `rm -f ` if still present), then proceed to Step E. + +The lockfile is intentionally committed *before* running the suite so other orchestrators can observe the lock even if the suite hangs. + +### Step D — Run the suite (lock-holder only) + +This orchestrator holds the lockfile. Run the project's full test suite as a cross-REQ safety net. + +1. **Suite command resolution** — unchanged. Use `config.test.suite_command` if set; otherwise try defaults in order (`npm test`, `npx vitest run`, `./vendor/bin/pest`), checking the runner exists before executing. If none found, log `No test suite configured or detected — skipping full suite run` and skip to Step D.4. +2. **Execute** the suite command. +3. **On failure** — apply the existing failure-attribution + 3-attempt fix loop unchanged: map failing test files to REQ commits via `git diff-tree --no-commit-id --name-only -r `, report the likely responsible REQ, fix the implementation, re-run; after 3 failed attempts, stop and report to the user. +4. **Release the lockfile** (regardless of pass/fail): + + ```bash + git rm {project}/.do-work/state/final-suite-running.md # or final-suite-M-running.md + git commit -m "chore: final-suite lock released" + ``` + +5. Proceed to the completion report below. + +### Step E — Sibling idle exit (drain check failed OR lockfile already held) + +This orchestrator does NOT run the suite. Emit exactly one idle log line, then exit cleanly: + +``` +[] Backlog drained for this orchestrator. sibling slot(s) still in flight ([, ...]). +Sibling will run the final suite when it finishes. +``` + +The user will see one final-suite report from whichever sibling finishes last. No further work, no polling, no lockfile writes. + +### Completion report and prompt + +Output the completion report: + +``` +Do Work loop complete. + +Processed: N REQs +Full suite: [passed / skipped — no test runner found] +All outputs committed. +Archive: {project}/.do-work/archive/ +``` + +When the effective budget is armed (non-empty), append a budget line to this report: `Estimated spend: $ / budget $ (tier-weighted estimate)`. This is the natural-exhaustion case (backlog emptied before the budget was hit); the **budget-stop report** (Step 3b.1) is the distinct early-stop case where the budget was reached with REQs still remaining. + +**Then, immediately after the report**, check whether to present next-step options: + +If `config.next_steps.enabled` is `true` **and** this agent is running standalone (not as a delegate inside the go agent): + +**Use the `AskUserQuestion` tool** (do NOT just print the options as text) with these options: + +1. **"Start new work"** — Run intake for a new UR +2. **"Review outputs"** — List archived REQs and their output paths +3. **"Skip"** — End the interaction + +If `config.next_steps.enabled` is `false`, missing, or this agent is running as a delegate inside go: skip the AskUserQuestion and stop. + +--- + diff --git a/references/tracker.md b/references/tracker.md new file mode 100644 index 0000000..3fab85d --- /dev/null +++ b/references/tracker.md @@ -0,0 +1,51 @@ +# Tracker backends (work-item store) + +Deep dive for multi-tracker configuration. Hard-stop and dual-write rules are summarized in `SKILL.md` (always loaded). Canonical contracts: [agents/tracker/port.md](../agents/tracker/port.md), [agents/config.md](../agents/config.md). Runtime sequences: [agents/tracker/markdown.md](../agents/tracker/markdown.md), [agents/tracker/linear.md](../agents/tracker/linear.md). + +Work items (URs, REQs, decisions, verify/close reports, run notes) are stored through a **tracker port**. Config key `tracker.backend` selects the implementation: + +| `tracker.backend` | Behavior | +|-------------------|----------| +| **unset / empty / missing** | Treat as **`markdown`** — no hard-stop, no Linear tools | +| **`markdown`** | Default: local `.do-work/` files + `lib/*.sh` (behavior matches today) | +| **`linear`** | Linear is the sole work-item store (no dual-write; hard-stop if Linear unusable) | + +**Load path** for every phase agent that touches work items: (1) load config (`agents/config.md`), (2) resolve `tracker.backend` (default **`markdown`** if missing/empty), (3) read `agents/tracker/port.md`, (4) read `agents/tracker/.md`, (5) call only named port ops for storage. Runtime/git (worktrees, merges, state locks, `config.yml`) stay local on every backend. Markdown remains the default; existing tests and conformance do not require Linear. + +**Hard-stop (no silent fallback):** when effective backend is `linear` and Linear is unusable (MCP missing/unauthenticated, team unresolved, missing `status_map` state) **or** `agents/tracker/linear.md` is missing/unreadable, agents **hard-stop** with setup instructions — they never fall through to markdown work-item paths. Canonical contract: `agents/tracker/port.md` + Load Config steps 6–7 in `agents/config.md`. + +**`tracker.linear.*` (when `backend: linear`).** Full schema and defaults live in `agents/config.md` (canonical template + schema reference). Summary: + +| Key area | Defaults / rules | +|----------|------------------| +| Team | `team_id` and/or `team_key` — **hard-fail** if neither resolves | +| MCP | Linear MCP tools must be discoverable — **hard-fail** with skill setup instructions if not | +| Hierarchy | **UR = Project Milestone** on shared product Project per local product; REQs = Issues with that milestone. Not Initiatives (MCP has no Initiative create tools). | +| `product_project` | Shared Linear Project (**name or UUID**) for all URs on this local product — **default empty** (not skill name `do-work`). Resolve: explicit `product_project` → `project.name` → git-root basename; `ensure_product_container` create-if-missing + **always persist UUID**. Example for this skill repo only: name `do-work`. | +| `ur_milestone_name_pattern` | Default `{ur_id}: {title}` | +| `status_map` | `backlog→Todo`, `in_progress→In Progress`, `stopped→Canceled`, `done→Done` — **hard-fail** if a mapped state is missing on the team (rename team state or override the map key) | +| Labels | `Layer/`, `path-unit`, `Size/` prefixes | +| Claim | `agent_claim_marker: ""`; heartbeat age defaults to `parallel.stale_threshold_seconds` when `heartbeat_max_age_seconds` is null | +| Docs | Team Docs `do-work/decisions` and `do-work/calibration` | + +`ledger`, `parallel`, `delivery`, `review`, and `layers` remain valid under Linear. Authoritative run notes are Linear Issue comments; local `.do-work/runs/` is optional telemetry when `ledger.enabled: true`. + +**No dual-write.** With `tracker.backend: linear`, Linear is the **only** work-item store. Agents must not mirror URs/REQs into local markdown as a second source of truth, and must not fall back to markdown when Linear fails (hard-stop instead). After idle migration (`/do-work upgrade migrate`), historical `.do-work/user-requests/` and `archive/` trees remain on disk as **read-only history** — work-item ops ignore them. + +**Linear commit / branch convention** (when `backend: linear`): + +``` +feat(ENG-123): short title + +Issue: ENG-123 +UR: UR-007 +Output: path/to/primary/output +``` + +- Subject uses the **Linear issue id** only (e.g. `ENG-123`) — not `REQ-NNN`. +- Footer: `Issue:` + id; `UR:` when known; `Output:` primary path. No `.do-work/archive/REQ-…` path required. +- Feature branch / worktree: `req/` (e.g. `req/ENG-123`); worktree dir hard-defaults to lowercase (`req-eng-123`). See `agents/run-worker.md` W2 / design §6.5. + +**Human assignee + agent claim comments (operator warning):** under Linear, the **human** remains the Issue assignee; agents claim via workflow state + a claim-protocol comment (`tracker.linear.agent_claim_marker`, default ``) with `agent_id`, timestamps, and `status: active`. **Do not clear, edit, or delete agent claim comments in the Linear UI while a `/do-work run` is live** — that breaks multi-agent claim/heartbeat and can strand or double-claim work. Recover stuck claims with `/do-work status`, then `/do-work resume` or `/do-work unblock` after the run is idle or the agent has stopped. Mid-flight Linear MCP failure leaves the claim active; resume/unblock after MCP recovers. + +**Markdown remains the default.** Unset/empty `tracker.backend` → `markdown`. No Linear MCP required for the happy path. Operator setup for Linear (MCP connect + `team_id` / `team_key`): [docs/troubleshooting.md](../docs/troubleshooting.md) § Linear tracker backend; deep dive [docs/HOW-IT-WORKS.md](../docs/HOW-IT-WORKS.md) § Multi-tracker; first-run pointer [docs/getting-started.md](../docs/getting-started.md). Full sequences: `agents/tracker/linear.md`.