@gobing-ai/spur 0.3.98 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/config.global.yaml +17 -0
  3. package/config/decisions/task-pipeline.yaml +55 -0
  4. package/config/plugin-scripts.json +15 -0
  5. package/config/rules/boundary/sp-runtime-path.yaml +2 -0
  6. package/config/rules/quality/coverage-gate.yaml +5 -3
  7. package/config/rules/strict/runtime-boundaries.yaml +11 -0
  8. package/config/rules/structure/protected-files.yaml +3 -0
  9. package/config/templates/task/brainstorm.md +1 -1
  10. package/config/templates/task/feature-impl.md +1 -1
  11. package/config/templates/task/issue.md +1 -1
  12. package/config/templates/task/meta.md +1 -1
  13. package/config/templates/task/review.md +1 -1
  14. package/config/templates/task/standard.md +1 -1
  15. package/config/workflows/task-pipeline.yaml +178 -18
  16. package/package.json +9 -9
  17. package/plugins/sp/README.md +14 -1
  18. package/plugins/sp/agents/super-reviewer.md +25 -11
  19. package/plugins/sp/commands/dev-changelog.md +5 -4
  20. package/plugins/sp/commands/dev-review-session.md +12 -7
  21. package/plugins/sp/commands/dev-run.md +5 -2
  22. package/plugins/sp/commands/dev-runall.md +5 -2
  23. package/plugins/sp/hooks/agent-lifecycle.ts +155 -0
  24. package/plugins/sp/hooks/fleet-guest-stop.ts +180 -0
  25. package/plugins/sp/hooks/hooks.json +45 -1
  26. package/plugins/sp/hooks/pi/guard-extension.ts +21 -0
  27. package/plugins/sp/lib/history-anatomy.generated.mjs +5 -0
  28. package/plugins/sp/lib/idea-handoff.generated.mjs +68 -10
  29. package/plugins/sp/lib/inline-run.generated.d.mts +1 -0
  30. package/plugins/sp/lib/inline-run.generated.mjs +83 -20
  31. package/plugins/sp/lib/residual-scan.generated.d.mts +5 -0
  32. package/plugins/sp/lib/residual-scan.generated.mjs +45 -5
  33. package/plugins/sp/lib/transcript.ts +160 -0
  34. package/plugins/sp/plugin.json +1 -1
  35. package/plugins/sp/scripts/history-anatomy-cache.mjs +5 -0
  36. package/plugins/sp/scripts/inline-run-setup.mjs +21 -20
  37. package/plugins/sp/scripts/inline-run-setup.ts +25 -31
  38. package/plugins/sp/scripts/persist-out-check.mjs +164 -0
  39. package/plugins/sp/scripts/persist-out-check.ts +185 -0
  40. package/plugins/sp/scripts/residual-scan.mjs +52 -15
  41. package/plugins/sp/scripts/residual-scan.ts +11 -11
  42. package/plugins/sp/scripts/run-summary.mjs +286 -0
  43. package/plugins/sp/scripts/run-summary.ts +241 -0
  44. package/plugins/sp/scripts/session-timeline.mjs +234 -0
  45. package/plugins/sp/scripts/session-timeline.ts +152 -0
  46. package/plugins/sp/scripts/task-diffstat.mjs +17 -5
  47. package/plugins/sp/scripts/task-diffstat.ts +25 -4
  48. package/plugins/sp/skills/code-verification/SKILL.md +8 -0
  49. package/plugins/sp/skills/code-verification/references/verdict-schema.md +17 -1
  50. package/plugins/sp/skills/fleet-join/SKILL.md +63 -0
  51. package/plugins/sp/skills/session-review/SKILL.md +46 -24
  52. package/plugins/sp/skills/spur-cli/SKILL.md +1 -0
  53. package/plugins/sp/skills/spur-cli/references/agent.md +108 -6
  54. package/plugins/sp/skills/spur-cli/references/decision.md +97 -0
  55. package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +22 -0
  56. package/plugins/sp/skills/spur-composer/SKILL.md +27 -1
  57. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +40 -7
  58. package/plugins/sp/skills/spur-dev/references/dev-operations.md +28 -16
  59. package/plugins/sp/skills/spur-dev/references/execution-batch.md +278 -42
  60. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +17 -1
  61. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +16 -4
  62. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +139 -10
  63. package/plugins/sp/skills/spur-dev/references/planning-workflow.md +6 -0
  64. package/schemas/decision-catalog.schema.json +226 -0
  65. package/schemas/spur-config.schema.json +25 -0
  66. package/spur.js +8856 -2564
  67. package/web/_astro/BoardApp.Bzr4OXrW.js +324 -0
  68. package/web/_astro/{BoardApp.Dflq5a8x.js → BoardApp.VeoxxLBy.js} +1 -1
  69. package/web/_astro/{TaskDetail.CK7oXddP.js → TaskDetail.LAyR_Pvn.js} +1 -1
  70. package/web/_astro/{arc.BIqkDATc.js → arc.f4e0OYLg.js} +1 -1
  71. package/web/_astro/{architectureDiagram-3BPJPVTR.D0B3SvWv.js → architectureDiagram-3BPJPVTR.SSIvxi-Z.js} +1 -1
  72. package/web/_astro/{blockDiagram-GPEHLZMM.C2YyKQmL.js → blockDiagram-GPEHLZMM.DSXIHOFf.js} +1 -1
  73. package/web/_astro/{c4Diagram-AAUBKEIU.DmtTYrt_.js → c4Diagram-AAUBKEIU.DG9Ubu85.js} +1 -1
  74. package/web/_astro/channel.CUFdtWhm.js +1 -0
  75. package/web/_astro/{chunk-2J33WTMH.HsRXbh7l.js → chunk-2J33WTMH.C7QU88Qh.js} +1 -1
  76. package/web/_astro/{chunk-4BX2VUAB.obE_P-R3.js → chunk-4BX2VUAB.26Z94xBj.js} +1 -1
  77. package/web/_astro/{chunk-55IACEB6.CjWTF5mz.js → chunk-55IACEB6.DYRiBkqw.js} +1 -1
  78. package/web/_astro/{chunk-727SXJPM.DMo799Ax.js → chunk-727SXJPM.D5Vfv5pe.js} +1 -1
  79. package/web/_astro/{chunk-AQP2D5EJ.-cE6JOKZ.js → chunk-AQP2D5EJ.DXHV47bN.js} +1 -1
  80. package/web/_astro/{chunk-FMBD7UC4.D6Rw6HIk.js → chunk-FMBD7UC4.D1suAfOs.js} +1 -1
  81. package/web/_astro/{chunk-ND2GUHAM.d5rBD73Z.js → chunk-ND2GUHAM.D6BmdHEY.js} +1 -1
  82. package/web/_astro/{chunk-QZHKN3VN.r3ox40Q3.js → chunk-QZHKN3VN.rnPU-Ak-.js} +1 -1
  83. package/web/_astro/{classDiagram-4FO5ZUOK.CGGbvjjJ.js → classDiagram-4FO5ZUOK.CvEfXIo1.js} +1 -1
  84. package/web/_astro/{classDiagram-v2-Q7XG4LA2.CGGbvjjJ.js → classDiagram-v2-Q7XG4LA2.CvEfXIo1.js} +1 -1
  85. package/web/_astro/{cose-bilkent-S5V4N54A.CmKtPd4l.js → cose-bilkent-S5V4N54A.DkMisLSm.js} +1 -1
  86. package/web/_astro/{cynefin-OW5HDTMX.QNg5GXs8.js → cynefin-OW5HDTMX.C77Q-pF6.js} +1 -1
  87. package/web/_astro/{dagre-BM42HDAG.B1SabsjC.js → dagre-BM42HDAG.YaV8ce27.js} +1 -1
  88. package/web/_astro/{diagram-2AECGRRQ.RtDbNj89.js → diagram-2AECGRRQ.CUyGEKge.js} +1 -1
  89. package/web/_astro/{diagram-5GNKFQAL.IzZpuM-Y.js → diagram-5GNKFQAL.CRgd8dgj.js} +1 -1
  90. package/web/_astro/{diagram-KO2AKTUF.DPTF_fgv.js → diagram-KO2AKTUF.Bmzys9hq.js} +1 -1
  91. package/web/_astro/{diagram-LMA3HP47.CCyza39P.js → diagram-LMA3HP47.BvmJY4Lw.js} +1 -1
  92. package/web/_astro/{diagram-OG6HWLK6.BXAi8zcZ.js → diagram-OG6HWLK6.0hEhD30d.js} +1 -1
  93. package/web/_astro/{erDiagram-TEJ5UH35.DUDX7_MR.js → erDiagram-TEJ5UH35.Bjt8lrnm.js} +1 -1
  94. package/web/_astro/{flowDiagram-I6XJVG4X.C63QTh3N.js → flowDiagram-I6XJVG4X.CJZTjRDV.js} +1 -1
  95. package/web/_astro/{ganttDiagram-6RSMTGT7.CnlZJSwG.js → ganttDiagram-6RSMTGT7._VqoVafc.js} +1 -1
  96. package/web/_astro/{gitGraphDiagram-PVQCEYII.CTqIrOPC.js → gitGraphDiagram-PVQCEYII.CEDb7tVo.js} +1 -1
  97. package/web/_astro/index.Dg_3bOaJ.css +1 -0
  98. package/web/_astro/{infoDiagram-5YYISTIA.BYMiDFh9.js → infoDiagram-5YYISTIA.CgwifPpK.js} +1 -1
  99. package/web/_astro/{ishikawaDiagram-YF4QCWOH.CRiQmI-z.js → ishikawaDiagram-YF4QCWOH.B99wx2J1.js} +1 -1
  100. package/web/_astro/{journeyDiagram-JHISSGLW.YQgWBsEU.js → journeyDiagram-JHISSGLW.2hRwJbEo.js} +1 -1
  101. package/web/_astro/{kanban-definition-UN3LZRKU.1ECnrrK1.js → kanban-definition-UN3LZRKU.BahJkKNC.js} +1 -1
  102. package/web/_astro/{linear.DzVA71yT.js → linear.DguUUe6V.js} +1 -1
  103. package/web/_astro/{mermaid.core.BK7P2Sr_.js → mermaid.core.CvgfmqVr.js} +4 -4
  104. package/web/_astro/{mindmap-definition-RKZ34NQL.BvXyWZK7.js → mindmap-definition-RKZ34NQL.B3qEA3x9.js} +1 -1
  105. package/web/_astro/{pieDiagram-4H26LBE5.BiXk1TW3.js → pieDiagram-4H26LBE5.FlWA-_ru.js} +1 -1
  106. package/web/_astro/{quadrantDiagram-W4KKPZXB.krvBH8lh.js → quadrantDiagram-W4KKPZXB.9bVn35X7.js} +1 -1
  107. package/web/_astro/{requirementDiagram-4Y6WPE33.ih1gL25c.js → requirementDiagram-4Y6WPE33.ChykIhct.js} +1 -1
  108. package/web/_astro/{sankeyDiagram-5OEKKPKP.CpUxkv4u.js → sankeyDiagram-5OEKKPKP.Z2Y3hkYb.js} +1 -1
  109. package/web/_astro/{sequenceDiagram-3UESZ5HK.clAfeN7y.js → sequenceDiagram-3UESZ5HK.J7JsHWDt.js} +1 -1
  110. package/web/_astro/{stateDiagram-AJRCARHV.DpXWb8-m.js → stateDiagram-AJRCARHV.DAPbiCSS.js} +1 -1
  111. package/web/_astro/{stateDiagram-v2-BHNVJYJU.ClZb9GIH.js → stateDiagram-v2-BHNVJYJU.D2jvSi0q.js} +1 -1
  112. package/web/_astro/{timeline-definition-PNZ67QCA.BLcX5dh7.js → timeline-definition-PNZ67QCA.psW2ci29.js} +1 -1
  113. package/web/_astro/{vennDiagram-CIIHVFJN.oGgpKMpx.js → vennDiagram-CIIHVFJN.DlILYVmg.js} +1 -1
  114. package/web/_astro/{wardleyDiagram-YWT4CUSO.BPHtMtt-.js → wardleyDiagram-YWT4CUSO.xNWwLJnn.js} +1 -1
  115. package/web/_astro/{xychartDiagram-2RQKCTM6.BNZOPYSk.js → xychartDiagram-2RQKCTM6.EU_BuH7V.js} +1 -1
  116. package/web/board-runtime.json +1 -1
  117. package/web/index.html +2 -2
  118. package/web/_astro/BoardApp.CYbe-OTb.js +0 -322
  119. package/web/_astro/channel.JuKVyuhZ.js +0 -1
  120. package/web/_astro/index.CbNz17Sx.css +0 -1
