@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.
Files changed (153) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +42 -0
  3. package/board/index.d.ts +38 -0
  4. package/config/plugin-scripts.json +5 -0
  5. package/config/rules/boundary/test-subpath-boundary.yaml +48 -0
  6. package/config/rules/strict/runtime-boundaries.yaml +2 -1
  7. package/config/rules/structure/protected-files.yaml +4 -0
  8. package/config/workflows/feature-verification.yaml +7 -4
  9. package/config/workflows/idea-pipeline.yaml +10 -4
  10. package/config/workflows/task-pipeline.yaml +71 -52
  11. package/config/workflows/wrapup-pipeline.yaml +92 -17
  12. package/package.json +15 -11
  13. package/plugins/sp/README.md +1 -1
  14. package/plugins/sp/commands/dev-review-session.md +4 -4
  15. package/plugins/sp/commands/dev-review.md +16 -5
  16. package/plugins/sp/commands/dev-verify.md +7 -4
  17. package/plugins/sp/commands/dev-verifyall.md +4 -3
  18. package/plugins/sp/commands/dev-wrap.md +1 -1
  19. package/plugins/sp/hooks/context-post-tool.ts +2 -2
  20. package/plugins/sp/hooks/context-session-start.ts +2 -1
  21. package/plugins/sp/hooks/context-session-stop.ts +22 -14
  22. package/plugins/sp/hooks/pi/guard-extension.ts +112 -172
  23. package/plugins/sp/lib/idea-handoff.generated.mjs +2 -2
  24. package/plugins/sp/lib/inline-run.generated.d.mts +4 -2
  25. package/plugins/sp/lib/inline-run.generated.mjs +35 -14
  26. package/plugins/sp/plugin.json +1 -1
  27. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +3 -3
  28. package/plugins/sp/scripts/daily-summary/daily-summary.ts +3 -3
  29. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +2 -0
  30. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +1 -0
  31. package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +9 -7
  32. package/plugins/sp/scripts/dogfood-testing/validate-report.ts +8 -7
  33. package/plugins/sp/scripts/feature-sync-bounded.mjs +5 -2
  34. package/plugins/sp/scripts/feature-sync-bounded.ts +3 -1
  35. package/plugins/sp/scripts/feature-verification-steps.ts +6 -6
  36. package/plugins/sp/scripts/history-anatomy-cache.mjs +3 -3
  37. package/plugins/sp/scripts/history-anatomy-cache.ts +4 -4
  38. package/plugins/sp/scripts/inline-run-setup.mjs +60 -12
  39. package/plugins/sp/scripts/inline-run-setup.ts +122 -15
  40. package/plugins/sp/scripts/pr-reviewing.mjs +2 -0
  41. package/plugins/sp/scripts/pr-reviewing.ts +1 -0
  42. package/plugins/sp/scripts/quality-gate.mjs +15 -6
  43. package/plugins/sp/scripts/quality-gate.ts +30 -9
  44. package/plugins/sp/scripts/residual-scan.mjs +4 -4
  45. package/plugins/sp/scripts/residual-scan.ts +5 -4
  46. package/plugins/sp/scripts/script-contract-check.ts +175 -46
  47. package/plugins/sp/scripts/script-root.mjs +125 -0
  48. package/plugins/sp/scripts/script-root.ts +195 -0
  49. package/plugins/sp/scripts/stage-registry-adapter.ts +1 -1
  50. package/plugins/sp/scripts/task-evidence-precheck.ts +4 -2
  51. package/plugins/sp/scripts/task-size-precheck.ts +3 -1
  52. package/plugins/sp/scripts/validate-commands.ts +12 -12
  53. package/plugins/sp/scripts/validate-flag-contracts.ts +42 -30
  54. package/plugins/sp/scripts/verify-answer-lint.ts +3 -1
  55. package/plugins/sp/scripts/workflow-step-profile.mjs +5 -2
  56. package/plugins/sp/scripts/workflow-step-profile.ts +3 -1
  57. package/plugins/sp/scripts/wrapup-drift-probe.mjs +5 -2
  58. package/plugins/sp/scripts/wrapup-drift-probe.ts +4 -2
  59. package/plugins/sp/scripts/wrapup-steps.mjs +32 -8
  60. package/plugins/sp/scripts/wrapup-steps.ts +56 -10
  61. package/plugins/sp/skills/code-verification/SKILL.md +12 -7
  62. package/plugins/sp/skills/code-verification/references/verdict-schema.md +11 -0
  63. package/plugins/sp/skills/functional-review/SKILL.md +1 -1
  64. package/plugins/sp/skills/next-router/references/routing-table.md +1 -1
  65. package/plugins/sp/skills/session-review/SKILL.md +11 -9
  66. package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +9 -1
  67. package/plugins/sp/skills/spur-dev/references/dev-operations.md +8 -2
  68. package/plugins/sp/skills/spur-dev/references/execution-batch.md +65 -17
  69. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +16 -3
  70. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +22 -5
  71. package/plugins/sp/tsconfig.json +8 -0
  72. package/spur.js +20934 -21087
  73. package/web/_astro/{BoardApp.CerSBgis.js → BoardApp.BxJuwD7I.js} +151 -152
  74. package/web/_astro/BoardApp.Cpxntzad.js +1 -0
  75. package/web/_astro/TaskDetail.C5bW4WGV.js +1 -0
  76. package/web/_astro/_commonjsHelpers.CqkleIqs.js +1 -0
  77. package/web/_astro/arc.CyjRvNMY.js +1 -0
  78. package/web/_astro/architectureDiagram-3BPJPVTR.B4lWRzJA.js +36 -0
  79. package/web/_astro/{blockDiagram-GPEHLZMM.DXVIiv0p.js → blockDiagram-GPEHLZMM.Be38USQ2.js} +1 -1
  80. package/web/_astro/board-facade-react-dom-client.B2U5wMvk.js +1 -0
  81. package/web/_astro/board-facade-react-dom.Cd2MuMhF.js +1 -0
  82. package/web/_astro/board-facade-react-jsx-dev-runtime.B_L3JzfC.js +1 -0
  83. package/web/_astro/board-facade-react-jsx-runtime.BY2yfHRi.js +1 -0
  84. package/web/_astro/board-facade-react-router-dom.Dy5pjzOw.js +1 -0
  85. package/web/_astro/board-facade-react-router.DjjqU27_.js +1 -0
  86. package/web/_astro/board-facade-react.Dgf4GRyj.js +1 -0
  87. package/web/_astro/{c4Diagram-AAUBKEIU.BbF_zCxW.js → c4Diagram-AAUBKEIU.DT9Fj5Qx.js} +1 -1
  88. package/web/_astro/channel.CI6N_tCg.js +1 -0
  89. package/web/_astro/{chunk-2J33WTMH.CAgQHpPC.js → chunk-2J33WTMH.SeSBWLg5.js} +1 -1
  90. package/web/_astro/{chunk-4BX2VUAB.BN-5tpw4.js → chunk-4BX2VUAB.DoO4VE14.js} +1 -1
  91. package/web/_astro/{chunk-55IACEB6.CnPkEEr0.js → chunk-55IACEB6.DrmTFA53.js} +1 -1
  92. package/web/_astro/{chunk-727SXJPM.BQzQeMVm.js → chunk-727SXJPM.Bi-TUb_V.js} +4 -4
  93. package/web/_astro/{chunk-AQP2D5EJ.B6xNyDnL.js → chunk-AQP2D5EJ.B-xp8Uhi.js} +1 -1
  94. package/web/_astro/{chunk-FMBD7UC4.C7f9Ih78.js → chunk-FMBD7UC4.DGWrDnUU.js} +1 -1
  95. package/web/_astro/chunk-JMJ3UQ3L.BQJEJHu4.js +34 -0
  96. package/web/_astro/{chunk-ND2GUHAM.CNV1dFXT.js → chunk-ND2GUHAM._BagvPDy.js} +1 -1
  97. package/web/_astro/{chunk-QZHKN3VN.Cudn2TkJ.js → chunk-QZHKN3VN.VOboYWQ0.js} +1 -1
  98. package/web/_astro/chunk-YNUBSHFH.CxeAygfi.js +7 -0
  99. package/web/_astro/{classDiagram-4FO5ZUOK.D1NwP50q.js → classDiagram-4FO5ZUOK.D_apfHS7.js} +1 -1
  100. package/web/_astro/{classDiagram-v2-Q7XG4LA2.D1NwP50q.js → classDiagram-v2-Q7XG4LA2.D_apfHS7.js} +1 -1
  101. package/web/_astro/client.BhluLQqe.js +1 -0
  102. package/web/_astro/client.DBpSRLnx.js +9 -0
  103. package/web/_astro/cose-bilkent-S5V4N54A.DNLU_L-x.js +1 -0
  104. package/web/_astro/{cynefin-OW5HDTMX.BmK52w8G.js → cynefin-OW5HDTMX.D8borhV-.js} +1 -1
  105. package/web/_astro/{dagre-BM42HDAG.Bfy5CTDT.js → dagre-BM42HDAG.Uc0lvjcV.js} +2 -2
  106. package/web/_astro/diagram-2AECGRRQ.DDIxtDBo.js +43 -0
  107. package/web/_astro/diagram-5GNKFQAL.DVoDnD3H.js +10 -0
  108. package/web/_astro/{diagram-KO2AKTUF.CW_vMJ4z.js → diagram-KO2AKTUF.CRubTJ-0.js} +3 -3
  109. package/web/_astro/{diagram-LMA3HP47.B_8ZGF67.js → diagram-LMA3HP47.m9SeYcXQ.js} +1 -1
  110. package/web/_astro/{diagram-OG6HWLK6.BppnHsdS.js → diagram-OG6HWLK6.C6s2ZjV5.js} +1 -1
  111. package/web/_astro/{erDiagram-TEJ5UH35.BEuHXcjJ.js → erDiagram-TEJ5UH35.nSzr5KTj.js} +5 -5
  112. package/web/_astro/{flowDiagram-I6XJVG4X.CH-UlnGr.js → flowDiagram-I6XJVG4X.DrZcys16.js} +4 -4
  113. package/web/_astro/ganttDiagram-6RSMTGT7.Do4M1b-K.js +292 -0
  114. package/web/_astro/{gitGraphDiagram-PVQCEYII.XnPxPPZN.js → gitGraphDiagram-PVQCEYII.BWc9oJ4l.js} +1 -1
  115. package/web/_astro/index.5dn-Zm-2.js +1 -0
  116. package/web/_astro/index.C-IqPpW2.js +1 -0
  117. package/web/_astro/index.CENnIEqT.css +1 -0
  118. package/web/_astro/index.Cyc_GA4f.js +1 -0
  119. package/web/_astro/index.tZfqwSut.js +1 -0
  120. package/web/_astro/{infoDiagram-5YYISTIA.JyjYRu_T.js → infoDiagram-5YYISTIA.BIdOctFk.js} +1 -1
  121. package/web/_astro/{ishikawaDiagram-YF4QCWOH.BBRBF-Fo.js → ishikawaDiagram-YF4QCWOH.CV5SuG2t.js} +5 -5
  122. package/web/_astro/{journeyDiagram-JHISSGLW.C_iymSyp.js → journeyDiagram-JHISSGLW.CxE-rEJ-.js} +1 -1
  123. package/web/_astro/jsx-runtime.DWOiEOII.js +1 -0
  124. package/web/_astro/{kanban-definition-UN3LZRKU.DdfW-Oqt.js → kanban-definition-UN3LZRKU.BPRdKWs_.js} +7 -7
  125. package/web/_astro/{linear.C2_IkbZT.js → linear.zRsuuDTE.js} +1 -1
  126. package/web/_astro/mermaid.core.jAJTcMKc.js +301 -0
  127. package/web/_astro/{mindmap-definition-RKZ34NQL.DAZIxQSK.js → mindmap-definition-RKZ34NQL.BffJxfCr.js} +2 -2
  128. package/web/_astro/{pieDiagram-4H26LBE5.CN8sIhKM.js → pieDiagram-4H26LBE5.CacwWaE3.js} +3 -3
  129. package/web/_astro/{quadrantDiagram-W4KKPZXB.3dGcX5GP.js → quadrantDiagram-W4KKPZXB.hXELD06-.js} +1 -1
  130. package/web/_astro/{requirementDiagram-4Y6WPE33.BV2y4dd6.js → requirementDiagram-4Y6WPE33.DdEbPcqT.js} +3 -3
  131. package/web/_astro/{sankeyDiagram-5OEKKPKP.Cqo15Tvo.js → sankeyDiagram-5OEKKPKP.DgaDocKv.js} +4 -4
  132. package/web/_astro/{sequenceDiagram-3UESZ5HK.CROCPMJB.js → sequenceDiagram-3UESZ5HK.B0KScnAu.js} +1 -1
  133. package/web/_astro/{stateDiagram-AJRCARHV.RfXZrkFE.js → stateDiagram-AJRCARHV.CV9M_WNc.js} +1 -1
  134. package/web/_astro/{stateDiagram-v2-BHNVJYJU.CPXmbBs9.js → stateDiagram-v2-BHNVJYJU.BOAo74Et.js} +1 -1
  135. package/web/_astro/{timeline-definition-PNZ67QCA.DdgKTiO8.js → timeline-definition-PNZ67QCA.CqqepUMe.js} +3 -3
  136. package/web/_astro/{vennDiagram-CIIHVFJN.CPNVSHF1.js → vennDiagram-CIIHVFJN.DDqe4dOG.js} +5 -5
  137. package/web/_astro/{wardleyDiagram-YWT4CUSO.CQhA0Jyr.js → wardleyDiagram-YWT4CUSO.B3R9OPdO.js} +3 -3
  138. package/web/_astro/{xychartDiagram-2RQKCTM6.n61BWyy4.js → xychartDiagram-2RQKCTM6.B3SvgXpQ.js} +1 -1
  139. package/web/board-runtime.json +69 -0
  140. package/web/index.html +2 -2
  141. package/web/_astro/BoardApp.eoTz0pZs.js +0 -1
  142. package/web/_astro/TaskDetail.DCqiC-OZ.js +0 -1
  143. package/web/_astro/arc.CPwg6Rw0.js +0 -1
  144. package/web/_astro/architectureDiagram-3BPJPVTR.DM_vp_hO.js +0 -36
  145. package/web/_astro/channel.MYZLKNwy.js +0 -1
  146. package/web/_astro/client.yhYJvxCU.js +0 -9
  147. package/web/_astro/cose-bilkent-S5V4N54A.B1wSL-Xb.js +0 -1
  148. package/web/_astro/diagram-2AECGRRQ.DhNnvUvX.js +0 -43
  149. package/web/_astro/diagram-5GNKFQAL.lTX5KwnS.js +0 -10
  150. package/web/_astro/ganttDiagram-6RSMTGT7.BO81S85v.js +0 -292
  151. package/web/_astro/index.De90oHcH.js +0 -1
  152. package/web/_astro/index.Hjbr15fG.css +0 -1
  153. 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 → append to token-ledger.jsonl)
