@gobing-ai/spur 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/config.global.yaml +4 -2
  3. package/config/decisions/maker-liveness.yaml +39 -0
  4. package/config/plugin-scripts.json +0 -4
  5. package/config/rules/README.md +1 -0
  6. package/config/rules/docs/sourced-api-claims.yaml +56 -12
  7. package/config/rules/strict/runtime-boundaries.yaml +7 -0
  8. package/config/script-placement-baseline.json +1 -19
  9. package/config/templates/AGENTS.md +1 -1
  10. package/config/workflows/feature-lifecycle.yaml +46 -1
  11. package/config/workflows/feature-verification.yaml +11 -0
  12. package/config/workflows/wrapup-pipeline.yaml +1 -1
  13. package/package.json +10 -10
  14. package/plugins/sp/README.md +23 -18
  15. package/plugins/sp/agents/super-planner.md +5 -6
  16. package/plugins/sp/agents/super-reviewer.md +43 -0
  17. package/plugins/sp/bunfig.toml +8 -5
  18. package/plugins/sp/commands/dev-review-session.md +2 -1
  19. package/plugins/sp/commands/dev-runall.md +18 -9
  20. package/plugins/sp/hooks/careful-guard.ts +1 -1
  21. package/plugins/sp/hooks/context-post-tool.ts +7 -404
  22. package/plugins/sp/hooks/context-session-start.ts +6 -192
  23. package/plugins/sp/hooks/context-session-stop.ts +6 -91
  24. package/plugins/sp/hooks/pi/guard-extension.ts +26 -12
  25. package/plugins/sp/hooks/task-write-guard.ts +1 -1
  26. package/plugins/sp/{hooks → lib}/agent-hint.ts +1 -1
  27. package/plugins/sp/lib/context-owner.ts +56 -0
  28. package/plugins/sp/lib/context-post-tool.ts +432 -0
  29. package/plugins/sp/lib/context-session-start.ts +214 -0
  30. package/plugins/sp/lib/context-session-stop.ts +196 -0
  31. package/plugins/sp/lib/dogfood-pipeline.ts +265 -0
  32. package/plugins/sp/lib/dogfood-report.ts +278 -0
  33. package/plugins/sp/lib/idea-handoff.generated.mjs +6 -4
  34. package/plugins/sp/lib/inline-run-app.ts +40 -0
  35. package/plugins/sp/lib/inline-run.generated.d.mts +3 -3
  36. package/plugins/sp/lib/inline-run.generated.mjs +1361 -87
  37. package/plugins/sp/lib/plan-projection.generated.d.mts +169 -0
  38. package/plugins/sp/lib/plan-projection.generated.mjs +704 -1
  39. package/plugins/sp/lib/quality-gate.generated.d.mts +0 -12
  40. package/plugins/sp/lib/quality-gate.generated.mjs +62 -231
  41. package/plugins/sp/lib/residual-scan.generated.d.mts +7 -1
  42. package/plugins/sp/lib/residual-scan.generated.mjs +71 -6
  43. package/plugins/sp/lib/run-summary-core.ts +34 -370
  44. package/plugins/sp/lib/transcript.ts +37 -200
  45. package/plugins/sp/plugin.json +1 -1
  46. package/plugins/sp/references/roles.md +1 -1
  47. package/plugins/sp/scripts/batch-plan.mjs +740 -1
  48. package/plugins/sp/scripts/batch-plan.ts +48 -3
  49. package/plugins/sp/scripts/batch-preflight.mjs +23 -1
  50. package/plugins/sp/scripts/batch-preflight.ts +35 -295
  51. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +108 -87
  52. package/plugins/sp/scripts/daily-summary/daily-summary.ts +111 -104
  53. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +2 -1
  54. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +7 -265
  55. package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +3 -0
  56. package/plugins/sp/scripts/dogfood-testing/validate-report.ts +6 -272
  57. package/plugins/sp/scripts/feature-verification-steps.mjs +7 -123
  58. package/plugins/sp/scripts/feature-verification-steps.ts +29 -181
  59. package/plugins/sp/scripts/inline-run-dispatch.mjs +27 -26
  60. package/plugins/sp/scripts/inline-run-dispatch.ts +1 -21
  61. package/plugins/sp/scripts/inline-run-setup.mjs +158 -120
  62. package/plugins/sp/scripts/inline-run-setup.ts +1 -21
  63. package/plugins/sp/scripts/pr-reviewing.mjs +19 -5
  64. package/plugins/sp/scripts/pr-reviewing.ts +34 -9
  65. package/plugins/sp/scripts/quality-gate.mjs +38 -208
  66. package/plugins/sp/scripts/residual-scan.mjs +73 -6
  67. package/plugins/sp/scripts/residual-scan.ts +8 -5
  68. package/plugins/sp/scripts/run-summary.mjs +160 -95
  69. package/plugins/sp/scripts/run-summary.ts +7 -5
  70. package/plugins/sp/scripts/session-timeline.mjs +57 -29
  71. package/plugins/sp/scripts/session-timeline.ts +4 -18
  72. package/plugins/sp/scripts/task-diffstat.mjs +43 -30
  73. package/plugins/sp/scripts/task-diffstat.ts +40 -71
  74. package/plugins/sp/skills/code-implementation/SKILL.md +7 -1
  75. package/plugins/sp/skills/code-verification/references/secu-review.md +2 -1
  76. package/plugins/sp/skills/code-verification/references/verdict-schema.md +4 -0
  77. package/plugins/sp/skills/dogfood-testing/SKILL.md +2 -2
  78. package/plugins/sp/skills/indexed-context/SKILL.md +14 -7
  79. package/plugins/sp/skills/next-feature/SKILL.md +1 -1
  80. package/plugins/sp/skills/parallel-execution/SKILL.md +2 -2
  81. package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +2 -2
  82. package/plugins/sp/skills/parallel-execution/references/fan-out-patterns.md +2 -2
  83. package/plugins/sp/skills/pr-reviewing/SKILL.md +7 -3
  84. package/plugins/sp/skills/session-review/SKILL.md +5 -3
  85. package/plugins/sp/skills/source-driven-development/SKILL.md +7 -4
  86. package/plugins/sp/skills/spur-cli/references/builder.md +3 -2
  87. package/plugins/sp/skills/spur-cli/references/decision.md +40 -8
  88. package/plugins/sp/skills/spur-cli/references/features.md +46 -5
  89. package/plugins/sp/skills/spur-cli/references/projects.md +7 -5
  90. package/plugins/sp/skills/spur-dev/SKILL.md +2 -0
  91. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +27 -0
  92. package/plugins/sp/skills/spur-dev/references/dev-operations.md +2 -3
  93. package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +6 -1
  94. package/plugins/sp/skills/spur-dev/references/execution-batch.md +56 -7
  95. package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +4 -1
  96. package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +22 -0
  97. package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +14 -0
  98. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +13 -11
  99. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +37 -25
  100. package/spur.js +89792 -76992
  101. package/web/_astro/{BoardApp.D66k_s_T.js → BoardApp.5IIl2O-z.js} +1 -1
  102. package/web/_astro/BoardApp.Drnx-KeM.js +324 -0
  103. package/web/_astro/TaskDetail.Ci4LNn9S.js +1 -0
  104. package/web/_astro/{arc._BUadrdH.js → arc.j2EVkYVd.js} +1 -1
  105. package/web/_astro/{architectureDiagram-3BPJPVTR.Dh7xS1Pp.js → architectureDiagram-3BPJPVTR.DZWSFqEG.js} +1 -1
  106. package/web/_astro/{blockDiagram-GPEHLZMM.DFDVpfXm.js → blockDiagram-GPEHLZMM.DE6GpXCN.js} +1 -1
  107. package/web/_astro/{c4Diagram-AAUBKEIU.V7_3_glh.js → c4Diagram-AAUBKEIU.DoVR1G6B.js} +1 -1
  108. package/web/_astro/channel.DmdDxGsq.js +1 -0
  109. package/web/_astro/{chunk-2J33WTMH.De1yx7Jz.js → chunk-2J33WTMH.Do6-Gbyf.js} +1 -1
  110. package/web/_astro/{chunk-4BX2VUAB.BDhHFuxx.js → chunk-4BX2VUAB.BYV1HGHx.js} +1 -1
  111. package/web/_astro/{chunk-55IACEB6.DDLkWnmC.js → chunk-55IACEB6.CvI1zkrc.js} +1 -1
  112. package/web/_astro/{chunk-727SXJPM.CwfiLnA7.js → chunk-727SXJPM.DNGSCRlX.js} +1 -1
  113. package/web/_astro/{chunk-AQP2D5EJ.CtGDmUnM.js → chunk-AQP2D5EJ.DXhhgT5z.js} +1 -1
  114. package/web/_astro/{chunk-FMBD7UC4.BOLjpw73.js → chunk-FMBD7UC4.dzkRKP2u.js} +1 -1
  115. package/web/_astro/{chunk-ND2GUHAM.BDhQls6O.js → chunk-ND2GUHAM.DujGLeEf.js} +1 -1
  116. package/web/_astro/{chunk-QZHKN3VN.76qBynDF.js → chunk-QZHKN3VN.BWtIT3ZU.js} +1 -1
  117. package/web/_astro/{classDiagram-4FO5ZUOK.N0mpPHT0.js → classDiagram-4FO5ZUOK.DyApg0Nn.js} +1 -1
  118. package/web/_astro/{classDiagram-v2-Q7XG4LA2.N0mpPHT0.js → classDiagram-v2-Q7XG4LA2.DyApg0Nn.js} +1 -1
  119. package/web/_astro/{cose-bilkent-S5V4N54A.CKknFfjB.js → cose-bilkent-S5V4N54A.DJQB-u_l.js} +1 -1
  120. package/web/_astro/{cynefin-OW5HDTMX.Cw3ZWBAh.js → cynefin-OW5HDTMX.bi3D_siV.js} +1 -1
  121. package/web/_astro/{dagre-BM42HDAG.D3yIKCRD.js → dagre-BM42HDAG.C-f7TrRg.js} +1 -1
  122. package/web/_astro/{diagram-2AECGRRQ.D51dO83Z.js → diagram-2AECGRRQ.DrPfjHQB.js} +1 -1
  123. package/web/_astro/{diagram-5GNKFQAL.Bb-FViDj.js → diagram-5GNKFQAL.C9S4WUHO.js} +1 -1
  124. package/web/_astro/{diagram-KO2AKTUF.D3mN8S6x.js → diagram-KO2AKTUF.Dzzye8QH.js} +1 -1
  125. package/web/_astro/{diagram-LMA3HP47.dMMyPcbJ.js → diagram-LMA3HP47.DX-LRiof.js} +1 -1
  126. package/web/_astro/{diagram-OG6HWLK6.AzbvzY4C.js → diagram-OG6HWLK6.Z-kAKwIz.js} +1 -1
  127. package/web/_astro/{erDiagram-TEJ5UH35.BR5I4OZb.js → erDiagram-TEJ5UH35.D6dyClSv.js} +1 -1
  128. package/web/_astro/{flowDiagram-I6XJVG4X.DFqvhGkn.js → flowDiagram-I6XJVG4X.Cx3q2cxD.js} +1 -1
  129. package/web/_astro/{ganttDiagram-6RSMTGT7.ByJbhH6J.js → ganttDiagram-6RSMTGT7.DU3gH6CT.js} +1 -1
  130. package/web/_astro/{gitGraphDiagram-PVQCEYII.CInHjQR3.js → gitGraphDiagram-PVQCEYII.ISmCnJ87.js} +1 -1
  131. package/web/_astro/index.Do2lLdgo.css +1 -0
  132. package/web/_astro/{infoDiagram-5YYISTIA.Cm0dGVjJ.js → infoDiagram-5YYISTIA.DR6_iNOK.js} +1 -1
  133. package/web/_astro/{ishikawaDiagram-YF4QCWOH.C3nOP6Sj.js → ishikawaDiagram-YF4QCWOH.D7wY-ZGA.js} +1 -1
  134. package/web/_astro/{journeyDiagram-JHISSGLW.4IvLa2MW.js → journeyDiagram-JHISSGLW.DkP9-WK8.js} +1 -1
  135. package/web/_astro/{kanban-definition-UN3LZRKU.DS87Cl-k.js → kanban-definition-UN3LZRKU.wtgY98M2.js} +1 -1
  136. package/web/_astro/{linear.MzmRrNER.js → linear.CI6djNi7.js} +1 -1
  137. package/web/_astro/{mermaid.core.fK-WNswS.js → mermaid.core.CuQPxS8b.js} +4 -4
  138. package/web/_astro/{mindmap-definition-RKZ34NQL.BqipOHhY.js → mindmap-definition-RKZ34NQL.5HNIEG7R.js} +1 -1
  139. package/web/_astro/{pieDiagram-4H26LBE5.0nBiPqFG.js → pieDiagram-4H26LBE5.CchhDFjE.js} +1 -1
  140. package/web/_astro/{quadrantDiagram-W4KKPZXB.Szj7Zpx5.js → quadrantDiagram-W4KKPZXB.PYXXJPSJ.js} +1 -1
  141. package/web/_astro/{requirementDiagram-4Y6WPE33.D4UR6TWo.js → requirementDiagram-4Y6WPE33.cUQuNAKU.js} +1 -1
  142. package/web/_astro/{sankeyDiagram-5OEKKPKP.DAE7BdOs.js → sankeyDiagram-5OEKKPKP.BO7MnRIA.js} +1 -1
  143. package/web/_astro/{sequenceDiagram-3UESZ5HK.BP9ZVq25.js → sequenceDiagram-3UESZ5HK.D9bElyD6.js} +1 -1
  144. package/web/_astro/{stateDiagram-AJRCARHV.BQW7mIxk.js → stateDiagram-AJRCARHV.DhGdRz7I.js} +1 -1
  145. package/web/_astro/{stateDiagram-v2-BHNVJYJU.PbK4nDih.js → stateDiagram-v2-BHNVJYJU.5GvLeAoL.js} +1 -1
  146. package/web/_astro/{timeline-definition-PNZ67QCA.Ci_GvUpo.js → timeline-definition-PNZ67QCA.LtAT9haM.js} +1 -1
  147. package/web/_astro/{vennDiagram-CIIHVFJN.DNw1Y5qx.js → vennDiagram-CIIHVFJN.DRth1yLA.js} +1 -1
  148. package/web/_astro/{wardleyDiagram-YWT4CUSO.CwlkaaNe.js → wardleyDiagram-YWT4CUSO.h9MlZfcr.js} +1 -1
  149. package/web/_astro/{xychartDiagram-2RQKCTM6.MI6ZuiH_.js → xychartDiagram-2RQKCTM6.DHqy9DYA.js} +1 -1
  150. package/web/index.html +2 -2
  151. package/config/proportional-route-table.ts +0 -155
  152. package/plugins/sp/commands/dev-parallel.md +0 -37
  153. package/plugins/sp/scripts/daily-summary/logger.ts +0 -28
  154. package/web/_astro/BoardApp.D6sjAJr_.js +0 -324
  155. package/web/_astro/TaskDetail.BynLnMNc.js +0 -1
  156. package/web/_astro/channel.DZVfto-r.js +0 -1
  157. package/web/_astro/index.COh_kBxN.css +0 -1
  158. /package/plugins/sp/{hooks → lib}/destructive-policy.ts +0 -0
  159. /package/plugins/sp/{hooks → lib}/task-file-policy.ts +0 -0