@@ -273,6 +273,7 @@ plugins/sp/
273
273
  ├── commands/ # Slash-command wrappers — the SSOT (hand-editable thin wrappers; see Commands below)
274
274
  ├── agents/ # 4 specialist subagents (expert-spur, super-coder, super-planner, super-reviewer)
275
275
  ├── hooks/ # hooks.json + task-write-guard.{ts,test.ts} + context-{session-start,post-tool,session-stop}.ts
276
+ │ # + agent-lifecycle.{ts,test.ts} + fleet-guest-stop.{ts,test.ts} (fleet member/guest hooks)
276
277
  │ # + careful-guard.{ts,test.ts} + context-hooks.test.ts + token-estimate.test.ts
277
278
  │ # + shared policy modules: agent-hint.ts, destructive-policy.{ts,test.ts}, task-file-policy.{ts,test.ts}
278
279
  │ # + pi/guard-extension.{ts,test.ts} (plugin.json `extensions.pi` entry point)
@@ -342,6 +343,7 @@ surface or run one workflow. All skills target the same five core platforms: `cl
342
343
  | `reverse-engineering` | 1.1 | Codebase analysis / HLD generation / audit — depth-driven reverse engineering with orthogonal mode, focus, and format controls; backs `/sp:dev-reverse` |
343
344
  | `issue-finding` | 2.0 | Session-log forensics — multi-source discovery, bottleneck ranking, optional topic focus, CLI-gated fix task generation; **legacy path** — superseded by `history-anatomy` for indexed reporting; backs no current command; directly invocable as sp:issue-finding |
344
345
  | `conflict-finding` | 1.0 | Authority-aware semantic audit — four-pillar (source/task/feature/authority) conflict discovery, claim-specific authority resolution, reproducible evidence, confirmed owner-routed remediation; backs `/sp:dev-find-conflict` |
346
+ | `fleet-join` | 1.0 | Guest fleet occupancy — join as a live guest occupant, loop `spur agent wait --inbox` to pull work, reply through `spur message reply`, then leave; G73 (task 1081) |
345
347
  | `next-feature` | 1.0 | Prompt-first feature frontier prioritizer — sync-first precondition, B3 actionability gate (cited, never restated), tiered rubric over measured signals, D1–D4 defect proposals conforming to the restructure map schema; backs `/sp:dev-find-next` |
346
348
  | `pr-reviewing` | 1.0 | GitHub Codex PR review — PR prepare/reuse, `@codex review` request with per-HEAD dedupe, bounded polling, findings normalization, validated fix + re-review; spine SSOT `pr-review.yaml` + `scripts/pr-reviewing.ts`; backs `/sp:dev-pr-review` |
347
349
  | `indexed-context` | 1.0 | Cross-agent project context — anatomy/learnings/pitfalls/buglog/memory in `.spur/context/`; hook-tracked token-ledger; graceful degradation on agents without hooks |
@@ -473,13 +475,16 @@ the agent provides an isolated context window.
473
475
  #### 4. Hooks (`hooks/`)
474
476
 
475
477
  Event-driven enforcement that runs automatically without user invocation. `hooks.json` registers
476
- four handlers:
478
+ six handlers across seven events:
477
479
 
478
480
  | Event | Matcher | Handler | Timeout |
479
481
  | -------------- | ------------------------------------- | ---------------------------------------------- | ------- |
480
482
  | `PreToolUse` | `Write\|Edit` | `superskill hook run sp task-write-guard` | 10s |
481
483
  | `PostToolUse` | `Bash\|Grep\|Glob\|Read\|Write\|Edit` | `superskill hook run sp context-post-tool` | 10s |
482
484
  | `SessionStart` | — | `superskill hook run sp context-session-start` | 15s |
485
+ | `SessionStart`, `UserPromptSubmit`, `Stop` | — | `superskill hook run sp agent-lifecycle` | 5s |
486
+ | `Notification` | `permission_prompt` | `superskill hook run sp agent-lifecycle` | 5s |
487
+ | `Stop` | — | `superskill hook run sp fleet-guest-stop` | 10s |
483
488
  | `SessionEnd` | — | `superskill hook run sp context-session-stop` | 15s |
484
489
 
485
490
  **Write guard.** The `PreToolUse` hook fires on every `Write`/`Edit` tool call and checks whether
@@ -493,6 +498,11 @@ estimates the token cost of each matched tool call and appends it to `.spur/cont
493
498
  (with redaction of sensitive argument fields), `context-session-start` seeds the indexed-context
