@gobing-ai/spur 0.3.99 → 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 (106) 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 +10 -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/workflows/task-pipeline.yaml +178 -18
  10. package/package.json +9 -9
  11. package/plugins/sp/README.md +14 -1
  12. package/plugins/sp/agents/super-reviewer.md +25 -11
  13. package/plugins/sp/commands/dev-changelog.md +5 -4
  14. package/plugins/sp/commands/dev-review-session.md +12 -7
  15. package/plugins/sp/commands/dev-run.md +5 -2
  16. package/plugins/sp/commands/dev-runall.md +5 -2
  17. package/plugins/sp/hooks/agent-lifecycle.ts +155 -0
  18. package/plugins/sp/hooks/fleet-guest-stop.ts +180 -0
  19. package/plugins/sp/hooks/hooks.json +45 -1
  20. package/plugins/sp/hooks/pi/guard-extension.ts +21 -0
  21. package/plugins/sp/lib/history-anatomy.generated.mjs +5 -0
  22. package/plugins/sp/lib/idea-handoff.generated.mjs +34 -9
  23. package/plugins/sp/lib/inline-run.generated.d.mts +1 -0
  24. package/plugins/sp/lib/inline-run.generated.mjs +49 -19
  25. package/plugins/sp/lib/transcript.ts +160 -0
  26. package/plugins/sp/plugin.json +1 -1
  27. package/plugins/sp/scripts/history-anatomy-cache.mjs +5 -0
  28. package/plugins/sp/scripts/inline-run-setup.mjs +21 -20
  29. package/plugins/sp/scripts/inline-run-setup.ts +25 -31
  30. package/plugins/sp/scripts/residual-scan.mjs +2 -7
  31. package/plugins/sp/scripts/residual-scan.ts +7 -8
  32. package/plugins/sp/scripts/run-summary.mjs +286 -0
  33. package/plugins/sp/scripts/run-summary.ts +241 -0
  34. package/plugins/sp/scripts/session-timeline.mjs +234 -0
  35. package/plugins/sp/scripts/session-timeline.ts +152 -0
  36. package/plugins/sp/skills/code-verification/SKILL.md +5 -0
  37. package/plugins/sp/skills/fleet-join/SKILL.md +63 -0
  38. package/plugins/sp/skills/session-review/SKILL.md +46 -24
  39. package/plugins/sp/skills/spur-cli/SKILL.md +1 -0
  40. package/plugins/sp/skills/spur-cli/references/agent.md +108 -6
  41. package/plugins/sp/skills/spur-cli/references/decision.md +97 -0
  42. package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +22 -0
  43. package/plugins/sp/skills/spur-composer/SKILL.md +27 -1
  44. package/plugins/sp/skills/spur-dev/references/dev-operations.md +28 -16
  45. package/plugins/sp/skills/spur-dev/references/execution-batch.md +64 -7
  46. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +14 -0
  47. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +16 -4
  48. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +32 -7
  49. package/plugins/sp/skills/spur-dev/references/planning-workflow.md +6 -0
  50. package/schemas/decision-catalog.schema.json +226 -0
  51. package/schemas/spur-config.schema.json +25 -0
  52. package/spur.js +7440 -2424
  53. package/web/_astro/BoardApp.Bzr4OXrW.js +324 -0
  54. package/web/_astro/{BoardApp.i4eh_fHA.js → BoardApp.VeoxxLBy.js} +1 -1
  55. package/web/_astro/{TaskDetail.C5Cu8LdB.js → TaskDetail.LAyR_Pvn.js} +1 -1
  56. package/web/_astro/{arc.Bu5XpBEz.js → arc.f4e0OYLg.js} +1 -1
  57. package/web/_astro/{architectureDiagram-3BPJPVTR.9vHQTvhF.js → architectureDiagram-3BPJPVTR.SSIvxi-Z.js} +1 -1
  58. package/web/_astro/{blockDiagram-GPEHLZMM.Cn2Xl7D6.js → blockDiagram-GPEHLZMM.DSXIHOFf.js} +1 -1
  59. package/web/_astro/{c4Diagram-AAUBKEIU.7o0P_QDo.js → c4Diagram-AAUBKEIU.DG9Ubu85.js} +1 -1
  60. package/web/_astro/channel.CUFdtWhm.js +1 -0
  61. package/web/_astro/{chunk-2J33WTMH.C3zZFsNt.js → chunk-2J33WTMH.C7QU88Qh.js} +1 -1
  62. package/web/_astro/{chunk-4BX2VUAB.CNy_M7fO.js → chunk-4BX2VUAB.26Z94xBj.js} +1 -1
  63. package/web/_astro/{chunk-55IACEB6.2TiPnLBL.js → chunk-55IACEB6.DYRiBkqw.js} +1 -1
  64. package/web/_astro/{chunk-727SXJPM.5CGSyKAp.js → chunk-727SXJPM.D5Vfv5pe.js} +1 -1
  65. package/web/_astro/{chunk-AQP2D5EJ.w-ZIgfsK.js → chunk-AQP2D5EJ.DXHV47bN.js} +1 -1
  66. package/web/_astro/{chunk-FMBD7UC4.BkcEyhKa.js → chunk-FMBD7UC4.D1suAfOs.js} +1 -1
  67. package/web/_astro/{chunk-ND2GUHAM.mxKKbBG4.js → chunk-ND2GUHAM.D6BmdHEY.js} +1 -1
  68. package/web/_astro/{chunk-QZHKN3VN.cyA0z9aT.js → chunk-QZHKN3VN.rnPU-Ak-.js} +1 -1
  69. package/web/_astro/{classDiagram-4FO5ZUOK.CqtUzwhX.js → classDiagram-4FO5ZUOK.CvEfXIo1.js} +1 -1
  70. package/web/_astro/{classDiagram-v2-Q7XG4LA2.CqtUzwhX.js → classDiagram-v2-Q7XG4LA2.CvEfXIo1.js} +1 -1
  71. package/web/_astro/{cose-bilkent-S5V4N54A.BExPTopI.js → cose-bilkent-S5V4N54A.DkMisLSm.js} +1 -1
  72. package/web/_astro/{cynefin-OW5HDTMX.Bq2i10Cn.js → cynefin-OW5HDTMX.C77Q-pF6.js} +1 -1
  73. package/web/_astro/{dagre-BM42HDAG.By_HsOuv.js → dagre-BM42HDAG.YaV8ce27.js} +1 -1
  74. package/web/_astro/{diagram-2AECGRRQ.BtU0b7hz.js → diagram-2AECGRRQ.CUyGEKge.js} +1 -1
  75. package/web/_astro/{diagram-5GNKFQAL.D00ztMdT.js → diagram-5GNKFQAL.CRgd8dgj.js} +1 -1
  76. package/web/_astro/{diagram-KO2AKTUF.DMVJWaUN.js → diagram-KO2AKTUF.Bmzys9hq.js} +1 -1
  77. package/web/_astro/{diagram-LMA3HP47.Cxqrjg5-.js → diagram-LMA3HP47.BvmJY4Lw.js} +1 -1
  78. package/web/_astro/{diagram-OG6HWLK6.2lz_1B5h.js → diagram-OG6HWLK6.0hEhD30d.js} +1 -1
  79. package/web/_astro/{erDiagram-TEJ5UH35.BVQEuJ3T.js → erDiagram-TEJ5UH35.Bjt8lrnm.js} +1 -1
  80. package/web/_astro/{flowDiagram-I6XJVG4X.Qlyo90ky.js → flowDiagram-I6XJVG4X.CJZTjRDV.js} +1 -1
  81. package/web/_astro/{ganttDiagram-6RSMTGT7.DRwb8HHZ.js → ganttDiagram-6RSMTGT7._VqoVafc.js} +1 -1
  82. package/web/_astro/{gitGraphDiagram-PVQCEYII.YfoPthfS.js → gitGraphDiagram-PVQCEYII.CEDb7tVo.js} +1 -1
  83. package/web/_astro/index.Dg_3bOaJ.css +1 -0
  84. package/web/_astro/{infoDiagram-5YYISTIA.DvFREck1.js → infoDiagram-5YYISTIA.CgwifPpK.js} +1 -1
  85. package/web/_astro/{ishikawaDiagram-YF4QCWOH.BzCj9Q0-.js → ishikawaDiagram-YF4QCWOH.B99wx2J1.js} +1 -1
  86. package/web/_astro/{journeyDiagram-JHISSGLW.BDMT_AFb.js → journeyDiagram-JHISSGLW.2hRwJbEo.js} +1 -1
  87. package/web/_astro/{kanban-definition-UN3LZRKU.D6Fh3gj9.js → kanban-definition-UN3LZRKU.BahJkKNC.js} +1 -1
  88. package/web/_astro/{linear.Dxw00fGQ.js → linear.DguUUe6V.js} +1 -1
  89. package/web/_astro/{mermaid.core.CsoxUWa0.js → mermaid.core.CvgfmqVr.js} +4 -4
  90. package/web/_astro/{mindmap-definition-RKZ34NQL.CN8oeHLu.js → mindmap-definition-RKZ34NQL.B3qEA3x9.js} +1 -1
  91. package/web/_astro/{pieDiagram-4H26LBE5.C7ADqQ_7.js → pieDiagram-4H26LBE5.FlWA-_ru.js} +1 -1
  92. package/web/_astro/{quadrantDiagram-W4KKPZXB.Cw9lKyHP.js → quadrantDiagram-W4KKPZXB.9bVn35X7.js} +1 -1
  93. package/web/_astro/{requirementDiagram-4Y6WPE33.B1ni-z4q.js → requirementDiagram-4Y6WPE33.ChykIhct.js} +1 -1
  94. package/web/_astro/{sankeyDiagram-5OEKKPKP.BhexdjVr.js → sankeyDiagram-5OEKKPKP.Z2Y3hkYb.js} +1 -1
  95. package/web/_astro/{sequenceDiagram-3UESZ5HK.C3Rm7dpQ.js → sequenceDiagram-3UESZ5HK.J7JsHWDt.js} +1 -1
  96. package/web/_astro/{stateDiagram-AJRCARHV.CkuMt27u.js → stateDiagram-AJRCARHV.DAPbiCSS.js} +1 -1
  97. package/web/_astro/{stateDiagram-v2-BHNVJYJU.Dc_aazUA.js → stateDiagram-v2-BHNVJYJU.D2jvSi0q.js} +1 -1
  98. package/web/_astro/{timeline-definition-PNZ67QCA.ja7sUFhH.js → timeline-definition-PNZ67QCA.psW2ci29.js} +1 -1
  99. package/web/_astro/{vennDiagram-CIIHVFJN.BxIshX_K.js → vennDiagram-CIIHVFJN.DlILYVmg.js} +1 -1
  100. package/web/_astro/{wardleyDiagram-YWT4CUSO.DfHlC0sh.js → wardleyDiagram-YWT4CUSO.xNWwLJnn.js} +1 -1
  101. package/web/_astro/{xychartDiagram-2RQKCTM6.DSnhHwCh.js → xychartDiagram-2RQKCTM6.EU_BuH7V.js} +1 -1
  102. package/web/board-runtime.json +1 -1
  103. package/web/index.html +2 -2
  104. package/web/_astro/BoardApp.BVp-LYi6.js +0 -322
  105. package/web/_astro/channel.6IFIKBhu.js +0 -1
  106. package/web/_astro/index.CbNz17Sx.css +0 -1
