@gobing-ai/spur 0.4.7 → 0.4.9

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 (145) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/pipeline-budgets.json +2 -2
  3. package/config/rules/boundary/config-loading-ownership.yaml +1 -0
  4. package/config/rules/boundary/sp-plugin-standalone.yaml +1 -1
  5. package/config/rules/boundary/sp-script-placement.yaml +1 -1
  6. package/config/rules/docs/sourced-api-claims.yaml +45 -7
  7. package/config/rules/strict/runtime-boundaries.yaml +40 -1
  8. package/config/rules/structure/protected-files.yaml +1 -1
  9. package/config/rules/surface/check-cli-surface.yaml +33 -0
  10. package/config/rules/typescript/no-debugger.yaml +3 -0
  11. package/config/workflow-candidates.json +2 -2
  12. package/config/workflows/feature-verification.yaml +6 -11
  13. package/config/workflows/history-anatomy.yaml +14 -28
  14. package/config/workflows/idea-pipeline.yaml +6 -19
  15. package/config/workflows/pr-review.yaml +9 -16
  16. package/config/workflows/task-pipeline.yaml +16 -23
  17. package/config/workflows/wrapup-pipeline.yaml +21 -63
  18. package/package.json +3 -3
  19. package/plugins/sp/README.md +70 -37
  20. package/plugins/sp/agents/super-planner.md +5 -5
  21. package/plugins/sp/agents/super-reviewer.md +2 -2
  22. package/plugins/sp/commands/dev-changelog.md +1 -1
  23. package/plugins/sp/commands/dev-daily.md +1 -1
  24. package/plugins/sp/commands/dev-pr-review.md +1 -1
  25. package/plugins/sp/commands/dev-review-session.md +1 -1
  26. package/plugins/sp/commands/dev-runall.md +4 -4
  27. package/plugins/sp/commands/dev-verify.md +4 -6
  28. package/plugins/sp/commands/dev-verifyall.md +4 -5
  29. package/plugins/sp/hooks/hooks.json +11 -1
  30. package/plugins/sp/lib/context-post-tool.ts +1 -1
  31. package/plugins/sp/lib/context-session-start.ts +1 -1
  32. package/plugins/sp/lib/context-session-stop.ts +1 -1
  33. package/plugins/sp/lib/destructive-policy.ts +65 -1
  34. package/plugins/sp/plugin.json +2 -5
  35. package/plugins/sp/{hooks → scripts/hooks}/agent-lifecycle.ts +1 -1
  36. package/plugins/sp/scripts/hooks/careful-guard.ts +94 -0
  37. package/plugins/sp/{hooks → scripts/hooks}/context-post-tool.ts +2 -2
  38. package/plugins/sp/{hooks → scripts/hooks}/context-session-start.ts +1 -1
  39. package/plugins/sp/{hooks → scripts/hooks}/context-session-stop.ts +1 -1
  40. package/plugins/sp/{hooks → scripts/hooks}/fleet-guest-stop.ts +6 -4
  41. package/plugins/sp/{hooks → scripts/hooks}/task-write-guard.ts +38 -21
  42. package/plugins/sp/skills/code-implementation/SKILL.md +1 -1
  43. package/plugins/sp/skills/code-verification/SKILL.md +8 -8
  44. package/plugins/sp/skills/code-verification/references/secu-review.md +4 -3
  45. package/plugins/sp/skills/code-verification/references/verdict-schema.md +9 -1
  46. package/plugins/sp/skills/daily-summary/SKILL.md +4 -5
  47. package/plugins/sp/skills/dogfood-testing/SKILL.md +9 -9
  48. package/plugins/sp/skills/dogfood-testing/references/report-template.md +1 -1
  49. package/plugins/sp/skills/history-anatomy/references/modes.md +2 -2
  50. package/plugins/sp/skills/next-router/references/routing-table.md +1 -1
  51. package/plugins/sp/skills/parallel-execution/SKILL.md +1 -1
  52. package/plugins/sp/skills/pr-reviewing/SKILL.md +14 -15
  53. package/plugins/sp/skills/session-review/SKILL.md +3 -3
  54. package/plugins/sp/skills/source-driven-development/SKILL.md +7 -4
  55. package/plugins/sp/skills/spur-check/SKILL.md +14 -13
  56. package/plugins/sp/skills/spur-cli/SKILL.md +1 -1
  57. package/plugins/sp/skills/spur-cli/references/agent.md +12 -0
  58. package/plugins/sp/skills/spur-cli/references/builder.md +27 -6
  59. package/plugins/sp/skills/spur-cli/references/features.md +6 -1
  60. package/plugins/sp/skills/spur-cli/references/history.md +3 -0
  61. package/plugins/sp/skills/spur-cli/references/self.md +5 -3
  62. package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +3 -0
  63. package/plugins/sp/skills/spur-cli/references/tasks.md +13 -3
  64. package/plugins/sp/skills/spur-cli/references/workflows.md +36 -1
  65. package/plugins/sp/skills/spur-dev/SKILL.md +1 -1
  66. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +1 -1
  67. package/plugins/sp/skills/spur-dev/references/dev-operations.md +12 -12
  68. package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +10 -11
  69. package/plugins/sp/skills/spur-dev/references/execution-batch.md +16 -17
  70. package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +1 -1
  71. package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +8 -11
  72. package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +6 -8
  73. package/plugins/sp/skills/spur-dev/references/gate-checklists.md +1 -1
  74. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +33 -40
  75. package/plugins/sp/skills/spur-dev/references/structured-trace-emission.md +24 -24
  76. package/plugins/sp/skills/spur-doctor/SKILL.md +5 -4
  77. package/spur.js +41901 -30598
  78. package/config/plugin-scripts.json +0 -116
  79. package/config/script-placement-baseline.json +0 -33
  80. package/plugins/sp/hooks/careful-guard.ts +0 -74
  81. package/plugins/sp/hooks/pi/guard-extension.ts +0 -304
  82. package/plugins/sp/lib/dogfood-pipeline.ts +0 -265
  83. package/plugins/sp/lib/dogfood-report.ts +0 -278
  84. package/plugins/sp/lib/history-anatomy.generated.d.mts +0 -112
  85. package/plugins/sp/lib/history-anatomy.generated.mjs +0 -691
  86. package/plugins/sp/lib/idea-handoff.generated.d.mts +0 -17
  87. package/plugins/sp/lib/idea-handoff.generated.mjs +0 -1446
  88. package/plugins/sp/lib/inline-run-app.ts +0 -68
  89. package/plugins/sp/lib/inline-run.generated.d.mts +0 -37
  90. package/plugins/sp/lib/inline-run.generated.mjs +0 -3103
  91. package/plugins/sp/lib/plan-projection.generated.d.mts +0 -197
  92. package/plugins/sp/lib/plan-projection.generated.mjs +0 -982
  93. package/plugins/sp/lib/quality-gate.generated.d.mts +0 -139
  94. package/plugins/sp/lib/quality-gate.generated.mjs +0 -30510
  95. package/plugins/sp/lib/residual-scan.generated.d.mts +0 -93
  96. package/plugins/sp/lib/residual-scan.generated.mjs +0 -383
  97. package/plugins/sp/lib/run-summary-core.ts +0 -225
  98. package/plugins/sp/lib/spur-bin.ts +0 -36
  99. package/plugins/sp/lib/step-profile.generated.d.mts +0 -71
  100. package/plugins/sp/lib/step-profile.generated.mjs +0 -174
  101. package/plugins/sp/lib/transcript.ts +0 -112
  102. package/plugins/sp/scripts/batch-plan.mjs +0 -1096
  103. package/plugins/sp/scripts/batch-plan.ts +0 -145
  104. package/plugins/sp/scripts/batch-preflight.mjs +0 -423
  105. package/plugins/sp/scripts/batch-preflight.ts +0 -270
  106. package/plugins/sp/scripts/commit-guard.mjs +0 -226
  107. package/plugins/sp/scripts/commit-guard.ts +0 -248
  108. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +0 -626
  109. package/plugins/sp/scripts/daily-summary/daily-summary.ts +0 -855
  110. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +0 -211
  111. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +0 -110
  112. package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +0 -282
  113. package/plugins/sp/scripts/dogfood-testing/validate-report.ts +0 -98
  114. package/plugins/sp/scripts/feature-verification-steps.mjs +0 -82
  115. package/plugins/sp/scripts/feature-verification-steps.ts +0 -164
  116. package/plugins/sp/scripts/history-anatomy-cache.mjs +0 -931
  117. package/plugins/sp/scripts/history-anatomy-cache.ts +0 -247
  118. package/plugins/sp/scripts/idea-handoff.mjs +0 -32
  119. package/plugins/sp/scripts/idea-handoff.ts +0 -47
  120. package/plugins/sp/scripts/inline-run-dispatch.mjs +0 -131
  121. package/plugins/sp/scripts/inline-run-dispatch.ts +0 -115
  122. package/plugins/sp/scripts/inline-run-setup.mjs +0 -746
  123. package/plugins/sp/scripts/inline-run-setup.ts +0 -198
  124. package/plugins/sp/scripts/persist-out-check.mjs +0 -217
  125. package/plugins/sp/scripts/persist-out-check.ts +0 -249
  126. package/plugins/sp/scripts/pr-reviewing.mjs +0 -712
  127. package/plugins/sp/scripts/pr-reviewing.ts +0 -952
  128. package/plugins/sp/scripts/quality-gate.mjs +0 -30491
  129. package/plugins/sp/scripts/quality-gate.ts +0 -80
  130. package/plugins/sp/scripts/residual-scan.mjs +0 -634
  131. package/plugins/sp/scripts/residual-scan.ts +0 -248
  132. package/plugins/sp/scripts/run-summary.mjs +0 -758
  133. package/plugins/sp/scripts/run-summary.ts +0 -107
  134. package/plugins/sp/scripts/script-root.mjs +0 -129
  135. package/plugins/sp/scripts/script-root.ts +0 -199
  136. package/plugins/sp/scripts/session-timeline.mjs +0 -359
  137. package/plugins/sp/scripts/session-timeline.ts +0 -190
  138. package/plugins/sp/scripts/task-diffstat.mjs +0 -181
  139. package/plugins/sp/scripts/task-diffstat.ts +0 -219
  140. package/plugins/sp/scripts/workflow-step-profile.mjs +0 -338
  141. package/plugins/sp/scripts/workflow-step-profile.ts +0 -165
  142. package/plugins/sp/scripts/wrapup-drift-probe.mjs +0 -190
  143. package/plugins/sp/scripts/wrapup-drift-probe.ts +0 -261
  144. package/plugins/sp/scripts/wrapup-steps.mjs +0 -627
  145. package/plugins/sp/scripts/wrapup-steps.ts +0 -891