494
499
  hint on first launch, and `context-session-stop` closes out the session record.
495
500
 
501
+ **Fleet hooks (1080/1081).** `agent-lifecycle` reports member state in the background via
502
+ `spur agent report --state working|idle|blocked` (a permission prompt is `blocked`) — only when
503
+ `SPUR_SPEC_ID` is set, so outside a fleet it makes no call. `fleet-guest-stop` delivers pending
504
+ inbox work to a session joined as a fleet guest at turn end; any other session passes through.
505
+
496
506
  **Available but unwired:** `careful-guard.ts` ships in `hooks/` (with tests) as an opt-in
497
507
  `PreToolUse` guard that asks before destructive shell commands (`rm -rf`, `DROP TABLE`,
498
508
  `git push --force`, …) — it is **not** registered in `hooks.json`. Fail-open by contract; escape
@@ -511,6 +521,7 @@ hold `SKILL.md` and prompt-side companions only.
511
521
  | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
512
522
  | `hooks/task-write-guard.ts` | Compatibility shim for older installs that still execute the script path directly. Forwards stdin to the stable PATH command `superskill hook run sp task-write-guard`, mirrors parseable PreToolUse decisions, and fails open if the runtime is unavailable. Performs no source-tree CLI lookup. |
513
523
  | `hooks/context-*.ts` | Runtime for the three registered context hooks (session-start, post-tool, session-stop) — token-cost estimation + ledger append for `sp:indexed-context` |
524
+ | `hooks/{agent-lifecycle,fleet-guest-stop}.ts` | Fleet hooks (1080/1081): member lifecycle reports via `spur agent report`; turn-end inbox delivery to joined guests |
514
525
  | `hooks/careful-guard.ts` | Opt-in destructive-command guard (unwired — see Hooks above) |
515
526
  | `scripts/batch-preflight.ts` | Pure TABLE A STOP evaluation for `super-planner` — skip doomed pipeline launches without spawning a Skill subprocess; recovery hints map stuck statuses to a single `/sp:dev-*` hop |
516
527
  | `scripts/workflow-step-profile.ts` | Thin glue over the generated lib bundle (`lib/step-profile.generated.mjs` from `packages/app/src/workflow/step-profile.ts`) — read-only step profile from `spur workflow trace`: per node and action-kind durations, idle gaps, session mode and `cacheHit` coverage, plus the ADR-115 cache-window flags `sp:spur-doctor` turns into workflow-optimization proposals |
@@ -518,6 +529,8 @@ hold `SKILL.md` and prompt-side companions only.
518
529
  | `scripts/dogfood-testing/validate-report.ts` | Pure `validateReport(md)` — footer-mandatory + 7-check finalize-or-abort contract with stable error codes |
519
530
  | `scripts/daily-summary/{daily-summary,logger}.ts` | ccusage + git-history orchestration helpers for `sp:daily-summary` |
520
531
  | `scripts/task-diffstat.ts` | 0943 triage-lane diffstat producer — numstat + untracked scan vs the run base; writes `<wbs>-diffstat.json` with the sensitive-path flag; fails safe (sensitive) |
