@gobing-ai/spur 0.3.93 → 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/rules/boundary/test-subpath-boundary.yaml +48 -0
- package/config/rules/strict/runtime-boundaries.yaml +2 -1
- package/config/rules/structure/protected-files.yaml +4 -0
- package/config/workflows/feature-verification.yaml +7 -4
- package/config/workflows/idea-pipeline.yaml +10 -4
- package/config/workflows/task-pipeline.yaml +71 -52
- package/config/workflows/wrapup-pipeline.yaml +92 -17
- package/package.json +15 -11
- package/plugins/sp/README.md +1 -1
- package/plugins/sp/commands/dev-review-session.md +4 -4
- package/plugins/sp/commands/dev-review.md +16 -5
- 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 +112 -172
- package/plugins/sp/lib/idea-handoff.generated.mjs +2 -2
- package/plugins/sp/lib/inline-run.generated.d.mts +4 -2
- package/plugins/sp/lib/inline-run.generated.mjs +35 -14
- 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 +60 -12
- package/plugins/sp/scripts/inline-run-setup.ts +122 -15
- 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 +15 -6
- package/plugins/sp/scripts/quality-gate.ts +30 -9
- package/plugins/sp/scripts/residual-scan.mjs +4 -4
- package/plugins/sp/scripts/residual-scan.ts +5 -4
- 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/session-review/SKILL.md +11 -9
- package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +9 -1
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +8 -2
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +65 -17
- package/plugins/sp/skills/spur-dev/references/flag-glossary.md +16 -3
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +22 -5
- package/plugins/sp/tsconfig.json +8 -0
- package/spur.js +20934 -21087
- package/web/_astro/{BoardApp.CerSBgis.js → BoardApp.BxJuwD7I.js} +151 -152
- 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.CyjRvNMY.js +1 -0
- package/web/_astro/architectureDiagram-3BPJPVTR.B4lWRzJA.js +36 -0
- package/web/_astro/{blockDiagram-GPEHLZMM.DXVIiv0p.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.BbF_zCxW.js → c4Diagram-AAUBKEIU.DT9Fj5Qx.js} +1 -1
- package/web/_astro/channel.CI6N_tCg.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.CAgQHpPC.js → chunk-2J33WTMH.SeSBWLg5.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.BN-5tpw4.js → chunk-4BX2VUAB.DoO4VE14.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.CnPkEEr0.js → chunk-55IACEB6.DrmTFA53.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.BQzQeMVm.js → chunk-727SXJPM.Bi-TUb_V.js} +4 -4
- package/web/_astro/{chunk-AQP2D5EJ.B6xNyDnL.js → chunk-AQP2D5EJ.B-xp8Uhi.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.C7f9Ih78.js → chunk-FMBD7UC4.DGWrDnUU.js} +1 -1
- package/web/_astro/chunk-JMJ3UQ3L.BQJEJHu4.js +34 -0
- package/web/_astro/{chunk-ND2GUHAM.CNV1dFXT.js → chunk-ND2GUHAM._BagvPDy.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.Cudn2TkJ.js → chunk-QZHKN3VN.VOboYWQ0.js} +1 -1
- package/web/_astro/chunk-YNUBSHFH.CxeAygfi.js +7 -0
- package/web/_astro/{classDiagram-4FO5ZUOK.D1NwP50q.js → classDiagram-4FO5ZUOK.D_apfHS7.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.D1NwP50q.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.BmK52w8G.js → cynefin-OW5HDTMX.D8borhV-.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.Bfy5CTDT.js → dagre-BM42HDAG.Uc0lvjcV.js} +2 -2
- package/web/_astro/diagram-2AECGRRQ.DDIxtDBo.js +43 -0
- package/web/_astro/diagram-5GNKFQAL.DVoDnD3H.js +10 -0
- package/web/_astro/{diagram-KO2AKTUF.CW_vMJ4z.js → diagram-KO2AKTUF.CRubTJ-0.js} +3 -3
- package/web/_astro/{diagram-LMA3HP47.B_8ZGF67.js → diagram-LMA3HP47.m9SeYcXQ.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.BppnHsdS.js → diagram-OG6HWLK6.C6s2ZjV5.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.BEuHXcjJ.js → erDiagram-TEJ5UH35.nSzr5KTj.js} +5 -5
- package/web/_astro/{flowDiagram-I6XJVG4X.CH-UlnGr.js → flowDiagram-I6XJVG4X.DrZcys16.js} +4 -4
- package/web/_astro/ganttDiagram-6RSMTGT7.Do4M1b-K.js +292 -0
- package/web/_astro/{gitGraphDiagram-PVQCEYII.XnPxPPZN.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.JyjYRu_T.js → infoDiagram-5YYISTIA.BIdOctFk.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.BBRBF-Fo.js → ishikawaDiagram-YF4QCWOH.CV5SuG2t.js} +5 -5
- package/web/_astro/{journeyDiagram-JHISSGLW.C_iymSyp.js → journeyDiagram-JHISSGLW.CxE-rEJ-.js} +1 -1
- package/web/_astro/jsx-runtime.DWOiEOII.js +1 -0
- package/web/_astro/{kanban-definition-UN3LZRKU.DdfW-Oqt.js → kanban-definition-UN3LZRKU.BPRdKWs_.js} +7 -7
- package/web/_astro/{linear.C2_IkbZT.js → linear.zRsuuDTE.js} +1 -1
- package/web/_astro/mermaid.core.jAJTcMKc.js +301 -0
- package/web/_astro/{mindmap-definition-RKZ34NQL.DAZIxQSK.js → mindmap-definition-RKZ34NQL.BffJxfCr.js} +2 -2
- package/web/_astro/{pieDiagram-4H26LBE5.CN8sIhKM.js → pieDiagram-4H26LBE5.CacwWaE3.js} +3 -3
- package/web/_astro/{quadrantDiagram-W4KKPZXB.3dGcX5GP.js → quadrantDiagram-W4KKPZXB.hXELD06-.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.BV2y4dd6.js → requirementDiagram-4Y6WPE33.DdEbPcqT.js} +3 -3
- package/web/_astro/{sankeyDiagram-5OEKKPKP.Cqo15Tvo.js → sankeyDiagram-5OEKKPKP.DgaDocKv.js} +4 -4
- package/web/_astro/{sequenceDiagram-3UESZ5HK.CROCPMJB.js → sequenceDiagram-3UESZ5HK.B0KScnAu.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.RfXZrkFE.js → stateDiagram-AJRCARHV.CV9M_WNc.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.CPXmbBs9.js → stateDiagram-v2-BHNVJYJU.BOAo74Et.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.DdgKTiO8.js → timeline-definition-PNZ67QCA.CqqepUMe.js} +3 -3
- package/web/_astro/{vennDiagram-CIIHVFJN.CPNVSHF1.js → vennDiagram-CIIHVFJN.DDqe4dOG.js} +5 -5
- package/web/_astro/{wardleyDiagram-YWT4CUSO.CQhA0Jyr.js → wardleyDiagram-YWT4CUSO.B3R9OPdO.js} +3 -3
- package/web/_astro/{xychartDiagram-2RQKCTM6.n61BWyy4.js → xychartDiagram-2RQKCTM6.B3SvgXpQ.js} +1 -1
- package/web/board-runtime.json +69 -0
- package/web/index.html +2 -2
- package/web/_astro/BoardApp.eoTz0pZs.js +0 -1
- package/web/_astro/TaskDetail.DCqiC-OZ.js +0 -1
- package/web/_astro/arc.CPwg6Rw0.js +0 -1
- package/web/_astro/architectureDiagram-3BPJPVTR.DM_vp_hO.js +0 -36
- package/web/_astro/channel.MYZLKNwy.js +0 -1
- package/web/_astro/client.yhYJvxCU.js +0 -9
- package/web/_astro/cose-bilkent-S5V4N54A.B1wSL-Xb.js +0 -1
- package/web/_astro/diagram-2AECGRRQ.DhNnvUvX.js +0 -43
- package/web/_astro/diagram-5GNKFQAL.lTX5KwnS.js +0 -10
- package/web/_astro/ganttDiagram-6RSMTGT7.BO81S85v.js +0 -292
- package/web/_astro/index.De90oHcH.js +0 -1
- package/web/_astro/index.Hjbr15fG.css +0 -1
- package/web/_astro/mermaid.core.GAOYeSR0.js +0 -303
|
@@ -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,12 +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 {
|
|
30
|
+
import { recordToolUseEvent, type ToolPayload } from '../context-post-tool';
|
|
31
|
+
import { recordSessionStart } from '../context-session-start';
|
|
32
|
+
import { recordSessionEnd } from '../context-session-stop';
|
|
28
33
|
import { classifyCommand } from '../destructive-policy';
|
|
29
34
|
import { couldBeTaskFile } from '../task-file-policy';
|
|
30
35
|
|
|
@@ -34,10 +39,6 @@ import { couldBeTaskFile } from '../task-file-policy';
|
|
|
34
39
|
// process lifetime, and lazy resolution keeps the extension testable (tests
|
|
35
40
|
// chdir into a temp project before driving handlers).
|
|
36
41
|
const spurContextDir = (): string => join(process.cwd(), '.spur', 'context');
|
|
37
|
-
const sessionFilePath = (): string => join(spurContextDir(), '.session.json');
|
|
38
|
-
const ledgerFilePath = (): string => join(spurContextDir(), 'token-ledger.jsonl');
|
|
39
|
-
const REDACTION_CAP = 4096;
|
|
40
|
-
const SUMMARY_MAX_CHARS = 200;
|
|
41
42
|
|
|
42
43
|
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
43
44
|
|
|
@@ -114,156 +115,86 @@ function resolveSpurTaskOwnership(filePath: string): TaskOwnership {
|
|
|
114
115
|
// `git push origin +main`, and warned on `git push --force-with-lease`). Do not
|
|
115
116
|
// re-introduce a local copy — add cases to `destructive-policy.test.ts` instead.
|
|
116
117
|
|
|
117
|
-
// ───
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
for (const
|
|
135
|
-
if (typeof
|
|
136
|
-
|
|
137
|
-
return s.length > SUMMARY_MAX_CHARS ? `${s.slice(0, SUMMARY_MAX_CHARS - 3)}...` : s;
|
|
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
138
|
}
|
|
139
139
|
}
|
|
140
|
-
return
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function redactText(text: string): string {
|
|
144
|
-
// Strip obvious secret patterns
|
|
145
|
-
let result = text.slice(0, REDACTION_CAP);
|
|
146
|
-
result = result.replace(
|
|
147
|
-
/-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----[\s\S]*?-----END\s+(?:RSA\s+)?PRIVATE\s+KEY-----/g,
|
|
148
|
-
'[REDACTED: PRIVATE KEY]',
|
|
149
|
-
);
|
|
150
|
-
result = result.replace(/ghp_[A-Za-z0-9]{36}/g, '[REDACTED: GITHUB_TOKEN]');
|
|
151
|
-
result = result.replace(/gho_[A-Za-z0-9]{36}/g, '[REDACTED: GITHUB_TOKEN]');
|
|
152
|
-
result = result.replace(/sk-[A-Za-z0-9]{20,}/g, '[REDACTED: API_KEY]');
|
|
153
|
-
result = result.replace(/api[_-]key['"]?\s*[:=]\s*['"][A-Za-z0-9_-]{16,}['"]/gi, '[REDACTED: API_KEY]');
|
|
154
|
-
result = result.replace(/AKIA[0-9A-Z]{16}/g, '[REDACTED: AWS_KEY]');
|
|
155
|
-
return result;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function appendToLedger(event: ToolEvent, command: string | undefined): void {
|
|
159
|
-
try {
|
|
160
|
-
if (!existsSync(spurContextDir())) return;
|
|
161
|
-
const sessionId = readSessionId();
|
|
162
|
-
if (!sessionId) return;
|
|
163
|
-
|
|
164
|
-
const summary = summarizeToolEvent(event);
|
|
165
|
-
const tokens = command ? estimateTokenCount(redactText(command)) : 0;
|
|
166
|
-
|
|
167
|
-
const ledgerEntry = JSON.stringify({
|
|
168
|
-
session: sessionId,
|
|
169
|
-
type: event.tool_name === 'Read' ? 'read' : 'write',
|
|
170
|
-
tool: event.tool_name,
|
|
171
|
-
path: event.tool_input?.file_path ?? null,
|
|
172
|
-
summary,
|
|
173
|
-
tokens,
|
|
174
|
-
timestamp: new Date().toISOString(),
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
appendFileSync(ledgerFilePath(), `${ledgerEntry}\n`);
|
|
178
|
-
} catch {
|
|
179
|
-
// fail-open: skip ledger writes on error
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function readSessionId(): string | undefined {
|
|
184
|
-
try {
|
|
185
|
-
if (!existsSync(sessionFilePath())) return undefined;
|
|
186
|
-
const data = JSON.parse(readFileSync(sessionFilePath(), 'utf-8')) as { session_id?: string };
|
|
187
|
-
return data.session_id;
|
|
188
|
-
} catch {
|
|
189
|
-
return undefined;
|
|
190
|
-
}
|
|
140
|
+
return parts.join('\n');
|
|
191
141
|
}
|
|
192
142
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
function cleanupSession(): void {
|
|
228
|
-
try {
|
|
229
|
-
if (!existsSync(sessionFilePath())) return;
|
|
230
|
-
const session = JSON.parse(readFileSync(sessionFilePath(), 'utf-8')) as { session_id?: string };
|
|
231
|
-
const sessionId = session.session_id;
|
|
232
|
-
|
|
233
|
-
// Compute rollup totals from ledger
|
|
234
|
-
let reads = 0;
|
|
235
|
-
let writes = 0;
|
|
236
|
-
let tokens = 0;
|
|
237
|
-
if (sessionId && existsSync(ledgerFilePath())) {
|
|
238
|
-
for (const line of readFileSync(ledgerFilePath(), 'utf-8').split('\n')) {
|
|
239
|
-
if (!line.trim()) continue;
|
|
240
|
-
try {
|
|
241
|
-
const evt = JSON.parse(line) as { session?: string; type?: string; tokens?: number };
|
|
242
|
-
if (evt.session !== sessionId) continue;
|
|
243
|
-
if (evt.type === 'read') reads++;
|
|
244
|
-
else if (evt.type === 'write') writes++;
|
|
245
|
-
if (evt.tokens) tokens += evt.tokens;
|
|
246
|
-
} catch {
|
|
247
|
-
// skip unparseable lines
|
|
248
|
-
}
|
|
249
|
-
}
|
|
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
|
+
};
|
|
250
177
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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;
|
|
267
198
|
}
|
|
268
199
|
}
|
|
269
200
|
|
|
@@ -272,8 +203,9 @@ function cleanupSession(): void {
|
|
|
272
203
|
export default function (pi: ExtensionAPI): void {
|
|
273
204
|
// ── task-write-guard + careful-guard ──────────────────────────────
|
|
274
205
|
pi.on('tool_call', async (event, ctx) => {
|
|
275
|
-
// task-write-guard: block Write/Edit to Spur task files
|
|
276
|
-
|
|
206
|
+
// task-write-guard: block Write/Edit to Spur task files.
|
|
207
|
+
// Both guards honor the same `off` escape hatches as the Claude hooks.
|
|
208
|
+
if ((event.toolName === 'write' || event.toolName === 'edit') && getEnvVar('SPUR_WRITE_GUARD') !== 'off') {
|
|
277
209
|
const input = event.input as Record<string, unknown> | undefined;
|
|
278
210
|
// Pi's write/edit tools use `path` (not Claude Code's `file_path`)
|
|
279
211
|
const filePath = resolveInputPath(input);
|
|
@@ -287,7 +219,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
287
219
|
}
|
|
288
220
|
|
|
289
221
|
// careful-guard: warn on destructive Bash commands
|
|
290
|
-
if (event.toolName === 'bash') {
|
|
222
|
+
if (event.toolName === 'bash' && getEnvVar('SPUR_CAREFUL') !== 'off') {
|
|
291
223
|
const input = event.input as Record<string, unknown> | undefined;
|
|
292
224
|
const command = typeof input?.command === 'string' ? input.command : '';
|
|
293
225
|
const hit = command ? classifyCommand(command) : null;
|
|
@@ -303,27 +235,35 @@ export default function (pi: ExtensionAPI): void {
|
|
|
303
235
|
return {};
|
|
304
236
|
});
|
|
305
237
|
|
|
306
|
-
// ── context-post-tool:
|
|
238
|
+
// ── context-post-tool: shared recordToolUseEvent (task 0969 R1) ──
|
|
307
239
|
pi.on('tool_result', async (event) => {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
+
}
|
|
318
250
|
});
|
|
319
251
|
|
|
320
|
-
// ── context-session-start:
|
|
252
|
+
// ── context-session-start: shared recordSessionStart (R2) ────────
|
|
321
253
|
pi.on('session_start', async () => {
|
|
322
|
-
|
|
254
|
+
try {
|
|
255
|
+
recordSessionStart(spurContextDir(), getEnvVars(), undefined, 'pi');
|
|
256
|
+
} catch {
|
|
257
|
+
// fail-open
|
|
258
|
+
}
|
|
323
259
|
});
|
|
324
260
|
|
|
325
|
-
// ── context-session-stop:
|
|
261
|
+
// ── context-session-stop: shared recordSessionEnd (R3) ──────────
|
|
326
262
|
pi.on('session_shutdown', async () => {
|
|
327
|
-
|
|
263
|
+
try {
|
|
264
|
+
recordSessionEnd(spurContextDir());
|
|
265
|
+
} catch {
|
|
266
|
+
// fail-open
|
|
267
|
+
}
|
|
328
268
|
});
|
|
329
269
|
}
|