@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,359 +0,0 @@
1
- #!/usr/bin/env node
2
- // @bun
3
-
4
- // plugins/sp/scripts/session-timeline.ts
5
- import { readFileSync } from "fs";
6
-
7
- // plugins/sp/lib/env.ts
8
- function getEnvVars() {
9
- return process.env;
10
- }
11
-
12
- // plugins/sp/lib/transcript.ts
13
- import { existsSync, readdirSync } from "node:fs";
14
- import { homedir } from "node:os";
15
- import { join } from "node:path";
16
-
17
- // plugins/sp/lib/plan-projection.generated.mjs
18
- var NON_ABORTING_PREFLIGHT_CODES = new Set([
19
- "L4.scenario-unverified",
20
- "L4.verifying-incomplete-tasks"
21
- ]);
22
- var zeroTokens = () => ({ input: 0, cacheCreate: 0, cacheRead: 0, output: 0 });
23
- function formatDuration(ms) {
24
- const s = Math.round(ms / 1000);
25
- const h = Math.floor(s / 3600);
26
- const m = Math.floor(s % 3600 / 60);
27
- const ss = String(s % 60).padStart(2, "0");
28
- return h > 0 ? `${h}:${String(m).padStart(2, "0")}:${ss}` : `${m}:${ss}`;
29
- }
30
- function formatTokens(n) {
31
- if (n >= 999500)
32
- return `${(n / 1e6).toFixed(1)}M`;
33
- return n >= 1000 ? `${Math.round(n / 1000)}k` : String(n);
34
- }
35
- function formatTokenSplit(t) {
36
- if (!t)
37
- return "n/a";
38
- const nonCached = t.input + t.cacheCreate + t.output;
39
- return `${formatTokens(nonCached + t.cacheRead)} / ${formatTokens(nonCached)}`;
40
- }
41
- var OPERATOR_TOOLS = new Set(["AskUserQuestion", "ask_user_question"]);
42
- function firstTextContent(row) {
43
- const content = row.message?.content;
44
- if (typeof content === "string")
45
- return content;
46
- if (!Array.isArray(content))
47
- return;
48
- return content.find((b) => b.type === "text")?.text;
49
- }
50
- function skillInvocationPrompt(text) {
51
- const name = /^\s*<skill name="([^"]+)"/.exec(text)?.[1];
52
- if (name === undefined)
53
- return;
54
- const close = text.lastIndexOf("</skill>");
55
- const args = close < 0 ? "" : text.slice(close + "</skill>".length).trim();
56
- return args === "" ? `/${name}` : `/${name} ${args}`;
57
- }
58
- function promptText(row) {
59
- if (row.type === "custom_message") {
60
- if (row.display === false)
61
- return;
62
- const text = typeof row.content === "string" ? row.content.trim() : "";
63
- return text === "" ? undefined : text;
64
- }
65
- if (row.type === "message") {
66
- if (row.message?.role !== "user")
67
- return;
68
- const text = firstTextContent(row);
69
- if (text === undefined)
70
- return;
71
- return skillInvocationPrompt(text) ?? text;
72
- }
73
- if (row.type !== "user" || row.isMeta || row.isCompactSummary)
74
- return;
75
- const content = row.message?.content;
76
- if (typeof content === "string")
77
- return content;
78
- if (!Array.isArray(content) || content.some((b) => b.type === "tool_result"))
79
- return;
80
- return content.find((b) => b.type === "text")?.text;
81
- }
82
- function sumTokens(all) {
83
- const total = zeroTokens();
84
- for (const t of all)
85
- for (const k of Object.keys(total))
86
- total[k] += t[k];
87
- return total;
88
- }
89
- function sumSpans(spans) {
90
- return {
91
- workMs: spans.reduce((n, s) => n + s.workMs, 0),
92
- waitMs: spans.reduce((n, s) => n + s.waitMs, 0),
93
- toolCalls: spans.reduce((n, s) => n + s.toolCalls, 0),
94
- tokens: sumTokens(spans.map((s) => s.tokens))
95
- };
96
- }
97
- var newAcc = (ts) => ({
98
- last: ts,
99
- askMs: 0,
100
- asks: new Map,
101
- toolIds: new Set,
102
- messages: new Map,
103
- compactions: 0
104
- });
105
- function accumulate(acc, row, ts) {
106
- acc.last = Math.max(acc.last, ts);
107
- if (row.type === "message") {
108
- const m = row.message;
109
- if (m?.role === "assistant") {
110
- const blocks = Array.isArray(m.content) ? m.content : [];
111
- for (const b of blocks) {
112
- if (b.type === "toolCall" && b.id) {
113
- acc.toolIds.add(b.id);
114
- if (b.name && OPERATOR_TOOLS.has(b.name))
115
- acc.asks.set(b.id, ts);
116
- }
117
- }
118
- const u = m.usage;
119
- if (u && row.id)
120
- acc.messages.set(row.id, {
121
- input: u.input ?? 0,
122
- cacheCreate: u.cacheWrite ?? 0,
123
- cacheRead: u.cacheRead ?? 0,
124
- output: u.output ?? 0
125
- });
126
- } else if (m?.role === "toolResult" && m.toolCallId && acc.asks.has(m.toolCallId)) {
127
- acc.askMs += ts - (acc.asks.get(m.toolCallId) ?? ts);
128
- }
129
- return;
130
- }
131
- if (row.type === "compaction")
132
- acc.compactions++;
133
- const blocks = Array.isArray(row.message?.content) ? row.message.content : [];
134
- for (const b of blocks) {
135
- if (b.type === "tool_use" && b.id) {
136
- acc.toolIds.add(b.id);
137
- if (b.name && OPERATOR_TOOLS.has(b.name))
138
- acc.asks.set(b.id, ts);
139
- } else if (b.type === "tool_result" && b.tool_use_id && acc.asks.has(b.tool_use_id)) {
140
- acc.askMs += ts - (acc.asks.get(b.tool_use_id) ?? ts);
141
- }
142
- }
143
- const u = row.message?.usage;
144
- if (row.type === "assistant" && u && row.message?.id) {
145
- acc.messages.set(row.message.id, {
146
- input: u.input_tokens ?? 0,
147
- cacheCreate: u.cache_creation_input_tokens ?? 0,
148
- cacheRead: u.cache_read_input_tokens ?? 0,
149
- output: u.output_tokens ?? 0
150
- });
151
- }
152
- }
153
- var SHELL_TOOLS = new Set(["bash", "shell", "exec", "exec_command", "run_command", "command"]);
154
- var SUBAGENT_TOOLS = new Set(["agent", "task", "invoke_subagent", "subagent", "dispatch_agent"]);
155
- var LOWERCASE_OPERATOR_TOOLS = new Set(["askuserquestion", "ask_user_question"]);
156
- var SENSITIVE_PREFIXES = ["drizzle/", "packages/config/", ".github/", "plugins/sp/hooks/"];
157
- var SENSITIVE_GLOBS = ["apps/server/src/**/auth*", "**/*secret*", "**/*.sql"];
158
- function globToRegex(glob) {
159
- const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replaceAll("**/", "\x00").replaceAll("**", "\x01").replaceAll("*", "[^/]*").replaceAll("\x00", "(?:.*/)?").replaceAll("\x01", ".*");
160
- return new RegExp(`^${escaped}$`, "s");
161
- }
162
- var SENSITIVE_MATCHERS = [
163
- ...SENSITIVE_PREFIXES.map((prefix) => ({
164
- label: prefix,
165
- test: (path) => path.startsWith(prefix)
166
- })),
167
- ...SENSITIVE_GLOBS.map((glob) => {
168
- const regex = globToRegex(glob);
169
- return { label: glob, test: (path) => regex.test(path) };
170
- })
171
- ];
172
-
173
- // plugins/sp/lib/transcript.ts
174
- var SESSION_ID = /^[A-Za-z0-9_-]+$/;
175
- var PI_ROLES = new Set(["user", "assistant", "toolResult"]);
176
- function sniffFormat(rows) {
177
- for (const row of rows) {
178
- if (row.type === "message" && PI_ROLES.has(row.message?.role ?? ""))
179
- return "pi";
180
- if (row.type === "user" || row.type === "assistant")
181
- return "claude";
182
- }
183
- return "unknown";
184
- }
185
- function parseRows(lines) {
186
- const rows = [];
187
- let skippedLines = 0;
188
- for (const line of lines) {
189
- if (!line.trim())
190
- continue;
191
- let row;
192
- try {
193
- row = JSON.parse(line);
194
- } catch {
195
- skippedLines++;
196
- continue;
197
- }
198
- const ts = row.timestamp ? Date.parse(row.timestamp) : Number.NaN;
199
- if (!Number.isNaN(ts))
200
- rows.push([row, ts]);
201
- }
202
- return { rows, skippedLines, format: sniffFormat(rows.map(([r]) => r)) };
203
- }
204
- function resolveTranscript(env, projectsRoot = join(homedir(), ".claude", "projects"), override) {
205
- if (override)
206
- return existsSync(override) ? { ok: true, path: override } : { ok: false, reason: "no transcript" };
207
- const id = env.CLAUDE_CODE_SESSION_ID;
208
- if (!id) {
209
- const piFile = env.PI_SESSION_FILE;
210
- if (piFile !== undefined && piFile !== "") {
211
- return existsSync(piFile) ? { ok: true, path: piFile } : { ok: false, reason: `PI_SESSION_FILE ${piFile} does not exist` };
212
- }
213
- return {
214
- ok: false,
215
- reason: "no host session id; pass --transcript <path> (pi: ~/.pi/agent/sessions/<cwd-slug>/<file>.jsonl, " + "or set PI_SESSION_FILE)"
216
- };
217
- }
218
- if (!SESSION_ID.test(id))
219
- return { ok: false, reason: "refusing a session id with path characters" };
220
- if (!existsSync(projectsRoot))
221
- return { ok: false, reason: `no transcript root ${projectsRoot}` };
222
- for (const dir of readdirSync(projectsRoot)) {
223
- const path = join(projectsRoot, dir, `${id}.jsonl`);
224
- if (existsSync(path))
225
- return { ok: true, path };
226
- }
227
- return { ok: false, reason: `no transcript for session ${id}` };
228
- }
229
-
230
- // plugins/sp/scripts/session-timeline.ts
231
- var PROMPT_EXCERPT = 80;
232
- var render = (span) => ({
233
- ...span,
234
- work: formatDuration(span.workMs),
235
- wait: formatDuration(span.waitMs),
236
- token: formatTokenSplit(span.tokens)
237
- });
238
- function parseGroups(spec, count) {
239
- let next = 1;
240
- return spec.split(",").map((part) => {
241
- const [a = Number.NaN, b = a] = part.trim().split("-").map(Number);
242
- if (!Number.isInteger(a) || !Number.isInteger(b) || a < next || b < a || b > count) {
243
- throw new Error(`invalid --group "${part}" (segments 1-${count}, ascending, no overlap)`);
244
- }
245
- next = b + 1;
246
- return [a, b];
247
- });
248
- }
249
- function buildTimeline(lines, group) {
250
- const open = [];
251
- const { rows, skippedLines, format } = parseRows(lines);
252
- const isPi = format === "pi";
253
- for (const [row, ts] of rows) {
254
- const prompt = promptText(row);
255
- if (prompt !== undefined) {
256
- open.push({ start: ts, prompt, ...newAcc(ts) });
257
- continue;
258
- }
259
- const seg = open.at(-1);
260
- if (seg)
261
- accumulate(seg, row, ts);
262
- }
263
- const segments = open.map((seg, i) => {
264
- const next = open[i + 1]?.start;
265
- const idle = next === undefined ? 0 : Math.max(0, next - seg.last);
266
- const oneLine = seg.prompt.replace(/\s+/g, " ").trim();
267
- const span = {
268
- index: i + 1,
269
- start: new Date(seg.start).toISOString(),
270
- prompt: oneLine.length > PROMPT_EXCERPT ? `${oneLine.slice(0, PROMPT_EXCERPT)}\u2026` : oneLine,
271
- workMs: seg.last - seg.start - seg.askMs,
272
- waitMs: idle + seg.askMs,
273
- toolCalls: seg.toolIds.size,
274
- tokens: sumTokens([...seg.messages.values()])
275
- };
276
- return render(isPi ? { ...span, compactions: seg.compactions } : span);
277
- });
278
- const first = open[0];
279
- const last = open.at(-1);
280
- const elapsedMs = first && last ? last.last - first.start : 0;
281
- const totals = { ...sumSpans(segments), elapsedMs, elapsed: formatDuration(elapsedMs) };
282
- const timeline = {
283
- available: true,
284
- segments,
285
- totals: render(isPi ? { ...totals, compactions: segments.reduce((n, s) => n + (s.compactions ?? 0), 0) } : totals),
286
- skippedLines
287
- };
288
- if (group) {
289
- timeline.stages = parseGroups(group, segments.length).map(([a, b]) => render({ ...sumSpans(segments.slice(a - 1, b)), segments: a === b ? `${a}` : `${a}-${b}` }));
290
- }
291
- return timeline;
292
- }
293
- var SESSION_TIMELINE_USAGE = 'usage: session-timeline [--transcript <path>] [--group "1-3,4,..."]';
294
- function zeroSegmentReason(rows) {
295
- const format = sniffFormat(rows.map(([r]) => r));
296
- if (format === "unknown") {
297
- const counts = new Map;
298
- for (const [r] of rows) {
299
- const kind = typeof r.type === "string" && r.type !== "" ? r.type : "(untyped)";
300
- counts.set(kind, (counts.get(kind) ?? 0) + 1);
301
- }
302
- const census = [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 5).map(([kind, n]) => `${kind} ${n}`).join(", ");
303
- return `unrecognized transcript format (row types: ${census})`;
304
- }
305
- return `${format} transcript with no operator prompts (${rows.length} rows)`;
306
- }
307
- function main(argv, env = getEnvVars(), write = (s) => process.stdout.write(s), projectsRoot, writeErr = (s) => process.stderr.write(s)) {
308
- let transcript;
309
- let group;
310
- for (let i = 0;i < argv.length; i++) {
311
- const arg = argv[i];
312
- if (arg === "--transcript" && argv[i + 1])
313
- transcript = argv[++i];
314
- else if (arg === "--group" && argv[i + 1])
315
- group = argv[++i];
316
- else if (arg === "--spur-bin" && argv[i + 1])
317
- i++;
318
- else {
319
- writeErr(`${SESSION_TIMELINE_USAGE}
320
- `);
321
- return 2;
322
- }
323
- }
324
- const resolved = resolveTranscript(env, projectsRoot, transcript);
325
- if (!resolved.ok) {
326
- write(`${JSON.stringify({ available: false, reason: resolved.reason })}
327
- `);
328
- return 0;
329
- }
330
- try {
331
- const lines = readFileSync(resolved.path, "utf8").split(`
332
- `);
333
- const timeline = buildTimeline(lines, group);
334
- if (timeline.segments.length === 0) {
335
- write(`${JSON.stringify({ available: false, reason: zeroSegmentReason(parseRows(lines).rows) })}
336
- `);
337
- return 0;
338
- }
339
- write(`${JSON.stringify({ ...timeline, transcript: resolved.path })}
340
- `);
341
- return 0;
342
- } catch (error) {
343
- writeErr(`session-timeline: ${error.message}
344
- `);
345
- return 2;
346
- }
347
- }
348
- process.exit(main(process.argv.slice(2)));
349
- export {
350
- SESSION_TIMELINE_USAGE,
351
- buildTimeline,
352
- formatDuration,
353
- formatTokenSplit,
354
- formatTokens,
355
- main,
356
- parseGroups,
357
- resolveTranscript,
358
- zeroSegmentReason
359
- };
@@ -1,190 +0,0 @@
1
- #!/usr/bin/env bun
2
- /**
3
- * session-timeline — measured time and token evidence for the sp:session-review Time breakdown.
4
- * The visible conversation carries no timestamps or usage (so the table read `n/a`); the host
5
- * transcript carries both. Reads the active Claude Code JSONL read-only
6
- * (`~/.claude/projects/<slug>/<CLAUDE_CODE_SESSION_ID>.jsonl`, or `--transcript`), one segment per
7
- * operator prompt: work = prompt → last activity; wait = idle until the next prompt plus time spent
8
- * answering an AskUserQuestion; tokens counted once per message id and rendered `<total> /
9
- * <non-cached>`. `--group "1-3,4"` sums segments into stages. No transcript → `{available:false}`,
10
- * exit 0, and the review renders `n/a` instead of guessing. Node builtins only (standalone plugin).
11
- */
12
- import { readFileSync } from 'node:fs';
13
- import { getEnvVars } from '../lib/env';
14
- import {
15
- type Acc,
16
- accumulate,
17
- formatDuration,
18
- formatTokenSplit,
19
- newAcc,
20
- parseRows,
21
- promptText,
22
- type Row,
23
- resolveTranscript,
24
- type Span,
25
- sniffFormat,
26
- sumSpans,
27
- sumTokens,
28
- } from '../lib/transcript';
29
-
30
- export { formatDuration, formatTokenSplit, formatTokens, resolveTranscript } from '../lib/transcript';
31
-
32
- /** A span plus its `M:SS` / `H:MM:SS` and token renderings, so the review table needs no arithmetic. */
33
- export type Rendered<T> = T & { work: string; wait: string; token: string };
34
-
35
- export interface Timeline {
36
- available: true;
37
- transcript?: string;
38
- segments: Rendered<Span & { index: number; start: string; prompt: string; compactions?: number }>[];
39
- stages?: Rendered<Span & { segments: string }>[];
40
- totals: Rendered<Span & { elapsedMs: number; elapsed: string; compactions?: number }>;
41
- skippedLines: number;
42
- }
43
-
44
- const PROMPT_EXCERPT = 80;
45
-
46
- const render = <T extends Span>(span: T): Rendered<T> => ({
47
- ...span,
48
- work: formatDuration(span.workMs),
49
- wait: formatDuration(span.waitMs),
50
- token: formatTokenSplit(span.tokens),
51
- });
52
-
53
- /** "1-3,4" → [[1,3],[4,4]]; 1-based, ascending, in range, non-overlapping. */
54
- export function parseGroups(spec: string, count: number): [number, number][] {
55
- let next = 1;
56
- return spec.split(',').map((part) => {
57
- const [a = Number.NaN, b = a] = part.trim().split('-').map(Number);
58
- if (!Number.isInteger(a) || !Number.isInteger(b) || a < next || b < a || b > count) {
59
- throw new Error(`invalid --group "${part}" (segments 1-${count}, ascending, no overlap)`);
60
- }
61
- next = b + 1;
62
- return [a, b] as [number, number];
63
- });
64
- }
65
-
66
- interface Open extends Acc {
67
- start: number;
68
- prompt: string;
69
- }
70
-
71
- export function buildTimeline(lines: string[], group?: string): Timeline {
72
- const open: Open[] = [];
73
- const { rows, skippedLines, format } = parseRows(lines);
74
- const isPi = format === 'pi';
75
-
76
- for (const [row, ts] of rows) {
77
- const prompt = promptText(row);
78
- if (prompt !== undefined) {
79
- open.push({ start: ts, prompt, ...newAcc(ts) });
80
- continue;
81
- }
82
- const seg = open.at(-1);
83
- if (seg) accumulate(seg, row, ts); // rows before the first prompt are session preamble
84
- }
85
-
86
- const segments = open.map(
87
- (seg, i): Rendered<Span & { index: number; start: string; prompt: string; compactions?: number }> => {
88
- const next = open[i + 1]?.start;
89
- const idle = next === undefined ? 0 : Math.max(0, next - seg.last);
90
- const oneLine = seg.prompt.replace(/\s+/g, ' ').trim();
91
- const span = {
92
- index: i + 1,
93
- start: new Date(seg.start).toISOString(),
94
- prompt: oneLine.length > PROMPT_EXCERPT ? `${oneLine.slice(0, PROMPT_EXCERPT)}…` : oneLine,
95
- workMs: seg.last - seg.start - seg.askMs,
96
- waitMs: idle + seg.askMs,
97
- toolCalls: seg.toolIds.size,
98
- tokens: sumTokens([...seg.messages.values()]),
99
- };
100
- return render(isPi ? { ...span, compactions: seg.compactions } : span);
101
- },
102
- );
103
-
104
- const first = open[0];
105
- const last = open.at(-1);
106
- const elapsedMs = first && last ? last.last - first.start : 0;
107
- const totals = { ...sumSpans(segments), elapsedMs, elapsed: formatDuration(elapsedMs) };
108
- const timeline: Timeline = {
109
- available: true,
110
- segments,
111
- totals: render(
112
- isPi ? { ...totals, compactions: segments.reduce((n, s) => n + (s.compactions ?? 0), 0) } : totals,
113
- ),
114
- skippedLines,
115
- };
116
- if (group) {
117
- timeline.stages = parseGroups(group, segments.length).map(([a, b]) =>
118
- render({ ...sumSpans(segments.slice(a - 1, b)), segments: a === b ? `${a}` : `${a}-${b}` }),
119
- );
120
- }
121
- return timeline;
122
- }
123
-
124
- export const SESSION_TIMELINE_USAGE = 'usage: session-timeline [--transcript <path>] [--group "1-3,4,..."]';
125
-
126
- /**
127
- * 1138 R3: a zero-segment result must say WHAT was detected, so an unparseable-but-recognised
128
- * transcript can never read as an authoritative all-zero timeline. A known format names the row
129
- * count; an unknown one lists the top-5 row-type census. (1151 R2 removed the pi-only injected
130
- * count: pi injects no skill bodies, so a wrapper row is an operator prompt and the class is gone.)
131
- */
132
- export function zeroSegmentReason(rows: [Row, number][]): string {
133
- const format = sniffFormat(rows.map(([r]) => r));
134
- if (format === 'unknown') {
135
- const counts = new Map<string, number>();
136
- for (const [r] of rows) {
137
- const kind = typeof r.type === 'string' && r.type !== '' ? r.type : '(untyped)';
138
- counts.set(kind, (counts.get(kind) ?? 0) + 1);
139
- }
140
- const census = [...counts.entries()]
141
- .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
142
- .slice(0, 5)
143
- .map(([kind, n]) => `${kind} ${n}`)
144
- .join(', ');
145
- return `unrecognized transcript format (row types: ${census})`;
146
- }
147
- return `${format} transcript with no operator prompts (${rows.length} rows)`;
148
- }
149
-
150
- export function main(
151
- argv: string[],
152
- env: Record<string, string | undefined> = getEnvVars(),
153
- write: (s: string) => void = (s) => process.stdout.write(s),
154
- projectsRoot?: string,
155
- writeErr: (s: string) => void = (s) => process.stderr.write(s),
156
- ): number {
157
- let transcript: string | undefined;
158
- let group: string | undefined;
159
- for (let i = 0; i < argv.length; i++) {
160
- const arg = argv[i];
161
- if (arg === '--transcript' && argv[i + 1]) transcript = argv[++i];
162
- else if (arg === '--group' && argv[i + 1]) group = argv[++i];
163
- else if (arg === '--spur-bin' && argv[i + 1])
164
- i++; // 0482 R2: accepted, unused (no spur calls)
165
- else {
166
- writeErr(`${SESSION_TIMELINE_USAGE}\n`);
167
- return 2;
168
- }
169
- }
170
- const resolved = resolveTranscript(env, projectsRoot, transcript);
171
- if (!resolved.ok) {
172
- write(`${JSON.stringify({ available: false, reason: resolved.reason })}\n`);
173
- return 0;
174
- }
175
- try {
176
- const lines = readFileSync(resolved.path, 'utf8').split('\n');
177
- const timeline = buildTimeline(lines, group);
178
- if (timeline.segments.length === 0) {
179
- write(`${JSON.stringify({ available: false, reason: zeroSegmentReason(parseRows(lines).rows) })}\n`);
180
- return 0;
181
- }
182
- write(`${JSON.stringify({ ...timeline, transcript: resolved.path })}\n`);
183
- return 0;
184
- } catch (error) {
185
- writeErr(`session-timeline: ${(error as Error).message}\n`);
186
- return 2;
187
- }
188
- }
189
-
190
- if (import.meta.main) process.exit(main(process.argv.slice(2)));