532
+ | `scripts/run-summary.ts` | Measured execution summary `/sp:dev-run` / `/sp:dev-runall` print by default (`--no-summary` skips) — windows the host transcript by `spur workflow progress` attempt timestamps; per stage/run time, wait, tool calls, total / non-cached tokens; `n/a` for subprocess windows |
533
+ | `scripts/session-timeline.ts` | Measured Time/Wait/Token evidence for `sp:session-review` — reads the active Claude Code transcript read-only, one segment per operator prompt, usage deduplicated by message id, tokens split total / non-cached; `{available:false}` exit 0 when no transcript |
521
534
  | `scripts/commands/validate-flag-contracts.ts` | Mechanical consistency gate — compares flag claims across command files, flag-glossary, cross-cutting, dev-operations, and ADR; reports disagreements |
522
535
  | `*.test.ts` | Unit suites — in `hooks/` for guards, in `tests/<skill>/` per ADR-031 pairing |
523
536
 
@@ -165,6 +165,16 @@ fails it. One explicit mapping, no transcription step anywhere downstream:
165
165
  Severity words stay visible in the same cell — the mapping adds the machine-readable label, it does
166
166
  not replace the semantics.
167
167
 
168
+ **Disposition column (1089, E71).** The findings table carries a `Disposition` cell. It is not
169
+ cosmetic: the record stage's residual sweep reads these rows (`parseReviewFindings` in
170
+ `packages/app/src/services/residual-scan.ts`), and a `P1`–`P3` row with no disposition classifies as
171
+ **blocking**, which downgrades a PASS verdict to PARTIAL and closes the `done` gate. A finding you
172
+ resolved inside the same task therefore says so in that cell — `RESOLVED`/`FIXED`/`DONE` (optionally
173
+ with the commit) drops the row from the sweep; `DEFER(<reason>)` reclassifies a P3 as deferrable
174
+ (never P1/P2). Prose such as "Closed." inside the *Finding* cell does not count — the sweep reads
175
+ the cell, not the sentence. `OPEN` states "still live", and `ACCEPTED` marks a non-blocking P4
176
+ observation you are deliberately not acting on; both behave as an ordinary open row.
177
+
168
178
  **Section-relative headings.** In WBS mode (standalone or pipeline) the report body is written
169
179
  *into* the task's `### Review` section, so every heading inside it MUST be `####` or deeper. A
170
180
  `##`/`###` heading in the body becomes a new top-level task section and corrupts the document. With
@@ -173,19 +183,17 @@ shallower.
173
183
 
174
184
  ```markdown
175
185
  #### Review Report — <wbs|path>
176
-
177
186
  **Scope:** <wbs diff | path glob>
178
187
  **Dimensions:** functional, security, efficiency, correctness, usability, architecture
179
188
  **Verdict:** PASS | PARTIAL | FAIL
180
-
181
189
  ##### Findings (ranked)
182
190
 
183
- | # | Priority | Dimension | Finding | Location |
184
- |---|----------|-----------|---------|----------|
185
- | 1 | P1 (blocker) | security | SQL injection in query builder | `src/api/users.ts:42` |
186
- | 2 | P2 (major) | architecture | Shallow pass-through UserService | `src/services/users.ts:15` |
187
- | 3 | P3 (minor) | correctness | Missing error branch in createUser | `src/api/users.ts:48` |
188
- | 4 | P4 (advisory) | usability | `createUser` error text omits the field name | `src/api/users.ts:51` |
191
+ | # | Priority | Dimension | Finding | Location | Disposition |
192
+ |---|----------|-----------|---------|----------|-------------|
193
+ | 1 | P1 (blocker) | security | SQL injection in query builder | `src/api/users.ts:42` | OPEN |
194
+ | 2 | P2 (major) | architecture | Shallow pass-through UserService | `src/services/users.ts:15` | OPEN |
195
+ | 3 | P3 (minor) | correctness | Missing error branch in createUser | `src/api/users.ts:48` | RESOLVED (fixed in <sha>) |
196
+ | 4 | P4 (advisory) | usability | `createUser` error text omits the field name | `src/api/users.ts:51` | ACCEPTED |
189
197
 
190
198
  ##### Functional Traceability
191
199
 
@@ -197,14 +205,20 @@ shallower.
197
205
  **Next:** <one-line action>
198
206
  ```
199
207
 
208
+ The **Verdict line is machine-read** by the task pipeline (session finding after 1088): `review →
209
+ verify|approve` requires a line matching `Verdict: PASS` (plain or bold), and ANYTHING ELSE —
210
+ FAIL, PARTIAL, a missing line, a line split across two lines — routes the run into
211
+ `review-fail-triage` and its bounded repair hop. Keep it on one line, in that shape, in every
212
+ review answer; never restate it as prose only.
213
+
200
214
  **No findings.** Never invent a defect to populate the table. Emit one substantive `P4 (advisory)`
201
215
  row that states what was reviewed and what was found — placeholder cells (empty, `—`, `n/a`) are
202
216
  rejected by the checker, and so they should be:
203
217
 
204
218
  ```markdown
205
- | # | Priority | Dimension | Finding | Location |
206
- |---|----------|-----------|---------|----------|
207
- | 1 | P4 (advisory) | — | No P1–P3 findings: 6 changed files reviewed across all six dimensions; R1–R3 traceable to tests | `packages/app/src/workflow/proof-input-fingerprint.ts:102-160` |
219
+ | # | Priority | Dimension | Finding | Location | Disposition |
220
+ |---|----------|-----------|---------|----------|-------------|
221
+ | 1 | P4 (advisory) | — | No P1–P3 findings: 6 changed files reviewed across all six dimensions; R1–R3 traceable to tests | `packages/app/src/workflow/proof-input-fingerprint.ts:102-160` | ACCEPTED |
208
222
  ```
209
223
 
210
224
  ## Out of scope
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  description: Generate changelog from git commits
3
3
  role: scribe
4
- argument-hint: "[--since <tag|commit>] [--until <tag|commit>] [--version <version>]"
5
- allowed-tools: ["Bash", "Read"]
4
+ argument-hint: "[<file>] [--since <tag|commit>] [--until <tag|commit>] [--version <version>]"
5
+ allowed-tools: ["Bash", "Read", "Edit"]
6
6
  ---
7
7
 
8
8
  # Dev Changelog