@@ -18,7 +18,8 @@ work time, operator wait, tool calls and tokens (total / non-cached) per stage,
18
18
  `session-timeline.mjs` — with durations in `M:SS` or `H:MM:SS` form and `n/a` only when that
19
19
  measurement is unavailable. The transcript resolves from `--transcript`, else
20
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.
21
+ pi `<skill name="…">` wrapper row is the operator's `/skill` invocation and opens a segment
22
+ rendered `/<name> <args>` (or `/<name>` argument-less), never an injected body.
22
23
 
23
24
  ## Argument Flags
24
25
 
@@ -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>]] [--no-summary] [--defer-gate]"
4
+ argument-hint: "--tasks <selector> [--feature <id>] [--mode <sequential|parallel|auto>] [--concurrency <n>] [--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
 
@@ -15,10 +15,10 @@ Wraps the **sp:spur-dev** skill.
15
15
  | --- | --- | --- |
16
16
  | `--tasks` `<selector>` | Task selector to run. | required |
17
17
  | `--feature` `<id>` | Restrict to a feature. | omitted |
18
- | `--mode` `<sequential\|parallel>` | Batch execution order. | sequential |
18
+ | `--mode` `<sequential\|parallel\|auto>` | Batch execution order. `auto` computes the frozen batch's dependency waves and runs parallel only when a wave holds ≥2 independent tasks (see below). | sequential |
19
19
  | `--keep-going` | Continue past per-task failures. | off |
