@gobing-ai/spur 0.4.1 → 0.4.2

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 (104) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/config.global.yaml +4 -2
  3. package/config/templates/AGENTS.md +1 -1
  4. package/config/workflows/wrapup-pipeline.yaml +1 -1
  5. package/package.json +10 -10
  6. package/plugins/sp/README.md +1 -2
  7. package/plugins/sp/agents/super-planner.md +5 -6
  8. package/plugins/sp/commands/dev-review-session.md +2 -1
  9. package/plugins/sp/commands/dev-runall.md +18 -9
  10. package/plugins/sp/hooks/context-owner.ts +56 -0
  11. package/plugins/sp/hooks/context-post-tool.ts +23 -0
  12. package/plugins/sp/hooks/context-session-start.ts +49 -27
  13. package/plugins/sp/hooks/context-session-stop.ts +131 -30
  14. package/plugins/sp/hooks/pi/guard-extension.ts +21 -8
  15. package/plugins/sp/lib/idea-handoff.generated.mjs +4 -2
  16. package/plugins/sp/lib/inline-run.generated.mjs +47 -30
  17. package/plugins/sp/lib/plan-projection.generated.d.mts +19 -0
  18. package/plugins/sp/lib/plan-projection.generated.mjs +113 -0
  19. package/plugins/sp/lib/quality-gate.generated.d.mts +0 -12
  20. package/plugins/sp/lib/quality-gate.generated.mjs +48 -230
  21. package/plugins/sp/lib/transcript.ts +49 -21
  22. package/plugins/sp/plugin.json +1 -1
  23. package/plugins/sp/references/roles.md +1 -1
  24. package/plugins/sp/scripts/batch-plan.mjs +154 -1
  25. package/plugins/sp/scripts/batch-plan.ts +48 -3
  26. package/plugins/sp/scripts/inline-run-setup.mjs +24 -13
  27. package/plugins/sp/scripts/pr-reviewing.mjs +19 -5
  28. package/plugins/sp/scripts/pr-reviewing.ts +34 -9
  29. package/plugins/sp/scripts/quality-gate.mjs +25 -207
  30. package/plugins/sp/scripts/run-summary.mjs +24 -13
  31. package/plugins/sp/scripts/session-timeline.mjs +26 -25
  32. package/plugins/sp/scripts/session-timeline.ts +4 -18
  33. package/plugins/sp/scripts/task-diffstat.mjs +32 -29
  34. package/plugins/sp/scripts/task-diffstat.ts +34 -34
  35. package/plugins/sp/skills/indexed-context/SKILL.md +14 -7
  36. package/plugins/sp/skills/parallel-execution/SKILL.md +2 -2
  37. package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +1 -1
  38. package/plugins/sp/skills/parallel-execution/references/fan-out-patterns.md +2 -2
  39. package/plugins/sp/skills/pr-reviewing/SKILL.md +7 -3
  40. package/plugins/sp/skills/session-review/SKILL.md +5 -3
  41. package/plugins/sp/skills/spur-cli/references/decision.md +9 -2
  42. package/plugins/sp/skills/spur-cli/references/projects.md +7 -5
  43. package/plugins/sp/skills/spur-dev/SKILL.md +2 -0
  44. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +27 -0
  45. package/plugins/sp/skills/spur-dev/references/dev-operations.md +2 -3
  46. package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +5 -0
  47. package/plugins/sp/skills/spur-dev/references/execution-batch.md +43 -4
  48. package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +4 -1
  49. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +13 -11
  50. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +8 -13
  51. package/spur.js +14261 -2846
  52. package/web/_astro/{BoardApp.D66k_s_T.js → BoardApp.B3GQZvj8.js} +1 -1
  53. package/web/_astro/{BoardApp.D6sjAJr_.js → BoardApp.BT7L2kSb.js} +192 -192
  54. package/web/_astro/TaskDetail.Cxov1_N_.js +1 -0
  55. package/web/_astro/{arc._BUadrdH.js → arc.vaT7tPOB.js} +1 -1
  56. package/web/_astro/{architectureDiagram-3BPJPVTR.Dh7xS1Pp.js → architectureDiagram-3BPJPVTR.DRMmTjkN.js} +1 -1
  57. package/web/_astro/{blockDiagram-GPEHLZMM.DFDVpfXm.js → blockDiagram-GPEHLZMM.cla0LO_q.js} +1 -1
  58. package/web/_astro/{c4Diagram-AAUBKEIU.V7_3_glh.js → c4Diagram-AAUBKEIU.D4YAIBKn.js} +1 -1
  59. package/web/_astro/channel.D4nG9YLf.js +1 -0
  60. package/web/_astro/{chunk-2J33WTMH.De1yx7Jz.js → chunk-2J33WTMH.BrSJbMRZ.js} +1 -1
  61. package/web/_astro/{chunk-4BX2VUAB.BDhHFuxx.js → chunk-4BX2VUAB.CuLMgKF5.js} +1 -1
  62. package/web/_astro/{chunk-55IACEB6.DDLkWnmC.js → chunk-55IACEB6.zkvgu54F.js} +1 -1
  63. package/web/_astro/{chunk-727SXJPM.CwfiLnA7.js → chunk-727SXJPM.DxWkACh8.js} +1 -1
  64. package/web/_astro/{chunk-AQP2D5EJ.CtGDmUnM.js → chunk-AQP2D5EJ.42XrmIlH.js} +1 -1
  65. package/web/_astro/{chunk-FMBD7UC4.BOLjpw73.js → chunk-FMBD7UC4.Bao2wMK9.js} +1 -1
  66. package/web/_astro/{chunk-ND2GUHAM.BDhQls6O.js → chunk-ND2GUHAM.BhydojD1.js} +1 -1
  67. package/web/_astro/{chunk-QZHKN3VN.76qBynDF.js → chunk-QZHKN3VN.CmZ82g-H.js} +1 -1
  68. package/web/_astro/{classDiagram-4FO5ZUOK.N0mpPHT0.js → classDiagram-4FO5ZUOK.Blr83Gt_.js} +1 -1
  69. package/web/_astro/{classDiagram-v2-Q7XG4LA2.N0mpPHT0.js → classDiagram-v2-Q7XG4LA2.Blr83Gt_.js} +1 -1
  70. package/web/_astro/{cose-bilkent-S5V4N54A.CKknFfjB.js → cose-bilkent-S5V4N54A.qmfv0SPn.js} +1 -1
  71. package/web/_astro/{cynefin-OW5HDTMX.Cw3ZWBAh.js → cynefin-OW5HDTMX.BtELhXPR.js} +1 -1
  72. package/web/_astro/{dagre-BM42HDAG.D3yIKCRD.js → dagre-BM42HDAG.AWmaAvXo.js} +1 -1
  73. package/web/_astro/{diagram-2AECGRRQ.D51dO83Z.js → diagram-2AECGRRQ.DdH34KDi.js} +1 -1
  74. package/web/_astro/{diagram-5GNKFQAL.Bb-FViDj.js → diagram-5GNKFQAL.BmNPix1k.js} +1 -1
  75. package/web/_astro/{diagram-KO2AKTUF.D3mN8S6x.js → diagram-KO2AKTUF.BNQd_Ppo.js} +1 -1
  76. package/web/_astro/{diagram-LMA3HP47.dMMyPcbJ.js → diagram-LMA3HP47.BAPgDi0e.js} +1 -1
  77. package/web/_astro/{diagram-OG6HWLK6.AzbvzY4C.js → diagram-OG6HWLK6.DZ4zyLdC.js} +1 -1
  78. package/web/_astro/{erDiagram-TEJ5UH35.BR5I4OZb.js → erDiagram-TEJ5UH35.Q1TySMmw.js} +1 -1
  79. package/web/_astro/{flowDiagram-I6XJVG4X.DFqvhGkn.js → flowDiagram-I6XJVG4X.BXhFBicG.js} +1 -1
  80. package/web/_astro/{ganttDiagram-6RSMTGT7.ByJbhH6J.js → ganttDiagram-6RSMTGT7.vHSgcuo2.js} +1 -1
  81. package/web/_astro/{gitGraphDiagram-PVQCEYII.CInHjQR3.js → gitGraphDiagram-PVQCEYII.Bi1f73C7.js} +1 -1
  82. package/web/_astro/{index.COh_kBxN.css → index.BhSwN8yY.css} +1 -1
  83. package/web/_astro/{infoDiagram-5YYISTIA.Cm0dGVjJ.js → infoDiagram-5YYISTIA.DPvXZEWm.js} +1 -1
  84. package/web/_astro/{ishikawaDiagram-YF4QCWOH.C3nOP6Sj.js → ishikawaDiagram-YF4QCWOH.Cz829fsV.js} +1 -1
  85. package/web/_astro/{journeyDiagram-JHISSGLW.4IvLa2MW.js → journeyDiagram-JHISSGLW.enRnB9Me.js} +1 -1
  86. package/web/_astro/{kanban-definition-UN3LZRKU.DS87Cl-k.js → kanban-definition-UN3LZRKU.DnTNk6d2.js} +1 -1
  87. package/web/_astro/{linear.MzmRrNER.js → linear.BzGnBwNL.js} +1 -1
  88. package/web/_astro/{mermaid.core.fK-WNswS.js → mermaid.core.CSbTpIVz.js} +4 -4
  89. package/web/_astro/{mindmap-definition-RKZ34NQL.BqipOHhY.js → mindmap-definition-RKZ34NQL.DGJDy8zT.js} +1 -1
  90. package/web/_astro/{pieDiagram-4H26LBE5.0nBiPqFG.js → pieDiagram-4H26LBE5.HSPdi5_8.js} +1 -1
  91. package/web/_astro/{quadrantDiagram-W4KKPZXB.Szj7Zpx5.js → quadrantDiagram-W4KKPZXB.6Gv8mz2Y.js} +1 -1
  92. package/web/_astro/{requirementDiagram-4Y6WPE33.D4UR6TWo.js → requirementDiagram-4Y6WPE33.CnBCtFwa.js} +1 -1
  93. package/web/_astro/{sankeyDiagram-5OEKKPKP.DAE7BdOs.js → sankeyDiagram-5OEKKPKP.5M8VWAu8.js} +1 -1
  94. package/web/_astro/{sequenceDiagram-3UESZ5HK.BP9ZVq25.js → sequenceDiagram-3UESZ5HK.DN41XN-U.js} +1 -1
  95. package/web/_astro/{stateDiagram-AJRCARHV.BQW7mIxk.js → stateDiagram-AJRCARHV.ErOO3W8j.js} +1 -1
  96. package/web/_astro/{stateDiagram-v2-BHNVJYJU.PbK4nDih.js → stateDiagram-v2-BHNVJYJU.B_ZHICM-.js} +1 -1
  97. package/web/_astro/{timeline-definition-PNZ67QCA.Ci_GvUpo.js → timeline-definition-PNZ67QCA.CWdosr07.js} +1 -1
  98. package/web/_astro/{vennDiagram-CIIHVFJN.DNw1Y5qx.js → vennDiagram-CIIHVFJN.BHzYjNV5.js} +1 -1
  99. package/web/_astro/{wardleyDiagram-YWT4CUSO.CwlkaaNe.js → wardleyDiagram-YWT4CUSO.h8x4IwPX.js} +1 -1
  100. package/web/_astro/{xychartDiagram-2RQKCTM6.MI6ZuiH_.js → xychartDiagram-2RQKCTM6.RTapW2X8.js} +1 -1
  101. package/web/index.html +2 -2
  102. package/plugins/sp/commands/dev-parallel.md +0 -37
  103. package/web/_astro/TaskDetail.BynLnMNc.js +0 -1
  104. package/web/_astro/channel.DZVfto-r.js +0 -1
