@gobing-ai/spur 0.3.99 → 0.4.1

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 (172) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/config.global.yaml +17 -0
  3. package/config/decisions/gates.yaml +36 -0
  4. package/config/decisions/history-anatomy.yaml +35 -0
  5. package/config/decisions/idea-pipeline.yaml +38 -0
  6. package/config/decisions/task-pipeline.yaml +55 -0
  7. package/config/plugin-scripts.json +25 -0
  8. package/config/rules/boundary/sp-runtime-path.yaml +2 -0
  9. package/config/rules/docs/sourced-api-claims.yaml +60 -0
  10. package/config/rules/quality/coverage-gate.yaml +5 -3
  11. package/config/rules/recommended-pre-check.yaml +8 -3
  12. package/config/rules/strict/runtime-boundaries.yaml +11 -0
  13. package/config/rules/structure/protected-files.yaml +3 -0
  14. package/config/rules/structure/scratch-evidence-pointer.yaml +35 -0
  15. package/config/script-placement-baseline.json +2 -2
  16. package/config/workflows/feature-lifecycle.yaml +9 -2
  17. package/config/workflows/history-anatomy.yaml +22 -0
  18. package/config/workflows/idea-pipeline.yaml +75 -0
  19. package/config/workflows/task-pipeline.yaml +368 -49
  20. package/config/workflows/wrapup-pipeline.yaml +106 -13
  21. package/package.json +9 -9
  22. package/plugins/sp/README.md +25 -2
  23. package/plugins/sp/agents/super-planner.md +11 -0
  24. package/plugins/sp/agents/super-reviewer.md +39 -11
  25. package/plugins/sp/bunfig.toml +8 -0
  26. package/plugins/sp/commands/dev-brainstorm.md +29 -16
  27. package/plugins/sp/commands/dev-changelog.md +5 -4
  28. package/plugins/sp/commands/dev-find-way.md +80 -0
  29. package/plugins/sp/commands/dev-idea.md +6 -0
  30. package/plugins/sp/commands/dev-parallel.md +8 -2
  31. package/plugins/sp/commands/dev-plan.md +6 -0
  32. package/plugins/sp/commands/dev-review-session.md +14 -7
  33. package/plugins/sp/commands/dev-run.md +17 -4
  34. package/plugins/sp/commands/dev-runall.md +21 -10
  35. package/plugins/sp/hooks/agent-lifecycle.ts +155 -0
  36. package/plugins/sp/hooks/context-session-start.ts +1 -1
  37. package/plugins/sp/hooks/fleet-guest-stop.ts +180 -0
  38. package/plugins/sp/hooks/hooks.json +45 -1
  39. package/plugins/sp/hooks/pi/guard-extension.ts +21 -0
  40. package/plugins/sp/lib/history-anatomy.generated.mjs +5 -0
  41. package/plugins/sp/lib/idea-handoff.generated.mjs +72 -11
  42. package/plugins/sp/lib/inline-run-app.ts +28 -0
  43. package/plugins/sp/lib/inline-run.generated.d.mts +6 -0
  44. package/plugins/sp/lib/inline-run.generated.mjs +364 -46
  45. package/plugins/sp/lib/plan-projection.generated.d.mts +28 -0
  46. package/plugins/sp/lib/plan-projection.generated.mjs +255 -0
  47. package/plugins/sp/lib/quality-gate.generated.d.mts +47 -1
  48. package/plugins/sp/lib/quality-gate.generated.mjs +30489 -82
  49. package/plugins/sp/lib/residual-scan.generated.d.mts +19 -0
  50. package/plugins/sp/lib/residual-scan.generated.mjs +66 -0
  51. package/plugins/sp/lib/run-summary-core.ts +561 -0
  52. package/plugins/sp/lib/transcript.ts +275 -0
  53. package/plugins/sp/plugin.json +1 -1
  54. package/plugins/sp/references/roles.md +1 -1
  55. package/plugins/sp/scripts/batch-plan.mjs +335 -0
  56. package/plugins/sp/scripts/batch-plan.ts +100 -0
  57. package/plugins/sp/scripts/batch-preflight.mjs +51 -3
  58. package/plugins/sp/scripts/batch-preflight.ts +72 -1
  59. package/plugins/sp/scripts/commit-guard.mjs +216 -0
  60. package/plugins/sp/scripts/commit-guard.ts +249 -0
  61. package/plugins/sp/scripts/history-anatomy-cache.mjs +5 -0
  62. package/plugins/sp/scripts/inline-run-dispatch.mjs +130 -0
  63. package/plugins/sp/scripts/inline-run-dispatch.ts +135 -0
  64. package/plugins/sp/scripts/inline-run-setup.mjs +563 -91
  65. package/plugins/sp/scripts/inline-run-setup.ts +65 -97
  66. package/plugins/sp/scripts/persist-out-check.mjs +90 -41
  67. package/plugins/sp/scripts/persist-out-check.ts +112 -57
  68. package/plugins/sp/scripts/quality-gate.mjs +30407 -73
  69. package/plugins/sp/scripts/quality-gate.ts +21 -5
  70. package/plugins/sp/scripts/residual-scan.mjs +88 -38
  71. package/plugins/sp/scripts/residual-scan.ts +35 -38
  72. package/plugins/sp/scripts/run-summary.mjs +693 -0
  73. package/plugins/sp/scripts/run-summary.ts +105 -0
  74. package/plugins/sp/scripts/session-timeline.mjs +331 -0
  75. package/plugins/sp/scripts/session-timeline.ts +204 -0
  76. package/plugins/sp/scripts/wrapup-steps.mjs +150 -2
  77. package/plugins/sp/scripts/wrapup-steps.ts +203 -1
  78. package/plugins/sp/skills/brainstorm/SKILL.md +9 -47
  79. package/plugins/sp/skills/branch-workflow/SKILL.md +1 -0
  80. package/plugins/sp/skills/branch-workflow/references/worktree-patterns.md +1 -0
  81. package/plugins/sp/skills/code-implementation/SKILL.md +48 -1
  82. package/plugins/sp/skills/code-verification/SKILL.md +8 -4
  83. package/plugins/sp/skills/code-verification/references/secu-review.md +3 -0
  84. package/plugins/sp/skills/code-verification/references/verdict-schema.md +68 -0
  85. package/plugins/sp/skills/fleet-join/SKILL.md +63 -0
  86. package/plugins/sp/skills/session-review/SKILL.md +77 -24
  87. package/plugins/sp/skills/source-driven-development/SKILL.md +5 -0
  88. package/plugins/sp/skills/spec-decomposition/references/decomposition.md +5 -3
  89. package/plugins/sp/skills/spur-cli/SKILL.md +6 -2
  90. package/plugins/sp/skills/spur-cli/references/agent.md +124 -17
  91. package/plugins/sp/skills/spur-cli/references/decision.md +98 -0
  92. package/plugins/sp/skills/spur-cli/references/message.md +7 -7
  93. package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +41 -3
  94. package/plugins/sp/skills/spur-cli/references/tasks.md +10 -7
  95. package/plugins/sp/skills/spur-cli/references/workflows/authoring-workflows.md +7 -0
  96. package/plugins/sp/skills/spur-cli/references/workflows.md +7 -2
  97. package/plugins/sp/skills/spur-composer/SKILL.md +28 -2
  98. package/plugins/sp/skills/spur-dev/SKILL.md +18 -14
  99. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +6 -3
  100. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +49 -14
  101. package/plugins/sp/skills/spur-dev/references/dev-operations.md +51 -18
  102. package/plugins/sp/skills/spur-dev/references/execution-batch-continuation.md +68 -0
  103. package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +194 -0
  104. package/plugins/sp/skills/spur-dev/references/execution-batch.md +113 -963
  105. package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +130 -0
  106. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +20 -0
  107. package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +471 -0
  108. package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +379 -0
  109. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +51 -9
  110. package/plugins/sp/skills/spur-dev/references/glossary.md +2 -2
  111. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +361 -156
  112. package/plugins/sp/skills/spur-dev/references/planning-workflow.md +6 -0
  113. package/plugins/sp/skills/spur-dev/references/structured-trace-emission.md +178 -0
  114. package/plugins/sp/skills/wayfinder/SKILL.md +10 -14
  115. package/schemas/decision-catalog.schema.json +226 -0
  116. package/schemas/spur-config.schema.json +25 -0
  117. package/schemas/state-machine-workflow.schema.json +23 -0
  118. package/spur.js +28165 -19051
  119. package/web/_astro/{BoardApp.i4eh_fHA.js → BoardApp.D66k_s_T.js} +1 -1
  120. package/web/_astro/BoardApp.D6sjAJr_.js +324 -0
  121. package/web/_astro/{TaskDetail.C5Cu8LdB.js → TaskDetail.BynLnMNc.js} +1 -1
  122. package/web/_astro/{arc.Bu5XpBEz.js → arc._BUadrdH.js} +1 -1
  123. package/web/_astro/{architectureDiagram-3BPJPVTR.9vHQTvhF.js → architectureDiagram-3BPJPVTR.Dh7xS1Pp.js} +1 -1
  124. package/web/_astro/{blockDiagram-GPEHLZMM.Cn2Xl7D6.js → blockDiagram-GPEHLZMM.DFDVpfXm.js} +1 -1
  125. package/web/_astro/{c4Diagram-AAUBKEIU.7o0P_QDo.js → c4Diagram-AAUBKEIU.V7_3_glh.js} +1 -1
  126. package/web/_astro/channel.DZVfto-r.js +1 -0
  127. package/web/_astro/{chunk-2J33WTMH.C3zZFsNt.js → chunk-2J33WTMH.De1yx7Jz.js} +1 -1
  128. package/web/_astro/{chunk-4BX2VUAB.CNy_M7fO.js → chunk-4BX2VUAB.BDhHFuxx.js} +1 -1
  129. package/web/_astro/{chunk-55IACEB6.2TiPnLBL.js → chunk-55IACEB6.DDLkWnmC.js} +1 -1
  130. package/web/_astro/{chunk-727SXJPM.5CGSyKAp.js → chunk-727SXJPM.CwfiLnA7.js} +1 -1
  131. package/web/_astro/{chunk-AQP2D5EJ.w-ZIgfsK.js → chunk-AQP2D5EJ.CtGDmUnM.js} +1 -1
  132. package/web/_astro/{chunk-FMBD7UC4.BkcEyhKa.js → chunk-FMBD7UC4.BOLjpw73.js} +1 -1
  133. package/web/_astro/{chunk-ND2GUHAM.mxKKbBG4.js → chunk-ND2GUHAM.BDhQls6O.js} +1 -1
  134. package/web/_astro/{chunk-QZHKN3VN.cyA0z9aT.js → chunk-QZHKN3VN.76qBynDF.js} +1 -1
  135. package/web/_astro/{classDiagram-4FO5ZUOK.CqtUzwhX.js → classDiagram-4FO5ZUOK.N0mpPHT0.js} +1 -1
  136. package/web/_astro/{classDiagram-v2-Q7XG4LA2.CqtUzwhX.js → classDiagram-v2-Q7XG4LA2.N0mpPHT0.js} +1 -1
  137. package/web/_astro/{cose-bilkent-S5V4N54A.BExPTopI.js → cose-bilkent-S5V4N54A.CKknFfjB.js} +1 -1
  138. package/web/_astro/{cynefin-OW5HDTMX.Bq2i10Cn.js → cynefin-OW5HDTMX.Cw3ZWBAh.js} +1 -1
  139. package/web/_astro/{dagre-BM42HDAG.By_HsOuv.js → dagre-BM42HDAG.D3yIKCRD.js} +1 -1
  140. package/web/_astro/{diagram-2AECGRRQ.BtU0b7hz.js → diagram-2AECGRRQ.D51dO83Z.js} +1 -1
  141. package/web/_astro/{diagram-5GNKFQAL.D00ztMdT.js → diagram-5GNKFQAL.Bb-FViDj.js} +1 -1
  142. package/web/_astro/{diagram-KO2AKTUF.DMVJWaUN.js → diagram-KO2AKTUF.D3mN8S6x.js} +1 -1
  143. package/web/_astro/{diagram-LMA3HP47.Cxqrjg5-.js → diagram-LMA3HP47.dMMyPcbJ.js} +1 -1
  144. package/web/_astro/{diagram-OG6HWLK6.2lz_1B5h.js → diagram-OG6HWLK6.AzbvzY4C.js} +1 -1
  145. package/web/_astro/{erDiagram-TEJ5UH35.BVQEuJ3T.js → erDiagram-TEJ5UH35.BR5I4OZb.js} +1 -1
  146. package/web/_astro/{flowDiagram-I6XJVG4X.Qlyo90ky.js → flowDiagram-I6XJVG4X.DFqvhGkn.js} +1 -1
  147. package/web/_astro/{ganttDiagram-6RSMTGT7.DRwb8HHZ.js → ganttDiagram-6RSMTGT7.ByJbhH6J.js} +1 -1
  148. package/web/_astro/{gitGraphDiagram-PVQCEYII.YfoPthfS.js → gitGraphDiagram-PVQCEYII.CInHjQR3.js} +1 -1
  149. package/web/_astro/index.COh_kBxN.css +1 -0
  150. package/web/_astro/{infoDiagram-5YYISTIA.DvFREck1.js → infoDiagram-5YYISTIA.Cm0dGVjJ.js} +1 -1
  151. package/web/_astro/{ishikawaDiagram-YF4QCWOH.BzCj9Q0-.js → ishikawaDiagram-YF4QCWOH.C3nOP6Sj.js} +1 -1
  152. package/web/_astro/{journeyDiagram-JHISSGLW.BDMT_AFb.js → journeyDiagram-JHISSGLW.4IvLa2MW.js} +1 -1
  153. package/web/_astro/{kanban-definition-UN3LZRKU.D6Fh3gj9.js → kanban-definition-UN3LZRKU.DS87Cl-k.js} +1 -1
  154. package/web/_astro/{linear.Dxw00fGQ.js → linear.MzmRrNER.js} +1 -1
  155. package/web/_astro/{mermaid.core.CsoxUWa0.js → mermaid.core.fK-WNswS.js} +4 -4
  156. package/web/_astro/{mindmap-definition-RKZ34NQL.CN8oeHLu.js → mindmap-definition-RKZ34NQL.BqipOHhY.js} +1 -1
  157. package/web/_astro/{pieDiagram-4H26LBE5.C7ADqQ_7.js → pieDiagram-4H26LBE5.0nBiPqFG.js} +1 -1
  158. package/web/_astro/{quadrantDiagram-W4KKPZXB.Cw9lKyHP.js → quadrantDiagram-W4KKPZXB.Szj7Zpx5.js} +1 -1
  159. package/web/_astro/{requirementDiagram-4Y6WPE33.B1ni-z4q.js → requirementDiagram-4Y6WPE33.D4UR6TWo.js} +1 -1
  160. package/web/_astro/{sankeyDiagram-5OEKKPKP.BhexdjVr.js → sankeyDiagram-5OEKKPKP.DAE7BdOs.js} +1 -1
  161. package/web/_astro/{sequenceDiagram-3UESZ5HK.C3Rm7dpQ.js → sequenceDiagram-3UESZ5HK.BP9ZVq25.js} +1 -1
  162. package/web/_astro/{stateDiagram-AJRCARHV.CkuMt27u.js → stateDiagram-AJRCARHV.BQW7mIxk.js} +1 -1
  163. package/web/_astro/{stateDiagram-v2-BHNVJYJU.Dc_aazUA.js → stateDiagram-v2-BHNVJYJU.PbK4nDih.js} +1 -1
  164. package/web/_astro/{timeline-definition-PNZ67QCA.ja7sUFhH.js → timeline-definition-PNZ67QCA.Ci_GvUpo.js} +1 -1
  165. package/web/_astro/{vennDiagram-CIIHVFJN.BxIshX_K.js → vennDiagram-CIIHVFJN.DNw1Y5qx.js} +1 -1
  166. package/web/_astro/{wardleyDiagram-YWT4CUSO.DfHlC0sh.js → wardleyDiagram-YWT4CUSO.CwlkaaNe.js} +1 -1
  167. package/web/_astro/{xychartDiagram-2RQKCTM6.DSnhHwCh.js → xychartDiagram-2RQKCTM6.MI6ZuiH_.js} +1 -1
  168. package/web/board-runtime.json +1 -1
  169. package/web/index.html +2 -2
  170. package/web/_astro/BoardApp.BVp-LYi6.js +0 -322
  171. package/web/_astro/channel.6IFIKBhu.js +0 -1
  172. package/web/_astro/index.CbNz17Sx.css +0 -1