@@ -1,758 +0,0 @@
1
- #!/usr/bin/env node
2
- // @bun
3
-
4
- // plugins/sp/scripts/run-summary.ts
5
- import { readFileSync as readFileSync2 } from "fs";
6
-
7
- // plugins/sp/lib/env.ts
8
- function getEnvVars() {
9
- return process.env;
10
- }
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
-
16
- // plugins/sp/lib/env.ts
17
- function getEnvVars2() {
18
- return process.env;
19
- }
20
-
21
- // plugins/sp/lib/plan-projection.generated.mjs
22
- var NON_ABORTING_PREFLIGHT_CODES = new Set([
23
- "L4.scenario-unverified",
24
- "L4.verifying-incomplete-tasks"
25
- ]);
26
- var zeroTokens = () => ({ input: 0, cacheCreate: 0, cacheRead: 0, output: 0 });
27
- function formatDuration(ms) {
28
- const s = Math.round(ms / 1000);
29
- const h = Math.floor(s / 3600);
30
- const m = Math.floor(s % 3600 / 60);
31
- const ss = String(s % 60).padStart(2, "0");
32
- return h > 0 ? `${h}:${String(m).padStart(2, "0")}:${ss}` : `${m}:${ss}`;
33
- }
34
- function formatTokens(n) {
35
- if (n >= 999500)
36
- return `${(n / 1e6).toFixed(1)}M`;
37
- return n >= 1000 ? `${Math.round(n / 1000)}k` : String(n);
38
- }
39
- function formatTokenSplit(t) {
40
- if (!t)
41
- return "n/a";
42
- const nonCached = t.input + t.cacheCreate + t.output;
43
- return `${formatTokens(nonCached + t.cacheRead)} / ${formatTokens(nonCached)}`;
44
- }
45
- var OPERATOR_TOOLS = new Set(["AskUserQuestion", "ask_user_question"]);
46
- function isCompaction(row) {
47
- return row.type === "compaction" || row.type === "system" && row.subtype === "compact_boundary";
48
- }
49
- function firstTextContent(row) {
50
- const content = row.message?.content;
51
- if (typeof content === "string")
52
- return content;
53
- if (!Array.isArray(content))
54
- return;
55
- return content.find((b) => b.type === "text")?.text;
56
- }
57
- function skillInvocationPrompt(text) {
58
- const name = /^\s*<skill name="([^"]+)"/.exec(text)?.[1];
59
- if (name === undefined)
60
- return;
61
- const close = text.lastIndexOf("</skill>");
62
- const args = close < 0 ? "" : text.slice(close + "</skill>".length).trim();
63
- return args === "" ? `/${name}` : `/${name} ${args}`;
64
- }
65
- function promptText(row) {
66
- if (row.type === "custom_message") {
67
- if (row.display === false)
68
- return;
69
- const text = typeof row.content === "string" ? row.content.trim() : "";
70
- return text === "" ? undefined : text;
71
- }
72
- if (row.type === "message") {
73
- if (row.message?.role !== "user")
74
- return;
75
- const text = firstTextContent(row);
76
- if (text === undefined)
77
- return;
78
- return skillInvocationPrompt(text) ?? text;
79
- }
80
- if (row.type !== "user" || row.isMeta || row.isCompactSummary)
81
- return;
82
- const content = row.message?.content;
83
- if (typeof content === "string")
84
- return content;
85
- if (!Array.isArray(content) || content.some((b) => b.type === "tool_result"))
86
- return;
87
- return content.find((b) => b.type === "text")?.text;
88
- }
89
- function sumTokens(all) {
90
- const total = zeroTokens();
91
- for (const t of all)
92
- for (const k of Object.keys(total))
93
- total[k] += t[k];
94
- return total;
95
- }
96
- function sumSpans(spans) {
97
- return {
98
- workMs: spans.reduce((n, s) => n + s.workMs, 0),
99
- waitMs: spans.reduce((n, s) => n + s.waitMs, 0),
100
- toolCalls: spans.reduce((n, s) => n + s.toolCalls, 0),
101
- tokens: sumTokens(spans.map((s) => s.tokens))
102
- };
103
- }
104
- var newAcc = (ts) => ({
105
- last: ts,
106
- askMs: 0,
107
- asks: new Map,
108
- toolIds: new Set,
109
- messages: new Map,
110
- compactions: 0
111
- });
112
- function accumulate(acc, row, ts) {
113
- acc.last = Math.max(acc.last, ts);
114
- if (row.type === "message") {
115
- const m = row.message;
116
- if (m?.role === "assistant") {
117
- const blocks = Array.isArray(m.content) ? m.content : [];
118
- for (const b of blocks) {
119
- if (b.type === "toolCall" && b.id) {
120
- acc.toolIds.add(b.id);
121
- if (b.name && OPERATOR_TOOLS.has(b.name))
122
- acc.asks.set(b.id, ts);
123
- }
124
- }
125
- const u = m.usage;
126
- if (u && row.id)
127
- acc.messages.set(row.id, {
128
- input: u.input ?? 0,
129
- cacheCreate: u.cacheWrite ?? 0,
130
- cacheRead: u.cacheRead ?? 0,
131
- output: u.output ?? 0
132
- });
133
- } else if (m?.role === "toolResult" && m.toolCallId && acc.asks.has(m.toolCallId)) {
134
- acc.askMs += ts - (acc.asks.get(m.toolCallId) ?? ts);
135
- }
136
- return;
137
- }
138
- if (row.type === "compaction")
139
- acc.compactions++;
140
- const blocks = Array.isArray(row.message?.content) ? row.message.content : [];
141
- for (const b of blocks) {
142
- if (b.type === "tool_use" && b.id) {
143
- acc.toolIds.add(b.id);
144
- if (b.name && OPERATOR_TOOLS.has(b.name))
145
- acc.asks.set(b.id, ts);
146
- } else if (b.type === "tool_result" && b.tool_use_id && acc.asks.has(b.tool_use_id)) {
147
- acc.askMs += ts - (acc.asks.get(b.tool_use_id) ?? ts);
148
- }
149
- }
150
- const u = row.message?.usage;
151
- if (row.type === "assistant" && u && row.message?.id) {
152
- acc.messages.set(row.message.id, {
153
- input: u.input_tokens ?? 0,
154
- cacheCreate: u.cache_creation_input_tokens ?? 0,
155
- cacheRead: u.cache_read_input_tokens ?? 0,
156
- output: u.output_tokens ?? 0
157
- });
158
- }
159
- }
160
- var IDLE_GAP_MS = 300000;
161
- var SHELL_TOOLS = new Set(["bash", "shell", "exec", "exec_command", "run_command", "command"]);
162
- var SUBAGENT_TOOLS = new Set(["agent", "task", "invoke_subagent", "subagent", "dispatch_agent"]);
163
- var LOWERCASE_OPERATOR_TOOLS = new Set(["askuserquestion", "ask_user_question"]);
164
- var ACTOR_PRECEDENCE = ["operator", "subagent", "shell", "other"];
165
- function actorOf(tool) {
166
- const t = tool.toLowerCase().replace(/[^a-z_]/g, "");
167
- if (LOWERCASE_OPERATOR_TOOLS.has(t))
168
- return "operator";
169
- if (SUBAGENT_TOOLS.has(t))
170
- return "subagent";
171
- if (SHELL_TOOLS.has(t))
172
- return "shell";
173
- return "other";
174
- }
175
- function unionMs(spans, start, end) {
176
- 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]);
177
- let total = 0;
178
- let cursor = Number.NaN;
179
- let openUntil = Number.NaN;
180
- for (const [a, b] of clipped) {
181
- if (!Number.isFinite(cursor)) {
182
- cursor = a;
183
- openUntil = b;
184
- continue;
185
- }
186
- if (a > openUntil) {
187
- total += openUntil - cursor;
188
- cursor = a;
189
- openUntil = b;
190
- } else {
191
- openUntil = Math.max(openUntil, b);
192
- }
193
- }
194
- if (Number.isFinite(cursor))
195
- total += openUntil - cursor;
196
- return total;
197
- }
198
- function subtract(spans, claimed) {
199
- let free = spans;
200
- for (const [ca, cb] of claimed) {
201
- const next = [];
202
- for (const [a, b] of free) {
203
- if (cb <= a || ca >= b) {
204
- next.push([a, b]);
205
- continue;
206
- }
207
- if (ca > a)
208
- next.push([a, ca]);
209
- if (cb < b)
210
- next.push([cb, b]);
211
- }
212
- free = next;
213
- }
214
- return free;
215
- }
216
- function actorSplit(rows, start, end) {
217
- const empty = {
218
- operatorMs: 0,
219
- longestOperatorWaitMs: 0,
220
- modelMs: 0,
221
- shellMs: 0,
222
- subagentMs: 0,
223
- otherToolMs: 0,
224
- idleMs: 0,
225
- compactions: 0,
226
- subagentErrors: 0
227
- };
228
- const window = end - start;
229
- if (window <= 0)
230
- return empty;
231
- const calls = new Map;
232
- const results = new Map;
233
- const eventTs = [];
234
- let compactions = 0;
235
- for (const [row, ts] of rows) {
236
- if (ts < start || ts > end)
237
- continue;
238
- eventTs.push(ts);
239
- if (isCompaction(row))
240
- compactions++;
241
- const blocks = Array.isArray(row.message?.content) ? row.message?.content : [];
242
- if (row.message?.role === "toolResult" && row.message.toolCallId !== undefined) {
243
- results.set(row.message.toolCallId, { ts, isError: row.message.isError === true });
244
- }
245
- for (const b of blocks) {
246
- if ((b.type === "tool_use" || b.type === "toolCall") && b.id !== undefined && b.name !== undefined) {
247
- calls.set(b.id, { id: b.id, actor: actorOf(b.name), ts });
248
- } else if (b.type === "tool_result" && b.tool_use_id !== undefined) {
249
- results.set(b.tool_use_id, { ts, isError: b.is_error === true });
250
- }
251
- }
252
- }
253
- let subagentErrors = 0;
254
- const resolved = [];
255
- for (const call of calls.values()) {
256
- const result = results.get(call.id);
257
- if (result?.isError === true && call.actor === "subagent")
258
- subagentErrors++;
259
- resolved.push({ ...call, end: Math.min(result?.ts ?? end, end) });
260
- }
261
- const claimed = [];
262
- const byActor = new Map;
263
- for (const actor of ACTOR_PRECEDENCE) {
264
- const mine = resolved.filter((r) => r.actor === actor).map((r) => [r.ts, r.end]);
265
- const free = subtract(mine, claimed);
266
- for (const span of free)
267
- claimed.push(span);
268
- const ms = unionMs(free, start, end);
269
- byActor.set(actor, ms);
270
- if (actor === "operator") {
271
- empty.longestOperatorWaitMs = Math.max(0, ...mine.map(([a, b]) => b - a));
272
- }
273
- }
274
- const boundaries = [...new Set([start, end, ...eventTs, ...claimed.flat()])].sort((a, b) => a - b);
275
- const claimedSorted = [...claimed].sort((a, b) => a[0] - b[0]);
276
- let idleMs = 0;
277
- for (let i = 0;i < boundaries.length - 1; i++) {
278
- const a = boundaries[i];
279
- const b = boundaries[i + 1];
280
- if (b - a < IDLE_GAP_MS)
281
- continue;
282
- if (claimedSorted.some(([ca, cb]) => ca < b && cb > a))
283
- continue;
284
- idleMs += b - a;
285
- }
286
- const accounted = (byActor.get("operator") ?? 0) + (byActor.get("subagent") ?? 0) + (byActor.get("shell") ?? 0) + (byActor.get("other") ?? 0) + idleMs;
287
- return {
288
- ...empty,
289
- operatorMs: byActor.get("operator") ?? 0,
290
- subagentMs: byActor.get("subagent") ?? 0,
291
- shellMs: byActor.get("shell") ?? 0,
292
- otherToolMs: byActor.get("other") ?? 0,
293
- idleMs,
294
- modelMs: Math.max(0, window - accounted),
295
- compactions,
296
- subagentErrors
297
- };
298
- }
299
- function attemptWindow(attempts) {
300
- const starts = attempts.map((a) => Date.parse(a.startedAt ?? "")).filter((n) => !Number.isNaN(n));
301
- const ends = attempts.map((a) => Date.parse(a.completedAt ?? "")).filter((n) => !Number.isNaN(n));
302
- return starts.length && ends.length ? [Math.min(...starts), Math.max(...ends)] : undefined;
303
- }
304
- function summaryRow(stage, status, span, measured) {
305
- return {
306
- stage,
307
- status,
308
- workMs: span.workMs,
309
- waitMs: span.waitMs,
310
- toolCalls: measured ? span.toolCalls : null,
311
- tokens: measured ? span.tokens : null,
312
- time: formatDuration(span.workMs),
313
- wait: formatDuration(span.waitMs),
314
- token: formatTokenSplit(measured ? span.tokens : null)
315
- };
316
- }
317
- function buildRunSummary(rows, inputs, opts = {}) {
318
- const { since, until } = opts;
319
- const windows = [];
320
- for (const { label, progress } of inputs) {
321
- const states = (progress.states ?? []).flatMap((s) => {
322
- const w = attemptWindow((s.actions ?? []).flatMap((a) => a.attempts ?? []));
323
- return w ? [{ s, w }] : [];
324
- });
325
- if (!states.length)
326
- throw new Error(`progress for run ${progress.runId} has no timed attempts`);
327
- if (inputs.length === 1) {
328
- for (const { s, w } of states) {
329
- const stage = (s.visit ?? 1) > 1 ? `${s.state} (visit ${s.visit})` : s.state;
330
- windows.push({ stage, status: s.status ?? "", start: w[0], end: w[1] });
331
- }
332
- } else {
333
- const stage = label ?? `${progress.workflow ?? "run"} ${progress.runId.slice(0, 8)}`;
334
- const start = Math.min(...states.map(({ w }) => w[0]));
335
- windows.push({
336
- stage,
337
- status: progress.status ?? "",
338
- start,
339
- end: Math.max(...states.map(({ w }) => w[1]))
340
- });
341
- }
342
- }
343
- windows.sort((a, b) => a.start - b.start);
344
- const measure = (start, end) => {
345
- const acc = newAcc(start);
346
- for (const [row, ts] of rows)
347
- if (ts >= start && ts <= end && promptText(row) === undefined)
348
- accumulate(acc, row, ts);
349
- const span = {
350
- workMs: end - start - acc.askMs,
351
- waitMs: acc.askMs,
352
- toolCalls: acc.toolIds.size,
353
- tokens: sumTokens([...acc.messages.values()])
354
- };
355
- return { span, measured: acc.toolIds.size > 0 || acc.messages.size > 0 };
356
- };
357
- const sinceMs = since ? Date.parse(since) : Number.NaN;
358
- const totalStart = Number.isNaN(sinceMs) ? Math.min(...windows.map((w) => w.start)) : sinceMs;
359
- let totalEnd = Math.max(...windows.map((w) => w.end));
360
- if (!Number.isNaN(sinceMs)) {
361
- for (const [, ts] of rows)
362
- if (ts >= sinceMs)
363
- totalEnd = Math.max(totalEnd, ts);
364
- }
365
- const untilMs = until ? Date.parse(until) : Number.NaN;
366
- if (!Number.isNaN(untilMs) && untilMs > totalStart)
367
- totalEnd = untilMs;
368
- const stageRows = windows.map((w) => {
369
- const { span, measured } = measure(w.start, w.end);
370
- return summaryRow(w.stage, w.status, span, measured);
371
- });
372
- const totalMeasure = measure(totalStart, totalEnd);
373
- const statuses = [...new Set(inputs.map((i) => i.progress.status ?? ""))].join("/");
374
- const total = summaryRow("Total", statuses, totalMeasure.span, totalMeasure.measured);
375
- const overlaps = windows.some((w, i) => i > 0 && w.start < (windows[i - 1]?.end ?? 0));
376
- const sum = sumSpans(stageRows.map((r) => ({ ...r, toolCalls: r.toolCalls ?? 0, tokens: r.tokens ?? zeroTokens() })));
377
- const overhead = {
378
- workMs: total.workMs - sum.workMs,
379
- waitMs: total.waitMs - sum.waitMs,
380
- toolCalls: (total.toolCalls ?? 0) - sum.toolCalls,
381
- tokens: Object.fromEntries(Object.keys(sum.tokens).map((k) => [k, (total.tokens?.[k] ?? 0) - sum.tokens[k]]))
382
- };
383
- if (!overlaps && overhead.workMs > 0)
384
- stageRows.push(summaryRow("driver overhead", "", overhead, totalMeasure.measured));
385
- return {
386
- available: true,
387
- rows: stageRows,
388
- total,
389
- actors: actorSplit(rows, totalStart, totalEnd),
390
- gate: { ms: null, reason: "no matching check receipt" },
391
- skippedLines: opts.skippedLines ?? 0
392
- };
393
- }
394
- var SENSITIVE_PREFIXES = ["drizzle/", "packages/config/", ".github/", "plugins/sp/hooks/"];
395
- var SENSITIVE_GLOBS = ["apps/server/src/**/auth*", "**/*secret*", "**/*.sql"];
396
- function globToRegex(glob) {
397
- const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replaceAll("**/", "\x00").replaceAll("**", "\x01").replaceAll("*", "[^/]*").replaceAll("\x00", "(?:.*/)?").replaceAll("\x01", ".*");
398
- return new RegExp(`^${escaped}$`, "s");
399
- }
400
- var SENSITIVE_MATCHERS = [
401
- ...SENSITIVE_PREFIXES.map((prefix) => ({
402
- label: prefix,
403
- test: (path) => path.startsWith(prefix)
404
- })),
405
- ...SENSITIVE_GLOBS.map((glob) => {
406
- const regex = globToRegex(glob);
407
- return { label: glob, test: (path) => regex.test(path) };
408
- })
409
- ];
410
-
411
- // plugins/sp/lib/transcript.ts
412
- import { existsSync, readdirSync } from "node:fs";
413
- import { homedir } from "node:os";
414
- import { join } from "node:path";
415
- var SESSION_ID = /^[A-Za-z0-9_-]+$/;
416
- var PI_ROLES = new Set(["user", "assistant", "toolResult"]);
417
- function sniffFormat(rows) {
418
- for (const row of rows) {
419
- if (row.type === "message" && PI_ROLES.has(row.message?.role ?? ""))
420
- return "pi";
421
- if (row.type === "user" || row.type === "assistant")
422
- return "claude";
423
- }
424
- return "unknown";
425
- }
426
- function parseRows(lines) {
427
- const rows = [];
428
- let skippedLines = 0;
429
- for (const line of lines) {
430
- if (!line.trim())
431
- continue;
432
- let row;
433
- try {
434
- row = JSON.parse(line);
435
- } catch {
436
- skippedLines++;
437
- continue;
438
- }
439
- const ts = row.timestamp ? Date.parse(row.timestamp) : Number.NaN;
440
- if (!Number.isNaN(ts))
441
- rows.push([row, ts]);
442
- }
443
- return { rows, skippedLines, format: sniffFormat(rows.map(([r]) => r)) };
444
- }
445
- function resolveTranscript(env, projectsRoot = join(homedir(), ".claude", "projects"), override) {
446
- if (override)
447
- return existsSync(override) ? { ok: true, path: override } : { ok: false, reason: "no transcript" };
448
- const id = env.CLAUDE_CODE_SESSION_ID;
449
- if (!id) {
450
- const piFile = env.PI_SESSION_FILE;
451
- if (piFile !== undefined && piFile !== "") {
452
- return existsSync(piFile) ? { ok: true, path: piFile } : { ok: false, reason: `PI_SESSION_FILE ${piFile} does not exist` };
453
- }
454
- return {
455
- ok: false,
456
- reason: "no host session id; pass --transcript <path> (pi: ~/.pi/agent/sessions/<cwd-slug>/<file>.jsonl, " + "or set PI_SESSION_FILE)"
457
- };
458
- }
459
- if (!SESSION_ID.test(id))
460
- return { ok: false, reason: "refusing a session id with path characters" };
461
- if (!existsSync(projectsRoot))
462
- return { ok: false, reason: `no transcript root ${projectsRoot}` };
463
- for (const dir of readdirSync(projectsRoot)) {
464
- const path = join(projectsRoot, dir, `${id}.jsonl`);
465
- if (existsSync(path))
466
- return { ok: true, path };
467
- }
468
- return { ok: false, reason: `no transcript for session ${id}` };
469
- }
470
-
471
- // plugins/sp/lib/run-summary-core.ts
472
- function renderSummaryMarkdown(summary) {
473
- const line = (r, bold = false) => {
474
- const stage = bold ? `**${r.stage}**` : r.stage;
475
- return `| ${stage} | ${r.status} | ${r.time} | ${r.wait} | ${r.toolCalls ?? "n/a"} | ${r.token} |`;
476
- };
477
- const a = summary.actors;
478
- const window = summary.total.workMs + summary.total.waitMs;
479
- const share = (ms) => window > 0 ? `${Math.round(ms / window * 100)}%` : "n/a";
480
- const gate = summary.gate.ms === null ? `n/a (${summary.gate.reason})` : formatDuration(summary.gate.ms);
481
- return [
482
- "| Stage | Status | Time | Wait | Tool calls | Token (total / non-cached) |",
483
- "| --- | --- | ---: | ---: | ---: | ---: |",
484
- ...summary.rows.map((r) => line(r)),
485
- line(summary.total, true),
486
- "",
487
- "| Actor | Time | Share |",
488
- "| --- | ---: | ---: |",
489
- `| operator wait (longest ${formatDuration(a.longestOperatorWaitMs)}) | ${formatDuration(a.operatorMs)} | ${share(a.operatorMs)} |`,
490
- `| model | ${formatDuration(a.modelMs)} | ${share(a.modelMs)} |`,
491
- `| shell | ${formatDuration(a.shellMs)} | ${share(a.shellMs)} |`,
492
- `| of which gate (informational) | ${gate} | — |`,
493
- `| subagent | ${formatDuration(a.subagentMs)} | ${share(a.subagentMs)} |`,
494
- `| other tools | ${formatDuration(a.otherToolMs)} | ${share(a.otherToolMs)} |`,
495
- `| idle / unattributed | ${formatDuration(a.idleMs)} | ${share(a.idleMs)} |`,
496
- "",
497
- `Compactions: ${a.compactions} · Subagent errors: ${a.subagentErrors}`
498
- ].join(`
499
- `);
500
- }
501
- function renderRollupMarkdown(rows) {
502
- const line = (label, r, actors, bold = false) => {
503
- const name = bold ? `**${label}**` : label;
504
- return `| ${name} | ${r.status} | ${r.time} | ${r.wait} | ${r.toolCalls ?? "n/a"} | ${r.token} | ${formatDuration(actors.operatorMs)} | ${formatDuration(actors.subagentMs)} |`;
505
- };
506
- const total = rows.reduce((acc, { summary }) => ({
507
- workMs: acc.workMs + summary.total.workMs,
508
- waitMs: acc.waitMs + summary.total.waitMs,
509
- toolCalls: (acc.toolCalls ?? 0) + (summary.total.toolCalls ?? 0),
510
- tokens: sumTokens([acc.tokens, summary.total.tokens ?? zeroTokens()])
511
- }), { workMs: 0, waitMs: 0, toolCalls: 0, tokens: zeroTokens() });
512
- const totalRow = {
513
- stage: "Batch total",
514
- status: "",
515
- workMs: total.workMs,
516
- waitMs: total.waitMs,
517
- toolCalls: total.toolCalls,
518
- tokens: total.tokens,
519
- time: formatDuration(total.workMs),
520
- wait: formatDuration(total.waitMs),
521
- token: formatTokenSplit(total.tokens)
522
- };
523
- const totalActors = {
524
- operatorMs: 0,
525
- longestOperatorWaitMs: 0,
526
- modelMs: 0,
527
- shellMs: 0,
528
- subagentMs: 0,
529
- otherToolMs: 0,
530
- idleMs: 0,
531
- compactions: 0,
532
- subagentErrors: 0
533
- };
534
- for (const { summary } of rows) {
535
- totalActors.operatorMs += summary.actors.operatorMs;
536
- totalActors.subagentMs += summary.actors.subagentMs;
537
- totalActors.compactions += summary.actors.compactions;
538
- totalActors.subagentErrors += summary.actors.subagentErrors;
539
- }
540
- const out = [
541
- "| Run | Status | Time | Wait | Tool calls | Token (total / non-cached) | Operator wait | Subagent |",
542
- "| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |",
543
- ...rows.map(({ label, summary }) => line(label, summary.total, summary.actors)),
544
- line("Batch total", totalRow, totalActors, true)
545
- ];
546
- const starts = rows.map((r) => Date.parse(r.summary.startedAt ?? "")).filter((n) => !Number.isNaN(n));
547
- const ends = rows.map((r) => Date.parse(r.summary.completedAt ?? "")).filter((n) => !Number.isNaN(n));
548
- 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 ?? "")));
549
- if (overlaps && starts.length > 0 && ends.length > 0) {
550
- out.push("", `Wall span: ${new Date(Math.min(...starts)).toISOString()} → ${new Date(Math.max(...ends)).toISOString()}`);
551
- }
552
- return out.join(`
553
- `);
554
- }
555
- function writeCloseSummary(run, opts = {}) {
556
- const cwd = opts.cwd ?? process.cwd();
557
- const runDir = join2(cwd, ".spur", "run");
558
- const rel = join2(".spur", "run", `${run.runId}-summary.md`);
559
- const abs = join2(cwd, rel);
560
- const na = (reason) => {
561
- mkdirSync(runDir, { recursive: true });
562
- writeFileSync(abs, `Execution summary: n/a (${reason})
563
- `);
564
- return rel;
565
- };
566
- try {
567
- if (run.startedAt === undefined)
568
- return na("run row has no started_at");
569
- const resolved = resolveTranscript(opts.env ?? getEnvVars2(), opts.projectsRoot);
570
- if (!resolved.ok)
571
- return na(resolved.reason);
572
- const { rows, skippedLines } = parseRows(readFileSync(resolved.path, "utf8").split(`
573
- `));
574
- const summary = buildRunSummary(rows, [{ progress: run.progress }], {
575
- since: run.startedAt,
576
- until: run.completedAt,
577
- skippedLines
578
- });
579
- if (summary.rows.length === 0)
580
- return na("no timed attempts");
581
- mkdirSync(runDir, { recursive: true });
582
- writeFileSync(abs, `${renderSummaryMarkdown(summary)}
583
- `);
584
- writeFileSync(join2(runDir, `${run.runId}-summary.json`), JSON.stringify({ ...summary, runId: run.runId, startedAt: run.startedAt, completedAt: run.completedAt }, null, 2));
585
- return rel;
586
- } catch (error) {
587
- return na(error instanceof Error ? error.message : String(error));
588
- }
589
- }
590
- function gateFromReceipt(cwd, runId) {
591
- const path = join2(cwd, ".spur", "run", `${runId}-check-receipt.json`);
592
- if (!existsSync2(path))
593
- return { ms: null, reason: "no matching check receipt" };
594
- try {
595
- const parsed = JSON.parse(readFileSync(path, "utf8"));
596
- if (parsed.runId !== runId)
597
- return { ms: null, reason: "check receipt belongs to another run" };
598
- const ms = parsed.gateRuntimeMs;
599
- if (typeof ms !== "number" || !Number.isFinite(ms))
600
- return { ms: null, reason: "check receipt has no gate time" };
601
- return { ms };
602
- } catch {
603
- return { ms: null, reason: "check receipt is unreadable" };
604
- }
605
- }
606
-
607
- // plugins/sp/lib/transcript.ts
608
- import { existsSync as existsSync3, readdirSync as readdirSync2 } from "node:fs";
609
- import { homedir as homedir2 } from "node:os";
610
- import { join as join3 } from "node:path";
611
- var SESSION_ID2 = /^[A-Za-z0-9_-]+$/;
612
- var PI_ROLES2 = new Set(["user", "assistant", "toolResult"]);
613
- function sniffFormat2(rows) {
614
- for (const row of rows) {
615
- if (row.type === "message" && PI_ROLES2.has(row.message?.role ?? ""))
616
- return "pi";
617
- if (row.type === "user" || row.type === "assistant")
618
- return "claude";
619
- }
620
- return "unknown";
621
- }
622
- function parseRows2(lines) {
623
- const rows = [];
624
- let skippedLines = 0;
625
- for (const line of lines) {
626
- if (!line.trim())
627
- continue;
628
- let row;
629
- try {
630
- row = JSON.parse(line);
631
- } catch {
632
- skippedLines++;
633
- continue;
634
- }
635
- const ts = row.timestamp ? Date.parse(row.timestamp) : Number.NaN;
636
- if (!Number.isNaN(ts))
637
- rows.push([row, ts]);
638
- }
639
- return { rows, skippedLines, format: sniffFormat2(rows.map(([r]) => r)) };
640
- }
641
- function resolveTranscript2(env, projectsRoot = join3(homedir2(), ".claude", "projects"), override) {
642
- if (override)
643
- return existsSync3(override) ? { ok: true, path: override } : { ok: false, reason: "no transcript" };
644
- const id = env.CLAUDE_CODE_SESSION_ID;
645
- if (!id) {
646
- const piFile = env.PI_SESSION_FILE;
647
- if (piFile !== undefined && piFile !== "") {
648
- return existsSync3(piFile) ? { ok: true, path: piFile } : { ok: false, reason: `PI_SESSION_FILE ${piFile} does not exist` };
649
- }
650
- return {
651
- ok: false,
652
- reason: "no host session id; pass --transcript <path> (pi: ~/.pi/agent/sessions/<cwd-slug>/<file>.jsonl, " + "or set PI_SESSION_FILE)"
653
- };
654
- }
655
- if (!SESSION_ID2.test(id))
656
- return { ok: false, reason: "refusing a session id with path characters" };
657
- if (!existsSync3(projectsRoot))
658
- return { ok: false, reason: `no transcript root ${projectsRoot}` };
659
- for (const dir of readdirSync2(projectsRoot)) {
660
- const path = join3(projectsRoot, dir, `${id}.jsonl`);
661
- if (existsSync3(path))
662
- return { ok: true, path };
663
- }
664
- return { ok: false, reason: `no transcript for session ${id}` };
665
- }
666
-
667
- // plugins/sp/scripts/run-summary.ts
668
- var RUN_SUMMARY_USAGE = "usage: run-summary (--progress [<label>=]<file> [--progress ...] | --rollup [<label>=]<summary.json> [--rollup ...]) [--transcript <path>] [--since <iso>] [--markdown]";
669
- function main(argv, env = getEnvVars(), write = (s) => process.stdout.write(s), projectsRoot) {
670
- let transcript;
671
- let since;
672
- let markdown = false;
673
- const progressArgs = [];
674
- const rollupArgs = [];
675
- for (let i = 0;i < argv.length; i++) {
676
- const arg = argv[i];
677
- if (arg === "--transcript" && argv[i + 1])
678
- transcript = argv[++i];
679
- else if (arg === "--progress" && argv[i + 1])
680
- progressArgs.push(argv[++i]);
681
- else if (arg === "--rollup" && argv[i + 1])
682
- rollupArgs.push(argv[++i]);
683
- else if (arg === "--since" && argv[i + 1])
684
- since = argv[++i];
685
- else if (arg === "--markdown")
686
- markdown = true;
687
- else if (arg === "--spur-bin" && argv[i + 1])
688
- i++;
689
- else {
690
- process.stderr.write(`${RUN_SUMMARY_USAGE}
691
- `);
692
- return 2;
693
- }
694
- }
695
- if (rollupArgs.length > 0 && (progressArgs.length > 0 || since !== undefined || transcript !== undefined)) {
696
- process.stderr.write(`${RUN_SUMMARY_USAGE}
697
- `);
698
- return 2;
699
- }
700
- if (rollupArgs.length > 0) {
701
- try {
702
- const inputs = rollupArgs.map((arg) => {
703
- const eq = arg.indexOf("=");
704
- const [label, file] = eq > 0 ? [arg.slice(0, eq), arg.slice(eq + 1)] : [undefined, arg];
705
- const summary = JSON.parse(readFileSync2(file, "utf8"));
706
- return { label: label ?? summary.runId ?? file, summary };
707
- });
708
- write(`${renderRollupMarkdown(inputs)}
709
- `);
710
- return 0;
711
- } catch (error) {
712
- process.stderr.write(`run-summary: ${error.message}
713
- `);
714
- return 2;
715
- }
716
- }
717
- if (!progressArgs.length) {
718
- process.stderr.write(`${RUN_SUMMARY_USAGE}
719
- `);
720
- return 2;
721
- }
722
- const resolved = resolveTranscript2(env, projectsRoot, transcript);
723
- if (!resolved.ok) {
724
- const out = markdown ? `Execution summary: n/a (${resolved.reason})` : JSON.stringify({ available: false, reason: resolved.reason });
725
- write(`${out}
726
- `);
727
- return 0;
728
- }
729
- try {
730
- const inputs = progressArgs.map((arg) => {
731
- const eq = arg.indexOf("=");
732
- const [label, file] = eq > 0 ? [arg.slice(0, eq), arg.slice(eq + 1)] : [undefined, arg];
733
- return { label, progress: JSON.parse(readFileSync2(file, "utf8")) };
734
- });
735
- const { rows, skippedLines } = parseRows2(readFileSync2(resolved.path, "utf8").split(`
736
- `));
737
- const summary = buildRunSummary(rows, inputs, { since, skippedLines });
738
- summary.gate = gateFromReceipt(process.cwd(), inputs[0]?.progress.runId ?? "");
739
- write(`${markdown ? renderSummaryMarkdown(summary) : JSON.stringify({ ...summary, transcript: resolved.path })}
740
- `);
741
- return 0;
742
- } catch (error) {
743
- process.stderr.write(`run-summary: ${error.message}
744
- `);
745
- return 2;
746
- }
747
- }
748
- process.exit(main(process.argv.slice(2)));
749
- export {
750
- RUN_SUMMARY_USAGE,
751
- actorSplit,
752
- buildRunSummary,
753
- gateFromReceipt,
754
- main,
755
- renderRollupMarkdown,
756
- renderSummaryMarkdown,
757
- writeCloseSummary
758
- };