@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/config/config.global.yaml +17 -0
- package/config/decisions/gates.yaml +36 -0
- package/config/decisions/history-anatomy.yaml +35 -0
- package/config/decisions/idea-pipeline.yaml +38 -0
- package/config/decisions/task-pipeline.yaml +55 -0
- package/config/plugin-scripts.json +25 -0
- package/config/rules/boundary/sp-runtime-path.yaml +2 -0
- package/config/rules/docs/sourced-api-claims.yaml +60 -0
- package/config/rules/quality/coverage-gate.yaml +5 -3
- package/config/rules/recommended-pre-check.yaml +8 -3
- package/config/rules/strict/runtime-boundaries.yaml +11 -0
- package/config/rules/structure/protected-files.yaml +3 -0
- package/config/rules/structure/scratch-evidence-pointer.yaml +35 -0
- package/config/script-placement-baseline.json +2 -2
- package/config/workflows/feature-lifecycle.yaml +9 -2
- package/config/workflows/history-anatomy.yaml +22 -0
- package/config/workflows/idea-pipeline.yaml +75 -0
- package/config/workflows/task-pipeline.yaml +368 -49
- package/config/workflows/wrapup-pipeline.yaml +106 -13
- package/package.json +9 -9
- package/plugins/sp/README.md +25 -2
- package/plugins/sp/agents/super-planner.md +11 -0
- package/plugins/sp/agents/super-reviewer.md +39 -11
- package/plugins/sp/bunfig.toml +8 -0
- package/plugins/sp/commands/dev-brainstorm.md +29 -16
- package/plugins/sp/commands/dev-changelog.md +5 -4
- package/plugins/sp/commands/dev-find-way.md +80 -0
- package/plugins/sp/commands/dev-idea.md +6 -0
- package/plugins/sp/commands/dev-parallel.md +8 -2
- package/plugins/sp/commands/dev-plan.md +6 -0
- package/plugins/sp/commands/dev-review-session.md +14 -7
- package/plugins/sp/commands/dev-run.md +17 -4
- package/plugins/sp/commands/dev-runall.md +21 -10
- package/plugins/sp/hooks/agent-lifecycle.ts +155 -0
- package/plugins/sp/hooks/context-session-start.ts +1 -1
- 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 +72 -11
- package/plugins/sp/lib/inline-run-app.ts +28 -0
- package/plugins/sp/lib/inline-run.generated.d.mts +6 -0
- package/plugins/sp/lib/inline-run.generated.mjs +364 -46
- package/plugins/sp/lib/plan-projection.generated.d.mts +28 -0
- package/plugins/sp/lib/plan-projection.generated.mjs +255 -0
- package/plugins/sp/lib/quality-gate.generated.d.mts +47 -1
- package/plugins/sp/lib/quality-gate.generated.mjs +30489 -82
- package/plugins/sp/lib/residual-scan.generated.d.mts +19 -0
- package/plugins/sp/lib/residual-scan.generated.mjs +66 -0
- package/plugins/sp/lib/run-summary-core.ts +561 -0
- package/plugins/sp/lib/transcript.ts +275 -0
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/references/roles.md +1 -1
- package/plugins/sp/scripts/batch-plan.mjs +335 -0
- package/plugins/sp/scripts/batch-plan.ts +100 -0
- package/plugins/sp/scripts/batch-preflight.mjs +51 -3
- package/plugins/sp/scripts/batch-preflight.ts +72 -1
- package/plugins/sp/scripts/commit-guard.mjs +216 -0
- package/plugins/sp/scripts/commit-guard.ts +249 -0
- package/plugins/sp/scripts/history-anatomy-cache.mjs +5 -0
- package/plugins/sp/scripts/inline-run-dispatch.mjs +130 -0
- package/plugins/sp/scripts/inline-run-dispatch.ts +135 -0
- package/plugins/sp/scripts/inline-run-setup.mjs +563 -91
- package/plugins/sp/scripts/inline-run-setup.ts +65 -97
- package/plugins/sp/scripts/persist-out-check.mjs +90 -41
- package/plugins/sp/scripts/persist-out-check.ts +112 -57
- package/plugins/sp/scripts/quality-gate.mjs +30407 -73
- package/plugins/sp/scripts/quality-gate.ts +21 -5
- package/plugins/sp/scripts/residual-scan.mjs +88 -38
- package/plugins/sp/scripts/residual-scan.ts +35 -38
- package/plugins/sp/scripts/run-summary.mjs +693 -0
- package/plugins/sp/scripts/run-summary.ts +105 -0
- package/plugins/sp/scripts/session-timeline.mjs +331 -0
- package/plugins/sp/scripts/session-timeline.ts +204 -0
- package/plugins/sp/scripts/wrapup-steps.mjs +150 -2
- package/plugins/sp/scripts/wrapup-steps.ts +203 -1
- package/plugins/sp/skills/brainstorm/SKILL.md +9 -47
- package/plugins/sp/skills/branch-workflow/SKILL.md +1 -0
- package/plugins/sp/skills/branch-workflow/references/worktree-patterns.md +1 -0
- package/plugins/sp/skills/code-implementation/SKILL.md +48 -1
- package/plugins/sp/skills/code-verification/SKILL.md +8 -4
- package/plugins/sp/skills/code-verification/references/secu-review.md +3 -0
- package/plugins/sp/skills/code-verification/references/verdict-schema.md +68 -0
- package/plugins/sp/skills/fleet-join/SKILL.md +63 -0
- package/plugins/sp/skills/session-review/SKILL.md +77 -24
- package/plugins/sp/skills/source-driven-development/SKILL.md +5 -0
- package/plugins/sp/skills/spec-decomposition/references/decomposition.md +5 -3
- package/plugins/sp/skills/spur-cli/SKILL.md +6 -2
- package/plugins/sp/skills/spur-cli/references/agent.md +124 -17
- package/plugins/sp/skills/spur-cli/references/decision.md +98 -0
- package/plugins/sp/skills/spur-cli/references/message.md +7 -7
- package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +41 -3
- package/plugins/sp/skills/spur-cli/references/tasks.md +10 -7
- package/plugins/sp/skills/spur-cli/references/workflows/authoring-workflows.md +7 -0
- package/plugins/sp/skills/spur-cli/references/workflows.md +7 -2
- package/plugins/sp/skills/spur-composer/SKILL.md +28 -2
- package/plugins/sp/skills/spur-dev/SKILL.md +18 -14
- package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +6 -3
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +49 -14
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +51 -18
- package/plugins/sp/skills/spur-dev/references/execution-batch-continuation.md +68 -0
- package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +194 -0
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +113 -963
- package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +130 -0
- package/plugins/sp/skills/spur-dev/references/execution-workflow.md +20 -0
- package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +471 -0
- package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +379 -0
- package/plugins/sp/skills/spur-dev/references/flag-glossary.md +51 -9
- package/plugins/sp/skills/spur-dev/references/glossary.md +2 -2
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +361 -156
- package/plugins/sp/skills/spur-dev/references/planning-workflow.md +6 -0
- package/plugins/sp/skills/spur-dev/references/structured-trace-emission.md +178 -0
- package/plugins/sp/skills/wayfinder/SKILL.md +10 -14
- package/schemas/decision-catalog.schema.json +226 -0
- package/schemas/spur-config.schema.json +25 -0
- package/schemas/state-machine-workflow.schema.json +23 -0
- package/spur.js +28165 -19051
- package/web/_astro/{BoardApp.i4eh_fHA.js → BoardApp.D66k_s_T.js} +1 -1
- package/web/_astro/BoardApp.D6sjAJr_.js +324 -0
- package/web/_astro/{TaskDetail.C5Cu8LdB.js → TaskDetail.BynLnMNc.js} +1 -1
- package/web/_astro/{arc.Bu5XpBEz.js → arc._BUadrdH.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.9vHQTvhF.js → architectureDiagram-3BPJPVTR.Dh7xS1Pp.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.Cn2Xl7D6.js → blockDiagram-GPEHLZMM.DFDVpfXm.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.7o0P_QDo.js → c4Diagram-AAUBKEIU.V7_3_glh.js} +1 -1
- package/web/_astro/channel.DZVfto-r.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.C3zZFsNt.js → chunk-2J33WTMH.De1yx7Jz.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.CNy_M7fO.js → chunk-4BX2VUAB.BDhHFuxx.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.2TiPnLBL.js → chunk-55IACEB6.DDLkWnmC.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.5CGSyKAp.js → chunk-727SXJPM.CwfiLnA7.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.w-ZIgfsK.js → chunk-AQP2D5EJ.CtGDmUnM.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.BkcEyhKa.js → chunk-FMBD7UC4.BOLjpw73.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.mxKKbBG4.js → chunk-ND2GUHAM.BDhQls6O.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.cyA0z9aT.js → chunk-QZHKN3VN.76qBynDF.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.CqtUzwhX.js → classDiagram-4FO5ZUOK.N0mpPHT0.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.CqtUzwhX.js → classDiagram-v2-Q7XG4LA2.N0mpPHT0.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.BExPTopI.js → cose-bilkent-S5V4N54A.CKknFfjB.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.Bq2i10Cn.js → cynefin-OW5HDTMX.Cw3ZWBAh.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.By_HsOuv.js → dagre-BM42HDAG.D3yIKCRD.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.BtU0b7hz.js → diagram-2AECGRRQ.D51dO83Z.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.D00ztMdT.js → diagram-5GNKFQAL.Bb-FViDj.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.DMVJWaUN.js → diagram-KO2AKTUF.D3mN8S6x.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.Cxqrjg5-.js → diagram-LMA3HP47.dMMyPcbJ.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.2lz_1B5h.js → diagram-OG6HWLK6.AzbvzY4C.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.BVQEuJ3T.js → erDiagram-TEJ5UH35.BR5I4OZb.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.Qlyo90ky.js → flowDiagram-I6XJVG4X.DFqvhGkn.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.DRwb8HHZ.js → ganttDiagram-6RSMTGT7.ByJbhH6J.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.YfoPthfS.js → gitGraphDiagram-PVQCEYII.CInHjQR3.js} +1 -1
- package/web/_astro/index.COh_kBxN.css +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.DvFREck1.js → infoDiagram-5YYISTIA.Cm0dGVjJ.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.BzCj9Q0-.js → ishikawaDiagram-YF4QCWOH.C3nOP6Sj.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.BDMT_AFb.js → journeyDiagram-JHISSGLW.4IvLa2MW.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.D6Fh3gj9.js → kanban-definition-UN3LZRKU.DS87Cl-k.js} +1 -1
- package/web/_astro/{linear.Dxw00fGQ.js → linear.MzmRrNER.js} +1 -1
- package/web/_astro/{mermaid.core.CsoxUWa0.js → mermaid.core.fK-WNswS.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.CN8oeHLu.js → mindmap-definition-RKZ34NQL.BqipOHhY.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.C7ADqQ_7.js → pieDiagram-4H26LBE5.0nBiPqFG.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.Cw9lKyHP.js → quadrantDiagram-W4KKPZXB.Szj7Zpx5.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.B1ni-z4q.js → requirementDiagram-4Y6WPE33.D4UR6TWo.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.BhexdjVr.js → sankeyDiagram-5OEKKPKP.DAE7BdOs.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.C3Rm7dpQ.js → sequenceDiagram-3UESZ5HK.BP9ZVq25.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.CkuMt27u.js → stateDiagram-AJRCARHV.BQW7mIxk.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.Dc_aazUA.js → stateDiagram-v2-BHNVJYJU.PbK4nDih.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.ja7sUFhH.js → timeline-definition-PNZ67QCA.Ci_GvUpo.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.BxIshX_K.js → vennDiagram-CIIHVFJN.DNw1Y5qx.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.DfHlC0sh.js → wardleyDiagram-YWT4CUSO.CwlkaaNe.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.DSnhHwCh.js → xychartDiagram-2RQKCTM6.MI6ZuiH_.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
|
@@ -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 `--
|
|
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. 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
|
-
| `--
|
|
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) —
|
|
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-
|
|
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
|
-
(
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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) —
|
|
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` (
|
|
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
|
+
}
|