@gobing-ai/spur 0.3.94 → 0.3.95
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/README.md +42 -0
- package/board/index.d.ts +38 -0
- package/config/plugin-scripts.json +5 -0
- package/config/workflows/feature-verification.yaml +7 -4
- package/config/workflows/idea-pipeline.yaml +10 -4
- package/config/workflows/task-pipeline.yaml +54 -26
- package/config/workflows/wrapup-pipeline.yaml +92 -17
- package/package.json +15 -11
- package/plugins/sp/commands/dev-verify.md +7 -4
- package/plugins/sp/commands/dev-verifyall.md +4 -3
- package/plugins/sp/commands/dev-wrap.md +1 -1
- package/plugins/sp/hooks/context-post-tool.ts +2 -2
- package/plugins/sp/hooks/context-session-start.ts +2 -1
- package/plugins/sp/hooks/context-session-stop.ts +22 -14
- package/plugins/sp/hooks/pi/guard-extension.ts +109 -150
- package/plugins/sp/lib/idea-handoff.generated.mjs +2 -2
- package/plugins/sp/lib/inline-run.generated.d.mts +2 -2
- package/plugins/sp/lib/inline-run.generated.mjs +10 -10
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/scripts/daily-summary/daily-summary.mjs +3 -3
- package/plugins/sp/scripts/daily-summary/daily-summary.ts +3 -3
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +2 -0
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +1 -0
- package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +9 -7
- package/plugins/sp/scripts/dogfood-testing/validate-report.ts +8 -7
- package/plugins/sp/scripts/feature-sync-bounded.mjs +5 -2
- package/plugins/sp/scripts/feature-sync-bounded.ts +3 -1
- package/plugins/sp/scripts/feature-verification-steps.ts +6 -6
- package/plugins/sp/scripts/history-anatomy-cache.mjs +3 -3
- package/plugins/sp/scripts/history-anatomy-cache.ts +4 -4
- package/plugins/sp/scripts/inline-run-setup.mjs +23 -13
- package/plugins/sp/scripts/inline-run-setup.ts +60 -27
- package/plugins/sp/scripts/pr-reviewing.mjs +2 -0
- package/plugins/sp/scripts/pr-reviewing.ts +1 -0
- package/plugins/sp/scripts/quality-gate.mjs +4 -2
- package/plugins/sp/scripts/quality-gate.ts +8 -2
- package/plugins/sp/scripts/residual-scan.mjs +3 -3
- package/plugins/sp/scripts/residual-scan.ts +3 -3
- package/plugins/sp/scripts/script-contract-check.ts +175 -46
- package/plugins/sp/scripts/script-root.mjs +125 -0
- package/plugins/sp/scripts/script-root.ts +195 -0
- package/plugins/sp/scripts/stage-registry-adapter.ts +1 -1
- package/plugins/sp/scripts/task-evidence-precheck.ts +4 -2
- package/plugins/sp/scripts/task-size-precheck.ts +3 -1
- package/plugins/sp/scripts/validate-commands.ts +12 -12
- package/plugins/sp/scripts/validate-flag-contracts.ts +42 -30
- package/plugins/sp/scripts/verify-answer-lint.ts +3 -1
- package/plugins/sp/scripts/workflow-step-profile.mjs +5 -2
- package/plugins/sp/scripts/workflow-step-profile.ts +3 -1
- package/plugins/sp/scripts/wrapup-drift-probe.mjs +5 -2
- package/plugins/sp/scripts/wrapup-drift-probe.ts +4 -2
- package/plugins/sp/scripts/wrapup-steps.mjs +32 -8
- package/plugins/sp/scripts/wrapup-steps.ts +56 -10
- package/plugins/sp/skills/code-verification/SKILL.md +12 -7
- package/plugins/sp/skills/code-verification/references/verdict-schema.md +11 -0
- package/plugins/sp/skills/functional-review/SKILL.md +1 -1
- package/plugins/sp/skills/next-router/references/routing-table.md +1 -1
- package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +9 -1
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +30 -10
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +6 -3
- package/plugins/sp/tsconfig.json +8 -0
- package/spur.js +5194 -4457
- package/web/_astro/{BoardApp.FTEs3-N8.js → BoardApp.BxJuwD7I.js} +150 -153
- package/web/_astro/BoardApp.Cpxntzad.js +1 -0
- package/web/_astro/TaskDetail.C5bW4WGV.js +1 -0
- package/web/_astro/_commonjsHelpers.CqkleIqs.js +1 -0
- package/web/_astro/{arc.uuAf51IT.js → arc.CyjRvNMY.js} +1 -1
- package/web/_astro/architectureDiagram-3BPJPVTR.B4lWRzJA.js +36 -0
- package/web/_astro/{blockDiagram-GPEHLZMM.D1mGCq3p.js → blockDiagram-GPEHLZMM.Be38USQ2.js} +1 -1
- package/web/_astro/board-facade-react-dom-client.B2U5wMvk.js +1 -0
- package/web/_astro/board-facade-react-dom.Cd2MuMhF.js +1 -0
- package/web/_astro/board-facade-react-jsx-dev-runtime.B_L3JzfC.js +1 -0
- package/web/_astro/board-facade-react-jsx-runtime.BY2yfHRi.js +1 -0
- package/web/_astro/board-facade-react-router-dom.Dy5pjzOw.js +1 -0
- package/web/_astro/board-facade-react-router.DjjqU27_.js +1 -0
- package/web/_astro/board-facade-react.Dgf4GRyj.js +1 -0
- package/web/_astro/{c4Diagram-AAUBKEIU.CMsolcde.js → c4Diagram-AAUBKEIU.DT9Fj5Qx.js} +1 -1
- package/web/_astro/channel.CI6N_tCg.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.CrGA3fik.js → chunk-2J33WTMH.SeSBWLg5.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.DsLVVla0.js → chunk-4BX2VUAB.DoO4VE14.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.Dxc59Tfi.js → chunk-55IACEB6.DrmTFA53.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.CaEVE2Wy.js → chunk-727SXJPM.Bi-TUb_V.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.BiJ4HXeI.js → chunk-AQP2D5EJ.B-xp8Uhi.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.Mxf1fru5.js → chunk-FMBD7UC4.DGWrDnUU.js} +1 -1
- package/web/_astro/chunk-JMJ3UQ3L.BQJEJHu4.js +34 -0
- package/web/_astro/{chunk-ND2GUHAM.pyOWQixH.js → chunk-ND2GUHAM._BagvPDy.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.BmEsg4vr.js → chunk-QZHKN3VN.VOboYWQ0.js} +1 -1
- package/web/_astro/chunk-YNUBSHFH.CxeAygfi.js +7 -0
- package/web/_astro/{classDiagram-4FO5ZUOK.BHhhMFTO.js → classDiagram-4FO5ZUOK.D_apfHS7.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.BHhhMFTO.js → classDiagram-v2-Q7XG4LA2.D_apfHS7.js} +1 -1
- package/web/_astro/client.BhluLQqe.js +1 -0
- package/web/_astro/client.DBpSRLnx.js +9 -0
- package/web/_astro/cose-bilkent-S5V4N54A.DNLU_L-x.js +1 -0
- package/web/_astro/{cynefin-OW5HDTMX.C2j1_lKL.js → cynefin-OW5HDTMX.D8borhV-.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.hZ2NCTdT.js → dagre-BM42HDAG.Uc0lvjcV.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.Bfw5_EzK.js → diagram-2AECGRRQ.DDIxtDBo.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.Bt1V_Tmk.js → diagram-5GNKFQAL.DVoDnD3H.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.DEk-YFwp.js → diagram-KO2AKTUF.CRubTJ-0.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.CDjndtQm.js → diagram-LMA3HP47.m9SeYcXQ.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.cFnUHScG.js → diagram-OG6HWLK6.C6s2ZjV5.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.DlhYp7NV.js → erDiagram-TEJ5UH35.nSzr5KTj.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.DNTpsxfx.js → flowDiagram-I6XJVG4X.DrZcys16.js} +1 -1
- package/web/_astro/ganttDiagram-6RSMTGT7.Do4M1b-K.js +292 -0
- package/web/_astro/{gitGraphDiagram-PVQCEYII.DkEqNI0P.js → gitGraphDiagram-PVQCEYII.BWc9oJ4l.js} +1 -1
- package/web/_astro/index.5dn-Zm-2.js +1 -0
- package/web/_astro/index.C-IqPpW2.js +1 -0
- package/web/_astro/index.CENnIEqT.css +1 -0
- package/web/_astro/index.Cyc_GA4f.js +1 -0
- package/web/_astro/index.tZfqwSut.js +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.CrjioCTG.js → infoDiagram-5YYISTIA.BIdOctFk.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.BcK0CN8n.js → ishikawaDiagram-YF4QCWOH.CV5SuG2t.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.p0CbBDX1.js → journeyDiagram-JHISSGLW.CxE-rEJ-.js} +1 -1
- package/web/_astro/jsx-runtime.DWOiEOII.js +1 -0
- package/web/_astro/{kanban-definition-UN3LZRKU.puHIFt6J.js → kanban-definition-UN3LZRKU.BPRdKWs_.js} +1 -1
- package/web/_astro/{linear.Bb-3a1d7.js → linear.zRsuuDTE.js} +1 -1
- package/web/_astro/{mermaid.core.CJDgXOJs.js → mermaid.core.jAJTcMKc.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.BoAZEI9t.js → mindmap-definition-RKZ34NQL.BffJxfCr.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.CJNSdqY7.js → pieDiagram-4H26LBE5.CacwWaE3.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.DtTy7_0Z.js → quadrantDiagram-W4KKPZXB.hXELD06-.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.BNYV98Xg.js → requirementDiagram-4Y6WPE33.DdEbPcqT.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.DRq9DGHp.js → sankeyDiagram-5OEKKPKP.DgaDocKv.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.B7KdBFCm.js → sequenceDiagram-3UESZ5HK.B0KScnAu.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.CD62ZJ2H.js → stateDiagram-AJRCARHV.CV9M_WNc.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.CeVk1TAj.js → stateDiagram-v2-BHNVJYJU.BOAo74Et.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.C_ZPA5tT.js → timeline-definition-PNZ67QCA.CqqepUMe.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.CrThO_FQ.js → vennDiagram-CIIHVFJN.DDqe4dOG.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.DSZCA5nl.js → wardleyDiagram-YWT4CUSO.B3R9OPdO.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.KI7baTuk.js → xychartDiagram-2RQKCTM6.B3SvgXpQ.js} +1 -1
- package/web/board-runtime.json +69 -0
- package/web/index.html +2 -2
- package/web/_astro/BoardApp.C02hAHPO.js +0 -1
- package/web/_astro/TaskDetail.C-GdsS-t.js +0 -1
- package/web/_astro/architectureDiagram-3BPJPVTR.CGe629A1.js +0 -36
- package/web/_astro/channel.fsgl7o5j.js +0 -1
- package/web/_astro/client.yhYJvxCU.js +0 -9
- package/web/_astro/cose-bilkent-S5V4N54A.2fH4YOlp.js +0 -1
- package/web/_astro/ganttDiagram-6RSMTGT7.DC_p36PI.js +0 -292
- package/web/_astro/index.De90oHcH.js +0 -1
- package/web/_astro/index.Hjbr15fG.css +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gobing-ai/spur",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.95",
|
|
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",
|
|
@@ -24,12 +24,16 @@
|
|
|
24
24
|
"spur": "spur.js"
|
|
25
25
|
},
|
|
26
26
|
"exports": {
|
|
27
|
-
"./schemas/*": "./schemas/*"
|
|
27
|
+
"./schemas/*": "./schemas/*",
|
|
28
|
+
"./board": {
|
|
29
|
+
"types": "./board/index.d.ts"
|
|
30
|
+
}
|
|
28
31
|
},
|
|
29
32
|
"files": [
|
|
30
33
|
"spur.js",
|
|
31
34
|
"config",
|
|
32
35
|
"schemas",
|
|
36
|
+
"board",
|
|
33
37
|
"web",
|
|
34
38
|
"README.md",
|
|
35
39
|
"plugins/",
|
|
@@ -45,7 +49,7 @@
|
|
|
45
49
|
"dev": "bun run src/index.ts",
|
|
46
50
|
"build": "bun run ../../scripts/spur-dev.ts build-cli",
|
|
47
51
|
"build:binaries": "bun run ../../scripts/spur-dev.ts build-binaries",
|
|
48
|
-
"build:bundle": "bun run ../../scripts/spur-dev.ts bundle-web && 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/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",
|
|
49
53
|
"test": "bun test tests --coverage --coverage-dir=../../.coverage --pass-with-no-tests",
|
|
50
54
|
"typecheck": "tsc --noEmit",
|
|
51
55
|
"prepack": "bun run build:bundle",
|
|
@@ -53,14 +57,14 @@
|
|
|
53
57
|
},
|
|
54
58
|
"devDependencies": {
|
|
55
59
|
"@commander-js/extra-typings": "^14.0.0",
|
|
56
|
-
"@gobing-ai/ts-db": "^0.5.
|
|
57
|
-
"@gobing-ai/ts-ai-runner": "^0.5.
|
|
58
|
-
"@gobing-ai/ts-dual-workflow-engine": "^0.5.
|
|
59
|
-
"@gobing-ai/ts-infra": "^0.5.
|
|
60
|
-
"@gobing-ai/ts-llm-jsonl-importer": "^0.5.
|
|
61
|
-
"@gobing-ai/ts-rule-engine": "^0.5.
|
|
62
|
-
"@gobing-ai/ts-runtime": "^0.5.
|
|
63
|
-
"@gobing-ai/ts-utils": "^0.5.
|
|
60
|
+
"@gobing-ai/ts-db": "^0.5.11",
|
|
61
|
+
"@gobing-ai/ts-ai-runner": "^0.5.11",
|
|
62
|
+
"@gobing-ai/ts-dual-workflow-engine": "^0.5.11",
|
|
63
|
+
"@gobing-ai/ts-infra": "^0.5.11",
|
|
64
|
+
"@gobing-ai/ts-llm-jsonl-importer": "^0.5.11",
|
|
65
|
+
"@gobing-ai/ts-rule-engine": "^0.5.11",
|
|
66
|
+
"@gobing-ai/ts-runtime": "^0.5.11",
|
|
67
|
+
"@gobing-ai/ts-utils": "^0.5.11",
|
|
64
68
|
"@types/bun": "1.3.14",
|
|
65
69
|
"@types/figlet": "^1.7.0",
|
|
66
70
|
"@types/node-notifier": "8.0.5",
|
|
@@ -40,10 +40,13 @@ See `sp:code-verification` § Shippable readiness gate.
|
|
|
40
40
|
|
|
41
41
|
- Apply the [inline-default execution-surface contract](../skills/spur-dev/references/cross-cutting.md#inline-default-execution-surface).
|
|
42
42
|
- `Skill(skill="sp:code-verification", args="verify $ARGUMENTS")`
|
|
43
|
-
- **Residual sweep (F96 R1):** after
|
|
44
|
-
`.spur/run/<wbs>-verdict.json
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
- **Residual sweep (F96 R1, post-record since 0987):** after `task verdict` derives
|
|
44
|
+
`.spur/run/<wbs>-verdict.json` and `spur task record` has flipped the verdict-proven R/AC boxes,
|
|
45
|
+
run `residual-scan scan` + `fold` (observe-only; folds the verdict's `residual-sweep` check,
|
|
46
|
+
PASS → PARTIAL when a blocking residual exists) — under every `--fix` mode — then, only on a
|
|
47
|
+
downgrade, re-run `spur task record <wbs> --verdict-file .spur/run/<wbs>-verdict.json` so Testing
|
|
48
|
+
carries the final verdict. Same order as the pipeline `record` state (0983): scanning before
|
|
49
|
+
record reads unticked-but-proven boxes and folds a clean PASS to PARTIAL. Resolve the script via `superskill script path sp
|
|
47
50
|
residual-scan.mjs`; never reference `plugins/sp/scripts/` in shipped surfaces
|
|
48
51
|
(script-contract-check rule 4). When the task reaches `done` through `--next`, run
|
|
49
52
|
`residual-scan settle` (best-effort). On a folded PARTIAL: the bounded remediation loop has
|
|
@@ -72,8 +72,9 @@ Without `--fix all`, shippable is not evaluated (optional note: use `--fix all`
|
|
|
72
72
|
- Batch orchestration: `Skill(skill="sp:spur-dev", args="verifyall $ARGUMENTS")`
|
|
73
73
|
- Per-task verification (inner): `Skill(skill="sp:code-verification", args="verify <wbs> $SHARED_FLAGS")`
|
|
74
74
|
- Shippable gate: once after the batch (same skill § Shippable readiness gate)
|
|
75
|
-
- **Residual sweep (F96 R1, per task):** same contract as
|
|
76
|
-
verdict artifact is derived
|
|
77
|
-
|
|
75
|
+
- **Residual sweep (F96 R1, per task, post-record since 0987):** same contract as
|
|
76
|
+
`/sp:dev-verify` — after each task's verdict artifact is derived and `spur task record` has
|
|
77
|
+
flipped its verdict-proven boxes, run `residual-scan scan` + `fold` (under every `--fix` mode),
|
|
78
|
+
and re-record only on a downgrade; on reaching `done` via `--next`, run `settle`. Script resolution:
|
|
78
79
|
`superskill script path sp residual-scan.mjs` (never `plugins/sp/scripts/` —
|
|
79
80
|
script-contract-check rule 4).
|
|
@@ -17,7 +17,7 @@ Wraps the **wrapup-pipeline.yaml** workflow.
|
|
|
17
17
|
| `--agent` `<inline\|auto\|name>` | Who runs the wrap's model-bearing steps. Wrap is workflow-backed (headless): `omit` and `--agent inline` resolve identically per task 0687 — tier substitution under objective trigger 3 (durable auditable run record required) with a warning naming the substituted executor; `auto` tier-resolves an executor; a name pins that executor into `vars.agent`. | inline |
|
|
18
18
|
| `--auto` | Skip objective HITL gates. | off |
|
|
19
19
|
| `--merge` | Merge the wrap branch. | off |
|
|
20
|
-
| `--dry-run` |
|
|
20
|
+
| `--dry-run` | Validate and walk transitions without executing actions. Conditional states therefore resolve to their declared failure edges (their guard files are never written), so a render ending in `failed` is **not** a run failure — it reports "this state's route cannot be decided without running it". Use a real run to preview a route. | off |
|
|
21
21
|
|
|
22
22
|
For shared semantics, see the [flag glossary](../skills/spur-dev/references/flag-glossary.md).
|
|
23
23
|
|
|
@@ -34,7 +34,7 @@ export const REDACTION_CAP_BYTES = 4096;
|
|
|
34
34
|
/** Max command/pattern summary length before ellipsis (chars). */
|
|
35
35
|
export const SUMMARY_MAX_CHARS = 200;
|
|
36
36
|
|
|
37
|
-
interface ToolPayload {
|
|
37
|
+
export interface ToolPayload {
|
|
38
38
|
session_id?: string;
|
|
39
39
|
tool_name?: string;
|
|
40
40
|
tool_input?: {
|
|
@@ -115,7 +115,7 @@ export function redactText(text: string, capBytes: number = REDACTION_CAP_BYTES)
|
|
|
115
115
|
if (bytes.length <= capBytes) return s;
|
|
116
116
|
// Truncate on byte boundary then append marker
|
|
117
117
|
let end = capBytes;
|
|
118
|
-
while (end > 0 && (bytes[end] & 0xc0) === 0x80) end--;
|
|
118
|
+
while (end > 0 && ((bytes[end] ?? 0) & 0xc0) === 0x80) end--;
|
|
119
119
|
return `${new TextDecoder().decode(bytes.subarray(0, end))}…[truncated]`;
|
|
120
120
|
}
|
|
121
121
|
|
|
@@ -126,6 +126,7 @@ export function recordSessionStart(
|
|
|
126
126
|
dir: string,
|
|
127
127
|
env: NodeJS.ProcessEnv = getEnvVars(),
|
|
128
128
|
now: () => Date = () => new Date(),
|
|
129
|
+
agentFallback?: string,
|
|
129
130
|
): string | null {
|
|
130
131
|
try {
|
|
131
132
|
mkdirSync(dir, { recursive: true });
|
|
@@ -140,7 +141,7 @@ export function recordSessionStart(
|
|
|
140
141
|
const pad = (n: number) => String(n).padStart(2, '0');
|
|
141
142
|
const sessionId = `session-${at.toISOString().slice(0, 10)}-${pad(at.getHours())}${pad(at.getMinutes())}`;
|
|
142
143
|
const ts = at.toISOString();
|
|
143
|
-
const agent = resolveAgentHint(env);
|
|
144
|
+
const agent = resolveAgentHint(env, agentFallback);
|
|
144
145
|
const model = resolveModelHint(env);
|
|
145
146
|
|
|
146
147
|
const sessionBody: Record<string, unknown> = {
|
|
@@ -49,30 +49,30 @@ function computeSessionTotals(
|
|
|
49
49
|
return { reads, writes, tokens };
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Finalize one session (task 0969 R3): read the `.session.json` pointer, compute the session's
|
|
54
|
+
* rollup totals, append the `session_end` row, remove the pointer (best-effort) and return the
|
|
55
|
+
* event. Shared by this hook's entrypoint and Pi's `session_shutdown` so both hosts roll up
|
|
56
|
+
* identically. Returns null on every fail-open path.
|
|
57
|
+
*/
|
|
58
|
+
export function recordSessionEnd(dir: string, now: () => Date = () => new Date()): Record<string, unknown> | null {
|
|
59
59
|
const sessionFile = join(dir, '.session.json');
|
|
60
|
-
if (!existsSync(sessionFile))
|
|
60
|
+
if (!existsSync(sessionFile)) return null;
|
|
61
61
|
|
|
62
62
|
let sessionId = '';
|
|
63
63
|
try {
|
|
64
64
|
const session = JSON.parse(readFileSync(sessionFile, 'utf-8')) as { session?: string };
|
|
65
65
|
sessionId = session.session ?? '';
|
|
66
66
|
} catch {
|
|
67
|
-
|
|
67
|
+
return null;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
if (!sessionId)
|
|
70
|
+
if (!sessionId) return null;
|
|
71
71
|
|
|
72
72
|
const totals = computeSessionTotals(join(dir, 'token-ledger.jsonl'), sessionId);
|
|
73
73
|
|
|
74
74
|
const event = {
|
|
75
|
-
ts:
|
|
75
|
+
ts: now().toISOString(),
|
|
76
76
|
session: sessionId,
|
|
77
77
|
type: 'session_end' as const,
|
|
78
78
|
totals,
|
|
@@ -81,7 +81,7 @@ async function main(): Promise<void> {
|
|
|
81
81
|
try {
|
|
82
82
|
appendFileSync(join(dir, 'token-ledger.jsonl'), `${JSON.stringify(event)}\n`);
|
|
83
83
|
} catch {
|
|
84
|
-
|
|
84
|
+
return null;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
try {
|
|
@@ -90,7 +90,15 @@ async function main(): Promise<void> {
|
|
|
90
90
|
// cleanup is best-effort
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
|
|
93
|
+
return event;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
// Entrypoint: run the shared core, then exit 0 (fail-open) as this hook always has.
|
|
97
|
+
if (import.meta.main) {
|
|
98
|
+
try {
|
|
99
|
+
recordSessionEnd(join(getEnvVar('CLAUDE_PROJECT_DIR') ?? process.cwd(), '.spur', 'context'));
|
|
100
|
+
} catch {
|
|
101
|
+
/* fail-open: every error path exits 0 */
|
|
102
|
+
}
|
|
103
|
+
process.exit(0);
|
|
104
|
+
}
|
|
@@ -7,9 +7,12 @@
|
|
|
7
7
|
* Implements:
|
|
8
8
|
* - task-write-guard (tool_call → block Write/Edit to Spur task files)
|
|
9
9
|
* - careful-guard (tool_call → warn on destructive Bash commands)
|
|
10
|
-
* - context-post-tool (tool_result →
|
|
11
|
-
* - context-session-start (session_start →
|
|
12
|
-
* - context-session-stop (session_shutdown →
|
|
10
|
+
* - context-post-tool (tool_result → shared recordToolUseEvent)
|
|
11
|
+
* - context-session-start (session_start → shared recordSessionStart)
|
|
12
|
+
* - context-session-stop (session_shutdown → shared recordSessionEnd)
|
|
13
|
+
*
|
|
14
|
+
* The ledger and session cores are shared with the Claude hooks (task 0969): this
|
|
15
|
+
* extension only normalizes Pi event shapes and adapts them to those cores.
|
|
13
16
|
*
|
|
14
17
|
* Installation:
|
|
15
18
|
* Add to ~/.pi/agent/settings.json packages:
|
|
@@ -19,13 +22,14 @@
|
|
|
19
22
|
*/
|
|
20
23
|
|
|
21
24
|
import { spawnSync } from 'node:child_process';
|
|
22
|
-
import {
|
|
25
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
23
26
|
import { homedir } from 'node:os';
|
|
24
27
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
25
28
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
26
29
|
import { getEnvVar, getEnvVars } from '../../lib/env';
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
30
|
+
import { recordToolUseEvent, type ToolPayload } from '../context-post-tool';
|
|
31
|
+
import { recordSessionStart } from '../context-session-start';
|
|
32
|
+
import { recordSessionEnd } from '../context-session-stop';
|
|
29
33
|
import { classifyCommand } from '../destructive-policy';
|
|
30
34
|
import { couldBeTaskFile } from '../task-file-policy';
|
|
31
35
|
|
|
@@ -35,8 +39,6 @@ import { couldBeTaskFile } from '../task-file-policy';
|
|
|
35
39
|
// process lifetime, and lazy resolution keeps the extension testable (tests
|
|
36
40
|
// chdir into a temp project before driving handlers).
|
|
37
41
|
const spurContextDir = (): string => join(process.cwd(), '.spur', 'context');
|
|
38
|
-
const sessionFilePath = (): string => join(spurContextDir(), '.session.json');
|
|
39
|
-
const ledgerFilePath = (): string => join(spurContextDir(), 'token-ledger.jsonl');
|
|
40
42
|
|
|
41
43
|
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
42
44
|
|
|
@@ -113,137 +115,86 @@ function resolveSpurTaskOwnership(filePath: string): TaskOwnership {
|
|
|
113
115
|
// `git push origin +main`, and warned on `git push --force-with-lease`). Do not
|
|
114
116
|
// re-introduce a local copy — add cases to `destructive-policy.test.ts` instead.
|
|
115
117
|
|
|
116
|
-
// ───
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (!existsSync(spurContextDir())) return;
|
|
138
|
-
const sessionId = readSessionId();
|
|
139
|
-
if (!sessionId) return;
|
|
140
|
-
|
|
141
|
-
const summary = summarizeToolEvent(event);
|
|
142
|
-
const tokens = command ? Math.ceil(cappedByteLength(command) / 4) : 0;
|
|
143
|
-
const input = event.tool_input ?? {};
|
|
144
|
-
// Pi tool names are lowercase (`read`) and its write/edit tools take `path`, not `file_path`.
|
|
145
|
-
const path = typeof input.file_path === 'string' ? input.file_path : input.path;
|
|
146
|
-
|
|
147
|
-
const ledgerEntry = JSON.stringify({
|
|
148
|
-
session: sessionId,
|
|
149
|
-
type: event.tool_name?.toLowerCase() === 'read' ? 'read' : 'write',
|
|
150
|
-
tool: event.tool_name,
|
|
151
|
-
file: typeof path === 'string' ? path : undefined,
|
|
152
|
-
summary,
|
|
153
|
-
tokens,
|
|
154
|
-
ts: new Date().toISOString(),
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
appendFileSync(ledgerFilePath(), `${ledgerEntry}\n`);
|
|
158
|
-
} catch {
|
|
159
|
-
// fail-open: skip ledger writes on error
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function readSessionId(): string | undefined {
|
|
164
|
-
try {
|
|
165
|
-
if (!existsSync(sessionFilePath())) return undefined;
|
|
166
|
-
const data = JSON.parse(readFileSync(sessionFilePath(), 'utf-8')) as { session_id?: string };
|
|
167
|
-
return data.session_id;
|
|
168
|
-
} catch {
|
|
169
|
-
return undefined;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// ─── Session helpers (agent/model hints from agent-hint.ts; mirrors context-session-stop.ts) ─
|
|
174
|
-
|
|
175
|
-
function generateSessionId(): string {
|
|
176
|
-
const timestamp = Date.now().toString(36);
|
|
177
|
-
const random = Math.random().toString(36).slice(2, 10);
|
|
178
|
-
return `${timestamp}-${random}`;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function initSession(): void {
|
|
182
|
-
try {
|
|
183
|
-
mkdirSync(spurContextDir(), { recursive: true });
|
|
184
|
-
const sessionId = generateSessionId();
|
|
185
|
-
const session = {
|
|
186
|
-
session_id: sessionId,
|
|
187
|
-
agent: resolveAgentHintShared(getEnvVars(), 'pi'),
|
|
188
|
-
model: resolveModelHintShared(getEnvVars()),
|
|
189
|
-
started_at: new Date().toISOString(),
|
|
190
|
-
};
|
|
191
|
-
writeFileSync(sessionFilePath(), `${JSON.stringify(session, null, 2)}\n`);
|
|
192
|
-
|
|
193
|
-
// Append session_start event to ledger
|
|
194
|
-
const startEntry = JSON.stringify({
|
|
195
|
-
session: sessionId,
|
|
196
|
-
type: 'session_start',
|
|
197
|
-
agent: session.agent,
|
|
198
|
-
model: session.model,
|
|
199
|
-
ts: session.started_at,
|
|
200
|
-
});
|
|
201
|
-
appendFileSync(ledgerFilePath(), `${startEntry}\n`);
|
|
202
|
-
} catch {
|
|
203
|
-
// fail-open
|
|
118
|
+
// ─── Pi → Claude payload normalization (task 0969 R1) ─────────────────────
|
|
119
|
+
|
|
120
|
+
/** Pi tool name → the Claude tool name the shared core records. */
|
|
121
|
+
const PI_TO_CLAUDE: Record<string, string> = {
|
|
122
|
+
read: 'Read',
|
|
123
|
+
write: 'Write',
|
|
124
|
+
edit: 'Edit',
|
|
125
|
+
bash: 'Bash',
|
|
126
|
+
grep: 'Grep',
|
|
127
|
+
find: 'Glob',
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/** Join the text parts of a Pi tool_result `content` payload. */
|
|
131
|
+
function piContentText(content: unknown): string {
|
|
132
|
+
if (typeof content === 'string') return content;
|
|
133
|
+
if (!Array.isArray(content)) return '';
|
|
134
|
+
const parts: string[] = [];
|
|
135
|
+
for (const part of content) {
|
|
136
|
+
if (part && typeof part === 'object' && typeof (part as { text?: unknown }).text === 'string') {
|
|
137
|
+
parts.push((part as { text: string }).text);
|
|
138
|
+
}
|
|
204
139
|
}
|
|
140
|
+
return parts.join('\n');
|
|
205
141
|
}
|
|
206
142
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
143
|
+
/**
|
|
144
|
+
* Pure Pi-event → Claude-{@link ToolPayload} adapter. An unmapped tool returns null so the
|
|
145
|
+
* shared core writes no row (R1's row filter). Search tools keep `path` (never `file_path`)
|
|
146
|
+
* because the core treats `file_path` as the written file, not a search root.
|
|
147
|
+
*/
|
|
148
|
+
export function normalizePiToolEvent(
|
|
149
|
+
toolName: string,
|
|
150
|
+
input: Record<string, unknown> | undefined,
|
|
151
|
+
content?: unknown,
|
|
152
|
+
): ToolPayload | null {
|
|
153
|
+
const mapped = PI_TO_CLAUDE[toolName];
|
|
154
|
+
if (mapped === undefined) return null;
|
|
155
|
+
const i = input ?? {};
|
|
156
|
+
const str = (value: unknown): string => (typeof value === 'string' ? value : '');
|
|
157
|
+
const path = str(i.path) || str(i.file_path);
|
|
158
|
+
const text = piContentText(content);
|
|
159
|
+
const tool_response = text.length > 0 ? { content: text } : undefined;
|
|
160
|
+
|
|
161
|
+
switch (mapped) {
|
|
162
|
+
case 'Read':
|
|
163
|
+
return { tool_name: mapped, tool_input: { file_path: path }, tool_response };
|
|
164
|
+
case 'Write':
|
|
165
|
+
return { tool_name: mapped, tool_input: { file_path: path, content: str(i.content) }, tool_response };
|
|
166
|
+
case 'Edit': {
|
|
167
|
+
const edits = Array.isArray(i.edits) ? (i.edits as Array<Record<string, unknown>>) : [];
|
|
168
|
+
return {
|
|
169
|
+
tool_name: mapped,
|
|
170
|
+
tool_input: {
|
|
171
|
+
file_path: path,
|
|
172
|
+
old_string: edits.map((e) => str(e.oldText)).join(''),
|
|
173
|
+
new_string: edits.map((e) => str(e.newText)).join(''),
|
|
174
|
+
},
|
|
175
|
+
tool_response,
|
|
176
|
+
};
|
|
230
177
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
178
|
+
case 'Grep':
|
|
179
|
+
return {
|
|
180
|
+
tool_name: mapped,
|
|
181
|
+
tool_input: {
|
|
182
|
+
pattern: str(i.pattern),
|
|
183
|
+
...(path ? { path } : {}),
|
|
184
|
+
...(str(i.glob) ? { glob: str(i.glob) } : {}),
|
|
185
|
+
},
|
|
186
|
+
tool_response,
|
|
187
|
+
};
|
|
188
|
+
case 'Glob':
|
|
189
|
+
return {
|
|
190
|
+
tool_name: mapped,
|
|
191
|
+
tool_input: { pattern: str(i.pattern), ...(path ? { path } : {}) },
|
|
192
|
+
tool_response,
|
|
193
|
+
};
|
|
194
|
+
case 'Bash':
|
|
195
|
+
return { tool_name: mapped, tool_input: { command: str(i.command) }, tool_response };
|
|
196
|
+
default:
|
|
197
|
+
return null;
|
|
247
198
|
}
|
|
248
199
|
}
|
|
249
200
|
|
|
@@ -284,27 +235,35 @@ export default function (pi: ExtensionAPI): void {
|
|
|
284
235
|
return {};
|
|
285
236
|
});
|
|
286
237
|
|
|
287
|
-
// ── context-post-tool:
|
|
238
|
+
// ── context-post-tool: shared recordToolUseEvent (task 0969 R1) ──
|
|
288
239
|
pi.on('tool_result', async (event) => {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
240
|
+
try {
|
|
241
|
+
const payload = normalizePiToolEvent(
|
|
242
|
+
event.toolName,
|
|
243
|
+
event.input as Record<string, unknown> | undefined,
|
|
244
|
+
(event as { content?: unknown }).content,
|
|
245
|
+
);
|
|
246
|
+
if (payload) recordToolUseEvent(spurContextDir(), payload);
|
|
247
|
+
} catch {
|
|
248
|
+
// fail-open
|
|
249
|
+
}
|
|
299
250
|
});
|
|
300
251
|
|
|
301
|
-
// ── context-session-start:
|
|
252
|
+
// ── context-session-start: shared recordSessionStart (R2) ────────
|
|
302
253
|
pi.on('session_start', async () => {
|
|
303
|
-
|
|
254
|
+
try {
|
|
255
|
+
recordSessionStart(spurContextDir(), getEnvVars(), undefined, 'pi');
|
|
256
|
+
} catch {
|
|
257
|
+
// fail-open
|
|
258
|
+
}
|
|
304
259
|
});
|
|
305
260
|
|
|
306
|
-
// ── context-session-stop:
|
|
261
|
+
// ── context-session-stop: shared recordSessionEnd (R3) ──────────
|
|
307
262
|
pi.on('session_shutdown', async () => {
|
|
308
|
-
|
|
263
|
+
try {
|
|
264
|
+
recordSessionEnd(spurContextDir());
|
|
265
|
+
} catch {
|
|
266
|
+
// fail-open
|
|
267
|
+
}
|
|
309
268
|
});
|
|
310
269
|
}
|