@@ -52,6 +52,12 @@ Flag → vars: `--auto` sets `profile=auto`, `idea_approved=true`, `design_appro
52
52
 
53
53
  ## Implementation
54
54
 
55
+ - **Publish the generated plan first (1105):** your first tool call — before invoking the skill or
56
+ reading any reference — is `spur workflow show idea-pipeline.yaml --no-logo --format todo --json`
57
+ (the CLI resolves the project→registered→shared layer); then publish every `.plan` row's `text`
58
+ verbatim as the host todo list (the `steps[]` inventory when `.plan` is `null`) before the
59
+ pipeline's first stage — [inline pipeline driver](../skills/spur-dev/references/inline-pipeline-driver.md)
60
+ run setup step 5.
55
61
  - Apply the [inline-default execution-surface contract](../skills/spur-dev/references/cross-cutting.md#inline-default-execution-surface).
56
62
  - Omitted/`inline`: drive `idea-pipeline.yaml` through the [inline pipeline driver](../skills/spur-dev/references/inline-pipeline-driver.md). Do not launch `spur workflow run`, `spur agent run`, or a native subagent unless the operator explicitly requests delegation.
57
63
  - `auto`/name: launch `spur workflow run idea-pipeline.yaml --async`, observe with one `workflow trace --follow`, and only report cancellation as stopped when `workflow cancel --json` returns `killed: true`.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  description: Fan out independent tasks or investigations in parallel via subagents — choose the right pattern and synthesize results
3
3
  role: planner
4
- argument-hint: "--tasks <selector> [--feature <id>] [--mode <fan-out|review-panel|investigation>] [--agent <inline|auto|name>] [--json]"
4
+ argument-hint: "--tasks <selector> [--feature <id>] [--mode <fan-out|review-panel|investigation>] [--agent <inline|auto|name>] [--json] [--defer-gate]"
5
5
  allowed-tools: ["Bash", "Read", "Skill"]
6
6
  ---
7
7
 
@@ -18,14 +18,20 @@ Wraps the **sp:parallel-execution** skill.
18
18
  | `--mode` `<fan-out\|review-panel\|investigation>` | Fan-out pattern. | fan-out |
19
19
  | `--agent` `<inline\|auto\|name>` | Who runs each dispatched slice. Parallel fan-out is dispatch, so explicit `--agent inline` runs the batch **sequentially in the host session** with a printed notice (zero dispatch); omit keeps the default fan-out semantics; `auto` tier-resolves an executor; a name pins that executor. | omit |
20
20
  | `--json` | Emit structured JSON. | off |
21
+ | `--defer-gate` | Opt-in parallel-batch gate policy (task 1111): each task's `test` hop runs the light tier and writes `DEFERRED`; the batch runs one integrated full gate on the base ref before the feature sync. Requires `--mode parallel`; default off. | off |
21
22
 
22
23
  For shared semantics, see the [flag glossary](../skills/spur-dev/references/flag-glossary.md).
23
24
 
24
25
  ## Usage
25
26
 
26
- /sp:dev-parallel --tasks <selector> [--feature <id>] [--mode <fan-out|review-panel|investigation>] [--agent <inline|auto|name>] [--json]
27
+ /sp:dev-parallel --tasks <selector> [--feature <id>] [--mode <fan-out|review-panel|investigation>] [--agent <inline|auto|name>] [--json] [--defer-gate]
27
28
 
28
29
  ## Implementation
29
30
 
31
+ - **Publish the visible batch plan first (1105):** as the batch orchestrator, publish `A Prepare
32
+ batch` (A1–A4) and `Z Batch report` before the first task, then one letter per task from
33
+ `batch-plan.mjs waves` after freeze/order and digit children from `batch-plan.mjs task-children` at
34
+ task start — [execution-batch.md](../skills/spur-dev/references/execution-batch.md#27-visible-batch-plan-1105-r3)
35
+ § 2.7 Visible batch plan.
30
36
  - Apply the [inline-default execution-surface contract](../skills/spur-dev/references/cross-cutting.md#inline-default-execution-surface) before choosing native subagents or `spur agent run`.
31
37
  - `Skill(skill="sp:parallel-execution", args="$ARGUMENTS")`
@@ -41,6 +41,12 @@ contract as `/sp:dev-idea`). If passed, ignore them with a one-line notice.
41
41
 
42
42
  ## Implementation
43
43
 
44
+ - **Publish the generated plan first (1105):** your first tool call — before invoking the skill or
45
+ reading any reference — is `spur workflow show idea-pipeline.yaml --no-logo --format todo --json`
46
+ (the CLI resolves the project→registered→shared layer); then publish every `.plan` row's `text`
47
+ verbatim as the host todo list (the `steps[]` inventory when `.plan` is `null`) before the
48
+ pipeline's first stage — [inline pipeline driver](../skills/spur-dev/references/inline-pipeline-driver.md)
49
+ run setup step 5.
44
50
  - Apply the [inline-default execution-surface contract](../skills/spur-dev/references/cross-cutting.md#inline-default-execution-surface).
45
51
  - Omitted/`inline`: drive `idea-pipeline.yaml` through the [inline pipeline driver](../skills/spur-dev/references/inline-pipeline-driver.md). Do not launch `spur workflow run`, `spur agent run`, or a native subagent unless the operator explicitly requests delegation.
46
52
  - `auto`/name: launch `spur workflow run idea-pipeline.yaml --async`, observe with one `workflow trace --follow`, and only report cancellation as stopped when `workflow cancel --json` returns `killed: true`.
@@ -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,23 @@ 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. The transcript resolves from `--transcript`, else
20
+ `CLAUDE_CODE_SESSION_ID`, else `PI_SESSION_FILE` (the pi host sets it and no Claude session id); a
21
+ skill-injected `<skill name="…">` body counts as activity in `injectedPrompts`, never as a segment.
17
22
 
18
23
  ## Argument Flags
19
24
 
20
25
  | Flag | Description | Default |
21
26
  | --- | --- | --- |
22
27
  | `[<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) |
