@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,338 +0,0 @@
1
- #!/usr/bin/env node
2
- // @bun
3
-
4
- // plugins/sp/scripts/workflow-step-profile.ts
5
- import { spawnSync } from "child_process";
6
-
7
- // plugins/sp/lib/spur-bin.ts
8
- import { existsSync } from "node:fs";
9
- import { fileURLToPath } from "node:url";
10
-
11
- // plugins/sp/lib/env.ts
12
- function getEnvVar(name, fallback) {
13
- const raw = process.env[name];
14
- return raw === undefined ? fallback : raw;
15
- }
16
-
17
- // plugins/sp/lib/spur-bin.ts
18
- function spurCommand(spurBin) {
19
- const parts = (spurBin ?? "spur").trim().split(/\s+/).filter((p) => p.length > 0);
20
- return { cmd: parts[0] ?? "spur", prefix: parts.slice(1) };
21
- }
22
- function defaultSpurBin() {
23
- const fromEnv = getEnvVar("SPUR_BIN");
24
- if (fromEnv)
25
- return fromEnv;
26
- const local = fileURLToPath(new URL("../../../apps/cli/src/index.ts", import.meta.url));
27
- if (existsSync(local))
28
- return `bun ${local}`;
29
- return "spur";
30
- }
31
-
32
- // plugins/sp/lib/step-profile.generated.mjs
33
- var DEFAULT_LAST = 20;
34
- var DEFAULT_WINDOW_SEC = 300;
35
- function nearestRankP50(values) {
36
- if (values.length === 0)
37
- return null;
38
- const sorted = [...values].sort((a, b) => a - b);
39
- return sorted[Math.ceil(sorted.length / 2) - 1] ?? null;
40
- }
41
- function numeric(value) {
42
- return typeof value === "number" && Number.isFinite(value) ? value : null;
43
- }
44
- function msBetween(from, to) {
45
- if (typeof from !== "string" || typeof to !== "string")
46
- return null;
47
- const start = Date.parse(from);
48
- const end = Date.parse(to);
49
- if (!Number.isFinite(start) || !Number.isFinite(end))
50
- return null;
51
- return end - start;
52
- }
53
- function sessionOf(event) {
54
- const value = event.invocation?.continue;
55
- if (value === true)
56
- return "resumed";
57
- if (value === false)
58
- return "fresh";
59
- return null;
60
- }
61
- function cacheHitOf(event) {
62
- return numeric(event.cost?.exact?.cacheHit);
63
- }
64
- function startKey(event) {
65
- const parsed = typeof event.startedAt === "string" ? Date.parse(event.startedAt) : Number.NaN;
66
- return Number.isFinite(parsed) ? parsed : Number.POSITIVE_INFINITY;
67
- }
68
- function extractExecutions(runId, events) {
69
- const actions = events.filter((e) => e.kind === "action").sort((a, b) => {
70
- const left = startKey(a);
71
- const right = startKey(b);
72
- return left === right ? 0 : left - right;
73
- });
74
- const executions = [];
75
- for (const [index, event] of actions.entries()) {
76
- const previous = index === 0 ? undefined : actions[index - 1];
77
- executions.push({
78
- runId,
79
- node: event.node ?? "",
80
- actionKind: event.actionKind ?? "",
81
- durationMs: numeric(event.durationMs),
82
- idleGapMs: msBetween(previous?.completedAt, event.startedAt),
83
- session: sessionOf(event),
84
- cacheHit: cacheHitOf(event)
85
- });
86
- }
87
- return executions;
88
- }
89
- function foldSession(executions) {
90
- const known = new Set(executions.map((e) => e.session).filter((s) => s !== null));
91
- if (known.size === 0)
92
- return null;
93
- if (known.size === 1)
94
- return [...known][0] ?? null;
95
- return "mixed";
96
- }
97
- function rowFlags(row, windowSec) {
98
- const wMs = windowSec * 1000;
99
- const flags = [];
100
- const agentRun = row.actionKind === "agent.run";
101
- const resumed = row.session === "resumed" || row.session === "mixed";
102
- const p50 = row.durationMs.p50;
103
- if (!agentRun && p50 !== null && p50 > wMs)
104
- flags.push("step-over-window");
105
- const idle = row.idleGapMs.p50;
106
- if (agentRun && resumed && idle !== null && idle > wMs)
107
- flags.push("resume-after-idle");
108
- const hit = row.cacheHit.p50;
109
- if (agentRun && resumed && row.cacheHit.known > 0 && hit !== null && hit < 0.5)
110
- flags.push("resume-cold-cache");
111
- if (agentRun && p50 !== null && p50 > 2 * wMs)
112
- flags.push("agent-run-over-2w");
113
- return flags;
114
- }
115
- function buildRows(executions, windowSec) {
116
- const grouped = new Map;
117
- for (const execution of executions) {
118
- const key = `${execution.node}\x00${execution.actionKind}`;
119
- const bucket = grouped.get(key);
120
- if (bucket === undefined)
121
- grouped.set(key, [execution]);
122
- else
123
- bucket.push(execution);
124
- }
125
- const rows = [];
126
- for (const bucket of grouped.values()) {
127
- const first = bucket[0];
128
- if (first === undefined)
129
- continue;
130
- const durations = bucket.map((e) => e.durationMs).filter((d) => d !== null);
131
- const gaps = bucket.map((e) => e.idleGapMs).filter((g) => g !== null);
132
- const hits = bucket.map((e) => e.cacheHit).filter((h) => h !== null);
133
- const row = {
134
- node: first.node,
135
- actionKind: first.actionKind,
136
- runs: new Set(bucket.map((e) => e.runId)).size,
137
- executions: bucket.length,
138
- durationMs: {
139
- p50: nearestRankP50(durations),
140
- max: durations.length === 0 ? null : Math.max(...durations)
141
- },
142
- idleGapMs: { p50: nearestRankP50(gaps) },
143
- session: first.actionKind === "agent.run" ? foldSession(bucket) : null,
144
- cacheHit: {
145
- p50: nearestRankP50(hits),
146
- known: hits.length,
147
- of: bucket.length
148
- },
149
- flags: []
150
- };
151
- row.flags = rowFlags(row, windowSec);
152
- rows.push(row);
153
- }
154
- return rows.sort((a, b) => a.node === b.node ? a.actionKind.localeCompare(b.actionKind) : a.node.localeCompare(b.node));
155
- }
156
- function buildStepProfile(input) {
157
- const executions = input.runs.flatMap((run) => extractExecutions(run.runId, run.events));
158
- return {
159
- workflow: input.workflow,
160
- windowSec: input.windowSec,
161
- sampledRuns: input.runs.length,
162
- rows: buildRows(executions, input.windowSec)
163
- };
164
- }
165
- function nonDryRuns(entries) {
166
- return entries.filter((e) => e.isDryRun !== true && typeof e.runId === "string" && e.runId.length > 0);
167
- }
168
- function seconds(value) {
169
- return value === null ? "?" : `${(value / 1000).toFixed(1)}s`;
170
- }
171
- function ratio(value) {
172
- return value === null ? "?" : value.toFixed(2);
173
- }
174
- function formatStepProfileHuman(profile) {
175
- const lines = [
176
- `step profile — ${profile.workflow} (window ${profile.windowSec}s, ${profile.sampledRuns} sampled runs)`
177
- ];
178
- for (const row of profile.rows) {
179
- lines.push([
180
- row.node.padEnd(20),
181
- row.actionKind.padEnd(14),
182
- `${row.runs}/${row.executions}`.padEnd(8),
183
- `p50=${seconds(row.durationMs.p50)}`.padEnd(12),
184
- `max=${seconds(row.durationMs.max)}`.padEnd(12),
185
- `idle=${seconds(row.idleGapMs.p50)}`.padEnd(12),
186
- `session=${row.session ?? "?"}`.padEnd(15),
187
- `cacheHit=${ratio(row.cacheHit.p50)} (${row.cacheHit.known}/${row.cacheHit.of})`.padEnd(20),
188
- `flags=${row.flags.length === 0 ? "-" : row.flags.join(",")}`
189
- ].join(" "));
190
- }
191
- return `${lines.join(`
192
- `)}
193
- `;
194
- }
195
-
196
- // plugins/sp/scripts/workflow-step-profile.ts
197
- function positiveInt(raw, fallback) {
198
- const parsed = Number.parseInt(raw ?? "", 10);
199
- return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
200
- }
201
- function parseStepProfileCliArgs(argv) {
202
- let workflow = "";
203
- let spurBin = defaultSpurBin();
204
- let last = DEFAULT_LAST;
205
- let windowSec = DEFAULT_WINDOW_SEC;
206
- let json = false;
207
- let help = false;
208
- for (let i = 0;i < argv.length; i++) {
209
- const a = argv[i];
210
- if (a === undefined)
211
- continue;
212
- if (a === "--help" || a === "-h")
213
- help = true;
214
- else if (a === "--json")
215
- json = true;
216
- else if (a === "--last")
217
- last = positiveInt(argv[++i], last);
218
- else if (a === "--window")
219
- windowSec = positiveInt(argv[++i], windowSec);
220
- else if (a === "--spur-bin")
221
- spurBin = argv[++i] ?? spurBin;
222
- else if (!a.startsWith("--") && workflow === "")
223
- workflow = a;
224
- }
225
- return { workflow, last, windowSec, spurBin, json, help };
226
- }
227
- function runSpurJson(spurBin, args) {
228
- const { cmd, prefix } = spurCommand(spurBin);
229
- const cmdArgs = [...prefix, ...args];
230
- const r = spawnSync(cmd, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], encoding: "utf8" });
231
- const decode = (b) => typeof b === "string" ? b : Buffer.from(b ?? []).toString("utf8");
232
- return {
233
- stdout: typeof r.stdout === "string" ? r.stdout : decode(r.stdout),
234
- stderr: typeof r.stderr === "string" ? r.stderr : decode(r.stderr),
235
- exitCode: r.status ?? (r.error ? 1 : 0),
236
- ok: (r.status ?? (r.error ? 1 : 0)) === 0
237
- };
238
- }
239
- var STEP_PROFILE_USAGE = `usage: workflow-step-profile <workflow> [--last <N>] [--window <sec>] [--json] [--spur-bin <cmd>]
240
-
241
- Read-only step profile from \`spur workflow trace\`: per node and action kind it reports runs,
242
- executions, p50/max durationMs, p50 idle gap, session mode and cacheHit p50 with coverage, then
243
- flags the satellite \xA710 cache-window budgets. --last defaults to ${DEFAULT_LAST} runs and --window
244
- (W) to ${DEFAULT_WINDOW_SEC} seconds. Unknown evidence is reported as \`?\` / null, never 0.
245
-
246
- Exit: 0 = profile produced (flags included); 1 = a spur call failed, its JSON did not parse, or the
247
- workflow argument is missing.`;
248
- function failure(message) {
249
- return { exitCode: 1, stdout: "", stderr: `workflow-step-profile: ${message}
250
- ` };
251
- }
252
- function runFailure(spurBin, result, what) {
253
- const detail = result.stderr.trim().split(`
254
- `).slice(-1)[0] ?? "";
255
- return failure(`${what} failed (exit ${result.exitCode}${detail === "" ? "" : `: ${detail}`}) [${spurBin}]`);
256
- }
257
- function runStepProfileCli(argv) {
258
- const args = parseStepProfileCliArgs(argv);
259
- if (args.help)
260
- return { exitCode: 0, stdout: `${STEP_PROFILE_USAGE}
261
- `, stderr: "" };
262
- if (args.workflow === "")
263
- return failure(`a workflow name is required
264
- ${STEP_PROFILE_USAGE}`);
265
- const listArgs = [
266
- "workflow",
267
- "trace",
268
- "--workflow",
269
- args.workflow,
270
- "--status",
271
- "done",
272
- "--last",
273
- String(args.last),
274
- "--json"
275
- ];
276
- const list = runSpurJson(args.spurBin, listArgs);
277
- if (!list.ok)
278
- return runFailure(args.spurBin, list, `spur ${listArgs.join(" ")}`);
279
- let entries;
280
- try {
281
- const parsed = JSON.parse(list.stdout);
282
- if (!Array.isArray(parsed.entries))
283
- throw new Error('no "entries" array');
284
- entries = parsed.entries;
285
- } catch (err) {
286
- return failure(`spur workflow trace output did not parse as JSON: ${String(err)}`);
287
- }
288
- const runs = [];
289
- for (const entry of nonDryRuns(entries)) {
290
- const runArgs = ["workflow", "trace", entry.runId, "--json"];
291
- const timeline = runSpurJson(args.spurBin, runArgs);
292
- if (!timeline.ok)
293
- return runFailure(args.spurBin, timeline, `spur ${runArgs.join(" ")}`);
294
- try {
295
- const parsed = JSON.parse(timeline.stdout);
296
- if (!Array.isArray(parsed.events))
297
- throw new Error('no "events" array');
298
- runs.push({ runId: entry.runId, events: parsed.events });
299
- } catch (err) {
300
- return failure(`spur workflow trace ${entry.runId} output did not parse as JSON: ${String(err)}`);
301
- }
302
- }
303
- const profile = buildStepProfile({ workflow: args.workflow, windowSec: args.windowSec, runs });
304
- return {
305
- exitCode: 0,
306
- stdout: args.json ? `${JSON.stringify(profile, null, 2)}
307
- ` : formatStepProfileHuman(profile),
308
- stderr: ""
309
- };
310
- }
311
- function main(argv) {
312
- const { exitCode, stdout, stderr } = runStepProfileCli(argv);
313
- if (stdout)
314
- process.stdout.write(stdout);
315
- if (stderr)
316
- process.stderr.write(stderr.endsWith(`
317
- `) ? stderr : `${stderr}
318
- `);
319
- return exitCode;
320
- }
321
- {
322
- process.exit(main(process.argv.slice(2)));
323
- }
324
- export {
325
- DEFAULT_LAST,
326
- DEFAULT_WINDOW_SEC,
327
- STEP_PROFILE_USAGE,
328
- buildRows,
329
- buildStepProfile,
330
- extractExecutions,
331
- formatStepProfileHuman,
332
- main,
333
- nearestRankP50,
334
- nonDryRuns,
335
- parseStepProfileCliArgs,
336
- rowFlags,
337
- runStepProfileCli
338
- };
@@ -1,165 +0,0 @@
1
- #!/usr/bin/env bun
2
- /**
3
- * workflow-step-profile — per-node step evidence for `sp:spur-doctor` (task 0827, feature I21).
4
- *
5
- * IO glue only (task 1005 R4, ADR-130 lib rule): argv parsing, `spur workflow trace --json`
6
- * spawning and the two output modes. The aggregation core (nearestRankP50, extractExecutions,
7
- * rowFlags, buildRows, buildStepProfile, nonDryRuns, formatStepProfileHuman and their types) lives
8
- * in packages/app/src/workflow/step-profile.ts and reaches this script through the generated
9
- * standalone bundle `plugins/sp/lib/step-profile.generated.mjs` — node-builtin only, so the
10
- * committed `.mjs` twin runs under bare `node`. Regenerate with `bun run build:scripts`.
11
- *
12
- * Exit: 0 whenever a profile is produced (including `sampledRuns: 0` and flagged rows). Flags are
13
- * evidence, not a gate. 1, with a stderr message, only when a `spur` call fails, its JSON does not
14
- * parse, or the workflow argument is missing.
15
- */
16
- import { spawnSync } from 'node:child_process';
17
- import { defaultSpurBin, spurCommand } from '../lib/spur-bin';
18
- import {
19
- buildStepProfile,
20
- DEFAULT_LAST,
21
- DEFAULT_WINDOW_SEC,
22
- formatStepProfileHuman,
23
- nonDryRuns,
24
- type StepProfileRun,
25
- type TraceActionEvent,
26
- type TraceRunEntry,
27
- } from '../lib/step-profile.generated.mjs';
28
-
29
- export * from '../lib/step-profile.generated.mjs';
30
- export interface StepProfileCliArgs {
31
- workflow: string;
32
- last: number;
33
- windowSec: number;
34
- spurBin: string;
35
- json: boolean;
36
- help: boolean;
37
- }
38
-
39
- function positiveInt(raw: string | undefined, fallback: number): number {
40
- const parsed = Number.parseInt(raw ?? '', 10);
41
- return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
42
- }
43
-
44
- export function parseStepProfileCliArgs(argv: string[]): StepProfileCliArgs {
45
- let workflow = '';
46
- let spurBin = defaultSpurBin();
47
- let last = DEFAULT_LAST;
48
- let windowSec = DEFAULT_WINDOW_SEC;
49
- let json = false;
50
- let help = false;
51
-
52
- for (let i = 0; i < argv.length; i++) {
53
- const a = argv[i];
54
- if (a === undefined) continue;
55
- if (a === '--help' || a === '-h') help = true;
56
- else if (a === '--json') json = true;
57
- else if (a === '--last') last = positiveInt(argv[++i], last);
58
- else if (a === '--window') windowSec = positiveInt(argv[++i], windowSec);
59
- else if (a === '--spur-bin') spurBin = argv[++i] ?? spurBin;
60
- else if (!a.startsWith('--') && workflow === '') workflow = a;
61
- }
62
- return { workflow, last, windowSec, spurBin, json, help };
63
- }
64
-
65
- type SpawnResult = { stdout: string; stderr: string; exitCode: number; ok: boolean };
66
-
67
- function runSpurJson(spurBin: string, args: string[]): SpawnResult {
68
- const { cmd, prefix } = spurCommand(spurBin);
69
- const cmdArgs = [...prefix, ...args];
70
- const r = spawnSync(cmd, cmdArgs, { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' });
71
- const decode = (b: unknown): string =>
72
- typeof b === 'string' ? b : Buffer.from((b as Uint8Array) ?? []).toString('utf8');
73
- return {
74
- stdout: typeof r.stdout === 'string' ? r.stdout : decode(r.stdout),
75
- stderr: typeof r.stderr === 'string' ? r.stderr : decode(r.stderr),
76
- exitCode: r.status ?? (r.error ? 1 : 0),
77
- ok: (r.status ?? (r.error ? 1 : 0)) === 0,
78
- };
79
- }
80
-
81
- export interface StepProfileCliResult {
82
- exitCode: number;
83
- stdout: string;
84
- stderr: string;
85
- }
86
-
87
- export const STEP_PROFILE_USAGE = `usage: workflow-step-profile <workflow> [--last <N>] [--window <sec>] [--json] [--spur-bin <cmd>]
88
-
89
- Read-only step profile from \`spur workflow trace\`: per node and action kind it reports runs,
90
- executions, p50/max durationMs, p50 idle gap, session mode and cacheHit p50 with coverage, then
91
- flags the satellite §10 cache-window budgets. --last defaults to ${DEFAULT_LAST} runs and --window
92
- (W) to ${DEFAULT_WINDOW_SEC} seconds. Unknown evidence is reported as \`?\` / null, never 0.
93
-
94
- Exit: 0 = profile produced (flags included); 1 = a spur call failed, its JSON did not parse, or the
95
- workflow argument is missing.`;
96
-
97
- function failure(message: string): StepProfileCliResult {
98
- return { exitCode: 1, stdout: '', stderr: `workflow-step-profile: ${message}\n` };
99
- }
100
-
101
- function runFailure(spurBin: string, result: SpawnResult, what: string): StepProfileCliResult {
102
- const detail = result.stderr.trim().split('\n').slice(-1)[0] ?? '';
103
- return failure(`${what} failed (exit ${result.exitCode}${detail === '' ? '' : `: ${detail}`}) [${spurBin}]`);
104
- }
105
-
106
- export function runStepProfileCli(argv: string[]): StepProfileCliResult {
107
- const args = parseStepProfileCliArgs(argv);
108
- if (args.help) return { exitCode: 0, stdout: `${STEP_PROFILE_USAGE}\n`, stderr: '' };
109
- if (args.workflow === '') return failure(`a workflow name is required\n${STEP_PROFILE_USAGE}`);
110
-
111
- const listArgs = [
112
- 'workflow',
113
- 'trace',
114
- '--workflow',
115
- args.workflow,
116
- '--status',
117
- 'done',
118
- '--last',
119
- String(args.last),
120
- '--json',
121
- ];
122
- const list = runSpurJson(args.spurBin, listArgs);
123
- if (!list.ok) return runFailure(args.spurBin, list, `spur ${listArgs.join(' ')}`);
124
-
125
- let entries: TraceRunEntry[];
126
- try {
127
- const parsed = JSON.parse(list.stdout) as { entries?: unknown };
128
- if (!Array.isArray(parsed.entries)) throw new Error('no "entries" array');
129
- entries = parsed.entries as TraceRunEntry[];
130
- } catch (err) {
131
- return failure(`spur workflow trace output did not parse as JSON: ${String(err)}`);
132
- }
133
-
134
- const runs: StepProfileRun[] = [];
135
- for (const entry of nonDryRuns(entries)) {
136
- const runArgs = ['workflow', 'trace', entry.runId, '--json'];
137
- const timeline = runSpurJson(args.spurBin, runArgs);
138
- if (!timeline.ok) return runFailure(args.spurBin, timeline, `spur ${runArgs.join(' ')}`);
139
- try {
140
- const parsed = JSON.parse(timeline.stdout) as { events?: unknown };
141
- if (!Array.isArray(parsed.events)) throw new Error('no "events" array');
142
- runs.push({ runId: entry.runId, events: parsed.events as TraceActionEvent[] });
143
- } catch (err) {
144
- return failure(`spur workflow trace ${entry.runId} output did not parse as JSON: ${String(err)}`);
145
- }
146
- }
147
-
148
- const profile = buildStepProfile({ workflow: args.workflow, windowSec: args.windowSec, runs });
149
- return {
150
- exitCode: 0,
151
- stdout: args.json ? `${JSON.stringify(profile, null, 2)}\n` : formatStepProfileHuman(profile),
152
- stderr: '',
153
- };
154
- }
155
-
156
- export function main(argv: string[]): number {
157
- const { exitCode, stdout, stderr } = runStepProfileCli(argv);
158
- if (stdout) process.stdout.write(stdout);
159
- if (stderr) process.stderr.write(stderr.endsWith('\n') ? stderr : `${stderr}\n`);
160
- return exitCode;
161
- }
162
-
163
- if (import.meta.main) {
164
- process.exit(main(process.argv.slice(2)));
165
- }
@@ -1,190 +0,0 @@
1
- #!/usr/bin/env node
2
- // @bun
3
-
4
- // plugins/sp/scripts/wrapup-drift-probe.ts
5
- import { spawnSync } from "child_process";
6
- import { mkdirSync, readFileSync, writeFileSync } from "fs";
7
- import { join } from "path";
8
-
9
- // plugins/sp/lib/env.ts
10
- function getEnvVars() {
11
- return process.env;
12
- }
13
-
14
- // plugins/sp/lib/spur-bin.ts
15
- function spurCommand(spurBin) {
16
- const parts = (spurBin ?? "spur").trim().split(/\s+/).filter((p) => p.length > 0);
17
- return { cmd: parts[0] ?? "spur", prefix: parts.slice(1) };
18
- }
19
-
20
- // plugins/sp/scripts/wrapup-drift-probe.ts
21
- var WORKFLOWS_GLOB = `${join("config", "workflows")}/**`;
22
- var DOC_OWNED_SURFACES = [
23
- "packages/contracts/**",
24
- "apps/cli/src/commands/**",
25
- "packages/config/src/**",
26
- "drizzle/*.sql",
27
- WORKFLOWS_GLOB,
28
- "plugins/sp/commands/**",
29
- "plugins/sp/skills/**",
30
- "plugins/sp/hooks/**",
31
- "package.json",
32
- "docs/00_ADR.md",
33
- "docs/03_ARCHITECTURE.md",
34
- "docs/04_DESIGN.md",
35
- "docs/design/**"
36
- ];
37
- var KNOWN_TOP_LEVEL = new Set([
38
- "apps",
39
- "packages",
40
- "plugins",
41
- "config",
42
- "docs",
43
- "drizzle",
44
- "scripts",
45
- "vendors",
46
- "package.json",
47
- "bun.lock",
48
- "bunfig.toml"
49
- ]);
50
- var CORPUS_PREFIXES = ["docs/tasks", "docs/features/"];
51
- function solutionSectionOf(content) {
52
- const heading = /^#{2,4}\s+Solution\s*$/m.exec(content);
53
- if (!heading)
54
- return null;
55
- const rest = content.slice(heading.index + heading[0].length);
56
- const next = /^#{2,4}\s+\S/m.exec(rest);
57
- return next ? rest.slice(0, next.index) : rest;
58
- }
59
- var CHANGE_ENTRY = /`([^`\r\n]+):(\d+)(?:-\d+)?`/g;
60
- function changedPathsOf(section) {
61
- if (section === null)
62
- return [];
63
- const paths = [];
64
- for (const match of section.matchAll(CHANGE_ENTRY)) {
65
- const path = match[1];
66
- if (path === undefined)
67
- continue;
68
- if (/\s/.test(path))
69
- continue;
70
- if (!path.includes("/") && !path.includes("."))
71
- continue;
72
- paths.push(path);
73
- }
74
- return paths;
75
- }
76
- function surfaceRegex(glob) {
77
- const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replaceAll("**", "\x00").replaceAll("*", "[^/]*").replaceAll("\x00", ".*");
78
- return new RegExp(`^${escaped}$`);
79
- }
80
- var SURFACE_MATCHERS = DOC_OWNED_SURFACES.map((glob) => ({ glob, regex: surfaceRegex(glob) }));
81
- function driftReasonForPath(path) {
82
- if (CORPUS_PREFIXES.some((prefix) => path.startsWith(prefix)))
83
- return null;
84
- const top = path.split("/")[0];
85
- if (path.includes("/") && top !== undefined && !KNOWN_TOP_LEVEL.has(top)) {
86
- return "new top-level workspace directory";
87
- }
88
- const match = SURFACE_MATCHERS.find((entry) => entry.regex.test(path));
89
- return match ? `matches doc-owned surface ${match.glob}` : null;
90
- }
91
- function defaultSpurRunner(env, cwd) {
92
- const { cmd, prefix } = spurCommand(env.spurBin);
93
- return (args) => {
94
- const run = spawnSync(cmd, [...prefix, ...args], {
95
- cwd,
96
- encoding: "utf8",
97
- env: getEnvVars()
98
- });
99
- return { status: run.status ?? 1, stdout: run.stdout ?? "" };
100
- };
101
- }
102
- function runDriftProbe(env, options = {}, spur = defaultSpurRunner(env, options.cwd)) {
103
- const cwd = options.cwd;
104
- const runId = env.__runId ?? "";
105
- if (runId.length === 0) {
106
- process.stderr.write(`wrapup-drift-probe: __runId is empty \u2014 refusing the legacy fixed-path fallback
107
- `);
108
- return { clean: false, reasons: [], paths: [], probeFile: "", modeFile: "", exitCode: 1 };
109
- }
110
- const relProbeFile = join(".spur", "run", `${runId}-drift-probe.json`);
111
- const relModeFile = join(".spur", "run", `${runId}-mode.txt`);
112
- const relTasksFile = join(".spur", "run", `${runId}-wrapup-tasks.json`);
113
- const abs = (p) => cwd ? join(cwd, p) : p;
114
- mkdirSync(abs(join(".spur", "run")), { recursive: true });
115
- writeFileSync(abs(relModeFile), `
116
- `);
117
- const finish = (clean, reasons, paths) => {
118
- writeFileSync(abs(relProbeFile), `${JSON.stringify({ clean, reasons, paths })}
119
- `);
120
- writeFileSync(abs(relModeFile), clean ? `fast
121
- ` : `
122
- `);
123
- return { clean, reasons, paths, probeFile: relProbeFile, modeFile: relModeFile, exitCode: 0 };
124
- };
125
- let tasks;
126
- try {
127
- tasks = JSON.parse(readFileSync(abs(relTasksFile), "utf8"));
128
- } catch {
129
- tasks = undefined;
130
- }
131
- if (!Array.isArray(tasks) || !tasks.every((wbs) => typeof wbs === "string")) {
132
- process.stderr.write(`wrapup-drift-probe: normalized task capture is missing or corrupted \u2014 failing safe (dirty)
133
- `);
134
- return finish(false, ["normalized task capture missing or corrupted"], []);
135
- }
136
- const reasons = [];
137
- const allPaths = new Set;
138
- for (const wbs of tasks) {
139
- const shown = spur(["task", "show", wbs, "--json"]);
140
- if (shown.status !== 0) {
141
- reasons.push(`${wbs}: task show failed (status=${shown.status})`);
142
- continue;
143
- }
144
- let content;
145
- try {
146
- content = JSON.parse(shown.stdout).content;
147
- } catch {
148
- content = undefined;
149
- }
150
- if (typeof content !== "string") {
151
- reasons.push(`${wbs}: task show output unparseable`);
152
- continue;
153
- }
154
- const changedPaths = changedPathsOf(solutionSectionOf(content));
155
- if (changedPaths.length === 0) {
156
- reasons.push(`${wbs}: Solution empty or unparseable`);
157
- continue;
158
- }
159
- for (const path of changedPaths) {
160
- if (CORPUS_PREFIXES.some((prefix) => path.startsWith(prefix)))
161
- continue;
162
- allPaths.add(path);
163
- const drift = driftReasonForPath(path);
164
- if (drift)
165
- reasons.push(`${wbs}: ${path} ${drift}`);
166
- }
167
- }
168
- return finish(reasons.length === 0, reasons, [...allPaths].sort());
169
- }
170
- var WRAPUP_DRIFT_PROBE_USAGE = "usage: wrapup-drift-probe (env: __runId, spurBin)";
171
- function main(argv, env = getEnvVars(), options = {}) {
172
- if (argv.length > 0) {
173
- process.stderr.write(`${WRAPUP_DRIFT_PROBE_USAGE}
174
- `);
175
- return 2;
176
- }
177
- return runDriftProbe(env, options).exitCode;
178
- }
179
- {
180
- process.exit(main(process.argv.slice(2)));
181
- }
182
- export {
183
- DOC_OWNED_SURFACES,
184
- WRAPUP_DRIFT_PROBE_USAGE,
185
- changedPathsOf,
186
- driftReasonForPath,
187
- main,
188
- runDriftProbe,
189
- solutionSectionOf
190
- };