@gobing-ai/spur 0.4.0 → 0.4.2

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