@gobing-ai/spur 0.4.8 → 0.4.10

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 (211) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/pipeline-budgets.json +2 -2
  3. package/config/rules/README.md +1 -1
  4. package/config/rules/boundary/config-loading-ownership.yaml +1 -0
  5. package/config/rules/boundary/env-var-hygiene.yaml +0 -1
  6. package/config/rules/boundary/sp-plugin-standalone.yaml +1 -1
  7. package/config/rules/boundary/sp-script-placement.yaml +3 -3
  8. package/config/rules/docs/sourced-api-claims.yaml +29 -64
  9. package/config/rules/strict/runtime-boundaries.yaml +40 -1
  10. package/config/rules/structure/protected-files.yaml +1 -1
  11. package/config/rules/surface/check-cli-surface.yaml +33 -0
  12. package/config/rules/typescript/no-debugger.yaml +3 -0
  13. package/config/workflow-candidates.json +2 -2
  14. package/config/workflows/feature-verification.yaml +6 -19
  15. package/config/workflows/history-anatomy.yaml +14 -28
  16. package/config/workflows/idea-pipeline.yaml +6 -19
  17. package/config/workflows/pr-review.yaml +9 -16
  18. package/config/workflows/task-pipeline.yaml +18 -25
  19. package/config/workflows/wrapup-pipeline.yaml +21 -63
  20. package/package.json +3 -3
  21. package/plugins/sp/README.md +70 -37
  22. package/plugins/sp/agents/expert-spur.md +0 -1
  23. package/plugins/sp/agents/super-planner.md +5 -5
  24. package/plugins/sp/agents/super-reviewer.md +2 -2
  25. package/plugins/sp/commands/dev-changelog.md +1 -1
  26. package/plugins/sp/commands/dev-daily.md +1 -1
  27. package/plugins/sp/commands/dev-pr-review.md +1 -1
  28. package/plugins/sp/commands/dev-review-session.md +1 -1
  29. package/plugins/sp/commands/dev-runall.md +4 -4
  30. package/plugins/sp/commands/dev-verify.md +4 -6
  31. package/plugins/sp/commands/dev-verifyall.md +4 -5
  32. package/plugins/sp/hooks/hooks.json +11 -1
  33. package/plugins/sp/lib/context-post-tool.ts +1 -1
  34. package/plugins/sp/lib/context-session-start.ts +1 -1
  35. package/plugins/sp/lib/context-session-stop.ts +1 -1
  36. package/plugins/sp/lib/destructive-policy.ts +65 -1
  37. package/plugins/sp/plugin.json +2 -5
  38. package/plugins/sp/{hooks → scripts/hooks}/agent-lifecycle.ts +1 -1
  39. package/plugins/sp/scripts/hooks/careful-guard.ts +94 -0
  40. package/plugins/sp/{hooks → scripts/hooks}/context-post-tool.ts +2 -2
  41. package/plugins/sp/{hooks → scripts/hooks}/context-session-start.ts +1 -1
  42. package/plugins/sp/{hooks → scripts/hooks}/context-session-stop.ts +1 -1
  43. package/plugins/sp/{hooks → scripts/hooks}/fleet-guest-stop.ts +6 -4
  44. package/plugins/sp/{hooks → scripts/hooks}/task-write-guard.ts +38 -21
  45. package/plugins/sp/skills/brainstorm/SKILL.md +12 -11
  46. package/plugins/sp/skills/brainstorm/agents/openai.yaml +1 -1
  47. package/plugins/sp/skills/brainstorm/examples/ideation-example.md +0 -4
  48. package/plugins/sp/skills/brainstorm/metadata.openclaw +1 -1
  49. package/plugins/sp/skills/brainstorm/references/workflows.md +4 -14
  50. package/plugins/sp/skills/code-implementation/SKILL.md +1 -1
  51. package/plugins/sp/skills/code-verification/SKILL.md +8 -8
  52. package/plugins/sp/skills/code-verification/references/secu-review.md +4 -4
  53. package/plugins/sp/skills/code-verification/references/verdict-schema.md +9 -1
  54. package/plugins/sp/skills/daily-summary/SKILL.md +4 -5
  55. package/plugins/sp/skills/dogfood-testing/SKILL.md +9 -9
  56. package/plugins/sp/skills/dogfood-testing/references/report-template.md +1 -1
  57. package/plugins/sp/skills/history-anatomy/references/modes.md +2 -2
  58. package/plugins/sp/skills/history-anatomy/references/operations.md +3 -4
  59. package/plugins/sp/skills/history-anatomy/references/report-contract.md +2 -3
  60. package/plugins/sp/skills/issue-finding/SKILL.md +1 -1
  61. package/plugins/sp/skills/next-router/references/routing-table.md +1 -1
  62. package/plugins/sp/skills/parallel-execution/SKILL.md +2 -2
  63. package/plugins/sp/skills/parallel-execution/references/result-synthesis.md +9 -10
  64. package/plugins/sp/skills/pr-reviewing/SKILL.md +14 -15
  65. package/plugins/sp/skills/reverse-engineering/SKILL.md +4 -6
  66. package/plugins/sp/skills/session-review/SKILL.md +13 -29
  67. package/plugins/sp/skills/source-driven-development/SKILL.md +26 -27
  68. package/plugins/sp/skills/spur-check/SKILL.md +14 -13
  69. package/plugins/sp/skills/spur-cli/SKILL.md +1 -1
  70. package/plugins/sp/skills/spur-cli/references/agent.md +17 -2
  71. package/plugins/sp/skills/spur-cli/references/builder.md +27 -6
  72. package/plugins/sp/skills/spur-cli/references/features.md +6 -1
  73. package/plugins/sp/skills/spur-cli/references/history.md +3 -0
  74. package/plugins/sp/skills/spur-cli/references/self.md +5 -3
  75. package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +3 -0
  76. package/plugins/sp/skills/spur-cli/references/tasks.md +13 -3
  77. package/plugins/sp/skills/spur-cli/references/workflows/authoring-workflows.md +1 -1
  78. package/plugins/sp/skills/spur-cli/references/workflows.md +36 -1
  79. package/plugins/sp/skills/spur-dev/SKILL.md +1 -1
  80. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +1 -1
  81. package/plugins/sp/skills/spur-dev/references/dev-operations.md +12 -13
  82. package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +10 -11
  83. package/plugins/sp/skills/spur-dev/references/execution-batch.md +16 -17
  84. package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +1 -1
  85. package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +9 -12
  86. package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +6 -8
  87. package/plugins/sp/skills/spur-dev/references/gate-checklists.md +1 -1
  88. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +33 -40
  89. package/plugins/sp/skills/spur-dev/references/structured-trace-emission.md +24 -24
  90. package/plugins/sp/skills/spur-doctor/SKILL.md +5 -4
  91. package/spur.js +12726 -1456
  92. package/web/_astro/{BoardApp.CaNvQBD_.js → BoardApp.B5rTVNd_.js} +1 -1
  93. package/web/_astro/{BoardApp.C5jnkkKP.js → BoardApp.BMprmPRc.js} +141 -141
  94. package/web/_astro/{TaskDetail.DGKndhI8.js → TaskDetail.Ddsur8qd.js} +1 -1
  95. package/web/_astro/{arc.DHrwdg3G.js → arc.DLqVVBxP.js} +1 -1
  96. package/web/_astro/{architectureDiagram-3BPJPVTR.pvKpK1s5.js → architectureDiagram-3BPJPVTR.C-Pk3uAi.js} +1 -1
  97. package/web/_astro/{blockDiagram-GPEHLZMM.BQmRcZsS.js → blockDiagram-GPEHLZMM.BRO1i_Nm.js} +1 -1
  98. package/web/_astro/{c4Diagram-AAUBKEIU.CCLjHeH1.js → c4Diagram-AAUBKEIU.BU_cWu6e.js} +1 -1
  99. package/web/_astro/channel.Bs3Illhy.js +1 -0
  100. package/web/_astro/{chunk-2J33WTMH.DL6Yuoma.js → chunk-2J33WTMH.D2VXiQ2t.js} +1 -1
  101. package/web/_astro/{chunk-4BX2VUAB.CzlyEI7l.js → chunk-4BX2VUAB.BcHT772J.js} +1 -1
  102. package/web/_astro/{chunk-55IACEB6.B60EWxp1.js → chunk-55IACEB6.DhDtICmo.js} +1 -1
  103. package/web/_astro/{chunk-727SXJPM.c7v_fRLo.js → chunk-727SXJPM.BoKHp7gV.js} +1 -1
  104. package/web/_astro/{chunk-AQP2D5EJ.8BxLWdDc.js → chunk-AQP2D5EJ.Chu4JRpE.js} +1 -1
  105. package/web/_astro/{chunk-FMBD7UC4.DmKuxGFT.js → chunk-FMBD7UC4.wHdwKPlq.js} +1 -1
  106. package/web/_astro/{chunk-ND2GUHAM.1Zwd4mqV.js → chunk-ND2GUHAM.ffN-M1JB.js} +1 -1
  107. package/web/_astro/{chunk-QZHKN3VN.BvYLu-A1.js → chunk-QZHKN3VN.FRubwg4S.js} +1 -1
  108. package/web/_astro/{classDiagram-4FO5ZUOK.CEqRt0WU.js → classDiagram-4FO5ZUOK.c22tQsc_.js} +1 -1
  109. package/web/_astro/{classDiagram-v2-Q7XG4LA2.CEqRt0WU.js → classDiagram-v2-Q7XG4LA2.c22tQsc_.js} +1 -1
  110. package/web/_astro/{cose-bilkent-S5V4N54A.C7JiUdj2.js → cose-bilkent-S5V4N54A.B9c6fiSj.js} +1 -1
  111. package/web/_astro/{cynefin-OW5HDTMX.B6mZ-1be.js → cynefin-OW5HDTMX.DrZsrUHl.js} +1 -1
  112. package/web/_astro/{dagre-BM42HDAG.BuryfDy6.js → dagre-BM42HDAG.DG1U4b3y.js} +1 -1
  113. package/web/_astro/{diagram-2AECGRRQ.dao39YQM.js → diagram-2AECGRRQ.CH15b-K3.js} +1 -1
  114. package/web/_astro/{diagram-5GNKFQAL.lnGZe4At.js → diagram-5GNKFQAL.BH1j7twR.js} +1 -1
  115. package/web/_astro/{diagram-KO2AKTUF.uWfhzEfT.js → diagram-KO2AKTUF.q2zCkbAj.js} +1 -1
  116. package/web/_astro/{diagram-LMA3HP47.ppq8-198.js → diagram-LMA3HP47.BLaaZnHV.js} +1 -1
  117. package/web/_astro/{diagram-OG6HWLK6.CKSCqDt5.js → diagram-OG6HWLK6.BcpRdQwI.js} +1 -1
  118. package/web/_astro/{erDiagram-TEJ5UH35.sFSf2yBN.js → erDiagram-TEJ5UH35.D7KBlqfM.js} +1 -1
  119. package/web/_astro/{flowDiagram-I6XJVG4X.BJQjKLbz.js → flowDiagram-I6XJVG4X.D_gmuRqg.js} +1 -1
  120. package/web/_astro/{ganttDiagram-6RSMTGT7.DAHKNj2N.js → ganttDiagram-6RSMTGT7.DzgWYniD.js} +1 -1
  121. package/web/_astro/{gitGraphDiagram-PVQCEYII.BlmzcW_O.js → gitGraphDiagram-PVQCEYII.576i3hm3.js} +1 -1
  122. package/web/_astro/index.DiC1a6lh.css +1 -0
  123. package/web/_astro/{infoDiagram-5YYISTIA.DGhjaqsi.js → infoDiagram-5YYISTIA.DMCclBiz.js} +1 -1
  124. package/web/_astro/{ishikawaDiagram-YF4QCWOH.gKIpgt6F.js → ishikawaDiagram-YF4QCWOH.9Kyca4SH.js} +1 -1
  125. package/web/_astro/{journeyDiagram-JHISSGLW.DG3tmP0R.js → journeyDiagram-JHISSGLW.chbdG-8q.js} +1 -1
  126. package/web/_astro/{kanban-definition-UN3LZRKU.CgHDSxkR.js → kanban-definition-UN3LZRKU.Bds54-e0.js} +1 -1
  127. package/web/_astro/{linear.BuGEpcuM.js → linear.D5ND9GN_.js} +1 -1
  128. package/web/_astro/{mermaid.core.DBVqkWBp.js → mermaid.core.D9N_PGbj.js} +4 -4
  129. package/web/_astro/{mindmap-definition-RKZ34NQL.D-kOcji9.js → mindmap-definition-RKZ34NQL.t0xop5vp.js} +1 -1
  130. package/web/_astro/{pieDiagram-4H26LBE5.BvGPwtZt.js → pieDiagram-4H26LBE5.DqgKh6hM.js} +1 -1
  131. package/web/_astro/{quadrantDiagram-W4KKPZXB.BKb_9FV6.js → quadrantDiagram-W4KKPZXB.DeuHVi2t.js} +1 -1
  132. package/web/_astro/{requirementDiagram-4Y6WPE33.Cx0fvD8v.js → requirementDiagram-4Y6WPE33.BdYFSG4W.js} +1 -1
  133. package/web/_astro/{sankeyDiagram-5OEKKPKP.fatULi0W.js → sankeyDiagram-5OEKKPKP.k1LPPETJ.js} +1 -1
  134. package/web/_astro/{sequenceDiagram-3UESZ5HK.ChHyKqco.js → sequenceDiagram-3UESZ5HK.DdyfpzoG.js} +1 -1
  135. package/web/_astro/{stateDiagram-AJRCARHV.BdUpKWyj.js → stateDiagram-AJRCARHV.DSEI8il0.js} +1 -1
  136. package/web/_astro/{stateDiagram-v2-BHNVJYJU.fmJxqIm5.js → stateDiagram-v2-BHNVJYJU.CoMnQRKo.js} +1 -1
  137. package/web/_astro/{timeline-definition-PNZ67QCA.BayDn5-V.js → timeline-definition-PNZ67QCA.DM1AHNm-.js} +1 -1
  138. package/web/_astro/{vennDiagram-CIIHVFJN.Bm2Aggwa.js → vennDiagram-CIIHVFJN.C-cJmVtl.js} +1 -1
  139. package/web/_astro/{wardleyDiagram-YWT4CUSO.DI75O-uG.js → wardleyDiagram-YWT4CUSO.D0xMzKPb.js} +1 -1
  140. package/web/_astro/{xychartDiagram-2RQKCTM6.DL90E7Q7.js → xychartDiagram-2RQKCTM6.BH5xJcEq.js} +1 -1
  141. package/web/index.html +2 -2
  142. package/config/plugin-scripts.json +0 -116
  143. package/config/script-placement-baseline.json +0 -33
  144. package/plugins/sp/hooks/careful-guard.ts +0 -74
  145. package/plugins/sp/hooks/pi/guard-extension.ts +0 -304
  146. package/plugins/sp/lib/dogfood-pipeline.ts +0 -265
  147. package/plugins/sp/lib/dogfood-report.ts +0 -278
  148. package/plugins/sp/lib/history-anatomy.generated.d.mts +0 -112
  149. package/plugins/sp/lib/history-anatomy.generated.mjs +0 -691
  150. package/plugins/sp/lib/idea-handoff.generated.d.mts +0 -17
  151. package/plugins/sp/lib/idea-handoff.generated.mjs +0 -1446
  152. package/plugins/sp/lib/inline-run-app.ts +0 -68
  153. package/plugins/sp/lib/inline-run.generated.d.mts +0 -37
  154. package/plugins/sp/lib/inline-run.generated.mjs +0 -3103
  155. package/plugins/sp/lib/plan-projection.generated.d.mts +0 -197
  156. package/plugins/sp/lib/plan-projection.generated.mjs +0 -982
  157. package/plugins/sp/lib/quality-gate.generated.d.mts +0 -139
  158. package/plugins/sp/lib/quality-gate.generated.mjs +0 -30510
  159. package/plugins/sp/lib/residual-scan.generated.d.mts +0 -93
  160. package/plugins/sp/lib/residual-scan.generated.mjs +0 -383
  161. package/plugins/sp/lib/run-summary-core.ts +0 -225
  162. package/plugins/sp/lib/spur-bin.ts +0 -36
  163. package/plugins/sp/lib/step-profile.generated.d.mts +0 -71
  164. package/plugins/sp/lib/step-profile.generated.mjs +0 -174
  165. package/plugins/sp/lib/transcript.ts +0 -112
  166. package/plugins/sp/scripts/batch-plan.mjs +0 -1096
  167. package/plugins/sp/scripts/batch-plan.ts +0 -145
  168. package/plugins/sp/scripts/batch-preflight.mjs +0 -423
  169. package/plugins/sp/scripts/batch-preflight.ts +0 -270
  170. package/plugins/sp/scripts/commit-guard.mjs +0 -226
  171. package/plugins/sp/scripts/commit-guard.ts +0 -248
  172. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +0 -626
  173. package/plugins/sp/scripts/daily-summary/daily-summary.ts +0 -855
  174. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +0 -211
  175. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +0 -110
  176. package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +0 -282
  177. package/plugins/sp/scripts/dogfood-testing/validate-report.ts +0 -98
  178. package/plugins/sp/scripts/feature-verification-steps.mjs +0 -82
  179. package/plugins/sp/scripts/feature-verification-steps.ts +0 -164
  180. package/plugins/sp/scripts/history-anatomy-cache.mjs +0 -931
  181. package/plugins/sp/scripts/history-anatomy-cache.ts +0 -247
  182. package/plugins/sp/scripts/idea-handoff.mjs +0 -32
  183. package/plugins/sp/scripts/idea-handoff.ts +0 -47
  184. package/plugins/sp/scripts/inline-run-dispatch.mjs +0 -131
  185. package/plugins/sp/scripts/inline-run-dispatch.ts +0 -115
  186. package/plugins/sp/scripts/inline-run-setup.mjs +0 -746
  187. package/plugins/sp/scripts/inline-run-setup.ts +0 -198
  188. package/plugins/sp/scripts/persist-out-check.mjs +0 -217
  189. package/plugins/sp/scripts/persist-out-check.ts +0 -249
  190. package/plugins/sp/scripts/pr-reviewing.mjs +0 -712
  191. package/plugins/sp/scripts/pr-reviewing.ts +0 -952
  192. package/plugins/sp/scripts/quality-gate.mjs +0 -30491
  193. package/plugins/sp/scripts/quality-gate.ts +0 -80
  194. package/plugins/sp/scripts/residual-scan.mjs +0 -634
  195. package/plugins/sp/scripts/residual-scan.ts +0 -248
  196. package/plugins/sp/scripts/run-summary.mjs +0 -758
  197. package/plugins/sp/scripts/run-summary.ts +0 -107
  198. package/plugins/sp/scripts/script-root.mjs +0 -129
  199. package/plugins/sp/scripts/script-root.ts +0 -199
  200. package/plugins/sp/scripts/session-timeline.mjs +0 -359
  201. package/plugins/sp/scripts/session-timeline.ts +0 -190
  202. package/plugins/sp/scripts/task-diffstat.mjs +0 -181
  203. package/plugins/sp/scripts/task-diffstat.ts +0 -219
  204. package/plugins/sp/scripts/workflow-step-profile.mjs +0 -338
  205. package/plugins/sp/scripts/workflow-step-profile.ts +0 -165
  206. package/plugins/sp/scripts/wrapup-drift-probe.mjs +0 -190
  207. package/plugins/sp/scripts/wrapup-drift-probe.ts +0 -261
  208. package/plugins/sp/scripts/wrapup-steps.mjs +0 -627
  209. package/plugins/sp/scripts/wrapup-steps.ts +0 -891
  210. package/web/_astro/channel.C7wG9RxA.js +0 -1
  211. package/web/_astro/index.BCY1JJHd.css +0 -1