@@ -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
+ }
@@ -1,5 +1,5 @@
1
1
  {
2
- "description": "Spur (sp) plugin hooks — task corpus write protection + indexed-context token-ledger tracking.",
2
+ "description": "Spur (sp) plugin hooks — task corpus write protection + indexed-context token-ledger tracking + fleet lifecycle reporting.",
3
3
  "hooks": {
4
4
  "PreToolUse": [
5
5
  {
@@ -32,6 +32,50 @@
32
32
  "type": "command",
33
33
  "command": "superskill hook run sp context-session-start",
34
34
  "timeout": 15
35
+ },
36
+ {
37
+ "type": "command",
38
+ "command": "superskill hook run sp agent-lifecycle",
39
+ "timeout": 5
40
+ }
41
+ ]
42
+ }
43
+ ],
44
+ "UserPromptSubmit": [
45
+ {
46
+ "hooks": [
47
+ {
48
+ "type": "command",
49
+ "command": "superskill hook run sp agent-lifecycle",
50
+ "timeout": 5
51
+ }
52
+ ]
53
+ }
54
+ ],
55
+ "Notification": [
56
+ {
57
+ "matcher": "permission_prompt",
58
+ "hooks": [
59
+ {
60
+ "type": "command",
61
+ "command": "superskill hook run sp agent-lifecycle",
62
+ "timeout": 5
63
+ }
64
+ ]
65
+ }
66
+ ],
67
+ "Stop": [
68
+ {
69
+ "hooks": [
70
+ {
71
+ "type": "command",
72
+ "command": "superskill hook run sp agent-lifecycle",
73
+ "timeout": 5
74
+ },
75
+ {
76
+ "type": "command",
77
+ "command": "superskill hook run sp fleet-guest-stop",
78
+ "timeout": 10
35
79
  }
36
80
  ]
37
81
  }
@@ -10,6 +10,7 @@
10
10
  * - context-post-tool (tool_result → shared recordToolUseEvent)
11
11
  * - context-session-start (session_start → shared recordSessionStart)
12
12
  * - context-session-stop (session_shutdown → shared recordSessionEnd)
13
+ * - agent-lifecycle (agent_start/agent_settled → shared reportLifecycleHook, G73 R4)
13
14
  *
14
15
  * The ledger and session cores are shared with the Claude hooks (task 0969): this
15
16
  * extension only normalizes Pi event shapes and adapts them to those cores.
@@ -27,6 +28,7 @@ import { homedir } from 'node:os';
27
28
  import { isAbsolute, join, resolve } from 'node:path';
28
29
  import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
29
30
  import { getEnvVar, getEnvVars } from '../../lib/env';
31
+ import { reportLifecycleHook } from '../agent-lifecycle';
30
32
  import { recordToolUseEvent, type ToolPayload } from '../context-post-tool';
31
33
  import { recordSessionStart } from '../context-session-start';
32
34
  import { recordSessionEnd } from '../context-session-stop';
@@ -266,4 +268,23 @@ export default function (pi: ExtensionAPI): void {
266
268
  // fail-open
267
269
  }
268
270
  });
271
+
272
+ // ── agent-lifecycle: fleet lifecycle reporting (G73 R4, task 1080) ──
273
+ // Pi has no native `blocked` signal (herdr finding P2), so only working/idle are
274
+ // reported here; a no-op outside a fleet, like every other hook path.
275
+ pi.on('agent_start', async () => {
276
+ try {
277
+ reportLifecycleHook({ hook_event_name: 'agent_start' });
278
+ } catch {
279
+ // fail-open
280
+ }
281
+ });
282
+
283
+ pi.on('agent_settled', async () => {
284
+ try {
285
+ reportLifecycleHook({ hook_event_name: 'agent_settled' });
286
+ } catch {
287
+ // fail-open
288
+ }
289
+ });
269
290
  }