20
20
  | `--auto` | Skip objective HITL gates. | off |
21
- | `--agent` `<inline\|auto\|name>` | Who runs each task's pipeline stages. Interactive sequential omit/`inline` uses the host-session driver with unified inline semantics (task 0687 — see below)). `auto`, a name, parallel mode, and headless invocation use subprocesses. The opt-in `fleet` selector (0942) maps each task run to `executor: 'fleet'` so `agent.run` stages dispatch through the fleet control plane (ADR-126) instead of a subprocess. | omit |
21
+ | `--agent` `<inline\|auto\|name>` | Who runs each task's pipeline stages. Interactive sequential omit/`inline` uses the host-session driver with unified inline semantics (task 0687 — see below). `auto`, a name, parallel mode, and headless invocation use subprocesses. The opt-in `fleet` selector (0942) maps each task run to `executor: 'fleet'` so `agent.run` stages dispatch through the fleet control plane (ADR-126) instead of a subprocess. | omit |
22
22
  | `--json` | Emit structured JSON. | off |
23
23
  | `--wrap` | Run the wrap hop **once for the batch** over the `done` subset only (Step 6 of execution-batch.md): `vars.feature` is passed only when every frozen task is `done`/`cancelled`; an empty done subset skips the wrap with a reason. The `--agent` selector is preserved into the `/sp:dev-wrap` handoff when supplied; omission remains omission. | off |
