@gobing-ai/spur 0.4.0 → 0.4.1

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 (154) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/decisions/gates.yaml +36 -0
  3. package/config/decisions/history-anatomy.yaml +35 -0
  4. package/config/decisions/idea-pipeline.yaml +38 -0
  5. package/config/plugin-scripts.json +15 -0
  6. package/config/rules/docs/sourced-api-claims.yaml +60 -0
  7. package/config/rules/recommended-pre-check.yaml +8 -3
  8. package/config/rules/structure/scratch-evidence-pointer.yaml +35 -0
  9. package/config/script-placement-baseline.json +2 -2
  10. package/config/workflows/feature-lifecycle.yaml +9 -2
  11. package/config/workflows/history-anatomy.yaml +22 -0
  12. package/config/workflows/idea-pipeline.yaml +75 -0
  13. package/config/workflows/task-pipeline.yaml +216 -57
  14. package/config/workflows/wrapup-pipeline.yaml +106 -13
  15. package/package.json +9 -9
  16. package/plugins/sp/README.md +11 -1
  17. package/plugins/sp/agents/super-planner.md +11 -0
  18. package/plugins/sp/agents/super-reviewer.md +14 -0
  19. package/plugins/sp/bunfig.toml +8 -0
  20. package/plugins/sp/commands/dev-brainstorm.md +29 -16
  21. package/plugins/sp/commands/dev-find-way.md +80 -0
  22. package/plugins/sp/commands/dev-idea.md +6 -0
  23. package/plugins/sp/commands/dev-parallel.md +8 -2
  24. package/plugins/sp/commands/dev-plan.md +6 -0
  25. package/plugins/sp/commands/dev-review-session.md +3 -1
  26. package/plugins/sp/commands/dev-run.md +12 -2
  27. package/plugins/sp/commands/dev-runall.md +18 -10
  28. package/plugins/sp/hooks/context-session-start.ts +1 -1
  29. package/plugins/sp/lib/idea-handoff.generated.mjs +41 -5
  30. package/plugins/sp/lib/inline-run-app.ts +28 -0
  31. package/plugins/sp/lib/inline-run.generated.d.mts +5 -0
  32. package/plugins/sp/lib/inline-run.generated.mjs +333 -45
  33. package/plugins/sp/lib/plan-projection.generated.d.mts +28 -0
  34. package/plugins/sp/lib/plan-projection.generated.mjs +255 -0
  35. package/plugins/sp/lib/quality-gate.generated.d.mts +47 -1
  36. package/plugins/sp/lib/quality-gate.generated.mjs +30489 -82
  37. package/plugins/sp/lib/residual-scan.generated.d.mts +19 -0
  38. package/plugins/sp/lib/residual-scan.generated.mjs +66 -0
  39. package/plugins/sp/lib/run-summary-core.ts +561 -0
  40. package/plugins/sp/lib/transcript.ts +121 -6
  41. package/plugins/sp/plugin.json +1 -1
  42. package/plugins/sp/references/roles.md +1 -1
  43. package/plugins/sp/scripts/batch-plan.mjs +335 -0
  44. package/plugins/sp/scripts/batch-plan.ts +100 -0
  45. package/plugins/sp/scripts/batch-preflight.mjs +51 -3
  46. package/plugins/sp/scripts/batch-preflight.ts +72 -1
  47. package/plugins/sp/scripts/commit-guard.mjs +216 -0
  48. package/plugins/sp/scripts/commit-guard.ts +249 -0
  49. package/plugins/sp/scripts/inline-run-dispatch.mjs +130 -0
  50. package/plugins/sp/scripts/inline-run-dispatch.ts +135 -0
  51. package/plugins/sp/scripts/inline-run-setup.mjs +551 -80
  52. package/plugins/sp/scripts/inline-run-setup.ts +52 -78
  53. package/plugins/sp/scripts/persist-out-check.mjs +90 -41
  54. package/plugins/sp/scripts/persist-out-check.ts +112 -57
  55. package/plugins/sp/scripts/quality-gate.mjs +30407 -73
  56. package/plugins/sp/scripts/quality-gate.ts +21 -5
  57. package/plugins/sp/scripts/residual-scan.mjs +86 -31
  58. package/plugins/sp/scripts/residual-scan.ts +28 -30
  59. package/plugins/sp/scripts/run-summary.mjs +421 -14
  60. package/plugins/sp/scripts/run-summary.ts +41 -177
  61. package/plugins/sp/scripts/session-timeline.mjs +109 -12
  62. package/plugins/sp/scripts/session-timeline.ts +71 -19
  63. package/plugins/sp/scripts/wrapup-steps.mjs +150 -2
  64. package/plugins/sp/scripts/wrapup-steps.ts +203 -1
  65. package/plugins/sp/skills/brainstorm/SKILL.md +9 -47
  66. package/plugins/sp/skills/branch-workflow/SKILL.md +1 -0
  67. package/plugins/sp/skills/branch-workflow/references/worktree-patterns.md +1 -0
  68. package/plugins/sp/skills/code-implementation/SKILL.md +48 -1
  69. package/plugins/sp/skills/code-verification/SKILL.md +3 -4
  70. package/plugins/sp/skills/code-verification/references/secu-review.md +3 -0
  71. package/plugins/sp/skills/code-verification/references/verdict-schema.md +68 -0
  72. package/plugins/sp/skills/fleet-join/SKILL.md +1 -1
  73. package/plugins/sp/skills/session-review/SKILL.md +38 -7
  74. package/plugins/sp/skills/source-driven-development/SKILL.md +5 -0
  75. package/plugins/sp/skills/spec-decomposition/references/decomposition.md +5 -3
  76. package/plugins/sp/skills/spur-cli/SKILL.md +5 -2
  77. package/plugins/sp/skills/spur-cli/references/agent.md +20 -15
  78. package/plugins/sp/skills/spur-cli/references/decision.md +7 -6
  79. package/plugins/sp/skills/spur-cli/references/message.md +7 -7
  80. package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +19 -3
  81. package/plugins/sp/skills/spur-cli/references/tasks.md +10 -7
  82. package/plugins/sp/skills/spur-cli/references/workflows/authoring-workflows.md +7 -0
  83. package/plugins/sp/skills/spur-cli/references/workflows.md +7 -2
  84. package/plugins/sp/skills/spur-composer/SKILL.md +1 -1
  85. package/plugins/sp/skills/spur-dev/SKILL.md +18 -14
  86. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +6 -3
  87. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +49 -14
  88. package/plugins/sp/skills/spur-dev/references/dev-operations.md +37 -16
  89. package/plugins/sp/skills/spur-dev/references/execution-batch-continuation.md +68 -0
  90. package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +194 -0
  91. package/plugins/sp/skills/spur-dev/references/execution-batch.md +113 -1020
  92. package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +130 -0
  93. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +6 -0
  94. package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +471 -0
  95. package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +379 -0
  96. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +35 -5
  97. package/plugins/sp/skills/spur-dev/references/glossary.md +2 -2
  98. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +343 -163
  99. package/plugins/sp/skills/spur-dev/references/structured-trace-emission.md +178 -0
  100. package/plugins/sp/skills/wayfinder/SKILL.md +10 -14
  101. package/schemas/state-machine-workflow.schema.json +23 -0
  102. package/spur.js +20728 -16630
  103. package/web/_astro/{BoardApp.VeoxxLBy.js → BoardApp.D66k_s_T.js} +1 -1
  104. package/web/_astro/{BoardApp.Bzr4OXrW.js → BoardApp.D6sjAJr_.js} +173 -173
  105. package/web/_astro/{TaskDetail.LAyR_Pvn.js → TaskDetail.BynLnMNc.js} +1 -1
  106. package/web/_astro/{arc.f4e0OYLg.js → arc._BUadrdH.js} +1 -1
  107. package/web/_astro/{architectureDiagram-3BPJPVTR.SSIvxi-Z.js → architectureDiagram-3BPJPVTR.Dh7xS1Pp.js} +1 -1
  108. package/web/_astro/{blockDiagram-GPEHLZMM.DSXIHOFf.js → blockDiagram-GPEHLZMM.DFDVpfXm.js} +1 -1
  109. package/web/_astro/{c4Diagram-AAUBKEIU.DG9Ubu85.js → c4Diagram-AAUBKEIU.V7_3_glh.js} +1 -1
  110. package/web/_astro/channel.DZVfto-r.js +1 -0
  111. package/web/_astro/{chunk-2J33WTMH.C7QU88Qh.js → chunk-2J33WTMH.De1yx7Jz.js} +1 -1
  112. package/web/_astro/{chunk-4BX2VUAB.26Z94xBj.js → chunk-4BX2VUAB.BDhHFuxx.js} +1 -1
  113. package/web/_astro/{chunk-55IACEB6.DYRiBkqw.js → chunk-55IACEB6.DDLkWnmC.js} +1 -1
  114. package/web/_astro/{chunk-727SXJPM.D5Vfv5pe.js → chunk-727SXJPM.CwfiLnA7.js} +1 -1
  115. package/web/_astro/{chunk-AQP2D5EJ.DXHV47bN.js → chunk-AQP2D5EJ.CtGDmUnM.js} +1 -1
  116. package/web/_astro/{chunk-FMBD7UC4.D1suAfOs.js → chunk-FMBD7UC4.BOLjpw73.js} +1 -1
  117. package/web/_astro/{chunk-ND2GUHAM.D6BmdHEY.js → chunk-ND2GUHAM.BDhQls6O.js} +1 -1
  118. package/web/_astro/{chunk-QZHKN3VN.rnPU-Ak-.js → chunk-QZHKN3VN.76qBynDF.js} +1 -1
  119. package/web/_astro/{classDiagram-4FO5ZUOK.CvEfXIo1.js → classDiagram-4FO5ZUOK.N0mpPHT0.js} +1 -1
  120. package/web/_astro/{classDiagram-v2-Q7XG4LA2.CvEfXIo1.js → classDiagram-v2-Q7XG4LA2.N0mpPHT0.js} +1 -1
  121. package/web/_astro/{cose-bilkent-S5V4N54A.DkMisLSm.js → cose-bilkent-S5V4N54A.CKknFfjB.js} +1 -1
  122. package/web/_astro/{cynefin-OW5HDTMX.C77Q-pF6.js → cynefin-OW5HDTMX.Cw3ZWBAh.js} +1 -1
  123. package/web/_astro/{dagre-BM42HDAG.YaV8ce27.js → dagre-BM42HDAG.D3yIKCRD.js} +1 -1
  124. package/web/_astro/{diagram-2AECGRRQ.CUyGEKge.js → diagram-2AECGRRQ.D51dO83Z.js} +1 -1
  125. package/web/_astro/{diagram-5GNKFQAL.CRgd8dgj.js → diagram-5GNKFQAL.Bb-FViDj.js} +1 -1
  126. package/web/_astro/{diagram-KO2AKTUF.Bmzys9hq.js → diagram-KO2AKTUF.D3mN8S6x.js} +1 -1
  127. package/web/_astro/{diagram-LMA3HP47.BvmJY4Lw.js → diagram-LMA3HP47.dMMyPcbJ.js} +1 -1
  128. package/web/_astro/{diagram-OG6HWLK6.0hEhD30d.js → diagram-OG6HWLK6.AzbvzY4C.js} +1 -1
  129. package/web/_astro/{erDiagram-TEJ5UH35.Bjt8lrnm.js → erDiagram-TEJ5UH35.BR5I4OZb.js} +1 -1
  130. package/web/_astro/{flowDiagram-I6XJVG4X.CJZTjRDV.js → flowDiagram-I6XJVG4X.DFqvhGkn.js} +1 -1
  131. package/web/_astro/{ganttDiagram-6RSMTGT7._VqoVafc.js → ganttDiagram-6RSMTGT7.ByJbhH6J.js} +1 -1
  132. package/web/_astro/{gitGraphDiagram-PVQCEYII.CEDb7tVo.js → gitGraphDiagram-PVQCEYII.CInHjQR3.js} +1 -1
  133. package/web/_astro/index.COh_kBxN.css +1 -0
  134. package/web/_astro/{infoDiagram-5YYISTIA.CgwifPpK.js → infoDiagram-5YYISTIA.Cm0dGVjJ.js} +1 -1
  135. package/web/_astro/{ishikawaDiagram-YF4QCWOH.B99wx2J1.js → ishikawaDiagram-YF4QCWOH.C3nOP6Sj.js} +1 -1
  136. package/web/_astro/{journeyDiagram-JHISSGLW.2hRwJbEo.js → journeyDiagram-JHISSGLW.4IvLa2MW.js} +1 -1
  137. package/web/_astro/{kanban-definition-UN3LZRKU.BahJkKNC.js → kanban-definition-UN3LZRKU.DS87Cl-k.js} +1 -1
  138. package/web/_astro/{linear.DguUUe6V.js → linear.MzmRrNER.js} +1 -1
  139. package/web/_astro/{mermaid.core.CvgfmqVr.js → mermaid.core.fK-WNswS.js} +4 -4
  140. package/web/_astro/{mindmap-definition-RKZ34NQL.B3qEA3x9.js → mindmap-definition-RKZ34NQL.BqipOHhY.js} +1 -1
  141. package/web/_astro/{pieDiagram-4H26LBE5.FlWA-_ru.js → pieDiagram-4H26LBE5.0nBiPqFG.js} +1 -1
  142. package/web/_astro/{quadrantDiagram-W4KKPZXB.9bVn35X7.js → quadrantDiagram-W4KKPZXB.Szj7Zpx5.js} +1 -1
  143. package/web/_astro/{requirementDiagram-4Y6WPE33.ChykIhct.js → requirementDiagram-4Y6WPE33.D4UR6TWo.js} +1 -1
  144. package/web/_astro/{sankeyDiagram-5OEKKPKP.Z2Y3hkYb.js → sankeyDiagram-5OEKKPKP.DAE7BdOs.js} +1 -1
  145. package/web/_astro/{sequenceDiagram-3UESZ5HK.J7JsHWDt.js → sequenceDiagram-3UESZ5HK.BP9ZVq25.js} +1 -1
  146. package/web/_astro/{stateDiagram-AJRCARHV.DAPbiCSS.js → stateDiagram-AJRCARHV.BQW7mIxk.js} +1 -1
  147. package/web/_astro/{stateDiagram-v2-BHNVJYJU.D2jvSi0q.js → stateDiagram-v2-BHNVJYJU.PbK4nDih.js} +1 -1
  148. package/web/_astro/{timeline-definition-PNZ67QCA.psW2ci29.js → timeline-definition-PNZ67QCA.Ci_GvUpo.js} +1 -1
  149. package/web/_astro/{vennDiagram-CIIHVFJN.DlILYVmg.js → vennDiagram-CIIHVFJN.DNw1Y5qx.js} +1 -1
  150. package/web/_astro/{wardleyDiagram-YWT4CUSO.xNWwLJnn.js → wardleyDiagram-YWT4CUSO.CwlkaaNe.js} +1 -1
  151. package/web/_astro/{xychartDiagram-2RQKCTM6.EU_BuH7V.js → xychartDiagram-2RQKCTM6.MI6ZuiH_.js} +1 -1
  152. package/web/index.html +2 -2
  153. package/web/_astro/channel.CUFdtWhm.js +0 -1
  154. package/web/_astro/index.Dg_3bOaJ.css +0 -1