@@ -1,304 +0,0 @@
1
- /**
2
- * Pi extension for Spur (sp) plugin hooks.
3
- *
4
- * Replaces @vahor/pi-hooks with native Pi event handlers — no spinner,
5
- * no pi.exec overhead, no "Operation aborted" on Esc.
6
- *
7
- * Implements:
8
- * - task-write-guard (tool_call → block Write/Edit to Spur task files)
9
- * - careful-guard (tool_call → warn on destructive Bash commands)
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
- * - agent-lifecycle (agent_start/agent_settled → shared reportLifecycleHook, G73 R4)
14
- *
15
- * The ledger and session cores are shared with the Claude hooks (task 0969): this
16
- * extension only normalizes Pi event shapes and adapts them to those cores.
17
- *
18
- * Installation:
19
- * Add to ~/.pi/agent/settings.json packages:
20
- * "npm:spur" (when published), or
21
- * Copy to ~/.pi/agent/extensions/sp-guard/ and reference via:
22
- * "pi": { "extensions": ["path/to/guard-extension.ts"] }
23
- */
24
-
25
- import { spawnSync } from 'node:child_process';
26
- import { existsSync, realpathSync } from 'node:fs';
27
- import { homedir } from 'node:os';
28
- import { isAbsolute, join, resolve } from 'node:path';
29
- import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
30
- import type { SessionOwner } from '../../lib/context-owner';
31
- import { recordToolUseEvent, type ToolPayload } from '../../lib/context-post-tool';
32
- import { recordSessionStart } from '../../lib/context-session-start';
33
- import { recordSessionEnd } from '../../lib/context-session-stop';
34
- import { classifyCommand } from '../../lib/destructive-policy';
35
- import { getEnvVar, getEnvVars } from '../../lib/env';
36
- import { couldBeTaskFile } from '../../lib/task-file-policy';
37
- import { reportLifecycleHook } from '../agent-lifecycle';
38
-
39
- // ─── Constants ───────────────────────────────────────────────────────────
40
-
41
- // Resolved per call, not cached at module load: Pi's cwd is fixed for the
42
- // process lifetime, and lazy resolution keeps the extension testable (tests
43
- // chdir into a temp project before driving handlers).
44
- const spurContextDir = (): string => join(process.cwd(), '.spur', 'context');
45
-
46
- // ─── Helpers ─────────────────────────────────────────────────────────────
47
-
48
- /**
49
- * Pi session owner identity (task 1159): the extension's own session-manager id, stable for the
50
- * life of the Pi session and distinct from any nested child session. Read here — not from a PID or
51
- * `SPUR_RUN_ID` — so a nested child's `session_shutdown` cannot claim the parent's pointer.
52
- */
53
- function piSessionOwner(ctx: ExtensionContext): SessionOwner | null {
54
- const sessionId = ctx.sessionManager.getSessionId().trim();
55
- return sessionId.length > 0 ? { host: 'pi', sessionId } : null;
56
- }
57
-
58
- type TaskOwnership = 'owned' | 'unowned' | 'unknown';
59
-
60
- /**
61
- * Extract the target path from a write/edit tool input. Pi uses `path`;
62
- * Claude Code uses `file_path`. Returns an absolute path when present.
63
- */
64
- function resolveInputPath(input: Record<string, unknown> | undefined): string {
65
- let raw = '';
66
- if (input) {
67
- if (typeof input.path === 'string') raw = input.path;
68
- else if (typeof input.file_path === 'string') raw = input.file_path;
69
- }
70
- if (!raw) return '';
71
- return isAbsolute(raw) ? raw : resolve(process.cwd(), raw);
72
- }
73
-
74
- /**
75
- * Candidate `spur` executable locations (symlinks). Pi launched from a GUI
76
- * may not have `~/.bun/bin` or node/bun on PATH, so we try each in order
77
- * and finally run spur.js via the current Bun executable directly.
78
- */
79
- const SPUR_BIN_CANDIDATES: string[] = [
80
- join(homedir(), '.bun', 'bin', 'spur'),
81
- '/opt/homebrew/bin/spur',
82
- '/usr/local/bin/spur',
83
- ];
84
-
85
- /** Resolve a candidate spur symlink to its real spur.js path (for execPath fallback). */
86
- function resolveSpurJsPath(candidate: string): string | undefined {
87
- try {
88
- return realpathSync(candidate);
89
- } catch {
90
- return existsSync(candidate) ? candidate : undefined;
91
- }
92
- }
93
-
94
- function resolveSpurTaskOwnership(filePath: string): TaskOwnership {
95
- const run = (cmd: string, args: string[]) =>
96
- spawnSync(cmd, args, { cwd: process.cwd(), encoding: 'utf-8', timeout: 8000 });
97
-
98
- // 1. SPUR_BIN env override (may include args) or `spur` on PATH
99
- const envBin = getEnvVar('SPUR_BIN') || 'spur';
100
- const envParts = envBin.split(' ');
101
- let res = run(envParts[0] ?? 'spur', [...envParts.slice(1), 'task', 'resolve', filePath, '--strict', '--json']);
102
- // Only 0 (owned) / 1 (unowned) are valid spur exit codes; 127 (interpreter
103
- // missing) and other codes mean the environment is broken — keep trying.
104
- if (!res.error && (res.status === 0 || res.status === 1)) return res.status === 0 ? 'owned' : 'unowned';
105
-
106
- // 2. Absolute symlink paths (needs node/bun on PATH for the shebang)
107
- for (const candidate of SPUR_BIN_CANDIDATES) {
108
- if (!existsSync(candidate)) continue;
109
- res = run(candidate, ['task', 'resolve', filePath, '--strict', '--json']);
110
- if (!res.error && (res.status === 0 || res.status === 1)) return res.status === 0 ? 'owned' : 'unowned';
111
- }
112
-
113
- // 3. Run spur.js via the current Bun executable (no PATH dependency)
114
- for (const candidate of SPUR_BIN_CANDIDATES) {
115
- const spurJs = resolveSpurJsPath(candidate);
116
- if (!spurJs) continue;
117
- res = run(process.execPath, [spurJs, 'task', 'resolve', filePath, '--strict', '--json']);
118
- if (!res.error && (res.status === 0 || res.status === 1)) return res.status === 0 ? 'owned' : 'unowned';
119
- }
120
-
121
- return 'unknown';
122
- }
123
-
124
- // Destructive-command classification is imported from `../destructive-policy`, the
125
- // single cross-platform policy. This file previously carried its own regex copy; it
126
- // diverged from the Claude matrix on 7 of 10 pinned cases (it allowed
127
- // `rm -rf node_modules /etc/nginx`, `rm -R --force /var/data`, `git push -f` and
128
- // `git push origin +main`, and warned on `git push --force-with-lease`). Do not
129
- // re-introduce a local copy — add cases to `destructive-policy.test.ts` instead.
130
-
131
- // ─── Pi → Claude payload normalization (task 0969 R1) ─────────────────────
132
-
133
- /** Pi tool name → the Claude tool name the shared core records. */
134
- const PI_TO_CLAUDE: Record<string, string> = {
135
- read: 'Read',
136
- write: 'Write',
137
- edit: 'Edit',
138
- bash: 'Bash',
139
- grep: 'Grep',
140
- find: 'Glob',
141
- };
142
-
143
- /** Join the text parts of a Pi tool_result `content` payload. */
144
- function piContentText(content: unknown): string {
145
- if (typeof content === 'string') return content;
146
- if (!Array.isArray(content)) return '';
147
- const parts: string[] = [];
148
- for (const part of content) {
149
- if (part && typeof part === 'object' && typeof (part as { text?: unknown }).text === 'string') {
150
- parts.push((part as { text: string }).text);
151
- }
152
- }
153
- return parts.join('\n');
154
- }
155
-
156
- /**
157
- * Pure Pi-event → Claude-{@link ToolPayload} adapter. An unmapped tool returns null so the
158
- * shared core writes no row (R1's row filter). Search tools keep `path` (never `file_path`)
159
- * because the core treats `file_path` as the written file, not a search root.
160
- */
161
- export function normalizePiToolEvent(
162
- toolName: string,
163
- input: Record<string, unknown> | undefined,
164
- content?: unknown,
165
- ): ToolPayload | null {
166
- const mapped = PI_TO_CLAUDE[toolName];
167
- if (mapped === undefined) return null;
168
- const i = input ?? {};
169
- const str = (value: unknown): string => (typeof value === 'string' ? value : '');
170
- const path = str(i.path) || str(i.file_path);
171
- const text = piContentText(content);
172
- const tool_response = text.length > 0 ? { content: text } : undefined;
173
-
174
- switch (mapped) {
175
- case 'Read':
176
- return { tool_name: mapped, tool_input: { file_path: path }, tool_response };
177
- case 'Write':
178
- return { tool_name: mapped, tool_input: { file_path: path, content: str(i.content) }, tool_response };
179
- case 'Edit': {
180
- const edits = Array.isArray(i.edits) ? (i.edits as Array<Record<string, unknown>>) : [];
181
- return {
182
- tool_name: mapped,
183
- tool_input: {
184
- file_path: path,
185
- old_string: edits.map((e) => str(e.oldText)).join(''),
186
- new_string: edits.map((e) => str(e.newText)).join(''),
187
- },
188
- tool_response,
189
- };
190
- }
191
- case 'Grep':
192
- return {
193
- tool_name: mapped,
194
- tool_input: {
195
- pattern: str(i.pattern),
196
- ...(path ? { path } : {}),
197
- ...(str(i.glob) ? { glob: str(i.glob) } : {}),
198
- },
199
- tool_response,
200
- };
201
- case 'Glob':
202
- return {
203
- tool_name: mapped,
204
- tool_input: { pattern: str(i.pattern), ...(path ? { path } : {}) },
205
- tool_response,
206
- };
207
- case 'Bash':
208
- return { tool_name: mapped, tool_input: { command: str(i.command) }, tool_response };
209
- default:
210
- return null;
211
- }
212
- }
213
-
214
- // ─── Extension entry point ───────────────────────────────────────────────
215
-
216
- export default function (pi: ExtensionAPI): void {
217
- // ── task-write-guard + careful-guard ──────────────────────────────
218
- pi.on('tool_call', async (event, ctx) => {
219
- // task-write-guard: block Write/Edit to Spur task files.
220
- // Both guards honor the same `off` escape hatches as the Claude hooks.
221
- if ((event.toolName === 'write' || event.toolName === 'edit') && getEnvVar('SPUR_WRITE_GUARD') !== 'off') {
222
- const input = event.input as Record<string, unknown> | undefined;
223
- // Pi's write/edit tools use `path` (not Claude Code's `file_path`)
224
- const filePath = resolveInputPath(input);
225
- // Skip the `spur task resolve` subprocess for paths that cannot name a
226
- // task file (shared predicate — see task-file-policy.ts).
227
- if (filePath && couldBeTaskFile(filePath) && resolveSpurTaskOwnership(filePath) === 'owned') {
228
- const msg = `Denied: ${filePath} is a Spur task file. Use 'spur task update' instead.`;
229
- ctx.ui.notify(msg, 'error');
230
- return { block: true, reason: msg };
231
- }
232
- }
233
-
234
- // careful-guard: warn on destructive Bash commands
235
- if (event.toolName === 'bash' && getEnvVar('SPUR_CAREFUL') !== 'off') {
236
- const input = event.input as Record<string, unknown> | undefined;
237
- const command = typeof input?.command === 'string' ? input.command : '';
238
- const hit = command ? classifyCommand(command) : null;
239
- if (hit !== null) {
240
- const msg = `Warning: destructive command — ${hit}: ${command.slice(0, 120)}`;
241
- ctx.ui.notify(msg, 'warning');
242
- // Ask for confirmation
243
- const ok = await ctx.ui.confirm('Destructive command', msg);
244
- if (!ok) return { block: true, reason: 'Cancelled by user' };
245
- }
246
- }
247
-
248
- return {};
249
- });
250
-
251
- // ── context-post-tool: shared recordToolUseEvent (task 0969 R1) ──
252
- pi.on('tool_result', async (event, ctx) => {
253
- try {
254
- const payload = normalizePiToolEvent(
255
- event.toolName,
256
- event.input as Record<string, unknown> | undefined,
257
- (event as { content?: unknown }).content,
258
- );
259
- // Host-native session id, as Claude's payload carries (H21 parity).
260
- const sessionId = piSessionOwner(ctx)?.sessionId;
261
- if (payload)
262
- recordToolUseEvent(spurContextDir(), sessionId ? { ...payload, session_id: sessionId } : payload);
263
- } catch {
264
- // fail-open
265
- }
266
- });
267
-
268
- // ── context-session-start: shared recordSessionStart (R2) ────────
269
- pi.on('session_start', async (_event, ctx) => {
270
- try {
271
- recordSessionStart(spurContextDir(), getEnvVars(), undefined, 'pi', piSessionOwner(ctx) ?? undefined);
272
- } catch {
273
- // fail-open
274
- }
275
- });
276
-
277
- // ── context-session-stop: shared recordSessionEnd (R3) ──────────
278
- pi.on('session_shutdown', async (_event, ctx) => {
279
- try {
280
- recordSessionEnd(spurContextDir(), undefined, piSessionOwner(ctx) ?? undefined);
281
- } catch {
282
- // fail-open
283
- }
284
- });
285
-
286
- // ── agent-lifecycle: fleet lifecycle reporting (G73 R4, task 1080) ──
287
- // Pi has no native `blocked` signal (herdr finding P2), so only working/idle are
288
- // reported here; a no-op outside a fleet, like every other hook path.
289
- pi.on('agent_start', async () => {
290
- try {
291
- reportLifecycleHook({ hook_event_name: 'agent_start' });
292
- } catch {
293
- // fail-open
294
- }
295
- });
296
-
297
- pi.on('agent_settled', async () => {
298
- try {
299
- reportLifecycleHook({ hook_event_name: 'agent_settled' });
300
- } catch {
301
- // fail-open
302
- }
303
- });
304
- }
@@ -1,265 +0,0 @@
1
- /**
2
- * dogfood pipeline-driving detection — the pure detector core.
3
- *
4
- * Extracted from `plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts` (task 1173, plan
5
- * P7/ADR-130): the reusable detection logic lives in lib, and the script keeps only the live
6
- * Phase 1.0 CLI gate. Callers and tests may keep importing the script — it re-exports this module.
7
- *
8
- * Contract (0274 §3 dogfood-pipeline-detect, 0277 R1):
9
- * - Word-boundary matchers with `-` treated as a word char (NOT a boundary).
10
- * A token counts only when it is a distinct hyphen-word, not when it is a
11
- * substring of a longer alphanumeric or hyphen run. This removes the
12
- * leading-space dependency of the @1.1 prose detector.
13
- * - Two token shapes, both with strict `[^\w-]` boundaries:
14
- * (a) complete tokens — `--next`, `dev-run`, `dev-runall`, `dev-wrap`,
15
- * `dev-wrapall`, `dev-idea` — matched as whole hyphen-words so the
16
- * slash form `/sp:dev-run` matches on its `dev-run` tail whether or
17
- * not it is preceded by a space, but `--next` does NOT match inside
18
- * `--next-gen` (the trailing `-gen` breaks the boundary).
19
- * (b) bare nouns — `run`, `runall`, `wrap`, `wrapall`, `idea` — matched
20
- * only as standalone words, so `run` matches in
21
- * `bun ... task run 0042` but NOT inside `runaway`/`prerun`, and
22
- * `idea` does NOT match inside `idealist`/`ideal`.
23
- * - Listing both shapes is what makes the detector leading-space invariant
24
- * (the slash form is caught by its `dev-*` tail; the bare noun is caught
25
- * when it appears as its own word) without letting `run` leak into every
26
- * `-run-` identifier.
27
- *
28
- * Non-goals of detectPipelineDriving: decides ambiguity only. It does NOT decide
29
- * observe vs fix — any explicit `--max-retry` value proceeds regardless.
30
- * Implement-heavy advisory is a separate helper (detectImplementHeavy).
31
- */
32
-
33
- /**
34
- * Tokens whose presence makes a testee pipeline-driving. Ordered for stable
35
- * diagnostics. Two shapes per the contract above: complete `dev-*` / `--next`
36
- * tokens first, then bare nouns.
37
- */
38
- const PIPELINE_TOKENS = [
39
- '--next',
40
- 'dev-runall',
41
- 'dev-wrapall',
42
- 'dev-refineall',
43
- 'dev-verifyall',
44
- 'dev-run',
45
- 'dev-wrap',
46
- 'dev-idea',
47
- 'refineall',
48
- 'verifyall',
49
- 'runall',
50
- 'wrapall',
51
- 'run',
52
- 'wrap',
53
- 'idea',
54
- ] as const;
55
-
56
- /** Refuse message when pipeline-driving and `--max-retry` was not passed (exact string). */
57
- export const PIPELINE_DRIVING_REFUSE_MESSAGE =
58
- '⚠ pipeline-driving testee detected; pass --max-retry 0 (observe-only) or --max-retry N (fix mode, tree mutation acknowledged)';
59
-
60
- /**
61
- * Refuse message when the testee carries a mutating `--fix` mode (`--fix all` /
62
- * `--fix blockers-first`) and `--max-retry` was not passed. A mutating fix mode
63
- * mutates the working tree independent of pipeline-driving (task 0293 R1/R2):
64
- * `--max-retry 0` bounds the **driver** only — the testee's own `--fix` pass
65
- * still mutates the tree — so the message must not imply otherwise.
66
- */
67
- export const MUTATING_FIX_REFUSE_MESSAGE =
68
- '⚠ mutating --fix mode detected (--fix all | --fix blockers-first); pass --max-retry 0 (observe-only for the driver; the testee still mutates the tree) or --max-retry N (fix mode, driver + testee both mutate)';
69
-
70
- /** Advisory when pipeline-driving + implement-heavy derived steps (exact string). */
71
- export const IMPLEMENT_HEAVY_ADVISORY_MESSAGE =
72
- '⚠ implement-heavy pipeline dogfood: prefer --max-retry 0 (observe-only) or step-split; operator --max-retry N overrides';
73
-
74
- /**
75
- * Tokens that mark a derived step as implement-heavy (mutates product/code or
76
- * drives a mutating pipeline leg). Deliberately excludes verify/review/unit-only
77
- * surfaces even when they carry `--next`.
78
- */
79
- const IMPLEMENT_HEAVY_TOKENS = [
80
- 'dev-runall',
81
- 'dev-wrapall',
82
- 'dev-run',
83
- 'dev-wrap',
84
- 'dev-idea',
85
- 'runall',
86
- 'wrapall',
87
- // bare `run` / `wrap` / `idea` — only when not clearly a non-mutating surface
88
- 'run',
89
- 'wrap',
90
- 'idea',
91
- 'implement',
92
- ] as const;
93
-
94
- /**
95
- * A verify/review surface stops being non-mutating the moment it carries a
96
- * repair mode: `--fix all` / `--fix blockers-first` applies Edit/Write repairs
97
- * to the working tree (0280 dogfood, finding P2). `--fix none` stays
98
- * observational. Boundary-guarded so `--prefix all` / `--focus all` never match.
99
- * Exported so the Phase 1.0 gate and tests share one matcher (task 0293 R1/R4).
100
- */
101
- export function hasMutatingFixMode(step: string): boolean {
102
- return /(?<![\w-])--fix[=\s]+(all|blockers-first)(?![\w-])/i.test(step);
103
- }
104
-
105
- /**
106
- * Match a token at a word boundary. A "word boundary" here is the position
107
- * between a non-`[\w-]` char (or string start) and the token, and between the
108
- * token and a non-`[\w-]` char (or string end). Treating `-` as a word char
109
- * is what makes `--next` reject `--next-gen` and `dev-run` reject `dev-runner`.
110
- */
111
- function tokenMatches(testee: string, token: string): boolean {
112
- const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
113
- // (?<![\w-]) — not preceded by a word char or hyphen.
114
- // (?![\w-]) — not followed by a word char or hyphen.
115
- const re = new RegExp(`(?<![\\w-])${escaped}(?![\\w-])`, 'i');
116
- return re.test(testee);
117
- }
118
-
119
- /**
120
- * Returns whether the testee string is pipeline-driving (contains any of the
121
- * pipeline-driving tokens as a distinct hyphen-word). Machine-checked
122
- * counterpart of the prose list in SKILL.md §Pipeline-driving detection and
123
- * dev-dogfood.md.
124
- */
125
- export function detectPipelineDriving(testee: string): boolean {
126
- if (typeof testee !== 'string' || testee.length === 0) {
127
- return false;
128
- }
129
- return PIPELINE_TOKENS.some((token) => tokenMatches(testee, token));
130
- }
131
-
132
- /**
133
- * True when a single step label (or the whole testee) is implement-heavy —
134
- * it chains into real implementation / wrap / idea work, not verify-only.
135
- *
136
- * Non-mutating surfaces (`dev-verify`, `dev-review`, `dev-unit`, plain
137
- * `dev-refine` without a further run) are never implement-heavy even if
138
- * pipeline-driving via `--next` — UNLESS they carry a mutating repair mode
139
- * (`--fix all` / `--fix blockers-first`), which turns the verify/review leg
140
- * into a tree-mutating fix pass (0280 dogfood, finding P2).
141
- */
142
- export function isImplementHeavyStep(step: string): boolean {
143
- if (typeof step !== 'string' || step.length === 0) return false;
144
- // Explicit non-mutating surfaces win unless a mutating token co-occurs.
145
- const nonMutatingOnly =
146
- tokenMatches(step, 'dev-verify') || tokenMatches(step, 'dev-review') || tokenMatches(step, 'dev-unit');
147
- const hasMutating = IMPLEMENT_HEAVY_TOKENS.some((token) => tokenMatches(step, token)) || hasMutatingFixMode(step);
148
- if (nonMutatingOnly && !hasMutating) return false;
149
- // refine alone is planning, not implement-heavy; refine+run/--next chain is.
150
- if (tokenMatches(step, 'dev-refine') && !hasMutating && !tokenMatches(step, '--next')) {
151
- return false;
152
- }
153
- if (tokenMatches(step, 'dev-refine') && tokenMatches(step, '--next')) {
154
- // refine --next chains into run → implement-heavy
155
- return true;
156
- }
157
- return hasMutating;
158
- }
159
-
160
- /**
161
- * True when the testee is implement-heavy: it carries a pipeline-driving token,
162
- * a mutating `--fix` mode, OR at least one derived step (or the testee itself
163
- * when no steps given) is implement-heavy. Used for the W8 Phase 1 advisory
164
- * after step derivation.
165
- *
166
- * Task 0293 R3: a mutating `--fix` mode alone is implement-heavy even with no
167
- * pipeline token — the verify/review leg itself becomes a tree-mutating fix
168
- * pass.
169
- */
170
- export function detectImplementHeavy(testee: string, derivedSteps: string[] = []): boolean {
171
- if (isImplementHeavyStep(testee)) return true;
172
- if (!detectPipelineDriving(testee)) return false;
173
- return derivedSteps.some((step) => isImplementHeavyStep(step));
174
- }
175
-
176
- export interface GateResult {
177
- pipelineDriving: boolean;
178
- mutatingFix: boolean;
179
- maxRetryPresent: boolean;
180
- implementHeavy: boolean;
181
- refuse: boolean;
182
- advisory: boolean;
183
- message: string | null;
184
- exitCode: 0 | 1 | 2;
185
- }
186
-
187
- /**
188
- * Phase 1.0 + W8 gate decision. Pure — no I/O. The CLI wrapper prints and exits.
189
- *
190
- * Refuse condition (task 0293 R1) is the union of two **independent** mutation
191
- * sources, each gated on `!maxRetryPresent`:
192
- *
193
- * 1. pipeline-driving testee (`--next`, `dev-run`, …) — refuses because the
194
- * testee chains lifecycle legs whose cumulative blast radius the driver
195
- * cannot pre-attribute.
196
- * 2. mutating `--fix` mode (`--fix all` / `--fix blockers-first`) — refuses
197
- * because the testee itself applies Edit/Write repairs even with no
198
- * pipeline token. `--max-retry 0` bounds the **driver** only; the testee
199
- * still mutates (honesty note in MUTATING_FIX_REFUSE_MESSAGE).
200
- *
201
- * Pipeline-driving is reported first when both co-occur (its refuse message is
202
- * the superset — chain + tree mutation).
203
- */
204
- export function evaluateDogfoodGate(
205
- testee: string,
206
- options: { maxRetryPresent?: boolean; steps?: string[] } = {},
207
- ): GateResult {
208
- const maxRetryPresent = options.maxRetryPresent === true;
209
- const steps = options.steps ?? [];
210
- const pipelineDriving = detectPipelineDriving(testee);
211
- const mutatingFix = hasMutatingFixMode(testee);
212
- const implementHeavy = detectImplementHeavy(testee, steps);
213
-
214
- if (pipelineDriving && !maxRetryPresent) {
215
- return {
216
- pipelineDriving,
217
- mutatingFix,
218
- maxRetryPresent,
219
- implementHeavy,
220
- refuse: true,
221
- advisory: false,
222
- message: PIPELINE_DRIVING_REFUSE_MESSAGE,
223
- exitCode: 2,
224
- };
225
- }
226
-
227
- if (mutatingFix && !maxRetryPresent) {
228
- return {
229
- pipelineDriving,
230
- mutatingFix,
231
- maxRetryPresent,
232
- implementHeavy,
233
- refuse: true,
234
- advisory: false,
235
- message: MUTATING_FIX_REFUSE_MESSAGE,
236
- exitCode: 2,
237
- };
238
- }
239
-
240
- if (implementHeavy) {
241
- return {
242
- pipelineDriving,
243
- mutatingFix,
244
- maxRetryPresent,
245
- implementHeavy,
246
- refuse: false,
247
- advisory: true,
248
- message: IMPLEMENT_HEAVY_ADVISORY_MESSAGE,
249
- exitCode: 0,
250
- };
251
- }
252
-
253
- return {
254
- pipelineDriving,
255
- mutatingFix,
256
- maxRetryPresent,
257
- implementHeavy,
258
- refuse: false,
259
- advisory: false,
260
- message: null,
261
- exitCode: 0,
262
- };
263
- }
264
-
265
- export { PIPELINE_TOKENS };