24
24
  | `--next` | Chain-to-completion via the next-router. | off |
@@ -26,14 +26,14 @@ Wraps the **sp:spur-dev** skill.
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
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) |
28
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 |
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`, or `--mode auto` resolved to parallel (dropped with a note otherwise); default off. | off |
30
30
 
31
31
  For shared semantics, see the [flag glossary](../skills/spur-dev/references/flag-glossary.md).
32
32
 
33
33
  ## Usage
34
34
 
35
35
  ```
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]
36
+ /sp:dev-runall --tasks <selector> [--feature <id>] [--mode <sequential|parallel|auto>] [--concurrency <n>] [--keep-going] [--auto] [--agent <inline|auto|name>] [--json] [--wrap] [--next] [--continue] [--worktree [<name>]] [--no-summary] [--defer-gate]
37
37
  ```
38
38
 
39
39
  Flags: `--tasks <selector>` (required — explicit WBS list, status pseudo-list, `feature:<id>`,
@@ -44,8 +44,8 @@ structured findings, before any task pipeline action, task 0510 R2; scoped: `L4.
44
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
45
  `NON_ABORTING_PREFLIGHT_CODES` in `plugins/sp/scripts/batch-preflight.ts`; `--fix` repairs land in the report's `autoRepairs`) —
46
46
  any other strict error aborts), `--mode`
47
- `<sequential|parallel>` (default `sequential`; `parallel` fans out a proven-independent subset —
48
- see `execution-parallel-isolation.md`), `--concurrency <n>`
47
+ `<sequential|parallel|auto>` (default `sequential`; `parallel` fans out a proven-independent subset —
48
+ see `execution-parallel-isolation.md`; `auto` picks one per batch — see below), `--concurrency <n>`
49
49
  (parallel-mode worker bound — at most <n> task pipelines at once, default 2; a dependent task starts
50
50
  only after its in-set dependencies are **integrated** onto the base ref), `--keep-going`
51
51
  (batch failure policy — skip a failed task's in-batch dependents, continue independents; default
@@ -62,6 +62,15 @@ FF-merge onto the base ref on full success, retain intact on any failure/halt/no
62
62
  creates a fresh tree, `<name>` form adopts an existing worktree by name/path/branch; see
63
63
  `execution-worktree-setup.md` + `execution-worktree-landing.md`).
64
64
 
65
+ **`--mode auto` (smart scheduling).** Ordering is always dependency-correct — every mode
66
+ topo-sorts the frozen set with `batch-plan.mjs order` (deterministic, never hand-sorted). `auto`
67
+ additionally reads that schedule's dependency waves (tasks declaring the same file are serialized)
68
+ and decides once, before the first task: `parallel` when some wave holds ≥2 independent tasks and
69
+ `--concurrency` ≥2; otherwise `sequential` in the host session. `--worktree` or explicit
70
+ `--agent inline` pins `sequential`. The batch report records `mode: auto → <resolved>` and why.
71
+ Default stays `sequential`: parallel costs ~N× tokens, worktree setup and subprocess executors, which
72
+ a dependency chain never pays back. Rules: execution-batch.md § 2.4a.
73
+
65
74
  **`--worktree` and parallel mode.** `--worktree --mode parallel` is **rejected** — parallel mode
66
75
  already isolates each task in its own worktree (see `execution-parallel-isolation.md`), and