@@ -2,19 +2,24 @@
2
2
  // @bun
3
3
 
4
4
  // plugins/sp/scripts/run-summary.ts
5
- import { readFileSync } from "fs";
5
+ import { readFileSync as readFileSync2 } from "fs";
6
6
 
7
7
  // plugins/sp/lib/env.ts
8
8
  function getEnvVars() {
9
9
  return process.env;
10
10
  }
11
11
 
12
+ // plugins/sp/lib/run-summary-core.ts
13
+ import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "node:fs";
14
+ import { join as join2 } from "node:path";
15
+
12
16
  // plugins/sp/lib/transcript.ts
13
17
  import { existsSync, readdirSync } from "node:fs";
14
18
  import { homedir } from "node:os";
15
19
  import { join } from "node:path";
16
20
  var SESSION_ID = /^[A-Za-z0-9_-]+$/;
17
- var OPERATOR_TOOLS = new Set(["AskUserQuestion"]);
21
+ var OPERATOR_TOOLS = new Set(["AskUserQuestion", "ask_user_question"]);
22
+ var PI_ROLES = new Set(["user", "assistant", "toolResult"]);
18
23
  var zeroTokens = () => ({ input: 0, cacheCreate: 0, cacheRead: 0, output: 0 });
19
24
  function formatDuration(ms) {
20
25
  const s = Math.round(ms / 1000);
@@ -35,6 +40,18 @@ function formatTokenSplit(t) {
35
40
  return `${formatTokens(nonCached + t.cacheRead)} / ${formatTokens(nonCached)}`;
36
41
  }
37
42
  function promptText(row) {
43
+ if (row.type === "message") {
44
+ if (row.message?.role !== "user")
45
+ return;
46
+ if (isInjectedPrompt(row))
47
+ return;
48
+ const content2 = row.message.content;
49
+ if (typeof content2 === "string")
50
+ return content2;
51
+ if (!Array.isArray(content2))
52
+ return;
53
+ return content2.find((b) => b.type === "text")?.text;
54
+ }
38
55
  if (row.type !== "user" || row.isMeta || row.isCompactSummary)
39
56
  return;
40
57
  const content = row.message?.content;
@@ -44,6 +61,13 @@ function promptText(row) {
44
61
  return;
45
62
  return content.find((b) => b.type === "text")?.text;
46
63
  }
64
+ function isInjectedPrompt(row) {
65
+ if (row.type !== "message" || row.message?.role !== "user")
66
+ return false;
67
+ const content = row.message.content;
68
+ const text = typeof content === "string" ? content : Array.isArray(content) ? content.find((b) => b.type === "text")?.text : undefined;
69
+ return typeof text === "string" && text.replace(/^\s+/, "").startsWith('<skill name="');
70
+ }
47
71
  function sumTokens(all) {
48
72
  const total = zeroTokens();
49
73
  for (const t of all)
@@ -64,10 +88,37 @@ var newAcc = (ts) => ({
64
88
  askMs: 0,
65
89
  asks: new Map,
66
90
  toolIds: new Set,
67
- messages: new Map
91
+ messages: new Map,
92
+ compactions: 0
68
93
  });
69
94
  function accumulate(acc, row, ts) {
70
95
  acc.last = Math.max(acc.last, ts);
96
+ if (row.type === "message") {
97
+ const m = row.message;
98
+ if (m?.role === "assistant") {
99
+ const blocks2 = Array.isArray(m.content) ? m.content : [];
100
+ for (const b of blocks2) {
101
+ if (b.type === "toolCall" && b.id) {
102
+ acc.toolIds.add(b.id);
103
+ if (b.name && OPERATOR_TOOLS.has(b.name))
104
+ acc.asks.set(b.id, ts);
105
+ }
106
+ }
107
+ const u2 = m.usage;
108
+ if (u2 && row.id)
109
+ acc.messages.set(row.id, {
110
+ input: u2.input ?? 0,
111
+ cacheCreate: u2.cacheWrite ?? 0,
112
+ cacheRead: u2.cacheRead ?? 0,
113
+ output: u2.output ?? 0
114
+ });
115
+ } else if (m?.role === "toolResult" && m.toolCallId && acc.asks.has(m.toolCallId)) {
116
+ acc.askMs += ts - (acc.asks.get(m.toolCallId) ?? ts);
117
+ }
118
+ return;
119
+ }
120
+ if (row.type === "compaction")
121
+ acc.compactions++;
71
122
  const blocks = Array.isArray(row.message?.content) ? row.message.content : [];
72
123
  for (const b of blocks) {
73
124
  if (b.type === "tool_use" && b.id) {
@@ -88,6 +139,18 @@ function accumulate(acc, row, ts) {
88
139
  });
89
140
  }
90
141
  }
142
+ function isCompaction(row) {
143
+ return row.type === "compaction" || row.type === "system" && row.subtype === "compact_boundary";
144
+ }
145
+ function sniffFormat(rows) {
146
+ for (const row of rows) {
147
+ if (row.type === "message" && PI_ROLES.has(row.message?.role ?? ""))
148
+ return "pi";
149
+ if (row.type === "user" || row.type === "assistant")
150
+ return "claude";
151
+ }
152
+ return "unknown";
153
+ }
91
154
  function parseRows(lines) {
92
155
  const rows = [];
93
156
  let skippedLines = 0;
@@ -105,14 +168,22 @@ function parseRows(lines) {
105
168
  if (!Number.isNaN(ts))
106
169
  rows.push([row, ts]);
107
170
  }
108
- return { rows, skippedLines };
171
+ return { rows, skippedLines, format: sniffFormat(rows.map(([r]) => r)) };
109
172
  }
110
173
  function resolveTranscript(env, projectsRoot = join(homedir(), ".claude", "projects"), override) {
111
174
  if (override)
112
175
  return existsSync(override) ? { ok: true, path: override } : { ok: false, reason: "no transcript" };
113
176
  const id = env.CLAUDE_CODE_SESSION_ID;
114
- if (!id)
115
- return { ok: false, reason: "no host session id (CLAUDE_CODE_SESSION_ID); pass --transcript <path>" };
177
+ if (!id) {
178
+ const piFile = env.PI_SESSION_FILE;
179
+ if (piFile !== undefined && piFile !== "") {
180
+ return existsSync(piFile) ? { ok: true, path: piFile } : { ok: false, reason: `PI_SESSION_FILE ${piFile} does not exist` };
181
+ }
182
+ return {
183
+ ok: false,
184
+ reason: "no host session id; pass --transcript <path> (pi: ~/.pi/agent/sessions/<cwd-slug>/<file>.jsonl, " + "or set PI_SESSION_FILE)"
185
+ };
186
+ }
116
187
  if (!SESSION_ID.test(id))
117
188
  return { ok: false, reason: "refusing a session id with path characters" };
118
189
  if (!existsSync(projectsRoot))
@@ -125,7 +196,151 @@ function resolveTranscript(env, projectsRoot = join(homedir(), ".claude", "proje
125
196
  return { ok: false, reason: `no transcript for session ${id}` };
126
197
  }
127
198
 
128
- // plugins/sp/scripts/run-summary.ts
199
+ // plugins/sp/lib/env.ts
200
+ function getEnvVars2() {
201
+ return process.env;
202
+ }
203
+
204
+ // plugins/sp/lib/run-summary-core.ts
205
+ var IDLE_GAP_MS = 300000;
206
+ var SHELL_TOOLS = new Set(["bash", "shell", "exec", "exec_command", "run_command", "command"]);
207
+ var SUBAGENT_TOOLS = new Set(["agent", "task", "invoke_subagent", "subagent", "dispatch_agent"]);
208
+ var OPERATOR_TOOLS2 = new Set(["askuserquestion", "ask_user_question"]);
209
+ var ACTOR_PRECEDENCE = ["operator", "subagent", "shell", "other"];
210
+ function actorOf(tool) {
211
+ const t = tool.toLowerCase().replace(/[^a-z_]/g, "");
212
+ if (OPERATOR_TOOLS2.has(t))
213
+ return "operator";
214
+ if (SUBAGENT_TOOLS.has(t))
215
+ return "subagent";
216
+ if (SHELL_TOOLS.has(t))
217
+ return "shell";
218
+ return "other";
219
+ }
220
+ function unionMs(spans, start, end) {
221
+ const clipped = spans.map(([a, b]) => [Math.max(a, start), Math.min(b, end)]).filter(([a, b]) => b > a).sort((x, y) => x[0] - y[0]);
222
+ let total = 0;
223
+ let cursor = Number.NaN;
224
+ let openUntil = Number.NaN;
225
+ for (const [a, b] of clipped) {
226
+ if (!Number.isFinite(cursor)) {
227
+ cursor = a;
228
+ openUntil = b;
229
+ continue;
230
+ }
231
+ if (a > openUntil) {
232
+ total += openUntil - cursor;
233
+ cursor = a;
234
+ openUntil = b;
235
+ } else {
236
+ openUntil = Math.max(openUntil, b);
237
+ }
238
+ }
239
+ if (Number.isFinite(cursor))
240
+ total += openUntil - cursor;
241
+ return total;
242
+ }
243
+ function subtract(spans, claimed) {
244
+ let free = spans;
245
+ for (const [ca, cb] of claimed) {
246
+ const next = [];
247
+ for (const [a, b] of free) {
248
+ if (cb <= a || ca >= b) {
249
+ next.push([a, b]);
250
+ continue;
251
+ }
252
+ if (ca > a)
253
+ next.push([a, ca]);
254
+ if (cb < b)
255
+ next.push([cb, b]);
256
+ }
257
+ free = next;
258
+ }
259
+ return free;
260
+ }
261
+ function actorSplit(rows, start, end) {
262
+ const empty = {
263
+ operatorMs: 0,
264
+ longestOperatorWaitMs: 0,
265
+ modelMs: 0,
266
+ shellMs: 0,
267
+ subagentMs: 0,
268
+ otherToolMs: 0,
269
+ idleMs: 0,
270
+ compactions: 0,
271
+ subagentErrors: 0
272
+ };
273
+ const window = end - start;
274
+ if (window <= 0)
275
+ return empty;
276
+ const calls = new Map;
277
+ const results = new Map;
278
+ const eventTs = [];
279
+ let compactions = 0;
280
+ for (const [row, ts] of rows) {
281
+ if (ts < start || ts > end)
282
+ continue;
283
+ eventTs.push(ts);
284
+ if (isCompaction(row))
285
+ compactions++;
286
+ const blocks = Array.isArray(row.message?.content) ? row.message?.content : [];
287
+ if (row.message?.role === "toolResult" && row.message.toolCallId !== undefined) {
288
+ results.set(row.message.toolCallId, { ts, isError: row.message.isError === true });
289
+ }
290
+ for (const b of blocks) {
291
+ if ((b.type === "tool_use" || b.type === "toolCall") && b.id !== undefined && b.name !== undefined) {
292
+ calls.set(b.id, { id: b.id, actor: actorOf(b.name), ts });
293
+ } else if (b.type === "tool_result" && b.tool_use_id !== undefined) {
294
+ results.set(b.tool_use_id, { ts, isError: b.is_error === true });
295
+ }
296
+ }
297
+ }
298
+ let subagentErrors = 0;
299
+ const resolved = [];
300
+ for (const call of calls.values()) {
301
+ const result = results.get(call.id);
302
+ if (result?.isError === true && call.actor === "subagent")
303
+ subagentErrors++;
304
+ resolved.push({ ...call, end: Math.min(result?.ts ?? end, end) });
305
+ }
306
+ const claimed = [];
307
+ const byActor = new Map;
308
+ for (const actor of ACTOR_PRECEDENCE) {
309
+ const mine = resolved.filter((r) => r.actor === actor).map((r) => [r.ts, r.end]);
310
+ const free = subtract(mine, claimed);
311
+ for (const span of free)
312
+ claimed.push(span);
313
+ const ms = unionMs(free, start, end);
314
+ byActor.set(actor, ms);
315
+ if (actor === "operator") {
316
+ empty.longestOperatorWaitMs = Math.max(0, ...mine.map(([a, b]) => b - a));
317
+ }
318
+ }
319
+ const boundaries = [...new Set([start, end, ...eventTs, ...claimed.flat()])].sort((a, b) => a - b);
320
+ const claimedSorted = [...claimed].sort((a, b) => a[0] - b[0]);
321
+ let idleMs = 0;
322
+ for (let i = 0;i < boundaries.length - 1; i++) {
323
+ const a = boundaries[i];
324
+ const b = boundaries[i + 1];
325
+ if (b - a < IDLE_GAP_MS)
326
+ continue;
327
+ if (claimedSorted.some(([ca, cb]) => ca < b && cb > a))
328
+ continue;
329
+ idleMs += b - a;
330
+ }
331
+ const accounted = (byActor.get("operator") ?? 0) + (byActor.get("subagent") ?? 0) + (byActor.get("shell") ?? 0) + (byActor.get("other") ?? 0) + idleMs;
332
+ return {
333
+ ...empty,
334
+ operatorMs: byActor.get("operator") ?? 0,
335
+ subagentMs: byActor.get("subagent") ?? 0,
336
+ shellMs: byActor.get("shell") ?? 0,
337
+ otherToolMs: byActor.get("other") ?? 0,
338
+ idleMs,
339
+ modelMs: Math.max(0, window - accounted),
340
+ compactions,
341
+ subagentErrors
342
+ };
343
+ }
129
344
  function attemptWindow(attempts) {
130
345
  const starts = attempts.map((a) => Date.parse(a.startedAt ?? "")).filter((n) => !Number.isNaN(n));
131
346
  const ends = attempts.map((a) => Date.parse(a.completedAt ?? "")).filter((n) => !Number.isNaN(n));
@@ -144,7 +359,7 @@ function summaryRow(stage, status, span, measured) {
144
359
  token: formatTokenSplit(measured ? span.tokens : null)
145
360
  };
146
361
  }
147
- function buildRunSummary(lines, inputs, since) {
362
+ function buildRunSummary(lines, inputs, since, until) {
148
363
  const windows = [];
149
364
  for (const { label, progress } of inputs) {
150
365
  const states = (progress.states ?? []).flatMap((s) => {
@@ -192,6 +407,9 @@ function buildRunSummary(lines, inputs, since) {
192
407
  if (ts >= sinceMs)
193
408
  totalEnd = Math.max(totalEnd, ts);
194
409
  }
410
+ const untilMs = until ? Date.parse(until) : Number.NaN;
411
+ if (!Number.isNaN(untilMs) && untilMs > totalStart)
412
+ totalEnd = untilMs;
195
413
  const stageRows = windows.map((w) => {
196
414
  const { span, measured } = measure(w.start, w.end);
197
415
  return summaryRow(w.stage, w.status, span, measured);
@@ -209,33 +427,195 @@ function buildRunSummary(lines, inputs, since) {
209
427
  };
210
428
  if (!overlaps && overhead.workMs > 0)
211
429
  stageRows.push(summaryRow("driver overhead", "", overhead, totalMeasure.measured));
212
- return { available: true, rows: stageRows, total, skippedLines };
430
+ return {
431
+ available: true,
432
+ rows: stageRows,
433
+ total,
434
+ actors: actorSplit(rows, totalStart, totalEnd),
435
+ gate: { ms: null, reason: "no matching check receipt" },
436
+ skippedLines
437
+ };
213
438
  }
214
439
  function renderSummaryMarkdown(summary) {
215
440
  const line = (r, bold = false) => {
216
441
  const stage = bold ? `**${r.stage}**` : r.stage;
217
442
  return `| ${stage} | ${r.status} | ${r.time} | ${r.wait} | ${r.toolCalls ?? "n/a"} | ${r.token} |`;
218
443
  };
444
+ const a = summary.actors;
445
+ const window = summary.total.workMs + summary.total.waitMs;
446
+ const share = (ms) => window > 0 ? `${Math.round(ms / window * 100)}%` : "n/a";
447
+ const gate = summary.gate.ms === null ? `n/a (${summary.gate.reason})` : formatDuration(summary.gate.ms);
219
448
  return [
220
449
  "| Stage | Status | Time | Wait | Tool calls | Token (total / non-cached) |",
221
450
  "| --- | --- | ---: | ---: | ---: | ---: |",
222
451
  ...summary.rows.map((r) => line(r)),
223
- line(summary.total, true)
452
+ line(summary.total, true),
453
+ "",
454
+ "| Actor | Time | Share |",
455
+ "| --- | ---: | ---: |",
456
+ `| operator wait (longest ${formatDuration(a.longestOperatorWaitMs)}) | ${formatDuration(a.operatorMs)} | ${share(a.operatorMs)} |`,
457
+ `| model | ${formatDuration(a.modelMs)} | ${share(a.modelMs)} |`,
458
+ `| shell | ${formatDuration(a.shellMs)} | ${share(a.shellMs)} |`,
459
+ `| of which gate (informational) | ${gate} | — |`,
460
+ `| subagent | ${formatDuration(a.subagentMs)} | ${share(a.subagentMs)} |`,
461
+ `| other tools | ${formatDuration(a.otherToolMs)} | ${share(a.otherToolMs)} |`,
462
+ `| idle / unattributed | ${formatDuration(a.idleMs)} | ${share(a.idleMs)} |`,
463
+ "",
464
+ `Compactions: ${a.compactions} · Subagent errors: ${a.subagentErrors}`
224
465
  ].join(`
225
466
  `);
226
467
  }
227
- var RUN_SUMMARY_USAGE = "usage: run-summary --progress [<label>=]<file> [--progress ...] [--transcript <path>] [--since <iso>] [--markdown]";
468
+ function renderRollupMarkdown(rows) {
469
+ const line = (label, r, actors, bold = false) => {
470
+ const name = bold ? `**${label}**` : label;
471
+ return `| ${name} | ${r.status} | ${r.time} | ${r.wait} | ${r.toolCalls ?? "n/a"} | ${r.token} | ${formatDuration(actors.operatorMs)} | ${formatDuration(actors.subagentMs)} |`;
472
+ };
473
+ const total = rows.reduce((acc, { summary }) => ({
474
+ workMs: acc.workMs + summary.total.workMs,
475
+ waitMs: acc.waitMs + summary.total.waitMs,
476
+ toolCalls: (acc.toolCalls ?? 0) + (summary.total.toolCalls ?? 0),
477
+ tokens: sumTokens([acc.tokens, summary.total.tokens ?? zeroTokens()])
478
+ }), { workMs: 0, waitMs: 0, toolCalls: 0, tokens: zeroTokens() });
479
+ const totalRow = {
480
+ stage: "Batch total",
481
+ status: "",
482
+ workMs: total.workMs,
483
+ waitMs: total.waitMs,
484
+ toolCalls: total.toolCalls,
485
+ tokens: total.tokens,
486
+ time: formatDuration(total.workMs),
487
+ wait: formatDuration(total.waitMs),
488
+ token: formatTokenSplit(total.tokens)
489
+ };
490
+ const totalActors = {
491
+ operatorMs: 0,
492
+ longestOperatorWaitMs: 0,
493
+ modelMs: 0,
494
+ shellMs: 0,
495
+ subagentMs: 0,
496
+ otherToolMs: 0,
497
+ idleMs: 0,
498
+ compactions: 0,
499
+ subagentErrors: 0
500
+ };
501
+ for (const { summary } of rows) {
502
+ totalActors.operatorMs += summary.actors.operatorMs;
503
+ totalActors.subagentMs += summary.actors.subagentMs;
504
+ totalActors.compactions += summary.actors.compactions;
505
+ totalActors.subagentErrors += summary.actors.subagentErrors;
506
+ }
507
+ const out = [
508
+ "| Run | Status | Time | Wait | Tool calls | Token (total / non-cached) | Operator wait | Subagent |",
509
+ "| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |",
510
+ ...rows.map(({ label, summary }) => line(label, summary.total, summary.actors)),
511
+ line("Batch total", totalRow, totalActors, true)
512
+ ];
513
+ const starts = rows.map((r) => Date.parse(r.summary.startedAt ?? "")).filter((n) => !Number.isNaN(n));
514
+ const ends = rows.map((r) => Date.parse(r.summary.completedAt ?? "")).filter((n) => !Number.isNaN(n));
515
+ const overlaps = starts.length === rows.length && rows.some((r, i) => rows.some((o, j) => i !== j && Date.parse(r.summary.startedAt ?? "") < Date.parse(o.summary.completedAt ?? "") && Date.parse(o.summary.startedAt ?? "") < Date.parse(r.summary.completedAt ?? "")));
516
+ if (overlaps && starts.length > 0 && ends.length > 0) {
517
+ out.push("", `Wall span: ${new Date(Math.min(...starts)).toISOString()} → ${new Date(Math.max(...ends)).toISOString()}`);
518
+ }
519
+ return out.join(`
520
+ `);
521
+ }
522
+ function writeCloseSummary(run, opts = {}) {
523
+ const cwd = opts.cwd ?? process.cwd();
524
+ const runDir = join2(cwd, ".spur", "run");
525
+ const rel = join2(".spur", "run", `${run.runId}-summary.md`);
526
+ const abs = join2(cwd, rel);
527
+ const na = (reason) => {
528
+ mkdirSync(runDir, { recursive: true });
529
+ writeFileSync(abs, `Execution summary: n/a (${reason})
530
+ `);
531
+ return rel;
532
+ };
533
+ try {
534
+ if (run.startedAt === undefined)
535
+ return na("run row has no started_at");
536
+ const resolved = resolveTranscript(opts.env ?? getEnvVars2(), opts.projectsRoot);
537
+ if (!resolved.ok)
538
+ return na(resolved.reason);
539
+ const lines = readFileSync(resolved.path, "utf8").split(`
540
+ `);
541
+ const summary = buildRunSummary(lines, [{ progress: run.progress }], run.startedAt, run.completedAt);
542
+ if (summary.rows.length === 0)
543
+ return na("no timed attempts");
544
+ mkdirSync(runDir, { recursive: true });
545
+ writeFileSync(abs, `${renderSummaryMarkdown(summary)}
546
+ `);
547
+ writeFileSync(join2(runDir, `${run.runId}-summary.json`), JSON.stringify({ ...summary, runId: run.runId, startedAt: run.startedAt, completedAt: run.completedAt }, null, 2));
548
+ return rel;
549
+ } catch (error) {
550
+ return na(error instanceof Error ? error.message : String(error));
551
+ }
552
+ }
553
+ function gateFromReceipt(cwd, runId) {
554
+ const path = join2(cwd, ".spur", "run", `${runId}-check-receipt.json`);
555
+ if (!existsSync2(path))
556
+ return { ms: null, reason: "no matching check receipt" };
557
+ try {
558
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
559
+ if (parsed.runId !== runId)
560
+ return { ms: null, reason: "check receipt belongs to another run" };
561
+ const ms = parsed.gateRuntimeMs;
562
+ if (typeof ms !== "number" || !Number.isFinite(ms))
563
+ return { ms: null, reason: "check receipt has no gate time" };
564
+ return { ms };
565
+ } catch {
566
+ return { ms: null, reason: "check receipt is unreadable" };
567
+ }
568
+ }
569
+
570
+ // plugins/sp/lib/transcript.ts
571
+ import { existsSync as existsSync3, readdirSync as readdirSync2 } from "node:fs";
572
+ import { homedir as homedir2 } from "node:os";
573
+ import { join as join3 } from "node:path";
574
+ var SESSION_ID2 = /^[A-Za-z0-9_-]+$/;
575
+ var OPERATOR_TOOLS3 = new Set(["AskUserQuestion", "ask_user_question"]);
576
+ var PI_ROLES2 = new Set(["user", "assistant", "toolResult"]);
577
+ function resolveTranscript2(env, projectsRoot = join3(homedir2(), ".claude", "projects"), override) {
578
+ if (override)
579
+ return existsSync3(override) ? { ok: true, path: override } : { ok: false, reason: "no transcript" };
580
+ const id = env.CLAUDE_CODE_SESSION_ID;
581
+ if (!id) {
582
+ const piFile = env.PI_SESSION_FILE;
583
+ if (piFile !== undefined && piFile !== "") {
584
+ return existsSync3(piFile) ? { ok: true, path: piFile } : { ok: false, reason: `PI_SESSION_FILE ${piFile} does not exist` };
585
+ }
586
+ return {
587
+ ok: false,
588
+ reason: "no host session id; pass --transcript <path> (pi: ~/.pi/agent/sessions/<cwd-slug>/<file>.jsonl, " + "or set PI_SESSION_FILE)"
589
+ };
590
+ }
591
+ if (!SESSION_ID2.test(id))
592
+ return { ok: false, reason: "refusing a session id with path characters" };
593
+ if (!existsSync3(projectsRoot))
594
+ return { ok: false, reason: `no transcript root ${projectsRoot}` };
595
+ for (const dir of readdirSync2(projectsRoot)) {
596
+ const path = join3(projectsRoot, dir, `${id}.jsonl`);
597
+ if (existsSync3(path))
598
+ return { ok: true, path };
599
+ }
600
+ return { ok: false, reason: `no transcript for session ${id}` };
601
+ }
602
+
603
+ // plugins/sp/scripts/run-summary.ts
604
+ var RUN_SUMMARY_USAGE = "usage: run-summary (--progress [<label>=]<file> [--progress ...] | --rollup [<label>=]<summary.json> [--rollup ...]) [--transcript <path>] [--since <iso>] [--markdown]";
228
605
  function main(argv, env = getEnvVars(), write = (s) => process.stdout.write(s), projectsRoot) {
229
606
  let transcript;
230
607
  let since;
231
608
  let markdown = false;
232
609
  const progressArgs = [];
610
+ const rollupArgs = [];
233
611
  for (let i = 0;i < argv.length; i++) {
234
612
  const arg = argv[i];
235
613
  if (arg === "--transcript" && argv[i + 1])
236
614
  transcript = argv[++i];
237
615
  else if (arg === "--progress" && argv[i + 1])
238
616
  progressArgs.push(argv[++i]);
617
+ else if (arg === "--rollup" && argv[i + 1])
618
+ rollupArgs.push(argv[++i]);
239
619
  else if (arg === "--since" && argv[i + 1])
240
620
  since = argv[++i];
241
621
  else if (arg === "--markdown")
@@ -244,6 +624,28 @@ function main(argv, env = getEnvVars(), write = (s) => process.stdout.write(s),
244
624
  i++;
245
625
  else {
246
626
  process.stderr.write(`${RUN_SUMMARY_USAGE}
627
+ `);
628
+ return 2;
629
+ }
630
+ }
631
+ if (rollupArgs.length > 0 && (progressArgs.length > 0 || since !== undefined || transcript !== undefined)) {
632
+ process.stderr.write(`${RUN_SUMMARY_USAGE}
633
+ `);
634
+ return 2;
635
+ }
636
+ if (rollupArgs.length > 0) {
637
+ try {
638
+ const inputs = rollupArgs.map((arg) => {
639
+ const eq = arg.indexOf("=");
640
+ const [label, file] = eq > 0 ? [arg.slice(0, eq), arg.slice(eq + 1)] : [undefined, arg];
641
+ const summary = JSON.parse(readFileSync2(file, "utf8"));
642
+ return { label: label ?? summary.runId ?? file, summary };
643
+ });
644
+ write(`${renderRollupMarkdown(inputs)}
645
+ `);
646
+ return 0;
647
+ } catch (error) {
648
+ process.stderr.write(`run-summary: ${error.message}
247
649
  `);
248
650
  return 2;
249
651
  }
@@ -253,7 +655,7 @@ function main(argv, env = getEnvVars(), write = (s) => process.stdout.write(s),
253
655
  `);
254
656
  return 2;
255
657
  }
256
- const resolved = resolveTranscript(env, projectsRoot, transcript);
658
+ const resolved = resolveTranscript2(env, projectsRoot, transcript);
257
659
  if (!resolved.ok) {
258
660
  const out = markdown ? `Execution summary: n/a (${resolved.reason})` : JSON.stringify({ available: false, reason: resolved.reason });
259
661
  write(`${out}
@@ -264,10 +666,11 @@ function main(argv, env = getEnvVars(), write = (s) => process.stdout.write(s),
264
666
  const inputs = progressArgs.map((arg) => {
265
667
  const eq = arg.indexOf("=");
266
668
  const [label, file] = eq > 0 ? [arg.slice(0, eq), arg.slice(eq + 1)] : [undefined, arg];
267
- return { label, progress: JSON.parse(readFileSync(file, "utf8")) };
669
+ return { label, progress: JSON.parse(readFileSync2(file, "utf8")) };
268
670
  });
269
- const summary = buildRunSummary(readFileSync(resolved.path, "utf8").split(`
671
+ const summary = buildRunSummary(readFileSync2(resolved.path, "utf8").split(`
270
672
  `), inputs, since);
673
+ summary.gate = gateFromReceipt(process.cwd(), inputs[0]?.progress.runId ?? "");
271
674
  write(`${markdown ? renderSummaryMarkdown(summary) : JSON.stringify({ ...summary, transcript: resolved.path })}
272
675
  `);
273
676
  return 0;
@@ -279,8 +682,12 @@ function main(argv, env = getEnvVars(), write = (s) => process.stdout.write(s),
279
682
  }
280
683
  process.exit(main(process.argv.slice(2)));
281
684
  export {
685
+ writeCloseSummary,
282
686
  renderSummaryMarkdown,
687
+ renderRollupMarkdown,
283
688
  main,
689
+ gateFromReceipt,
284
690
  buildRunSummary,
691
+ actorSplit,
285
692
  RUN_SUMMARY_USAGE
286
693
  };