@@ -13,6 +13,7 @@ Implements an inline procedure — see [dev-operations.md](../skills/spur-dev/re
13
13
 
14
14
  | Flag | Description | Default |
15
15
  | --- | --- | --- |
16
+ | `<file>` | Optional positional target (e.g. `CHANGELOG.md`); the new section is inserted above its newest release. | stdout |
16
17
  | `--since` `<tag\|commit>` | Start of the commit range. | latest tag |
17
18
  | `--until` `<tag\|commit>` | End of the commit range. | HEAD |
18
19
  | `--version` `<version>` | Override the detected release version. | detected |
@@ -21,9 +22,9 @@ For shared semantics, see the [flag glossary](../skills/spur-dev/references/flag
21
22
 
22
23
  ## Usage
23
24
 
24
- /sp:dev-changelog [--since <tag|commit>] [--until <tag|commit>] [--version <version>]
25
+ /sp:dev-changelog [<file>] [--since <tag|commit>] [--until <tag|commit>] [--version <version>]
25
26
 
26
27
  ## Implementation
27
28
 
28
- Follow the inline procedure in [dev-operations.md](../skills/spur-dev/references/dev-operations.md#8-changelog) (changelog).
29
+ Follow the inline procedure in [dev-operations.md](../skills/spur-dev/references/dev-operations.md#8-changelog) (changelog). This is a docs-only generator: run no quality gates (lint, tests, `spur-check`, rules, citation checks).
29
30
 
@@ -1,7 +1,7 @@
1
1
  ---
2
- description: "Review the active coding-agent session immediately: summarize outcomes, distinguish resolved and open issues with evidence, and propose bounded improvements. With --triage: apply pure-doc / one-to-two-line fixes inline, then file remaining findings as one or more implement-ready tasks. Triggers: review this session, session wrap-up, immediate retrospective, what happened, what was resolved, triage findings"
2
+ description: "Review the active coding-agent session immediately: summarize outcomes, distinguish resolved and open issues with evidence, and propose bounded improvements. With --fix auto (alias --triage): apply pure-doc / one-to-two-line fixes inline, then file remaining findings as one or more implement-ready tasks. With --fix all: fix every actionable finding inline and file no task. Triggers: review this session, session wrap-up, immediate retrospective, what happened, what was resolved, triage findings"
3
3
  role: reviewer
4
- argument-hint: "[<focus>] [--triage]"
4
+ argument-hint: "[<focus>] [--fix <none|auto|all>] [--triage]"
5
5
  allowed-tools: ["Bash", "Read", "Grep", "Glob", "Skill"]
6
6
  ---
7
7
 
@@ -10,17 +10,21 @@ allowed-tools: ["Bash", "Read", "Grep", "Glob", "Skill"]
10
10
  Wraps the **sp:session-review** skill for a lightweight review of the active host session. By
11
11
  default it is report-only: current conversation plus read-only repository evidence, run inline so
12
12
  the session context is preserved — no workflow launch, history import, task creation, or
13
- remediation. With `--triage`, it first triages the findings, then applies direct fixes (pure
13
+ remediation. With `--fix auto`, it first triages the findings, then applies direct fixes (pure
14
14
  documentation work and one-to-two-line fixes) inline and files everything remaining as one or more
15
- implement-ready tasks for further fixing. The result includes a non-overlapping time breakdown with durations in
16
- `M:SS` or `H:MM:SS` form and `n/a` for unavailable measurements.
15
+ implement-ready tasks for further fixing; `--triage` is an alias of `--fix auto`. With `--fix all`,
16
+ it defers nothing: every actionable finding is fixed inline and no task is filed. The result includes a non-overlapping time breakdown —
17
+ work time, operator wait, tool calls and tokens (total / non-cached) per stage, measured from the host transcript by
18
+ `session-timeline.mjs` — with durations in `M:SS` or `H:MM:SS` form and `n/a` only when that
19
+ measurement is unavailable.
17
20
 
18
21
  ## Argument Flags
19
22
 
20
23
  | Flag | Description | Default |
21
24
  | --- | --- | --- |
22
25
  | `[<focus>]` | Optional question or operation to emphasize without excluding material session outcomes. | full active session |
23
- | `--triage` | After the report: bucket findings → apply pure-doc / 1–2-line fixes inline → file the remainder as one or more implement-ready tasks. | off (report-only) |
26
+ | `--fix` `<none\|auto\|all>` | Remediation after the report. `none`: report-only. `auto`: bucket findings → apply pure-doc / 1–2-line fixes inline → file the remainder as one or more implement-ready tasks. `all`: fix every actionable finding inline, file no task. | none |
27
+ | `--triage` | Alias of `--fix auto`; an explicit `--fix` wins when both are passed. | off |
24
28
 
25
29
  For shared semantics, see the [flag glossary](../skills/spur-dev/references/flag-glossary.md).
26
30
 
@@ -29,7 +33,8 @@ For shared semantics, see the [flag glossary](../skills/spur-dev/references/flag
29
33
  ```
30
34
  /sp:dev-review-session
31
35
  /sp:dev-review-session "why the verification loop repeated"
32
- /sp:dev-review-session --triage
36
+ /sp:dev-review-session --fix auto # same as --triage
37
+ /sp:dev-review-session --fix all
33
38
  /sp:dev-review-session "F95 findings" --triage
34
39
  ```
35
40
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  description: Run a task — full pipeline (precheck→implement→test→review→approve→verify→record→done) or single-step (implement)
3
3
  role: coder
4
- argument-hint: "<wbs> [--mode <full|implement>] [--agent <inline|auto|name>] [--auto] [--next] [--wrap] [--continue] [--worktree [<name>]]"
4
+ argument-hint: "<wbs> [--mode <full|implement>] [--agent <inline|auto|name>] [--auto] [--next] [--wrap] [--continue] [--worktree [<name>]] [--no-summary]"
5
5
  allowed-tools: ["Bash", "Read", "Write", "Edit", "Skill"]
6
6
  ---
7
7
 
@@ -21,12 +21,13 @@ Wraps the **sp:spur-dev** and **sp:code-implementation** skills.
21
21
  | `--wrap` | Run the wrap hop after the main step. The `--agent` selector is preserved into the `/sp:dev-wrap <wbs>` handoff when supplied; omission remains omission. The wrap hop is workflow-backed and reports its trigger-3 subprocess override. | off |
22
22
  | `--continue` | Resume an interrupted task from its checkpoint. | off |
23
23
  | `--worktree` `[<name>]` | Run the task pipeline in an isolated git worktree; FF-merge on success, retain on failure. Bare `--worktree` creates a fresh tree; `--worktree <name>` adopts an existing worktree by name/path/branch. Full mode only. | off |
24
+ | `--no-summary` | Skip the measured execution summary (per-stage time, wait, tool calls, total / non-cached tokens) printed after a full-mode run. | off (summary printed) |
24
25
 
25
26
  For shared semantics, see the [flag glossary](../skills/spur-dev/references/flag-glossary.md).
26
27
 
27
28
  ## Usage
28
29
 
29
- /sp:dev-run <wbs> [--mode <full|implement>] [--agent <inline|auto|name>] [--auto] [--next] [--wrap] [--continue] [--worktree [<name>]]
30
+ /sp:dev-run <wbs> [--mode <full|implement>] [--agent <inline|auto|name>] [--auto] [--next] [--wrap] [--continue] [--worktree [<name>]] [--no-summary]
30
31
 
31
32
  ## Implementation
32
33
 
@@ -37,6 +38,8 @@ For shared semantics, see the [flag glossary](../skills/spur-dev/references/flag
37
38
  `Skill(skill="sp:spur-dev", args="run-inline $ARGUMENTS")`; `--agent auto` or a named executor
38
39
  uses `Skill(skill="sp:spur-dev", args="run $ARGUMENTS")` and the workflow subprocess path.
39
40
  - Implement step only (`--mode implement`): `Skill(skill="sp:code-implementation", args="$ARGUMENTS")`
41
+ - After a full-mode run settles (done, failed or paused), print the
42
+ [execution summary](../skills/spur-dev/references/dev-operations.md#execution-summary) unless `--no-summary`.
40
43
 
41
44
  **Flags:**
42
45
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  description: Run a batch of tasks through their pipelines in dependency-correct order — resolve a set, topo-sort, run each via task-pipeline.yaml, emit a batch report
3
3
  role: planner
4
- argument-hint: "--tasks <selector> [--feature <id>] [--mode <sequential|parallel>] [--keep-going] [--auto] [--agent <inline|auto|name>] [--json] [--wrap] [--next] [--continue] [--worktree [<name>]]"
4
+ argument-hint: "--tasks <selector> [--feature <id>] [--mode <sequential|parallel>] [--keep-going] [--auto] [--agent <inline|auto|name>] [--json] [--wrap] [--next] [--continue] [--worktree [<name>]] [--no-summary]"
5
5
  allowed-tools: ["Bash", "Read", "Skill"]
6
6
  ---
7
7
 
@@ -24,6 +24,7 @@ Wraps the **sp:spur-dev** skill.
24
24
  | `--next` | Chain-to-completion via the next-router. | off |
25
25
  | `--continue` | Resume an interrupted batch. | off |
26
26
  | `--worktree` `[<name>]` | Run the batch in an isolated git worktree; FF-merge on success, retain on failure. Bare `--worktree` creates a fresh tree; `--worktree <name>` adopts an existing worktree by name/path/branch. | off |
27
+ | `--no-summary` | Skip the measured execution summary (one row per task run: time, wait, tool calls, total / non-cached tokens) printed after the batch report. | off (summary printed) |
27
28
  | `--concurrency` `<n>` | Parallel-mode worker bound: at most `<n>` task pipelines run at once (task 0931); a dependent starts only after its in-set dependencies are integrated onto the base ref. No-op in sequential mode (ignored). | 2 |
28
29
 
29
30
  For shared semantics, see the [flag glossary](../skills/spur-dev/references/flag-glossary.md).
@@ -31,7 +32,7 @@ For shared semantics, see the [flag glossary](../skills/spur-dev/references/flag
31
32
  ## Usage
32
33
 
33
34
  ```
34
- /sp:dev-runall --tasks <selector> [--feature <id>] [--mode <sequential|parallel>] [--keep-going] [--auto] [--agent <inline|auto|name>] [--json] [--wrap] [--next] [--continue] [--worktree [<name>]]
35
+ /sp:dev-runall --tasks <selector> [--feature <id>] [--mode <sequential|parallel>] [--keep-going] [--auto] [--agent <inline|auto|name>] [--json] [--wrap] [--next] [--continue] [--worktree [<name>]] [--no-summary]
35
36
  ```
36
37
 
37
38
  Flags: `--tasks <selector>` (required — explicit WBS list, status pseudo-list, `feature:<id>`,
@@ -91,3 +92,5 @@ full distinction.
91
92
  - Apply the [shared startup contract](../skills/spur-dev/references/cross-cutting.md#shared-startup-contract-task-0814-r1r3r4r6r7r8) — bootstrap checklist, quick readiness before isolation, workflow inventory before YAML, comprehensive checks at owning boundaries (task 0814 R1/R3/R4/R7/R8).
92
93
  - Interactive sequential omit/inline: `Skill(skill="sp:spur-dev", args="runall-inline $ARGUMENTS")`.
93
94
  - Explicit executor or parallel mode: `Skill(skill="sp:spur-dev", args="runall $ARGUMENTS")` → `sp:super-planner` agent.
95
+ - After the batch report, print the [execution summary](../skills/spur-dev/references/dev-operations.md#execution-summary) over every attempted run unless
96
+ `--no-summary`.
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * agent-lifecycle — report this member's lifecycle state from host hooks (G73 R4, task 1080).
5
+ *
6
+ * A fleet member that blocks on a human (a permission prompt) looks exactly like an
7
+ * idle one to the strategy, which then dispatches work at a member that cannot run it.
8
+ * The host's own hook events say otherwise, so this hook maps them to a
9
+ * `spur agent report --state … --seq …` call.
10
+ *
11
+ * **Fail-open contract (R4):** outside a fleet (`SPUR_SPEC_ID` unset) the hook exits
12
+ * without any CLI call, every error path exits 0, and the report runs in a detached,
13
+ * unref'd child — the agent never waits on it.
14
+ *
15
+ * Event → state:
16
+ * SessionStart → idle (docs.claude.com/en/docs/claude-code/hooks)
17
+ * UserPromptSubmit → working
18
+ * Notification → blocked (matcher `permission_prompt`: the host fires it once a
19
+ * tool-use/sandbox approval prompt has waited ~6 s)
20
+ * Stop → idle
21
+ * agent_start → working (Pi, ADR-129 — same core from the extension)
22
+ * agent_settled → idle
23
+ *
24
+ * Self-contained by design: hooks are bundled standalone at install time, so this file
25
+ * imports only `node:*` builtins and relative paths (plugin-standalone contract).
26
+ */
27
+
28
+ import { spawn } from 'node:child_process';
29
+ import { getEnvVars } from '../lib/env';
30
+
31
+ /** Lifecycle states `spur agent report` accepts — mirrors MEMBER_LIFECYCLE_STATES in the domain. */
32
+ export type MemberLifecycleState = 'working' | 'idle' | 'blocked';
33
+
34
+ /** The env var that marks a run as a fleet member (stamped by the supervisor and AgentService). */
35
+ export const SPEC_ID_ENV = 'SPUR_SPEC_ID';
36
+
37
+ /** The host hook payload fields this hook reads. */
38
+ export interface LifecycleHookPayload {
39
+ hook_event_name?: unknown;
40
+ notification_type?: unknown;
41
+ }
42
+
43
+ /**
44
+ * Map one host event to the state to report; `null` means this event reports nothing.
45
+ *
46
+ * The Notification type check is a second filter, not the primary one: the host matches
47
+ * `matcher: "permission_prompt"` before the hook runs, so a host that omits the field still
48
+ * reports `blocked`. It exists so a widened or mis-abbreviated matcher cannot report a
49
+ * member blocked for an unrelated alert (idle/auth/elicitation prompts are not work blockers).
50
+ */
51
+ export function lifecycleStateFor(
52
+ eventName: unknown,
53
+ notificationType: unknown = undefined,
54
+ ): MemberLifecycleState | null {
55
+ switch (eventName) {
56
+ case 'SessionStart':
57
+ return 'idle';
58
+ case 'UserPromptSubmit':
59
+ return 'working';
60
+ case 'Notification':
61
+ return notificationType === undefined || notificationType === 'permission_prompt' ? 'blocked' : null;
62
+ case 'Stop':
63
+ return 'idle';
64
+ // Pi event names (ADR-129) — the extension calls this same core.
65
+ case 'agent_start':
66
+ return 'working';
67
+ case 'agent_settled':
68
+ return 'idle';
69
+ default:
70
+ return null;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Monotonic-enough report sequence (G73 Q&A, CLOSED): wall-clock nanoseconds plus the
76
+ * sub-millisecond part of the monotonic clock. Ties lose under the strict `>` rule, which
77
+ * is the intended stale-drop behavior.
78
+ */
79
+ export function reportSeq(now: number = Date.now(), hrtimeNs: bigint = process.hrtime.bigint()): string {
80
+ return (BigInt(now) * 1_000_000n + (hrtimeNs % 1_000_000n)).toString();
81
+ }
82
+
83
+ /** Dispatch seam — tests inject a recorder instead of spawning. */
84
+ export type LifecycleSpawn = (argv: readonly string[]) => void;
85
+
86
+ /** The one child we ever spawn: detached + unref'd + silent, so the agent is never delayed. */
87
+ const spawnDetached: LifecycleSpawn = (argv) => {
88
+ const [command, ...args] = argv;
89
+ if (command === undefined) return;
90
+ const child = spawn(command, args, { stdio: 'ignore', detached: true });
91
+ child.unref();
92
+ };
93
+
94
+ /** Injectable seams for tests. */
95
+ export interface ReportDeps {
96
+ spawnDetached?: LifecycleSpawn;
97
+ /** Environment to read `SPUR_SPEC_ID` from; defaults to the live env. */
98
+ env?: Record<string, string | undefined>;
99
+ /** Fixed sequence for a deterministic assertion. */
100
+ seq?: string;
101
+ }
102
+
103
+ /** Outcome of one hook invocation — returned for tests, ignored by the entrypoint. */
104
+ export interface ReportOutcome {
105
+ reported: boolean;
106
+ state?: MemberLifecycleState;
107
+ reason?: string;
108
+ }
109
+
110
+ /**
111
+ * Report the payload's lifecycle state to the fleet, or explain why nothing was reported.
112
+ * Never throws: a hook that fails must not fail the agent's turn.
113
+ */
114
+ export function reportLifecycleHook(payload: LifecycleHookPayload, deps: ReportDeps = {}): ReportOutcome {
115
+ const env = deps.env ?? getEnvVars();
116
+ const specId = env[SPEC_ID_ENV];
117
+ if (typeof specId !== 'string' || specId === '') {
118
+ return { reported: false, reason: 'outside a fleet (no SPUR_SPEC_ID)' };
119
+ }
120
+ const state = lifecycleStateFor(payload.hook_event_name, payload.notification_type);
121
+ if (state === null) {
122
+ return { reported: false, reason: 'event reports no lifecycle state' };
123
+ }
124
+ const seq = deps.seq ?? reportSeq();
125
+ try {
126
+ (deps.spawnDetached ?? spawnDetached)(['spur', 'agent', 'report', '--state', state, '--seq', seq]);
127
+ } catch (error) {
128
+ return { reported: false, state, reason: error instanceof Error ? error.message : 'spawn failed' };
129
+ }
130
+ return { reported: true, state };
131
+ }
132
+
133
+ /**
134
+ * Parse a hook stdin payload. An empty payload is an empty object; malformed JSON throws
135
+ * and is caught by {@link runHookFromStdin} — the fail-open boundary, not this parser.
136
+ */
137
+ export function parseHookPayload(raw: string): LifecycleHookPayload {
138
+ if (raw.trim() === '') return {};
139
+ return JSON.parse(raw) as LifecycleHookPayload;
140
+ }
141
+
142
+ /** One hook invocation from raw stdin — the whole entrypoint behavior, minus the exit. */
143
+ export function runHookFromStdin(raw: string, deps: ReportDeps = {}): ReportOutcome {
144
+ try {
145
+ return reportLifecycleHook(parseHookPayload(raw), deps);
146
+ } catch {
147
+ return { reported: false, reason: 'fail-open: unreadable payload' };
148
+ }
149
+ }
150
+
151
+ // Entrypoint — minimal so unit coverage focuses on the pure helpers above.
152
+ if (import.meta.main) {
153
+ runHookFromStdin(await Bun.stdin.text());
154
+ process.exit(0);
155
+ }
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * fleet-guest-stop — deliver pending fleet inbox work to a joined guest session at turn end
5
+ * (G73 R4, task 1081).
6
+ *
7
+ * A guest pulls its own work; this hook is the accelerant that makes the pull arrive at a turn
8
+ * boundary instead of only when the `fleet-join` skill's `wait --inbox` loop wakes. It is
9
+ * deliberately conservative:
10
+ *
11
+ * - **Only a joined session blocks.** The hook matches the hook payload's `session_id` against
12
+ * `.spur/run/guests/*.json`; no match ⇒ exit 0 with no output (the agent finishes normally).
13
+ * - **Never loops.** A host that reports `stop_hook_active` (Codex) is already continuing → exit 0.
14
+ * - **Fail-open.** Every error path exits 0 silently; a hook must never fail the agent's turn.
15
+ * - **Bounded.** The pending check is `spur agent wait --inbox <id> --timeout 2000`, which also
16
+ * heartbeats the guest lease (R5), and the reason names at most the first few messages.
17
+ *
18
+ * Block contract (both hosts that support it): exit 0 with JSON on stdout —
19
+ * `{"decision":"block","reason":"<non-empty>"}`. Claude Code uses the reason as the stop reason;
20
+ * Codex turns it into the next user prompt. Hosts that drop the Stop event (`codex exec` on
21
+ * 0.160.0 — see `docs/plans/2026-10-05-codex-guest-spike.md`) simply never call it.
22
+ *
23
+ * Self-contained by design: hooks are bundled standalone at install time, so this file imports
24
+ * only `node:*` builtins and relative paths (plugin-standalone contract).
25
+ */
26
+
27
+ import { spawnSync } from 'node:child_process';
28
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
29
+ import { join } from 'node:path';
30
+ import { getEnvVar } from '../lib/env';
31
+
32
+ /** Budget for the pending-work probe; the hook must never noticeably slow a turn. */
33
+ export const GUEST_STOP_PROBE_MS = 2000;
34
+
35
+ /** How many pending messages the delivered reason names. */
36
+ export const GUEST_STOP_MAX_MESSAGES = 5;
37
+
38
+ /** Characters of each message body the reason carries. */
39
+ export const GUEST_STOP_BODY_CHARS = 140;
40
+
41
+ /** Hard per-invocation timeout for a `spur` child the hook shells out to. */
42
+ export const GUEST_STOP_CLI_TIMEOUT_MS = 5000;
43
+
44
+ /** The hook payload fields this hook reads (Claude Code and Codex both send `session_id`). */
45
+ export interface GuestStopPayload {
46
+ session_id?: unknown;
47
+ stop_hook_active?: unknown;
48
+ }
49
+
50
+ /** The guest record fields the hook needs. */
51
+ export interface GuestStopRecord {
52
+ id: string;
53
+ sessionId?: string;
54
+ }
55
+
56
+ /** Injection seam for tests: run a `spur` invocation, returning exit code and stdout. */
57
+ export type GuestStopCli = (args: readonly string[]) => { status: number | null; stdout: string };
58
+
59
+ const defaultCli: GuestStopCli = (args) => {
60
+ const result = spawnSync('spur', [...args], { encoding: 'utf-8', timeout: GUEST_STOP_CLI_TIMEOUT_MS });
61
+ return { status: result.status, stdout: result.stdout ?? '' };
62
+ };
63
+
64
+ /**
65
+ * Guest records under `<cwd>/.spur/run/guests/`. Unreadable or malformed records are skipped —
66
+ * a broken record must not block a session's turn.
67
+ */
68
+ export function readGuestRecords(cwd: string): GuestStopRecord[] {
69
+ const dir = join(cwd, '.spur', 'run', 'guests');
70
+ if (!existsSync(dir)) return [];
71
+ const records: GuestStopRecord[] = [];
72
+ for (const name of readdirSync(dir)) {
73
+ if (!name.endsWith('.json')) continue;
74
+ try {
75
+ const parsed = JSON.parse(readFileSync(join(dir, name), 'utf-8')) as Record<string, unknown>;
76
+ if (typeof parsed.id !== 'string') continue;
77
+ records.push({
78
+ id: parsed.id,
79
+ ...(typeof parsed.sessionId === 'string' ? { sessionId: parsed.sessionId } : {}),
80
+ });
81
+ } catch {
82
+ // Skip the unreadable record.
83
+ }
84
+ }
85
+ return records;
86
+ }
87
+
88
+ /** The guest joined by this host session, or null. */
89
+ export function guestForSession(records: readonly GuestStopRecord[], sessionId: unknown): GuestStopRecord | null {
90
+ if (typeof sessionId !== 'string' || sessionId === '') return null;
91
+ return records.find((record) => record.sessionId === sessionId) ?? null;
92
+ }
93
+
94
+ /**
95
+ * Build the Stop decision for a pending message list. Returns the JSON line to print, or null
96
+ * when there is nothing to deliver (no messages / unparseable output).
97
+ */
98
+ export function stopDecisionFor(
99
+ guestId: string,
100
+ inboxJson: string,
101
+ maxMessages: number = GUEST_STOP_MAX_MESSAGES,
102
+ ): string | null {
103
+ let rows: unknown;
104
+ try {
105
+ rows = JSON.parse(inboxJson);
106
+ } catch {
107
+ return null;
108
+ }
109
+ const list = Array.isArray(rows) ? rows : undefined;
110
+ if (list === undefined) return null;
111
+ const pending = list
112
+ .map((row) => row as { from_id?: unknown; fromId?: unknown; body?: unknown; status?: unknown })
113
+ .filter((row) => row.body !== undefined && (row.status === undefined || row.status === 'queued'))
114
+ .slice(0, maxMessages);
115
+ if (pending.length === 0) return null;
116
+ const lines = pending.map((row) => {
117
+ const from =
118
+ typeof row.from_id === 'string' ? row.from_id : typeof row.fromId === 'string' ? row.fromId : 'fleet';
119
+ const body = String(row.body ?? '')
120
+ .replace(/\s+/g, ' ')
121
+ .trim();
122
+ return `- ${from}: ${body.length > GUEST_STOP_BODY_CHARS ? `${body.slice(0, GUEST_STOP_BODY_CHARS)}…` : body}`;
123
+ });
124
+ const reason = `Pending fleet messages for ${guestId}:\n${lines.join('\n')}\n\nHandle them, then \`spur message reply <msgId> …\` and continue.`;
125
+ return JSON.stringify({ decision: 'block', reason });
126
+ }
127
+
128
+ /** Injectable seams for the hook entrypoint. */
129
+ export interface GuestStopDeps {
130
+ cwd?: string;
131
+ cli?: GuestStopCli;
132
+ probeMs?: number;
133
+ maxMessages?: number;
134
+ }
135
+
136
+ /**
137
+ * One hook invocation: return the stdout line to print, or null for a silent exit 0.
138
+ * Never throws.
139
+ */
140
+ export function runFleetGuestStop(payload: GuestStopPayload, deps: GuestStopDeps = {}): string | null {
141
+ try {
142
+ // A host that already continued this turn must not continue it again.
143
+ if (payload.stop_hook_active === true) return null;
144
+ const cwd = deps.cwd ?? getEnvVar('CLAUDE_PROJECT_DIR') ?? process.cwd();
145
+ const guest = guestForSession(readGuestRecords(cwd), payload.session_id);
146
+ if (guest === null) return null;
147
+ const cli = deps.cli ?? defaultCli;
148
+ // `wait --inbox` returns 0 only when queued work exists, and it heartbeats the lease.
149
+ const probe = cli([
150
+ 'agent',
151
+ 'wait',
152
+ '--inbox',
153
+ guest.id,
154
+ '--timeout',
155
+ String(deps.probeMs ?? GUEST_STOP_PROBE_MS),
156
+ ]);
157
+ if (probe.status !== 0) return null;
158
+ const inbox = cli(['message', 'inbox', '--agent', guest.id, '--json']);
159
+ if (inbox.status !== 0) return null;
160
+ return stopDecisionFor(guest.id, inbox.stdout, deps.maxMessages);
161
+ } catch {
162
+ return null;
163
+ }
164
+ }
165
+
166
+ // Entrypoint — minimal so unit coverage focuses on the pure helpers above.
167
+ export function runStopFromStdin(raw: string, deps: GuestStopDeps = {}): string | null {
168
+ try {
169
+ const payload = raw.trim() === '' ? {} : (JSON.parse(raw) as GuestStopPayload);
170
+ return runFleetGuestStop(payload, deps);
171
+ } catch {
172
+ return null;
173
+ }
174
+ }
175
+
176
+ if (import.meta.main) {
177
+ const line = runStopFromStdin(await Bun.stdin.text());
178
+ if (line !== null) process.stdout.write(`${line}\n`);
179
+ process.exit(0);
180
+ }