67
76
  reuse mode (`--worktree <name>`) has no per-task meaning. Run parallel batches without
@@ -98,7 +107,7 @@ full distinction.
98
107
  § 2.7 Visible batch plan.
99
108
  - 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.
100
109
  - 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).
101
- - Interactive sequential omit/inline: `Skill(skill="sp:spur-dev", args="runall-inline $ARGUMENTS")`.
102
- - Explicit executor or parallel mode: `Skill(skill="sp:spur-dev", args="runall $ARGUMENTS")` → `sp:super-planner` agent.
110
+ - Interactive sequential omit/inline, and `--mode auto` without an explicit executor: `Skill(skill="sp:spur-dev", args="runall-inline $ARGUMENTS")` — auto resolves in the host after freeze/order (§ 2.4a) and hands a parallel resolution to the `runall` path with the frozen explicit WBS list.
111
+ - Explicit executor (any mode, including `auto`) or parallel mode: `Skill(skill="sp:spur-dev", args="runall $ARGUMENTS")` → `sp:super-planner` agent.
103
112
  - After the batch report, print the [execution summary](../skills/spur-dev/references/dev-operations.md#execution-summary) over every attempted run unless
104
113
  `--no-summary`.
@@ -22,8 +22,8 @@
22
22
  * adapter. This file owns the Claude Code payload/decision shape only.
23
23
  */
24
24
 
25
+ import { classifyCommand } from '../lib/destructive-policy';
25
26
  import { getEnvVar } from '../lib/env';
26
- import { classifyCommand } from './destructive-policy';
27
27
 
28
28
  interface ToolPayload {
29
29
  tool_name?: string;
@@ -1,415 +1,18 @@
1
1
  #!/usr/bin/env bun
2
-
3
2
  /**
4
- * context-post-tool — PostToolUse hook for indexed-context
3
+ * context-post-tool — installed PostToolUse entry for indexed-context
5
4
  * (matcher: Bash|Grep|Glob|Read|Write|Edit — task 0248).
6
5
  *
7
- * Appends one event line to `token-ledger.jsonl` per tool call: path and/or short
8
- * summary, action, and token estimate. Reads session ID (+ optional agent/model)
9
- * from `.spur/context/.session.json`.
10
- *
11
- * Token cascade (task 0246): tool_response.content → Write tool_input.content →
12
- * Edit old/new strings → Read file stat → omit tokens (never store 0 for unknown).
13
- *
14
- * Redaction (task 0248): store summary only (never full stdout); cap text ~4 KiB;
15
- * strip obvious secret patterns. Bash tokens estimate from capped stdout/stderr length.
16
- *
17
- * **Fail-open contract:** every error path exits 0 with no output.
18
- *
19
- * Self-contained by design (task 0232/0246/0248). Installed hook configs use
20
- * `superskill hook run sp context-post-tool`.
6
+ * Task 1174: the capture logic lives in `plugins/sp/lib/context-post-tool.ts`; this file is the
7
+ * argv/stdin adapter the installed `superskill hook run sp context-post-tool` entry resolves to.
8
+ * The redaction, byte-cap and fail-open contract (every error path exits 0 with no output) is
9
+ * unchanged — the adapter adds no policy of its own.
21
10
  */
22
11
 
23
- import { execSync } from 'node:child_process';
24
- import { appendFileSync, existsSync, readFileSync, statSync, writeFileSync } from 'node:fs';
25
- import { join, resolve, sep } from 'node:path';
12
+ import { join } from 'node:path';
13
+ import { recordToolUseEvent, type ToolPayload } from '../lib/context-post-tool';
26
14
  import { getEnvVar } from '../lib/env';
27
15
 
28
- /** Tools recorded by this hook (must match hooks.json PostToolUse matcher). */
29
- export const ALLOWED_TOOLS = new Set(['Bash', 'Grep', 'Glob', 'Read', 'Write', 'Edit']);
30
-
31
- /** Max stored summary / token-estimate input size (bytes). Task 0248 ~2–4 KiB. */
32
- export const REDACTION_CAP_BYTES = 4096;
33
-
34
- /** Max command/pattern summary length before ellipsis (chars). */
35
- export const SUMMARY_MAX_CHARS = 200;
36
-
37
- export interface ToolPayload {
38
- session_id?: string;
39
- tool_name?: string;
40
- tool_input?: {
41
- file_path?: string;
42
- content?: string;
43
- old_string?: string;
44
- new_string?: string;
45
- /** Bash */
46
- command?: string;
47
- /** Grep / Glob (Claude Code shapes vary) */
48
- pattern?: string;
49
- glob_pattern?: string;
50
- glob?: string;
51
- path?: string;
52
- };
53
- tool_response?: {
54
- content?: string | unknown;
55
- filePath?: string;
56
- stdout?: string;
57
- stderr?: string;
58
- };
59
- }
60
-
61
- interface SessionFile {
62
- session?: string;
63
- agent?: string;
64
- model?: string;
65
- }
66
-
67
- /** Estimate tokens from raw byte count: Math.ceil(bytes / 4). */
68
- export function estimateTokens(text: string): number {
69
- return Math.ceil(new TextEncoder().encode(text).length / 4);
70
- }
71
-
72
- /** Scrub secret-like patterns without size capping. */
73
- export function scrubSecrets(text: string): string {
74
- let s = text;
75
- // Bearer / API key prefixes first (before generic key=value)
76
- s = s.replace(/\bBearer\s+[A-Za-z0-9._\-+=/]+/gi, 'Bearer ***');
77
- // `sk-` keys. Segments may be hyphen-separated (`sk-ant-api03-…`), so match the
78
- // whole run rather than a single alphanumeric block — anchoring on `[A-Za-z0-9]{8,}`
79
- // alone stopped at the first hyphen and let every Anthropic key through, which is
80
- // the most likely secret to appear in a Claude-agent harness.
81
- s = s.replace(/\bsk-[A-Za-z0-9_-]{8,}/g, 'sk-***');
82
- s = s.replace(/\bAKIA[0-9A-Z]{16}\b/g, 'AKIA***');
83
- // Provider token prefixes with a fixed, unambiguous shape.
84
- s = s.replace(/\bgithub_pat_[A-Za-z0-9_]{20,}/g, 'github_pat_***');
85
- s = s.replace(/\bgh[pousr]_[A-Za-z0-9]{20,}/g, (m) => `${m.slice(0, 4)}***`);
86
- // Key=value style secrets (value = rest of non-space token). The key may carry a
87
- // prefix (`ANTHROPIC_API_KEY`, `GITHUB_TOKEN`): `_` is a word character, so a
88
- // leading `\b` would not match there and every namespaced env var leaked. Allow an
89
- // optional `WORD_`-style prefix instead of requiring a boundary.
90
- // The whole key (prefix included) is captured so the ledger still says *which*
91
- // variable was scrubbed — `ANTHROPIC_API_KEY=***`, not a bare `API_KEY=***`.
92
- s = s.replace(
93
- /\b((?:[A-Za-z0-9]+[_-])*(?:api[_-]?key|password|passwd|secret|token|access[_-]?key|private[_-]?key))\s*[:=]\s*\S+/gi,
94
- '$1=***',
95
- );
96
- // Authorization: <scheme> <credentials...>
97
- s = s.replace(/\bAuthorization\s*:\s*\S+(?:\s+\S+)*/gi, 'Authorization: ***');
98
- // PEM private key blocks — redact the payload, keep the header as a marker.
99
- s = s.replace(
100
- /-----BEGIN (?:[A-Z ]+ )?PRIVATE KEY-----[\s\S]*?(?:-----END (?:[A-Z ]+ )?PRIVATE KEY-----|$)/g,
101
- '[private-key-redacted]',
102
- );
103
- // Collapse long base64 only when + / or = present (avoid pure a-z body blobs)
104
- s = s.replace(/\b[A-Za-z0-9+/]{80,}={0,2}\b/g, (m) => (/[+/=]/.test(m) ? '[base64-redacted]' : m));
105
- return s;
106
- }
107
-
108
- /**
109
- * Cap text to {@link REDACTION_CAP_BYTES} and scrub obvious secret patterns.
110
- * Used for any stored summary fragments — never full env dumps.
111
- */
112
- export function redactText(text: string, capBytes: number = REDACTION_CAP_BYTES): string {
113
- const s = scrubSecrets(text);
114
- const bytes = new TextEncoder().encode(s);
115
- if (bytes.length <= capBytes) return s;
116
- // Truncate on byte boundary then append marker
117
- let end = capBytes;
118
- while (end > 0 && ((bytes[end] ?? 0) & 0xc0) === 0x80) end--;
119
- return `${new TextDecoder().decode(bytes.subarray(0, end))}…[truncated]`;
120
- }
121
-
122
- /** Byte length after scrub + hard cap (no truncation marker) — for token estimates. */
123
- export function cappedByteLength(text: string, capBytes: number = REDACTION_CAP_BYTES): number {
124
- const scrubbed = scrubSecrets(text);
125
- return Math.min(new TextEncoder().encode(scrubbed).length, capBytes);
126
- }
127
-
128
- /** Truncate a one-line summary for the ledger (after light redaction). */
129
- export function truncateSummary(text: string, maxChars: number = SUMMARY_MAX_CHARS): string {
130
- const cleaned = redactText(text.replace(/\s+/g, ' ').trim(), REDACTION_CAP_BYTES);
131
- if (cleaned.length <= maxChars) return cleaned;
132
- return `${cleaned.slice(0, Math.max(0, maxChars - 1))}…`;
133
- }
134
-
135
- /**
136
- * Map platform tool name → ledger `type` (task 0248 R2).
137
- * Edit stays `write` with action=edit (0245/0246 convention).
138
- */
139
- export function mapToolType(toolName: string): string {
140
- switch (toolName) {
141
- case 'Read':
142
- return 'read';
143
- case 'Write':
144
- case 'Edit':
145
- return 'write';
146
- case 'Bash':
147
- return 'bash';
148
- case 'Grep':
149
- return 'grep';
150
- case 'Glob':
151
- return 'glob';
152
- default:
153
- return toolName.toLowerCase();
154
- }
155
- }
156
-
157
- /**
158
- * Short summary for Bash/Grep/Glob (command truncated, pattern, path glob).
159
- * Not full stdout. Returns undefined when nothing useful to store.
160
- */
161
- export function buildToolSummary(toolName: string, toolInput: ToolPayload['tool_input']): string | undefined {
162
- if (!toolInput) return undefined;
163
- if (toolName === 'Bash') {
164
- const cmd = typeof toolInput.command === 'string' ? toolInput.command : '';
165
- if (!cmd.trim()) return undefined;
166
- return truncateSummary(cmd);
167
- }
168
- if (toolName === 'Grep') {
169
- const pattern = typeof toolInput.pattern === 'string' ? toolInput.pattern : '';
170
- const path = typeof toolInput.path === 'string' ? toolInput.path : '';
171
- const glob =
172
- (typeof toolInput.glob === 'string' && toolInput.glob) ||
173
- (typeof toolInput.glob_pattern === 'string' && toolInput.glob_pattern) ||
174
- '';
175
- const parts = [pattern && `/${pattern}/`, path, glob].filter(Boolean);
176
- if (parts.length === 0) return undefined;
177
- return truncateSummary(parts.join(' '));
178
- }
179
- if (toolName === 'Glob') {
180
- const pattern =
181
- (typeof toolInput.pattern === 'string' && toolInput.pattern) ||
182
- (typeof toolInput.glob_pattern === 'string' && toolInput.glob_pattern) ||
183
- (typeof toolInput.glob === 'string' && toolInput.glob) ||
184
- '';
185
- const path = typeof toolInput.path === 'string' ? toolInput.path : '';
186
- const parts = [pattern, path].filter(Boolean);
187
- if (parts.length === 0) return undefined;
188
- return truncateSummary(parts.join(' in '));
189
- }
190
- return undefined;
191
- }
192
-
193
- /** Response text used only for size/token estimate — never written to the ledger. */
194
- function responseTextForEstimate(toolResponse: ToolPayload['tool_response']): string {
195
- if (!toolResponse) return '';
196
- const parts: string[] = [];
197
- if (typeof toolResponse.content === 'string') parts.push(toolResponse.content);
198
- if (typeof toolResponse.stdout === 'string') parts.push(toolResponse.stdout);
199
- if (typeof toolResponse.stderr === 'string') parts.push(toolResponse.stderr);
200
- return parts.join('\n');
201
- }
202
-
203
- /**
204
- * Token cascade (tasks 0246 / 0248). Returns undefined when unknown — callers omit
205
- * the field rather than writing tokens: 0.
206
- *
207
- * Bash: capped stdout/stderr length. Grep/Glob: capped result size or undefined.
208
- */
209
- export function resolveTokenEstimate(
210
- toolName: string,
211
- toolInput: ToolPayload['tool_input'],
212
- toolResponse: ToolPayload['tool_response'],
213
- ): number | undefined {
214
- // Bash: estimate from capped response only (never uncapped multi-MB stdout).
215
- if (toolName === 'Bash') {
216
- const raw = responseTextForEstimate(toolResponse);
217
- if (raw.length === 0) return undefined;
218
- return Math.ceil(cappedByteLength(raw) / 4);
219
- }
220
-
221
- // Grep / Glob: result size after cap, or omit when empty.
222
- if (toolName === 'Grep' || toolName === 'Glob') {
223
- const raw = responseTextForEstimate(toolResponse);
224
- if (raw.length === 0) return undefined;
225
- return Math.ceil(cappedByteLength(raw) / 4);
226
- }
227
-
228
- const responseContent = toolResponse?.content;
229
- if (typeof responseContent === 'string' && responseContent.length > 0) {
230
- return estimateTokens(responseContent);
231
- }
232
- if (toolName === 'Write' && typeof toolInput?.content === 'string' && toolInput.content.length > 0) {
233
- return estimateTokens(toolInput.content);
234
- }
235
- if (toolName === 'Edit') {
236
- const parts = [toolInput?.old_string ?? '', toolInput?.new_string ?? ''].join('');
237
- if (parts.length > 0) return estimateTokens(parts);
238
- }
239
- if (toolName === 'Read' && typeof toolInput?.file_path === 'string' && toolInput.file_path) {
240
- try {
241
- if (existsSync(toolInput.file_path)) {
242
- const size = statSync(toolInput.file_path).size;
243
- if (size > 0) return Math.ceil(size / 4);
244
- }
245
- } catch {
246
- /* fail-open: skip stat */
247
- }
248
- }
249
- return undefined;
250
- }
251
-
252
- function readSessionFile(dir: string): SessionFile {
253
- const sessionFile = join(dir, '.session.json');
254
- if (!existsSync(sessionFile)) return {};
255
- try {
256
- return JSON.parse(readFileSync(sessionFile, 'utf-8')) as SessionFile;
257
- } catch {
258
- return {};
259
- }
260
- }
261
-
262
- /**
263
- * Core record path (testable). Returns the written event, or null when nothing was logged
264
- * (unknown tool, missing session, missing path/summary, I/O failure).
265
- */
266
- export function recordToolUseEvent(
267
- contextDir: string,
268
- payload: ToolPayload,
269
- now: () => Date = () => new Date(),
270
- ): Record<string, unknown> | null {
271
- const toolName = payload.tool_name ?? '';
272
- if (!ALLOWED_TOOLS.has(toolName)) return null;
273
-
274
- const filePath = payload.tool_input?.file_path ?? '';
275
- const summary = buildToolSummary(toolName, payload.tool_input);
276
-
277
- // Read/Write/Edit still require a path; Bash/Grep/Glob require a short summary.
278
- if (toolName === 'Read' || toolName === 'Write' || toolName === 'Edit') {
279
- if (!filePath) return null;
280
- } else if (!summary) {
281
- return null;
282
- }
283
-
284
- const sessionMeta = readSessionFile(contextDir);
285
- const session = sessionMeta.session ?? '';
286
- if (!session) return null;
287
-
288
- // Freshness stamp (task 0711 R4): the indexed-context producer is the agent
289
- // following the skill's update guidance; the observable producer moment in
290
- // code is a Write/Edit landing on a `.spur/context/*.md` file. Each such
291
- // write refreshes `.freshness.json` so staleness checks can compare the
292
- // sidecar's source commit against HEAD until the next regeneration.
293
- if ((toolName === 'Write' || toolName === 'Edit') && isContextIndexFile(contextDir, filePath)) {
294
- stampContextFreshness(contextDir, currentHeadCommit(), now());
295
- }
296
-
297
- const tokens = resolveTokenEstimate(toolName, payload.tool_input, payload.tool_response);
298
- const ts = now().toISOString();
299
- const type = mapToolType(toolName);
300
- const action = toolName === 'Write' ? 'create' : toolName === 'Edit' ? 'edit' : undefined;
301
-
302
- const event: Record<string, unknown> = { ts, session, type };
303
- if (filePath) event.file = filePath;
304
- if (summary) event.summary = summary;
305
- if (tokens !== undefined) event.tokens = tokens;
306
- if (action) event.action = action;
307
-
308
- // Best-effort identity fields — never block logging.
309
- if (typeof payload.session_id === 'string' && payload.session_id) {
310
- event.sessionId = payload.session_id;
311
- }
312
- if (typeof sessionMeta.agent === 'string' && sessionMeta.agent) event.agent = sessionMeta.agent;
313
- if (typeof sessionMeta.model === 'string' && sessionMeta.model) event.model = sessionMeta.model;
314
-
315
- try {
316
- appendFileSync(join(contextDir, 'token-ledger.jsonl'), `${JSON.stringify(event)}\n`);
317
- } catch {
318
- return null;
319
- }
320
-
321
- return event;
322
- }
323
-
324
- // ─── Context freshness sidecar (task 0711 R4) ───────────────────────────
325
-
326
- export interface ContextFreshness {
327
- schema_version: number;
328
- source_commit: string;
329
- generated_at: string;
330
- }
331
-
332
- export const CONTEXT_FRESHNESS_SCHEMA_VERSION = 1;
333
-
334
- /** True when `filePath` is a regenerated context index inside `contextDir` (not the sidecars). */
335
- export function isContextIndexFile(contextDir: string, filePath: string): boolean {
336
- const resolvedDir = resolve(contextDir);
337
- const resolvedFile = resolve(filePath);
338
- if (!resolvedFile.startsWith(resolvedDir + sep)) return false;
339
- const name = resolvedFile.slice(resolvedDir.length + 1);
340
- return name.endsWith('.md');
341
- }
342
-
343
- /** Current HEAD, best-effort — null when git is unavailable or the dir is not a work tree. */
344
- export function currentHeadCommit(): string | null {
345
- try {
346
- const out = execSync('git rev-parse HEAD', { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
347
- const commit = out.trim();
348
- return commit.length > 0 ? commit : null;
349
- } catch {
350
- return null;
351
- }
352
- }
353
-
354
- /**
355
- * Write/refresh `.spur/context/.freshness.json`. Best-effort: any I/O failure
356
- * leaves the previous sidecar (or none) in place — the hook contract is
357
- * fail-open, never blocking.
358
- */
359
- export function stampContextFreshness(contextDir: string, sourceCommit: string | null, at: Date): void {
360
- if (!sourceCommit) return;
361
- const body: ContextFreshness = {
362
- schema_version: CONTEXT_FRESHNESS_SCHEMA_VERSION,
363
- source_commit: sourceCommit,
364
- generated_at: at.toISOString(),
365
- };
366
- try {
367
- writeFileSync(join(contextDir, '.freshness.json'), JSON.stringify(body));
368
- } catch {
369
- /* fail-open */
370
- }
371
- }
372
-
373
- export interface ContextFreshnessCheck {
374
- stale: boolean;
375
- reason?: string;
376
- }
377
-
378
- /**
379
- * Compare a freshness sidecar against the current HEAD (task 0711 R4). A
380
- * missing/unparsable sidecar is stale (`never stamped`); a commit mismatch is
381
- * stale until the producer regenerates the index.
382
- */
383
- export function checkContextFreshness(raw: string | null, headCommit: string | null): ContextFreshnessCheck {
384
- if (raw === null) return { stale: true, reason: 'never stamped' };
385
- let body: ContextFreshness;
386
- try {
387
- body = JSON.parse(raw) as ContextFreshness;
388
- } catch {
389
- return { stale: true, reason: 'malformed sidecar' };
390
- }
391
- if (body.schema_version !== CONTEXT_FRESHNESS_SCHEMA_VERSION) {
392
- return { stale: true, reason: `schema_version ${String(body.schema_version)}` };
393
- }
394
- if (typeof body.source_commit !== 'string' || body.source_commit === '') {
395
- return { stale: true, reason: 'missing source_commit' };
396
- }
397
- if (headCommit !== null && body.source_commit !== headCommit) {
398
- return { stale: true, reason: 'source commit changed since generation' };
399
- }
400
- return { stale: false };
401
- }
402
-
403
- /** Read the freshness sidecar, or null when absent/unreadable. */
404
- export function readContextFreshness(contextDir: string): string | null {
405
- try {
406
- return readFileSync(join(contextDir, '.freshness.json'), 'utf-8');
407
- } catch {
408
- return null;
409
- }
410
- }
411
-
412
- // Entrypoint — thin wrapper; logic lives in {@link recordToolUseEvent} for unit coverage.
413
16
  if (import.meta.main) {
414
17
  void (async () => {
415
18
  const dir = join(getEnvVar('CLAUDE_PROJECT_DIR') ?? process.cwd(), '.spur', 'context');