11
- * - context-session-start (session_start → init .session.json)
12
- * - context-session-stop (session_shutdown → rollup + cleanup)
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 { appendFileSync, existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
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 { resolveAgentHint as resolveAgentHintShared, resolveModelHint as resolveModelHintShared } from '../agent-hint';
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
- // ─── Token ledger helpers (mirrors context-post-tool.ts) ──────────────────
118
-
119
- interface ToolEvent {
120
- session_id?: string;
121
- tool_name?: string;
122
- tool_input?: Record<string, unknown>;
123
- }
124
-
125
- function estimateTokenCount(text: string): number {
126
- if (!text) return 0;
127
- // Rough estimate: ~4 chars per token for English text
128
- return Math.ceil(text.length / 4);
129
- }
130
-
131
- function summarizeToolEvent(event: ToolEvent): string {
132
- const input = event.tool_input ?? {};
133
- const candidates = [input.file_path, input.command, input.pattern, input.glob_pattern, input.glob, input.path];
134
- for (const c of candidates) {
135
- if (typeof c === 'string' && c.trim()) {
136
- const s = c.trim();
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 `(${event.tool_name ?? 'unknown'})`;
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
- // ─── Session helpers (agent/model hints from agent-hint.ts; mirrors context-session-stop.ts) ─
194
-
195
- function generateSessionId(): string {
196
- const timestamp = Date.now().toString(36);
197
- const random = Math.random().toString(36).slice(2, 10);
198
- return `${timestamp}-${random}`;
199
- }
200
-
201
- function initSession(): void {
202
- try {
203
- mkdirSync(spurContextDir(), { recursive: true });
204
- const sessionId = generateSessionId();
205
- const session = {
206
- session_id: sessionId,
207
- agent: resolveAgentHintShared(getEnvVars(), 'pi'),
208
- model: resolveModelHintShared(getEnvVars()),
209
- started_at: new Date().toISOString(),
210
- };
211
- writeFileSync(sessionFilePath(), `${JSON.stringify(session, null, 2)}\n`);
212
-
213
- // Append session_start event to ledger
214
- const startEntry = JSON.stringify({
215
- session: sessionId,
216
- type: 'session_start',
217
- agent: session.agent,
218
- model: session.model,
219
- timestamp: session.started_at,
220
- });
221
- appendFileSync(ledgerFilePath(), `${startEntry}\n`);
222
- } catch {
223
- // fail-open
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
- // Append session_end event
253
- const endEntry = JSON.stringify({
254
- session: sessionId,
255
- type: 'session_end',
256
- reads,
257
- writes,
258
- tokens,
259
- timestamp: new Date().toISOString(),
260
- });
261
- appendFileSync(ledgerFilePath(), `${endEntry}\n`);
262
-
263
- // Cleanup session file
264
- rmSync(sessionFilePath(), { force: true });
265
- } catch {
266
- // fail-open
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
- if (event.toolName === 'write' || event.toolName === 'edit') {
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: append to token-ledger.jsonl ───────────────
238
+ // ── context-post-tool: shared recordToolUseEvent (task 0969 R1) ──
307
239
  pi.on('tool_result', async (event) => {
308
- const input = event.input as Record<string, unknown> | undefined;
309
- const command = typeof input?.command === 'string' ? input.command : undefined;
310
- appendToLedger(
311
- {
312
- session_id: readSessionId(),
313
- tool_name: event.toolName,
314
- tool_input: input as Record<string, unknown>,
315
- },
316
- command,
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: init session tracking ─────────────────
252
+ // ── context-session-start: shared recordSessionStart (R2) ────────
321
253
  pi.on('session_start', async () => {
322
- initSession();
254
+ try {
255
+ recordSessionStart(spurContextDir(), getEnvVars(), undefined, 'pi');
256
+ } catch {
257
+ // fail-open
258
+ }
323
259
  });
324
260
 
325
- // ── context-session-stop: rollup + cleanup ───────────────────────
261
+ // ── context-session-stop: shared recordSessionEnd (R3) ──────────
326
262
  pi.on('session_shutdown', async () => {
327
- cleanupSession();
263
+ try {
264
+ recordSessionEnd(spurContextDir());
265
+ } catch {
266
+ // fail-open
267
+ }
328
268
  });
329
269
  }