@@ -7,7 +7,7 @@
7
7
  "plugins": [
8
8
  {
9
9
  "name": "sp",
10
- "version": "0.4.1",
10
+ "version": "0.4.2",
11
11
  "source": "./plugins/sp"
12
12
  }
13
13
  ]
@@ -157,8 +157,10 @@ workflows:
157
157
  # project-relative); the first layer wins per file basename (ADR-113 style).
158
158
  # `maker` selects the default DecisionMaker for every decision point, and
159
159
  # `makers` overrides it per decision id. Built-in makers: typesafe, fm-local,
160
- # laya-local (laya-local needs Apple Silicon). An unregistered name is a config
161
- # error, never a silent fallback. Inspect the effective choice with
160
+ # laya-local (laya-local needs Apple Silicon) and clef (hosted Cloudflare
161
+ # Workers AI, registered only when CLOUDFLARE_ACCOUNT_ID and
162
+ # CLOUDFLARE_AUTH_TOKEN are both set). An unregistered name is a config error,
163
+ # never a silent fallback. Inspect the effective choice with
162
164
  # `spur decision status`. `init` never overwrites this file — until you add the
163
165
  # keys, the shipped catalog's maker applies (see `status`).
164
166
  decisions:
@@ -36,7 +36,7 @@ All product development work goes through the harness by default.
36
36
  | ------ | ---------- | -------- |