28
+ | `--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 |
29
+ | `--triage` | Alias of `--fix auto`; an explicit `--fix` wins when both are passed. | off |
24
30
 
25
31
  For shared semantics, see the [flag glossary](../skills/spur-dev/references/flag-glossary.md).
26
32
 
@@ -29,7 +35,8 @@ For shared semantics, see the [flag glossary](../skills/spur-dev/references/flag
29
35
  ```
30
36
  /sp:dev-review-session
31
37
  /sp:dev-review-session "why the verification loop repeated"
32
- /sp:dev-review-session --triage
38
+ /sp:dev-review-session --fix auto # same as --triage
39
+ /sp:dev-review-session --fix all
33
40
  /sp:dev-review-session "F95 findings" --triage
34
41
  ```
35
42
 
@@ -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,22 +21,34 @@ 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
 
34
+ - **Bootstrap reads (1128):** before the first dispatch, read [SKILL.md § Bootstrap reads](../skills/spur-dev/SKILL.md#bootstrap-reads) and load only that mode's files — everything else under `references/` is on demand.
35
+ - **Publish the generated plan first (1105):** your first tool call — before invoking the skill,
36
+ reading any reference, or showing the task — is `spur workflow show task-pipeline.yaml --no-logo
37
+ --format todo --json` (the CLI resolves the project→registered→shared layer); then publish every
38
+ `.plan` row's `text` verbatim as the host todo list (the `steps[]` inventory when `.plan` is
39
+ `null`) before readiness, Git, or isolation work —
40
+ [shared startup contract](../skills/spur-dev/references/cross-cutting.md#shared-startup-contract-task-0814-r1r3r4r6r7r8)
41
+ step 1 / [inline pipeline driver](../skills/spur-dev/references/inline-pipeline-driver.md) run
42
+ setup step 5.
33
43
  - Apply the [inline-default execution-surface contract](../skills/spur-dev/references/cross-cutting.md#inline-default-execution-surface).
34
- - 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 their owning boundaries (task 0814 R1/R3/R4/R7/R8).
44
+ - Apply the [shared startup contract](../skills/spur-dev/references/cross-cutting.md#shared-startup-contract-task-0814-r1r3r4r6r7r8) — publish the generated plan first, quick readiness before isolation, digest binding, comprehensive checks at their owning boundaries (task 0814 R1/R3/R4/R7/R8; 1105 R1).
35
45
  - Full pipeline (default `--mode full`): interactive omit/`--agent inline` uses the
36
46
  [inline pipeline driver](../skills/spur-dev/references/inline-pipeline-driver.md) via
37
47
  `Skill(skill="sp:spur-dev", args="run-inline $ARGUMENTS")`; `--agent auto` or a named executor
38
48
  uses `Skill(skill="sp:spur-dev", args="run $ARGUMENTS")` and the workflow subprocess path.
39
49
  - Implement step only (`--mode implement`): `Skill(skill="sp:code-implementation", args="$ARGUMENTS")`
50
+ - After a full-mode run settles (done, failed or paused), print the
51
+ [execution summary](../skills/spur-dev/references/dev-operations.md#execution-summary) unless `--no-summary`.
40
52
 
41
53
  **Flags:**
42
54
 
@@ -52,7 +64,8 @@ For shared semantics, see the [flag glossary](../skills/spur-dev/references/flag
52
64
  base ref on full success, retain intact on any failure/halt/non-FF; bare form creates a fresh tree,
53
65
  `<name>` form adopts an existing worktree by name/path/branch). The lifecycle — dirty-tree precheck,
54
66
  creation or adoption, crash-safe `.spur/run/` marker, merge-or-retain, `--continue` re-entry — is
55
- `execution-batch.md` § Worktree isolation applied to a batch of one: marker `command` is `dev-run`
67
+ `execution-worktree-setup.md` (creation/adoption, WT-1…WT-3b) + `execution-worktree-landing.md`
68
+ (WT-4/WT-5) applied to a batch of one: marker `command` is `dev-run`
56
69
  and `selector` is the `<wbs>`, the derived branch is `sp/run-<wbs>-<short-id>`, and the success
57
70
  condition is the task reaching terminal `done` with no failed stage. A failing gate, a non-PASS
58
71
  verify verdict, or a HITL pause that ends the run all take the retention path.
@@ -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] [--defer-gate]"
5
5
  allowed-tools: ["Bash", "Read", "Skill"]
6
6
  ---
7
7
 
@@ -24,25 +24,28 @@ 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 |
29
+ | `--defer-gate` | Opt-in parallel-batch gate policy (task 1111): each task's `test` hop runs the light tier and writes `DEFERRED`; the batch runs one integrated full gate on the base ref before the feature sync. Requires `--mode parallel`; default off. | off |
28
30
 
29
31
  For shared semantics, see the [flag glossary](../skills/spur-dev/references/flag-glossary.md).
30
32
 
31
33
  ## Usage
32
34
 
33
35
  ```
