@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/config/config.global.yaml +4 -2
- package/config/templates/AGENTS.md +1 -1
- package/config/workflows/wrapup-pipeline.yaml +1 -1
- package/package.json +10 -10
- package/plugins/sp/README.md +1 -2
- package/plugins/sp/agents/super-planner.md +5 -6
- package/plugins/sp/commands/dev-review-session.md +2 -1
- package/plugins/sp/commands/dev-runall.md +18 -9
- package/plugins/sp/hooks/context-owner.ts +56 -0
- package/plugins/sp/hooks/context-post-tool.ts +23 -0
- package/plugins/sp/hooks/context-session-start.ts +49 -27
- package/plugins/sp/hooks/context-session-stop.ts +131 -30
- package/plugins/sp/hooks/pi/guard-extension.ts +21 -8
- package/plugins/sp/lib/idea-handoff.generated.mjs +4 -2
- package/plugins/sp/lib/inline-run.generated.mjs +47 -30
- package/plugins/sp/lib/plan-projection.generated.d.mts +19 -0
- package/plugins/sp/lib/plan-projection.generated.mjs +113 -0
- package/plugins/sp/lib/quality-gate.generated.d.mts +0 -12
- package/plugins/sp/lib/quality-gate.generated.mjs +48 -230
- package/plugins/sp/lib/transcript.ts +49 -21
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/references/roles.md +1 -1
- package/plugins/sp/scripts/batch-plan.mjs +154 -1
- package/plugins/sp/scripts/batch-plan.ts +48 -3
- package/plugins/sp/scripts/inline-run-setup.mjs +24 -13
- package/plugins/sp/scripts/pr-reviewing.mjs +19 -5
- package/plugins/sp/scripts/pr-reviewing.ts +34 -9
- package/plugins/sp/scripts/quality-gate.mjs +25 -207
- package/plugins/sp/scripts/run-summary.mjs +24 -13
- package/plugins/sp/scripts/session-timeline.mjs +26 -25
- package/plugins/sp/scripts/session-timeline.ts +4 -18
- package/plugins/sp/scripts/task-diffstat.mjs +32 -29
- package/plugins/sp/scripts/task-diffstat.ts +34 -34
- package/plugins/sp/skills/indexed-context/SKILL.md +14 -7
- package/plugins/sp/skills/parallel-execution/SKILL.md +2 -2
- package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +1 -1
- package/plugins/sp/skills/parallel-execution/references/fan-out-patterns.md +2 -2
- package/plugins/sp/skills/pr-reviewing/SKILL.md +7 -3
- package/plugins/sp/skills/session-review/SKILL.md +5 -3
- package/plugins/sp/skills/spur-cli/references/decision.md +9 -2
- package/plugins/sp/skills/spur-cli/references/projects.md +7 -5
- package/plugins/sp/skills/spur-dev/SKILL.md +2 -0
- package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +27 -0
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +2 -3
- package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +5 -0
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +43 -4
- package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +4 -1
- package/plugins/sp/skills/spur-dev/references/flag-glossary.md +13 -11
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +8 -13
- package/spur.js +14261 -2846
- package/web/_astro/{BoardApp.D66k_s_T.js → BoardApp.B3GQZvj8.js} +1 -1
- package/web/_astro/{BoardApp.D6sjAJr_.js → BoardApp.BT7L2kSb.js} +192 -192
- package/web/_astro/TaskDetail.Cxov1_N_.js +1 -0
- package/web/_astro/{arc._BUadrdH.js → arc.vaT7tPOB.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.Dh7xS1Pp.js → architectureDiagram-3BPJPVTR.DRMmTjkN.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.DFDVpfXm.js → blockDiagram-GPEHLZMM.cla0LO_q.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.V7_3_glh.js → c4Diagram-AAUBKEIU.D4YAIBKn.js} +1 -1
- package/web/_astro/channel.D4nG9YLf.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.De1yx7Jz.js → chunk-2J33WTMH.BrSJbMRZ.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.BDhHFuxx.js → chunk-4BX2VUAB.CuLMgKF5.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.DDLkWnmC.js → chunk-55IACEB6.zkvgu54F.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.CwfiLnA7.js → chunk-727SXJPM.DxWkACh8.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.CtGDmUnM.js → chunk-AQP2D5EJ.42XrmIlH.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.BOLjpw73.js → chunk-FMBD7UC4.Bao2wMK9.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.BDhQls6O.js → chunk-ND2GUHAM.BhydojD1.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.76qBynDF.js → chunk-QZHKN3VN.CmZ82g-H.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.N0mpPHT0.js → classDiagram-4FO5ZUOK.Blr83Gt_.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.N0mpPHT0.js → classDiagram-v2-Q7XG4LA2.Blr83Gt_.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.CKknFfjB.js → cose-bilkent-S5V4N54A.qmfv0SPn.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.Cw3ZWBAh.js → cynefin-OW5HDTMX.BtELhXPR.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.D3yIKCRD.js → dagre-BM42HDAG.AWmaAvXo.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.D51dO83Z.js → diagram-2AECGRRQ.DdH34KDi.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.Bb-FViDj.js → diagram-5GNKFQAL.BmNPix1k.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.D3mN8S6x.js → diagram-KO2AKTUF.BNQd_Ppo.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.dMMyPcbJ.js → diagram-LMA3HP47.BAPgDi0e.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.AzbvzY4C.js → diagram-OG6HWLK6.DZ4zyLdC.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.BR5I4OZb.js → erDiagram-TEJ5UH35.Q1TySMmw.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.DFqvhGkn.js → flowDiagram-I6XJVG4X.BXhFBicG.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.ByJbhH6J.js → ganttDiagram-6RSMTGT7.vHSgcuo2.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.CInHjQR3.js → gitGraphDiagram-PVQCEYII.Bi1f73C7.js} +1 -1
- package/web/_astro/{index.COh_kBxN.css → index.BhSwN8yY.css} +1 -1
- package/web/_astro/{infoDiagram-5YYISTIA.Cm0dGVjJ.js → infoDiagram-5YYISTIA.DPvXZEWm.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.C3nOP6Sj.js → ishikawaDiagram-YF4QCWOH.Cz829fsV.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.4IvLa2MW.js → journeyDiagram-JHISSGLW.enRnB9Me.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.DS87Cl-k.js → kanban-definition-UN3LZRKU.DnTNk6d2.js} +1 -1
- package/web/_astro/{linear.MzmRrNER.js → linear.BzGnBwNL.js} +1 -1
- package/web/_astro/{mermaid.core.fK-WNswS.js → mermaid.core.CSbTpIVz.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.BqipOHhY.js → mindmap-definition-RKZ34NQL.DGJDy8zT.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.0nBiPqFG.js → pieDiagram-4H26LBE5.HSPdi5_8.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.Szj7Zpx5.js → quadrantDiagram-W4KKPZXB.6Gv8mz2Y.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.D4UR6TWo.js → requirementDiagram-4Y6WPE33.CnBCtFwa.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.DAE7BdOs.js → sankeyDiagram-5OEKKPKP.5M8VWAu8.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.BP9ZVq25.js → sequenceDiagram-3UESZ5HK.DN41XN-U.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.BQW7mIxk.js → stateDiagram-AJRCARHV.ErOO3W8j.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.PbK4nDih.js → stateDiagram-v2-BHNVJYJU.B_ZHICM-.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.Ci_GvUpo.js → timeline-definition-PNZ67QCA.CWdosr07.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.DNw1Y5qx.js → vennDiagram-CIIHVFJN.BHzYjNV5.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.CwlkaaNe.js → wardleyDiagram-YWT4CUSO.h8x4IwPX.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.MI6ZuiH_.js → xychartDiagram-2RQKCTM6.RTapW2X8.js} +1 -1
- package/web/index.html +2 -2
- package/plugins/sp/commands/dev-parallel.md +0 -37
- package/web/_astro/TaskDetail.BynLnMNc.js +0 -1
- package/web/_astro/channel.DZVfto-r.js +0 -1
|
@@ -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)
|
|
161
|
-
#
|
|
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`**
|
|
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.
|
|
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.
|
|
61
|
-
"@gobing-ai/ts-ai-runner": "^0.5.
|
|
62
|
-
"@gobing-ai/ts-dual-workflow-engine": "^0.5.
|
|
63
|
-
"@gobing-ai/ts-infra": "^0.5.
|
|
64
|
-
"@gobing-ai/ts-llm-jsonl-importer": "^0.5.
|
|
65
|
-
"@gobing-ai/ts-rule-engine": "^0.5.
|
|
66
|
-
"@gobing-ai/ts-runtime": "^0.5.
|
|
67
|
-
"@gobing-ai/ts-utils": "^0.5.
|
|
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",
|
package/plugins/sp/README.md
CHANGED
|
@@ -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 "
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
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
|
-
|
|
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;
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
222
|
+
recordSessionStartFromPayloadText(await Bun.stdin.text());
|
|
201
223
|
} catch {
|
|
202
224
|
/* fail-open */
|
|
203
225
|
}
|