@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/config/config.global.yaml +17 -0
- package/config/decisions/task-pipeline.yaml +55 -0
- package/config/plugin-scripts.json +10 -0
- package/config/rules/boundary/sp-runtime-path.yaml +2 -0
- package/config/rules/quality/coverage-gate.yaml +5 -3
- package/config/rules/strict/runtime-boundaries.yaml +11 -0
- package/config/rules/structure/protected-files.yaml +3 -0
- package/config/workflows/task-pipeline.yaml +178 -18
- package/package.json +9 -9
- package/plugins/sp/README.md +14 -1
- package/plugins/sp/agents/super-reviewer.md +25 -11
- package/plugins/sp/commands/dev-changelog.md +5 -4
- package/plugins/sp/commands/dev-review-session.md +12 -7
- package/plugins/sp/commands/dev-run.md +5 -2
- package/plugins/sp/commands/dev-runall.md +5 -2
- package/plugins/sp/hooks/agent-lifecycle.ts +155 -0
- package/plugins/sp/hooks/fleet-guest-stop.ts +180 -0
- package/plugins/sp/hooks/hooks.json +45 -1
- package/plugins/sp/hooks/pi/guard-extension.ts +21 -0
- package/plugins/sp/lib/history-anatomy.generated.mjs +5 -0
- package/plugins/sp/lib/idea-handoff.generated.mjs +34 -9
- package/plugins/sp/lib/inline-run.generated.d.mts +1 -0
- package/plugins/sp/lib/inline-run.generated.mjs +49 -19
- package/plugins/sp/lib/transcript.ts +160 -0
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/scripts/history-anatomy-cache.mjs +5 -0
- package/plugins/sp/scripts/inline-run-setup.mjs +21 -20
- package/plugins/sp/scripts/inline-run-setup.ts +25 -31
- package/plugins/sp/scripts/residual-scan.mjs +2 -7
- package/plugins/sp/scripts/residual-scan.ts +7 -8
- package/plugins/sp/scripts/run-summary.mjs +286 -0
- package/plugins/sp/scripts/run-summary.ts +241 -0
- package/plugins/sp/scripts/session-timeline.mjs +234 -0
- package/plugins/sp/scripts/session-timeline.ts +152 -0
- package/plugins/sp/skills/code-verification/SKILL.md +5 -0
- package/plugins/sp/skills/fleet-join/SKILL.md +63 -0
- package/plugins/sp/skills/session-review/SKILL.md +46 -24
- package/plugins/sp/skills/spur-cli/SKILL.md +1 -0
- package/plugins/sp/skills/spur-cli/references/agent.md +108 -6
- package/plugins/sp/skills/spur-cli/references/decision.md +97 -0
- package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +22 -0
- package/plugins/sp/skills/spur-composer/SKILL.md +27 -1
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +28 -16
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +64 -7
- package/plugins/sp/skills/spur-dev/references/execution-workflow.md +14 -0
- package/plugins/sp/skills/spur-dev/references/flag-glossary.md +16 -4
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +32 -7
- package/plugins/sp/skills/spur-dev/references/planning-workflow.md +6 -0
- package/schemas/decision-catalog.schema.json +226 -0
- package/schemas/spur-config.schema.json +25 -0
- package/spur.js +7440 -2424
- package/web/_astro/BoardApp.Bzr4OXrW.js +324 -0
- package/web/_astro/{BoardApp.i4eh_fHA.js → BoardApp.VeoxxLBy.js} +1 -1
- package/web/_astro/{TaskDetail.C5Cu8LdB.js → TaskDetail.LAyR_Pvn.js} +1 -1
- package/web/_astro/{arc.Bu5XpBEz.js → arc.f4e0OYLg.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.9vHQTvhF.js → architectureDiagram-3BPJPVTR.SSIvxi-Z.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.Cn2Xl7D6.js → blockDiagram-GPEHLZMM.DSXIHOFf.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.7o0P_QDo.js → c4Diagram-AAUBKEIU.DG9Ubu85.js} +1 -1
- package/web/_astro/channel.CUFdtWhm.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.C3zZFsNt.js → chunk-2J33WTMH.C7QU88Qh.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.CNy_M7fO.js → chunk-4BX2VUAB.26Z94xBj.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.2TiPnLBL.js → chunk-55IACEB6.DYRiBkqw.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.5CGSyKAp.js → chunk-727SXJPM.D5Vfv5pe.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.w-ZIgfsK.js → chunk-AQP2D5EJ.DXHV47bN.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.BkcEyhKa.js → chunk-FMBD7UC4.D1suAfOs.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.mxKKbBG4.js → chunk-ND2GUHAM.D6BmdHEY.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.cyA0z9aT.js → chunk-QZHKN3VN.rnPU-Ak-.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.CqtUzwhX.js → classDiagram-4FO5ZUOK.CvEfXIo1.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.CqtUzwhX.js → classDiagram-v2-Q7XG4LA2.CvEfXIo1.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.BExPTopI.js → cose-bilkent-S5V4N54A.DkMisLSm.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.Bq2i10Cn.js → cynefin-OW5HDTMX.C77Q-pF6.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.By_HsOuv.js → dagre-BM42HDAG.YaV8ce27.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.BtU0b7hz.js → diagram-2AECGRRQ.CUyGEKge.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.D00ztMdT.js → diagram-5GNKFQAL.CRgd8dgj.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.DMVJWaUN.js → diagram-KO2AKTUF.Bmzys9hq.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.Cxqrjg5-.js → diagram-LMA3HP47.BvmJY4Lw.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.2lz_1B5h.js → diagram-OG6HWLK6.0hEhD30d.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.BVQEuJ3T.js → erDiagram-TEJ5UH35.Bjt8lrnm.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.Qlyo90ky.js → flowDiagram-I6XJVG4X.CJZTjRDV.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.DRwb8HHZ.js → ganttDiagram-6RSMTGT7._VqoVafc.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.YfoPthfS.js → gitGraphDiagram-PVQCEYII.CEDb7tVo.js} +1 -1
- package/web/_astro/index.Dg_3bOaJ.css +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.DvFREck1.js → infoDiagram-5YYISTIA.CgwifPpK.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.BzCj9Q0-.js → ishikawaDiagram-YF4QCWOH.B99wx2J1.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.BDMT_AFb.js → journeyDiagram-JHISSGLW.2hRwJbEo.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.D6Fh3gj9.js → kanban-definition-UN3LZRKU.BahJkKNC.js} +1 -1
- package/web/_astro/{linear.Dxw00fGQ.js → linear.DguUUe6V.js} +1 -1
- package/web/_astro/{mermaid.core.CsoxUWa0.js → mermaid.core.CvgfmqVr.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.CN8oeHLu.js → mindmap-definition-RKZ34NQL.B3qEA3x9.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.C7ADqQ_7.js → pieDiagram-4H26LBE5.FlWA-_ru.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.Cw9lKyHP.js → quadrantDiagram-W4KKPZXB.9bVn35X7.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.B1ni-z4q.js → requirementDiagram-4Y6WPE33.ChykIhct.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.BhexdjVr.js → sankeyDiagram-5OEKKPKP.Z2Y3hkYb.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.C3Rm7dpQ.js → sequenceDiagram-3UESZ5HK.J7JsHWDt.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.CkuMt27u.js → stateDiagram-AJRCARHV.DAPbiCSS.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.Dc_aazUA.js → stateDiagram-v2-BHNVJYJU.D2jvSi0q.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.ja7sUFhH.js → timeline-definition-PNZ67QCA.psW2ci29.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.BxIshX_K.js → vennDiagram-CIIHVFJN.DlILYVmg.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.DfHlC0sh.js → wardleyDiagram-YWT4CUSO.xNWwLJnn.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.DSnhHwCh.js → xychartDiagram-2RQKCTM6.EU_BuH7V.js} +1 -1
- package/web/board-runtime.json +1 -1
- package/web/index.html +2 -2
- package/web/_astro/BoardApp.BVp-LYi6.js +0 -322
- package/web/_astro/channel.6IFIKBhu.js +0 -1
- 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 `--
|
|
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
|
|
16
|
-
|
|
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
|
-
| `--
|
|
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
|
}
|