37
37
  | Plan a feature (intake → AC → tasks) | `/sp:dev-plan`, `/sp:dev-idea` | Freeform feature files without gates |
38
38
  | Drive one task end-to-end | `/sp:dev-run <wbs>` or **`sp:super-planner`** | Implement with no task / no pipeline |
39
- | Batch or parallel task runs | `/sp:dev-runall` (host sequential; **`sp:super-planner`** explicit/parallel), `/sp:dev-parallel` | Unordered multi-task thrash |
39
+ | Batch or parallel task runs | `/sp:dev-runall` (host sequential; `--mode parallel\|auto` → **`sp:super-planner`**) | Unordered multi-task thrash |
40
40
  | Batch-refine tasks under a feature | `/sp:dev-refineall --feature <id> --auto` | Hand-looping `/sp:dev-refine` per WBS |
41
41
  | Multi-step corpus CLI (tasks/features/rules/workflows) | **`sp:expert-spur`** | Raw Write/Edit on corpus files |
42
42
  | Look up `spur` verbs / flags / `--json` | Skill **`sp:spur-cli`** | Inventing flags from memory |
@@ -382,7 +382,7 @@ states:
382
382
  command: >-
383
383
  mkdir -p .spur/run &&
384
384
  R=".spur/run/$__runId-wrapup-repair.status" &&
385
- V="$(cat .spur/run/$__runId-wrapup-learnings.status 2>/dev/null)" &&
385
+ V="$(cat .spur/run/$__runId-wrapup-learnings.status 2>/dev/null || true)" &&
386
386
  if [ "$V" = invalid-learnings-shape ]; then printf 'invalid-learnings-shape: narration-only learnings capture — append skipped, repair recorded (0986)\n' > "$R";
387
387
  else printf 'contract-violation: doc-sync learnings capture missed its declared contract — repair skipped re-dispatch (ADR-118 pilot)\n' > "$R";
388
388
  fi &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/spur",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Spur CLI — local-first harness for mainstream coding agents: constraint checking, workflow orchestration, agent health, and history analytics. Bun-native; exposes the `spur` command.",