34
- /sp:dev-runall --tasks <selector> [--feature <id>] [--mode <sequential|parallel>] [--keep-going] [--auto] [--agent <inline|auto|name>] [--json] [--wrap] [--next] [--continue] [--worktree [<name>]]
36
+ /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] [--defer-gate]
35
37
  ```
36
38
 
37
39
  Flags: `--tasks <selector>` (required — explicit WBS list, status pseudo-list, `feature:<id>`,
38
40
  or `ready`), `--feature <id>` (sugar for `feature:<id>`; when the effective selector is
39
- feature-derived, `dev-runall` runs `spur feature check <id> --strict --json` **once** before
41
+ feature-derived, `dev-runall` runs `spur feature check <id> --fix --json` then `spur feature check <id> --strict --json` **once** before
40
42
  resolving tasks — a non-zero strict check aborts the batch with verdict `aborted` and the
41
- structured findings, before any task pipeline action, task 0510 R2; scoped: `L4.scenario-unverified`
42
- (the expected pre-run state of any not-yet-run feature) is reported verbatim but does not abort —
43
+ structured findings, before any task pipeline action, task 0510 R2; scoped: `L4.scenario-unverified` and `L4.verifying-incomplete-tasks`
44
+ (expected pre-run states of a not-yet-run or reopened feature) are reported verbatim but do not abort (the non-aborting set is
45
+ `NON_ABORTING_PREFLIGHT_CODES` in `plugins/sp/scripts/batch-preflight.ts`; `--fix` repairs land in the report's `autoRepairs`) —
43
46
  any other strict error aborts), `--mode`
44
47
  `<sequential|parallel>` (default `sequential`; `parallel` fans out a proven-independent subset —
45
- see `execution-batch.md` § Parallel isolation), `--concurrency <n>`
48
+ see `execution-parallel-isolation.md`), `--concurrency <n>`
46
49
  (parallel-mode worker bound — at most <n> task pipelines at once, default 2; a dependent task starts
47
50
  only after its in-set dependencies are **integrated** onto the base ref), `--keep-going`
48
51
  (batch failure policy — skip a failed task's in-batch dependents, continue independents; default
@@ -57,10 +60,10 @@ report as JSON), `--wrap` (trigger
57
60
  `--worktree` `[<name>]` (run the batch in an isolated git worktree —
58
61
  FF-merge onto the base ref on full success, retain intact on any failure/halt/non-FF; bare form
59
62
  creates a fresh tree, `<name>` form adopts an existing worktree by name/path/branch; see
60
- `execution-batch.md` § Worktree isolation).
63
+ `execution-worktree-setup.md` + `execution-worktree-landing.md`).
61
64
 
62
65
  **`--worktree` and parallel mode.** `--worktree --mode parallel` is **rejected** — parallel mode
63
- already isolates each task in its own worktree (see `execution-batch.md` § Parallel isolation), and
66
+ already isolates each task in its own worktree (see `execution-parallel-isolation.md`), and
64
67
  reuse mode (`--worktree <name>`) has no per-task meaning. Run parallel batches without
65
68
  `--worktree`, or run them sequentially with it.
66
69
 
@@ -80,14 +83,22 @@ advancing the feature lifecycle). **was: `--next` deliberately omitted; the old
80
83
  = batch failure policy (does a failure halt the batch or skip dependents?);
81
84
  `--continue` = resume from
82
85
  checkpoint against the batch's original frozen identity — persisted plan membership, worktree
83
- marker, identity-filtered checkpoints as hints (execution-batch.md § Batch continuation, task
86
+ marker, identity-filtered checkpoints as hints (execution-batch-continuation.md, task
84
87
  0919); `--next`
85
88
  = chain each task to terminal status + batch-once wrap. See `dev-operations.md` § runall for the
86
89
  full distinction.
87
90
 
88
91
  ## Implementation
89
92
 
93
+ - **Bootstrap reads (1128):** before the first dispatch, read [SKILL.md § Bootstrap reads](../skills/spur-dev/SKILL.md#bootstrap-reads) and load only that mode's files — the section whose trigger has not fired is on demand.
94
+ - **Publish the visible batch plan first (1105):** publish `A Prepare batch` (A1–A4) and `Z Batch
95
+ report` before the first task, then one letter per task from `batch-plan.mjs waves` after
96
+ freeze/order and digit children from `batch-plan.mjs task-children` at task start —
97
+ [execution-batch.md](../skills/spur-dev/references/execution-batch.md#27-visible-batch-plan-1105-r3)
98
+ § 2.7 Visible batch plan.
90
99
  - Apply the [inline-default execution-surface contract](../skills/spur-dev/references/cross-cutting.md#inline-default-execution-surface). Interactive sequential omit/`inline` keeps the orchestrator in the host session through the [inline pipeline driver](../skills/spur-dev/references/inline-pipeline-driver.md); the resolved selector applies uniformly — eligible `agent.run` stages dispatch once to a native subagent with host fallback (0508). `--agent auto`, a name, or parallel mode retains the isolated per-task workflow boundary.
91
- - 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).
100
+ - Apply the [shared startup contract](../skills/spur-dev/references/cross-cutting.md#shared-startup-contract-task-0814-r1r3r4r6r7r8) — publish the generated plan first, quick readiness before isolation, digest binding, comprehensive checks at owning boundaries (task 0814 R1/R3/R4/R7/R8; 1105 R1).
92
101
  - Interactive sequential omit/inline: `Skill(skill="sp:spur-dev", args="runall-inline $ARGUMENTS")`.
93
102
  - Explicit executor or parallel mode: `Skill(skill="sp:spur-dev", args="runall $ARGUMENTS")` → `sp:super-planner` agent.
103
+ - After the batch report, print the [execution summary](../skills/spur-dev/references/dev-operations.md#execution-summary) over every attempted run unless
104
+ `--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
+ }
@@ -74,7 +74,7 @@ interface SessionFileBody {
74
74
  *
75
75
  * 1. {@link AGENT_RUN_ID_ENV} present → this process is a descendant of an agent run, definitively.
76
76
  * Reuse the recorded session with no time bound: a pipeline step legitimately runs for the full
77
- * `implementTimeoutMs` (30 min) and longer batches run for hours, so any wall-clock window would
77
+ * `implementTimeoutMs` (45 min) and longer batches run for hours, so any wall-clock window would
78
78
  * eventually split a run that is demonstrably still in flight.
79
79
  * 2. No marker → fall back to the {@link SESSION_REUSE_IDLE_MS} window on the `started` stamp, for
80
80
  * hosts that nest without propagating a correlation.
@@ -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
+ }