5
5
  "keywords": [
6
6
  "spur",
@@ -49,7 +49,7 @@
49
49
  "dev": "bun run src/index.ts",
50
50
  "build": "bun run ../../scripts/spur-dev.ts build-cli",
51
51
  "build:binaries": "bun run ../../scripts/spur-dev.ts build-binaries",
52
- "build:bundle": "bun run ../../scripts/commands/bundle-plugin-lib.ts && bun run ../../scripts/spur-dev.ts bundle-web && bun run ../../scripts/spur-dev.ts emit-board-types && bun build src/index.ts --target=bun --outfile spur.js && bun run ../../scripts/spur-dev.ts bundle-config config && bun run ../../scripts/spur-dev.ts bundle-plugins",
52
+ "build:bundle": "bun run ../../scripts/commands/bundle-plugin-lib.ts && bun run ../../scripts/spur-dev.ts bundle-web && bun run ../../scripts/spur-dev.ts emit-board-types && bun build src/index.ts --target=bun --outfile spur.js --external @johnhenry/backend-webgpu && bun run ../../scripts/spur-dev.ts bundle-config config && bun run ../../scripts/spur-dev.ts bundle-plugins",
53
53
  "test": "bun test tests --coverage --coverage-dir=../../.coverage --pass-with-no-tests",
54
54
  "typecheck": "tsc --noEmit",
55
55
  "prepack": "bun run build:bundle",
@@ -57,14 +57,14 @@
57
57
  },
58
58
  "devDependencies": {
59
59
  "@commander-js/extra-typings": "^14.0.0",
60
- "@gobing-ai/ts-db": "^0.5.21",
61
- "@gobing-ai/ts-ai-runner": "^0.5.21",
62
- "@gobing-ai/ts-dual-workflow-engine": "^0.5.21",
63
- "@gobing-ai/ts-infra": "^0.5.21",
64
- "@gobing-ai/ts-llm-jsonl-importer": "^0.5.21",
65
- "@gobing-ai/ts-rule-engine": "^0.5.21",
66
- "@gobing-ai/ts-runtime": "^0.5.21",
67
- "@gobing-ai/ts-utils": "^0.5.21",
60
+ "@gobing-ai/ts-db": "^0.5.22",
61
+ "@gobing-ai/ts-ai-runner": "^0.5.22",
62
+ "@gobing-ai/ts-dual-workflow-engine": "^0.5.22",
63
+ "@gobing-ai/ts-infra": "^0.5.22",
64
+ "@gobing-ai/ts-llm-jsonl-importer": "^0.5.22",
65
+ "@gobing-ai/ts-rule-engine": "^0.5.22",
66
+ "@gobing-ai/ts-runtime": "^0.5.22",
67
+ "@gobing-ai/ts-utils": "^0.5.22",
68
68
  "@types/bun": "1.3.14",
69
69
  "@types/figlet": "^1.7.0",
70
70
  "@types/node-notifier": "8.0.5",
@@ -120,7 +120,6 @@ list this README is checked against.
120
120
  | Command | What it does |
121
121
  | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
122
122
  | `dev-runall` | 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 |
123
- | `dev-parallel` | Fan out independent tasks or investigations in parallel via subagents — choose the right pattern and synthesize results |
124
123
  | `dev-refineall` | Batch-refine tasks (feature or selector) — planning-half bulk fill of Background/Requirements/AC/Design/Plan before runall; `--depth ready` audits, corrects, freezes and promotes each task |
125
124
  | `dev-verifyall` | Batch-verify tasks against requirements and AC — resolves a set, runs per-task verification, produces consolidated PASS/PARTIAL/FAIL summary report |
126
125
  | `dev-wrapall` | Wrap up a batch of completed tasks — learnings, metrics, doc-sync, feature transition, optional branch cleanup |
@@ -333,7 +332,7 @@ surface or run one workflow. All skills target the same five core platforms: `cl
333
332
  | `source-driven-development` | 1.0 | Source-first verification — verify framework/API/library facts against primary sources before generating code; separates "the API exists" from "used correctly under its contract" |
334
333
  | `dogfood-testing` | 1.2 | Dogfood backbone — drives a testee end-to-end with bounded auto-fix, a live monitor ledger, and a structured report; @1.2 adds footer-mandatory reports, 7-check finalize-or-abort, and the `validate-report` CLI; backs `/sp:dev-dogfood` |
335
334
  | `next-router` | 1.0 | Status→command router — resolve a task WBS or feature frontier, TABLE A/B/C lookup with light-gate short-circuit, single dispatch or HITL stop; backs `/sp:dev-next` |
336
- | `parallel-execution` | 1.0 | Fan-out decision framework — when to parallelize, four proven fan-out patterns, and result synthesis; backs `/sp:dev-parallel` |
335
+ | `parallel-execution` | 1.0 | Fan-out decision framework — when to parallelize, four proven fan-out patterns, and result synthesis; backs `/sp:dev-runall --mode parallel\|auto` |
337
336
  | `sys-debugging` | 1.0 | Structured debugging protocol — reproduce→isolate→root cause→fix→regression test; "ask the debugger before the LLM" principle |
338
337
  | `branch-workflow` | 1.0 | Branch-lifecycle discipline — create→worktree→commit→self-review→merge→cleanup; git worktree patterns for parallel branches |
339
338
  | `test-driven-development` | 1.0.0 | TDD workflow companion — red-green-refactor cycle, behavior-first test design, AAA structure, data builders, mock-at-boundary anti-patterns |
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: super-planner
3
3
  description: |
4
- Use PROACTIVELY for "run this task end to end", "drive the batch", "execute the todo set", "/sp:dev-runall", or "/sp:dev-parallel". Planning and execution orchestration: owns the product/project management framing (intake, scope, sequencing) and drives the batch driver loop in sp:spur-dev/references/execution-batch.md — resolve+freeze -> topo-sort -> per-task run -> verdict inspect -> continue/halt -> batch report, with optional parallel fan-out. For a one-off verb, /sp:dev-run <wbs> is lighter; for "what single step next?", prefer /sp:dev-next — NOT this agent's job.
4
+ Use PROACTIVELY for "run this task end to end", "drive the batch", "execute the todo set", "/sp:dev-runall", or "fan out these tasks". Planning and execution orchestration: owns the product/project management framing (intake, scope, sequencing) and drives the batch driver loop in sp:spur-dev/references/execution-batch.md — resolve+freeze -> topo-sort -> per-task run -> verdict inspect -> continue/halt -> batch report, with optional parallel fan-out. For a one-off verb, /sp:dev-run <wbs> is lighter; for "what single step next?", prefer /sp:dev-next — NOT this agent's job.
5
5
 
6
6
  <example>
7
7
  Context: Batch execution of a feature's tasks
@@ -72,7 +72,6 @@ You own the spaces **between** task runs:
72
72
  - **Resolve + freeze** the task set:
73
73
  - from `--tasks <selector>`, or
74
74
  - from the convenience `--feature <id>` (normalized by the command layer / resolver to `feature:<id>` per execution-batch.md Step 1).
75
- (See also dev-runall and dev-parallel which now both accept `--feature`.)
76
75
  - **Topologically order** the frozen set by `dependencies[]` (Step 2). Abort on cycle; pre-block
77
76
  unmet out-of-set deps.
78
77
  - **Preflight (Step 2.5 / 3.0)** - before each pipeline launch, evaluate TABLE A STOP rows
@@ -110,7 +109,7 @@ You explicitly do **NOT** own step-level execution:
110
109
 
111
110
  ## When to use
112
111
 
113
- - `/sp:dev-runall --feature <id>` (or `--tasks feature:<id>`), `/sp:dev-parallel --feature <id>`, or equivalent is invoked.
112
+ - `/sp:dev-runall --feature <id>` (or `--tasks feature:<id>`, with any `--mode`), or equivalent is invoked.
114
113
  - The operator asks to "run all tasks", "run the batch", "execute the todo set", "runall ready", or "fan out tasks for feature X".
115
114
  - A feature's task batch is decomposed and ready for end-to-end (sequential or parallel) execution.
116
115
 
@@ -121,7 +120,7 @@ routes to you. On other platforms, invoke this agent directly when the batch dri
121
120
 
122
121
  | Platform | Invocation |
123
122
  | ---------- | ----------- |
124
- | Claude Code | Spawned by `/sp:dev-runall --feature <id>` (or `--tasks ...`) -> `Skill(skill="sp:spur-dev", args="runall $ARGUMENTS")` -> this agent (also handles parallel via dev-parallel) |
123
+ | Claude Code | Spawned by `/sp:dev-runall --feature <id>` (or `--tasks ...`) -> `Skill(skill="sp:spur-dev", args="runall $ARGUMENTS")` -> this agent (also handles `--mode parallel` and parallel `--mode auto` resolutions) |
125
124
  | Other platforms | Spawn this agent directly; read execution-batch.md and drive the loop |
126
125
 
127
126
  ## Decision autonomy
@@ -173,8 +172,8 @@ When you fan out or dispatch a subagent, apply the four disciplines the SSOT
173
172
  cardinality is 1. Never loop recovery until done.
174
173
  - [ ] Emit the batch report at completion (clean / halted / aborted), including preflight skips.
175
174
  - [ ] Default to sequential execution. Enter parallel mode only when explicitly requested
176
- (`--mode parallel` or `/sp:dev-parallel`) and the `sp:parallel-execution` decision framework
177
- clears dependency, file-overlap, and token-budget checks.
175
+ (`--mode parallel`, or `--mode auto` resolved to parallel by the § 2.4a schedule) and the
176
+ `sp:parallel-execution` decision framework clears dependency, file-overlap, and token-budget checks.
178
177
  - [ ] To resolve a deferred `feature_id` under operator-chosen strict rigor, use the sp:spur-dev
179
178
  feature-link helper (single-task or sweep) - never invoke it automatically from within a
180
179
  batch run; surface it only when the operator explicitly requests strict traceability.
@@ -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`.
@@ -0,0 +1,56 @@
1
+ /**
2
+ * context-owner — host-native session ownership identity shared by the indexed-context hooks
3
+ * (task 1159).
4
+ *
5
+ * Extracted from `context-session-start`/`context-session-stop` so the Claude stdin-payload →
6
+ * owner derivation is a pure, importable function. A hook's `if (import.meta.main)` entrypoint runs
7
+ * in a subprocess whose body Bun's coverage never attributes, so any real logic left there is
8
+ * untestable; this module keeps that logic where a unit test can reach it.
9
+ *
10
+ * Standalone-safe (sp-plugin-standalone): no imports, so a hook bundles it as a relative path and
11
+ * `context-session-stop` stays free of the session-start module's heavier dependency surface.
12
+ */
13
+
14
+ /**
15
+ * Who owns a session ledger pointer (task 1159): the host that opened the session plus that host's
16
+ * own native session id — Claude's hook payload `session_id`, Pi's `ctx.sessionManager.getSessionId()`.
17
+ * Deliberately **not** the ledger id (shared by every nested run), `process.pid` (a fresh hook process
18
+ * per event) or `SPUR_RUN_ID` (present on nested children too): none of those separate owner from
19
+ * child, so none may gate teardown; the host-native id can.
20
+ */
21
+ export interface SessionOwner {
22
+ host: 'claude' | 'pi';
23
+ sessionId: string;
24
+ }
25
+
26
+ /**
27
+ * Claude hook identity (task 1159): the payload's `session_id`, falling back to
28
+ * `CLAUDE_CODE_SESSION_ID` only when the payload id is absent or empty (some Claude hook payloads
29
+ * omit `session_id`). Returns null when neither yields a nonempty id — the pointer is then written
30
+ * ownerless, so a later shutdown cannot prove ownership of it (bounded legacy policy in
31
+ * context-session-stop).
32
+ */
33
+ export function claudeSessionOwner(payload: unknown, env: NodeJS.ProcessEnv): SessionOwner | null {
34
+ const raw = payload && typeof payload === 'object' ? (payload as { session_id?: unknown }).session_id : undefined;
35
+ const fromPayload = typeof raw === 'string' ? raw.trim() : '';
36
+ const fromEnv = (env.CLAUDE_CODE_SESSION_ID ?? '').trim();
37
+ const sessionId = fromPayload.length > 0 ? fromPayload : fromEnv;
38
+ return sessionId.length > 0 ? { host: 'claude', sessionId } : null;
39
+ }
40
+
41
+ /**
42
+ * Derive the Claude owner from a raw hook stdin payload (task 1159).
43
+ *
44
+ * Malformed or empty stdin carries no payload identity, so it degrades to the env-only fallback
45
+ * rather than throwing — both hooks keep their fail-open contract. Behaviour is identical to the
46
+ * former inline `JSON.parse(await Bun.stdin.text())` try/catch in each entrypoint.
47
+ */
48
+ export function claudeHookOwnerFromPayloadText(text: string, env: NodeJS.ProcessEnv): SessionOwner | null {
49
+ let payload: unknown;
50
+ try {
51
+ payload = JSON.parse(text) as unknown;
52
+ } catch {
53
+ payload = undefined;
54
+ }
55
+ return claudeSessionOwner(payload, env);
56
+ }
@@ -72,6 +72,29 @@ export function estimateTokens(text: string): number {
72
72
  /** Scrub secret-like patterns without size capping. */
73
73
  export function scrubSecrets(text: string): string {
74
74
  let s = text;
75
+ // Quoted JSON fields first (task 1158): in `{"password": "…"}` the key is itself
76
+ // quoted, so the bare `key=value` pass below never sees a separator directly after
77
+ // the key and would persist the value verbatim. Match the quoted sensitive key, keep
78
+ // it, and replace the entire value. The key may be spelled three ways in real Bash:
79
+ // `"key"`, `'key'`, and the shell-escaped `\"key\"` inside a double-quoted argument
80
+ // (`curl -d "{\"password\":\"…\"}"`); the value may be quoted the same way or left
81
+ // unquoted (`{"password": 12345}`). Escape-aware: a `\"` or `\\` inside a value is
82
+ // content, never a terminator, and an unterminated value (including a trailing lone
83
+ // backslash) is redacted through end-of-input so a truncated command cannot leak a
84
+ // suffix. In the shell-escaped opener form the JSON quote shows up in three raw
85
+ // spellings: a terminator `\"`, a JSON-level escaped quote `\\\"` (escaped
86
+ // backslash + escaped quote) that is value content, and a bare `"` that is neither.
87
+ // That form gets a dedicated alternative whose body consumes `\\\"` as content and
88
+ // stops only at a clean `\"` terminator; when no clean terminator follows it fails
89
+ // closed and redacts through end-of-input, so a bare `"` cannot end the value early
90
+ // and leak its suffix. Runs before
91
+ // Bearer/`key=` passes so the whole value is replaced, not prefixed. The key must end
92
+ // on a sensitive word, so `token_count`, `tokens`, `tokenizer` and
93
+ // `authorization_header` stay readable.
94
+ s = s.replace(
95
+ /((\\?(["']))(?:[A-Za-z0-9]+[_-])*(?:api[_-]?key|password|passwd|secret|token|access[_-]?key|private[_-]?key|authorization)\2\s*:\s*)(?:\\\3(?:[^"\\]|\\\\(?:\\\3)|\\[^"])*(?:\\\3|[\s\S]*$)|"(?:[^"\\]|\\.)*(?:"|\\?$)|'(?:[^'\\]|\\.)*(?:'|\\?$)|[^\s,}\][{"']+)/gi,
96
+ '$1"***"',
97
+ );
75
98
  // Bearer / API key prefixes first (before generic key=value)
76
99
  s = s.replace(/\bBearer\s+[A-Za-z0-9._\-+=/]+/gi, 'Bearer ***');
77
100
  // `sk-` keys. Segments may be hyphen-separated (`sk-ant-api03-…`), so match the
@@ -4,8 +4,10 @@
4
4
  * context-session-start — SessionStart hook for indexed-context.
5
5
  *
6
6
  * Generates a session ID, writes `.spur/context/.session.json` (tracking the current session
7
- * for PostToolUse and Stop hooks), and appends a `session_start` event to
8
- * `token-ledger.jsonl`. Task 0246: best-effort agent/model hints on session file + event.
7
+ * for PostToolUse and Stop hooks), and appends a `session_start` event to `token-ledger.jsonl`.
8
+ * Task 0246: best-effort agent/model hints on session file + event. Task 1159: the pointer records
9
+ * the host-native {@link SessionOwner} that created it, so a nested child can reuse the ledger for
10
+ * tool recording without being able to tear it down.
9
11
  *
10
12
  * **Fail-open contract:** every error path exits 0 with no output.
11
13
  *
@@ -13,13 +15,19 @@
13
15
  */
14
16
 
15
17
  import { execSync } from 'node:child_process';
18
+ import { randomUUID } from 'node:crypto';
16
19
  import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
17
20
  import { join } from 'node:path';
18
21
  import { getEnvVar, getEnvVars } from '../lib/env';
19
22
  import { resolveAgentHint, resolveModelHint } from './agent-hint';
23
+ import { claudeHookOwnerFromPayloadText, type SessionOwner } from './context-owner';
20
24
  import { checkContextFreshness } from './context-post-tool';
21
25
 
22
26
  export { resolveAgentHint, resolveModelHint } from './agent-hint';
27
+ export type { SessionOwner } from './context-owner';
28
+ // Owner identity + Claude payload derivation live in ./context-owner (pure, unit-testable);
29
+ // re-exported here because this module stays the hook surface consumers reach for.
30
+ export { claudeHookOwnerFromPayloadText, claudeSessionOwner } from './context-owner';
23
31
 
24
32
  /**
25
33
  * Ancestor-run marker exported by `@gobing-ai/ts-ai-runner` ≥ 0.4.15 (`AGENT_RUN_ID_ENV`).
@@ -64,38 +72,29 @@ interface SessionFileBody {
64
72
  * Return the id of the session already in flight, or null when a new one should be minted.
65
73
  *
66
74
  * **Why this exists (0398 R3).** `SessionStart` fires in every nested `agent.run` subprocess, not
67
- * once per host session. Each firing used to mint a fresh `session-<date>-<HHMM>` id, append a
75
+ * once per host session. Each firing used to mint a fresh `session-<date>-<HHMM>` id (now
76
+ * seconds-plus-random, 1166), append a
68
77
  * `session_start` row, and overwrite the `.session.json` pointer that `context-post-tool` reads —
69
78
  * so a single pipeline run registered as dozens of sessions and orphaned the parent's event
70
79
  * stream. The H6 corpus shows the signature: 332 `session_start` against 157 `session_end` over
71
80
  * 18 days, 298 distinct ids, 39 starts in a 2-day window that held a handful of real sessions.
72
81
  *
73
- * **Two signals, in precedence order.**
74
- *
75
- * 1. {@link AGENT_RUN_ID_ENV} present → this process is a descendant of an agent run, definitively.
76
- * Reuse the recorded session with no time bound: a pipeline step legitimately runs for the full
77
- * `implementTimeoutMs` (45 min) and longer batches run for hours, so any wall-clock window would
78
- * eventually split a run that is demonstrably still in flight.
79
- * 2. No marker → fall back to the {@link SESSION_REUSE_IDLE_MS} window on the `started` stamp, for
80
- * hosts that nest without propagating a correlation.
81
- *
82
- * A crashed session leaves a stale `.session.json`; on path 2 the idle window retires it. On path 1
83
- * a stale file is only reachable from inside a live agent run, where reusing it is the correct
84
- * answer anyway.
82
+ * **Two signals, in precedence order.** (1) {@link AGENT_RUN_ID_ENV} present → definitively a
83
+ * descendant of an agent run: reuse the recorded session with no time bound, because a pipeline step
84
+ * legitimately runs for the full `implementTimeoutMs` (45 min) and longer batches for hours, so any
85
+ * wall-clock window would eventually split a run that is still in flight. (2) No marker → fall back
86
+ * to the {@link SESSION_REUSE_IDLE_MS} window on the `started` stamp, for hosts that nest without
87
+ * propagating a correlation. A crashed session leaves a stale `.session.json`: on (2) the idle window
88
+ * retires it, on (1) a stale file is only reachable from inside a live agent run, where reusing it is
89
+ * the correct answer anyway.
85
90
  */
86
91
  export function resolveActiveSession(dir: string, now: Date, env: NodeJS.ProcessEnv = getEnvVars()): string | null {
87
- let raw: string;
88
- try {
89
- raw = readFileSync(join(dir, '.session.json'), 'utf-8');
90
- } catch {
91
- return null; // no in-flight session (or unreadable) → mint a new one
92
- }
93
-
94
92
  let body: SessionFileBody;
95
93
  try {
96
- body = JSON.parse(raw) as SessionFileBody;
94
+ // No in-flight session, unreadable file or corrupt pointer → mint a new one.
95
+ body = JSON.parse(readFileSync(join(dir, '.session.json'), 'utf-8')) as SessionFileBody;
97
96
  } catch {
98
- return null; // corrupt pointer → mint a new one
97
+ return null;
99
98
  }
100
99
 
101
100
  if (typeof body.session !== 'string' || body.session.length === 0) return null;
@@ -121,12 +120,17 @@ export function resolveActiveSession(dir: string, now: Date, env: NodeJS.Process
121
120
  *
122
121
  * Idempotent per in-flight session (0398 R3): when `resolveActiveSession` finds one, this returns
123
122
  * that id and writes nothing — no ledger row, no pointer rewrite.
123
+ *
124
+ * Ownership (task 1159): a **new** pointer records a valid `owner` only; a reused start never
125
+ * rewrites the pointer, so a nested child cannot take ownership from the parent that opened it,
126
+ * and only the recorded owner's shutdown can finalize the ledger.
124
127
  */
125
128
  export function recordSessionStart(
126
129
  dir: string,
127
130
  env: NodeJS.ProcessEnv = getEnvVars(),
128
131
  now: () => Date = () => new Date(),
129
132
  agentFallback?: string,
133
+ owner?: SessionOwner,
130
134
  ): string | null {
131
135
  try {
132
136
  mkdirSync(dir, { recursive: true });
@@ -139,7 +143,11 @@ export function recordSessionStart(
139
143
  const active = resolveActiveSession(dir, at, env);
140
144
  if (active !== null) return active;
141
145
  const pad = (n: number) => String(n).padStart(2, '0');
142
- const sessionId = `session-${at.toISOString().slice(0, 10)}-${pad(at.getHours())}${pad(at.getMinutes())}`;
146
+ // Uniqueness suffix (1166): minute-granular ids collided when a session started in the same
147
+ // minute an earlier one ended, and the frozen end algorithm then suppressed the new session's
148
+ // `session_end` row as a duplicate. Seconds + a random suffix make collisions practically
149
+ // impossible; the id stays an opaque ledger key everywhere else.
150
+ const sessionId = `session-${at.toISOString().slice(0, 10)}-${pad(at.getHours())}${pad(at.getMinutes())}${pad(at.getSeconds())}-${randomUUID().slice(0, 8)}`;
143
151
  const ts = at.toISOString();
144
152
  const agent = resolveAgentHint(env, agentFallback);
145
153
  const model = resolveModelHint(env);
@@ -153,6 +161,9 @@ export function recordSessionStart(
153
161
  };
154
162
  if (agent) sessionBody.agent = agent;
155
163
  if (model) sessionBody.model = model;
164
+ const ownerId = typeof owner?.sessionId === 'string' ? owner.sessionId.trim() : '';
165
+ const host = owner?.host;
166
+ if (ownerId !== '' && (host === 'claude' || host === 'pi')) sessionBody.owner = { host, sessionId: ownerId };
156
167
 
157
168
  const sessionFile = join(dir, '.session.json');
158
169
  try {
@@ -194,10 +205,21 @@ export function recordSessionStart(
194
205
  return sessionId;
195
206
  }
196
207
 
197
- // Entrypoint — kept minimal so unit coverage focuses on pure helpers above.
208
+ /**
209
+ * Open (or reuse) the session for a raw Claude stdin payload (task 1159, testable half).
210
+ *
211
+ * Malformed/absent text still opens the session ownerless (legacy policy); the dir resolution lives
212
+ * here rather than in the entrypoint so the untestable subprocess body stays a few lines.
213
+ */
214
+ export function recordSessionStartFromPayloadText(text: string, env: NodeJS.ProcessEnv = getEnvVars()): string | null {
215
+ const dir = join(getEnvVar('CLAUDE_PROJECT_DIR') ?? process.cwd(), '.spur', 'context');
216
+ return recordSessionStart(dir, env, undefined, undefined, claudeHookOwnerFromPayloadText(text, env) ?? undefined);
217
+ }
218
+
219
+ // Entrypoint — thin stdin adapter; logic lives in the exported helpers above (task 1159 coverage).
198
220
  if (import.meta.main) {
199
221
  try {
200
- recordSessionStart(join(getEnvVar('CLAUDE_PROJECT_DIR') ?? process.cwd(), '.spur', 'context'));
222
+ recordSessionStartFromPayloadText(await Bun.stdin.text());
201
223
  } catch {
202
224
  /* fail-open */
203
225
  }