@gobing-ai/spur 0.4.8 → 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,627 +0,0 @@
1
- #!/usr/bin/env node
2
- // @bun
3
-
4
- // plugins/sp/scripts/wrapup-steps.ts
5
- import { spawnSync } from "child_process";
6
- import { appendFileSync, existsSync, 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-steps.ts
21
- function jqPick(...values) {
22
- for (const value of values) {
23
- if (value !== null && value !== undefined && value !== false)
24
- return value;
25
- }
26
- return values[values.length - 1];
27
- }
28
- function jqText(value) {
29
- return typeof value === "string" ? value : JSON.stringify(value);
30
- }
31
- var WBS_PATTERN = /^[0-9]{4}$/;
32
- function spur(env, args, options = {}) {
33
- const { cmd, prefix } = spurCommand(env.spurBin);
34
- const result = spawnSync(cmd, [...prefix, ...args], {
35
- cwd: options.cwd,
36
- encoding: "utf8",
37
- ...options.stderr === "inherit" ? { stdio: ["ignore", "pipe", "inherit"] } : {}
38
- });
39
- if (result.error !== undefined)
40
- return { status: result.status ?? 1, stdout: "" };
41
- return { status: result.status ?? 1, stdout: result.stdout ?? "" };
42
- }
43
- function taskStatusOf(taskJson) {
44
- let parsed;
45
- try {
46
- parsed = JSON.parse(taskJson);
47
- } catch {
48
- return { resolved: null, present: false };
49
- }
50
- if (parsed === null || typeof parsed !== "object")
51
- return { resolved: null, present: false };
52
- const frontmatter = parsed.frontmatter;
53
- const fmStatus = frontmatter !== null && typeof frontmatter === "object" ? frontmatter.status : undefined;
54
- const status = parsed.status;
55
- if (fmStatus === null || fmStatus === undefined) {
56
- return { resolved: status ?? null, present: status !== undefined && status !== null };
57
- }
58
- return { resolved: fmStatus, present: true };
59
- }
60
- function statusDisplay(taskJson) {
61
- const { resolved } = taskStatusOf(taskJson);
62
- if (resolved === null || resolved === undefined || resolved === false)
63
- return "unresolved";
64
- return jqText(resolved);
65
- }
66
- function resolveTasks(env, options = {}) {
67
- const cwd = options.cwd;
68
- const runId = env.__runId ?? "";
69
- if (runId.length === 0) {
70
- process.stderr.write(`task-resolve: __runId is empty \u2014 refusing the legacy fixed-path fallback
71
- `);
72
- return { status: "FAIL", statusFile: "", tasksFile: "", exitCode: 1 };
73
- }
74
- mkdirSync(cwd ? join(cwd, ".spur", "run") : join(".spur", "run"), { recursive: true });
75
- const relTasksFile = join(".spur", "run", `${runId}-wrapup-tasks.json`);
76
- const relReasonFile = join(".spur", "run", `${runId}-route-reason.txt`);
77
- const relStatusFile = join(".spur", "run", `${runId}-wrapup-resolve.status`);
78
- const abs = (p) => cwd ? join(cwd, p) : p;
79
- const writeFail = (reason) => {
80
- writeFileSync(abs(relReasonFile), reason);
81
- writeFileSync(abs(relStatusFile), `FAIL
82
- `);
83
- return { status: "FAIL", statusFile: relStatusFile, tasksFile: relTasksFile, exitCode: 0 };
84
- };
85
- let parsedTasks;
86
- try {
87
- parsedTasks = JSON.parse(env.tasks ?? "");
88
- } catch {
89
- parsedTasks = undefined;
90
- }
91
- const validArray = Array.isArray(parsedTasks) && parsedTasks.every((w) => typeof w === "string" && WBS_PATTERN.test(w));
92
- if (!validArray) {
93
- process.stderr.write(`task-resolve: tasks must be a JSON array of canonical four-digit WBS strings (whitespace is rejected, not trimmed)
94
- `);
95
- return writeFail("failed:tasks is not a JSON array of canonical four-digit WBS strings");
96
- }
97
- const deduped = [];
98
- for (const wbs of parsedTasks) {
99
- if (!deduped.includes(wbs))
100
- deduped.push(wbs);
101
- }
102
- writeFileSync(abs(relTasksFile), `${JSON.stringify(deduped)}
103
- `);
104
- let unresolved = false;
105
- for (const wbs of deduped) {
106
- const shown = spur(env, ["task", "show", wbs, "--json"], { cwd });
107
- const status = shown.status === 0 ? statusDisplay(shown.stdout) : "unresolved";
108
- if (status !== "done" && status !== "cancelled") {
109
- process.stderr.write(`task-resolve: task ${wbs} did not resolve to a completed status (status=${status})
110
- `);
111
- unresolved = true;
112
- }
113
- }
114
- if (unresolved) {
115
- return writeFail(`failed:unresolved or non-completed task (see ${relTasksFile})`);
116
- }
117
- if ((env.feature ?? "") !== "") {
118
- const pf = preflightFeature(env, cwd);
119
- if (pf)
120
- return writeFail(pf);
121
- }
122
- writeFileSync(abs(relStatusFile), `PASS
123
- `);
124
- return { status: "PASS", statusFile: relStatusFile, tasksFile: relTasksFile, exitCode: 0 };
125
- }
126
- function isFindingList(value) {
127
- return Array.isArray(value) && value.every((f) => f !== null && typeof f === "object" && typeof f.severity === "string" && typeof f.code === "string" && f.code.length > 0);
128
- }
129
- function sortedUniqueCodes(findings) {
130
- const codes = findings.filter((f) => f.severity === "error").map((f) => String(f.code ?? "")).filter((c) => c.length > 0);
131
- return [...new Set(codes)].sort();
132
- }
133
- function preflightFeature(env, cwd) {
134
- const feature = env.feature ?? "";
135
- const abs = (p) => cwd ? join(cwd, p) : p;
136
- const syncOut = spur(env, ["feature", "sync", feature, "--dry-run", "--json"], { cwd });
137
- let sync;
138
- if (syncOut.status === 0) {
139
- try {
140
- const parsed = JSON.parse(syncOut.stdout);
141
- if (parsed !== null && typeof parsed === "object")
142
- sync = parsed;
143
- } catch {}
144
- }
145
- if (sync === undefined)
146
- return "failed:preflight:sync-unreadable";
147
- const proposal = sync.proposal;
148
- if (proposal === null || typeof proposal !== "object" || Array.isArray(proposal) || proposal.gateBlocked !== undefined && typeof proposal.gateBlocked !== "boolean" || proposal.gateFindings !== undefined && !isFindingList(proposal.gateFindings) || proposal.hops !== undefined && (!Array.isArray(proposal.hops) || !proposal.hops.every((hop) => typeof hop === "string")) || proposal.gateBlocked !== true && (typeof proposal.from !== "string" || typeof proposal.to !== "string")) {
149
- return "failed:preflight:sync-unreadable";
150
- }
151
- if (proposal.gateBlocked === true) {
152
- return `failed:preflight:gate-blocked ${sortedUniqueCodes(proposal.gateFindings ?? []).join(",")}`;
153
- }
154
- const reachesDone = proposal.to === "done" || (proposal.hops ?? []).includes("done");
155
- if (!reachesDone)
156
- return null;
157
- const checkOut = spur(env, ["feature", "check", feature, "--strict", "--as", "done", "--json"], { cwd });
158
- let check;
159
- try {
160
- check = JSON.parse(checkOut.stdout);
161
- } catch {
162
- return "failed:preflight:check-unreadable";
163
- }
164
- const payload = Array.isArray(check) && check.length === 1 ? check[0] : check;
165
- const findings = payload !== null && typeof payload === "object" ? payload.findings : undefined;
166
- if (!isFindingList(findings) || checkOut.status !== 0 && (checkOut.status !== 1 || !findings.some((f) => f.severity === "error"))) {
167
- return "failed:preflight:check-unreadable";
168
- }
169
- const eligible = proposal.from === "verifying" ? findings : findings.filter((f) => !String(f.code ?? "").startsWith("L4.feature-receipt-"));
170
- const runId = env.__runId ?? "";
171
- if (runId.length > 0) {
172
- mkdirSync(abs(join(".spur", "run")), { recursive: true });
173
- writeFileSync(abs(join(".spur", "run", `${runId}-wrapup-preflight.json`)), `${JSON.stringify(check)}
174
- `);
175
- }
176
- const codes = sortedUniqueCodes(eligible);
177
- return codes.length > 0 ? `failed:preflight:done-gate ${codes.join(",")}` : null;
178
- }
179
- var ROUTE_REASON_TABLE = {
180
- fast: "fast:evidence complete+consistent",
181
- "": "safety:missing evidence (mode empty)",
182
- unknown: "safety:unknown evidence quality",
183
- conflict: "safety:conflicting evidence",
184
- safety: "safety:operator-forced doc-sync"
185
- };
186
- function writeRouteReason(env, options = {}) {
187
- const cwd = options.cwd;
188
- const runId = env.__runId ?? "";
189
- if (runId.length === 0) {
190
- process.stderr.write(`task-resolve: __runId is empty \u2014 refusing to write a route reason
191
- `);
192
- return { reason: "", reasonFile: "", exitCode: 1 };
193
- }
194
- mkdirSync(cwd ? join(cwd, ".spur", "run") : join(".spur", "run"), { recursive: true });
195
- mkdirSync(cwd ? join(cwd, ".spur", "memory") : join(".spur", "memory"), { recursive: true });
196
- const relReasonFile = join(".spur", "run", `${runId}-route-reason.txt`);
197
- const abs = (p) => cwd ? join(cwd, p) : p;
198
- const statusFile = join(".spur", "run", `${runId}-wrapup-resolve.status`);
199
- if (readFileSyncSafe(abs(statusFile))?.trim() === "FAIL") {
200
- return { reason: "", reasonFile: relReasonFile, exitCode: 0 };
201
- }
202
- let taskCount = -1;
203
- try {
204
- const parsed = JSON.parse(readFileSync(abs(join(".spur", "run", `${runId}-wrapup-tasks.json`)), "utf8"));
205
- if (Array.isArray(parsed))
206
- taskCount = parsed.length;
207
- } catch {}
208
- let probeClean = false;
209
- try {
210
- const probe = JSON.parse(readFileSync(abs(join(".spur", "run", `${runId}-drift-probe.json`)), "utf8"));
211
- probeClean = probe?.clean === true;
212
- } catch {}
213
- const mode = env.mode ?? "";
214
- let reason;
215
- if (taskCount === 0) {
216
- reason = "skipped:empty task list";
217
- } else if (mode === "fast" && probeClean) {
218
- reason = "fast:drift-probe-clean";
219
- } else {
220
- reason = ROUTE_REASON_TABLE[mode] ?? `safety:unrecognized evidence (mode=${mode})`;
221
- }
222
- writeFileSync(abs(relReasonFile), `${reason}
223
- `);
224
- appendFileSync(abs(join(".spur", "memory", "wrapup-routes.log")), `${runId} ${reason}
225
- `);
226
- return { reason, reasonFile: relReasonFile, exitCode: 0 };
227
- }
228
- function readFileSyncSafe(path) {
229
- try {
230
- return readFileSync(path, "utf8");
231
- } catch {
232
- return null;
233
- }
234
- }
235
- function verdictFromTestingSection(content) {
236
- const heading = /^#{1,6}\s+Testing\s*$/m.exec(content);
237
- if (!heading)
238
- return null;
239
- const level = heading[0].match(/^#+/)?.[0]?.length ?? 2;
240
- const rest = content.slice(heading.index + heading[0].length);
241
- const next = new RegExp(`^#{1,${level}}\\s+\\S`, "m").exec(rest);
242
- const section = next ? rest.slice(0, next.index) : rest;
243
- for (const line of section.split(`
244
- `)) {
245
- const m = /^(?:-\s*|\*\*)?Verdict:\s*(PASS|PARTIAL|FAIL|UNKNOWN)\b/i.exec(line.trim());
246
- if (m?.[1] !== undefined)
247
- return m[1].toUpperCase();
248
- }
249
- return null;
250
- }
251
- function verdictOfArtifact(verdictPath) {
252
- try {
253
- const raw = jqPick(JSON.parse(readFileSync(verdictPath, "utf8")).verdict, "");
254
- const text = jqText(raw);
255
- return text.length > 0 ? text : null;
256
- } catch {
257
- return null;
258
- }
259
- }
260
- function runMetrics(env, options = {}) {
261
- const cwd = options.cwd;
262
- const runId = env.__runId ?? "";
263
- mkdirSync(cwd ? join(cwd, ".spur", "run") : join(".spur", "run"), { recursive: true });
264
- mkdirSync(cwd ? join(cwd, ".spur", "memory") : join(".spur", "memory"), { recursive: true });
265
- const relStatusFile = join(".spur", "run", `${runId}-wrapup-metrics.status`);
266
- const relTasksFile = join(".spur", "run", `${runId}-wrapup-tasks.json`);
267
- const relMetricsFile = join(".spur", "memory", "wrapup-metrics.jsonl");
268
- const abs = (p) => cwd ? join(cwd, p) : p;
269
- const fail = () => {
270
- writeFileSync(abs(relStatusFile), `FAIL
271
- `);
272
- return { status: "FAIL", statusFile: relStatusFile };
273
- };
274
- let captured;
275
- try {
276
- captured = JSON.parse(readFileSync(abs(relTasksFile), "utf8"));
277
- } catch {
278
- captured = undefined;
279
- }
280
- const validCapture = Array.isArray(captured) && captured.every((w) => typeof w === "string" && WBS_PATTERN.test(w));
281
- if (!validCapture) {
282
- process.stderr.write(`metrics-record: run-scoped task capture missing, corrupted or non-canonical \u2014 refusing to record metrics
283
- `);
284
- return fail();
285
- }
286
- let metricsRc = 0;
287
- for (const wbs of captured) {
288
- const shown = spur(env, ["task", "show", wbs, "--json"], { cwd });
289
- const lookup = shown.status === 0 ? taskStatusOf(shown.stdout) : { resolved: null, present: false };
290
- if (!lookup.present) {
291
- process.stderr.write(`metrics-record: task ${wbs} lookup failed or was malformed \u2014 recording FAIL instead of silently omitting its metrics row
292
- `);
293
- metricsRc = 1;
294
- continue;
295
- }
296
- const parsed = JSON.parse(shown.stdout);
297
- const frontmatter = parsed.frontmatter !== null && typeof parsed.frontmatter === "object" ? parsed.frontmatter : {};
298
- const featureId = String(jqPick(frontmatter.feature_id, parsed.feature_id, ""));
299
- const status = String(jqPick(frontmatter.status, parsed.status, "unknown"));
300
- const durable = join(".spur", "memory", "evidence", `${wbs}-verdict.json`);
301
- const hasDurable = existsSync(abs(durable));
302
- const verdictPath = hasDurable ? durable : join(".spur", "run", `${wbs}-verdict.json`);
303
- const artifactVerdict = verdictOfArtifact(abs(verdictPath)) ?? (hasDurable ? "UNKNOWN" : null);
304
- const trackedVerdict = verdictFromTestingSection(typeof parsed.content === "string" ? parsed.content : "");
305
- const verdict = artifactVerdict ?? trackedVerdict ?? "UNKNOWN";
306
- if (verdict === "UNKNOWN") {
307
- process.stderr.write(`metrics-record: task ${wbs} has no certifying verdict \u2014 ${verdictPath}: ${artifactVerdict ?? "missing or carries none"}, tracked Testing: ${trackedVerdict ?? "no Verdict: line"} \u2014 recording UNKNOWN telemetry
308
- `);
309
- }
310
- const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
311
- const row = { wbs, feature_id: featureId, status, verdict, timestamp };
312
- try {
313
- appendFileSync(abs(relMetricsFile), `${JSON.stringify(row)}
314
- `);
315
- } catch {
316
- process.stderr.write(`metrics-record: metrics append failed for task ${wbs} \u2014 recording FAIL instead of claiming the row landed
317
- `);
318
- metricsRc = 1;
319
- }
320
- }
321
- const status = metricsRc === 0 ? "PASS" : "FAIL";
322
- writeFileSync(abs(relStatusFile), `${status}
323
- `);
324
- return { status, statusFile: relStatusFile };
325
- }
326
- function classifySync(syncOutput, syncRc, feature, observed) {
327
- if (syncRc !== 0) {
328
- return { reason: `sync exited nonzero (rc=${syncRc})`, applied: "unreadable", syncOk: false };
329
- }
330
- let parsed;
331
- try {
332
- parsed = JSON.parse(syncOutput);
333
- } catch {
334
- parsed = undefined;
335
- }
336
- const obj = parsed !== null && typeof parsed === "object" ? parsed : undefined;
337
- const proposal = obj?.proposal !== null && typeof obj?.proposal === "object" ? obj.proposal : undefined;
338
- const shapeOk = obj !== undefined && proposal !== undefined && typeof proposal.featureId === "string" && typeof proposal.from === "string" && typeof proposal.to === "string" && typeof obj.applied === "boolean";
339
- if (!shapeOk) {
340
- return { reason: "malformed or unreadable sync result", applied: "unreadable", syncOk: false };
341
- }
342
- const applied = obj.applied === true ? "true" : "false";
343
- const pFrom = String(jqPick(proposal.from, ""));
344
- const pTo = String(jqPick(proposal.to, ""));
345
- if (String(jqPick(proposal.featureId, "")) !== feature) {
346
- return { reason: `sync proposal does not match feature ${feature}`, applied, syncOk: false };
347
- }
348
- if (proposal.gateBlocked === true) {
349
- return {
350
- reason: "sync proposal is gate-blocked \u2014 a blocked sync is not a no-change success",
351
- applied,
352
- syncOk: false
353
- };
354
- }
355
- if (proposal.requiresConfirm === true) {
356
- return { reason: "sync proposal requires operator confirmation", applied, syncOk: false };
357
- }
358
- if (applied === "true" && observed !== pTo) {
359
- return {
360
- reason: `applied sync did not land on the proposal target (observed=${observed}, to=${pTo})`,
361
- applied,
362
- syncOk: false
363
- };
364
- }
365
- if (applied === "false" && (pFrom !== pTo || observed !== pTo)) {
366
- return {
367
- reason: `sync applied nothing without a from==to observed no-op (from=${pFrom}, to=${pTo}, observed=${observed})`,
368
- applied,
369
- syncOk: false
370
- };
371
- }
372
- return { reason: "", applied, syncOk: true };
373
- }
374
- function runFeatureTransition(env, options = {}) {
375
- const cwd = options.cwd;
376
- const runId = env.__runId ?? "";
377
- const feature = env.feature ?? "";
378
- mkdirSync(cwd ? join(cwd, ".spur", "run") : join(".spur", "run"), { recursive: true });
379
- const relStatusFile = join(".spur", "run", `${runId}-wrapup-sync.status`);
380
- const abs = (p) => cwd ? join(cwd, p) : p;
381
- if (feature.length === 0) {
382
- process.stderr.write(`feature-transition: vars.feature is empty \u2014 refusing no-op feature sync (mis-invocation, not a blocked sync)
383
- `);
384
- return { status: "FAIL", statusFile: relStatusFile, exitCode: 1 };
385
- }
386
- let syncOutput = "";
387
- let syncRc = 1;
388
- const sync = spur(env, ["feature", "sync", feature, "--json"], { cwd, stderr: "inherit" });
389
- syncOutput = sync.stdout;
390
- syncRc = sync.status;
391
- process.stdout.write(`${syncOutput}
392
- `);
393
- const shown = spur(env, ["feature", "show", feature, "--json"], { cwd });
394
- let observed = "";
395
- if (shown.status === 0) {
396
- try {
397
- const parsed = JSON.parse(shown.stdout);
398
- const frontmatter = parsed.frontmatter !== null && typeof parsed.frontmatter === "object" ? parsed.frontmatter : {};
399
- const picked = jqPick(parsed.status, frontmatter.status, "");
400
- observed = picked === "" ? "" : jqText(picked);
401
- } catch {
402
- observed = "";
403
- }
404
- }
405
- if (observed.length === 0)
406
- observed = "unreadable";
407
- const classified = classifySync(syncOutput, syncRc, feature, observed);
408
- const { reason, applied } = classified;
409
- const syncOk = classified.syncOk;
410
- let gate = "skipped";
411
- if (applied === "true" || syncRc !== 0) {
412
- process.stdout.write(`feature-transition: sync applied or failed after a possible partial transition for ${feature} \u2014 running feature gate: ${env.featureGateCmd ?? ""}
413
- `);
414
- const gateResult = spawnSync("sh", ["-c", env.featureGateCmd ?? ""], { cwd, stdio: "inherit" });
415
- if ((gateResult.status ?? 1) === 0) {
416
- gate = "PASS";
417
- process.stdout.write(`feature-transition: feature gate PASS for feature ${feature}
418
- `);
419
- } else {
420
- gate = "FAIL";
421
- process.stderr.write(`feature-transition: feature gate FAIL for feature ${feature} \u2014 inspect findings before reporting the transition complete
422
- `);
423
- }
424
- } else {
425
- process.stdout.write(`feature-transition: sync did not apply a transition (rc=${syncRc}, applied=${applied}) \u2014 feature gate skipped
426
- `);
427
- }
428
- let syncStatus;
429
- if (!syncOk || gate === "FAIL") {
430
- syncStatus = "FAIL";
431
- process.stderr.write(`feature-transition: required synchronization failed for ${feature} \u2014 ${reason}; gate=${gate}
432
- `);
433
- } else if (applied === "false") {
434
- syncStatus = "PASS";
435
- process.stdout.write(`feature-transition: feature sync verified for ${feature} (from==to observed at ${observed}, gate=${gate}) \u2014 explicit no-change
436
- `);
437
- } else {
438
- syncStatus = "PASS";
439
- process.stdout.write(`feature-transition: feature sync verified for ${feature} (applied, observed=${observed}, gate=${gate})
440
- `);
441
- }
442
- writeFileSync(abs(relStatusFile), `${syncStatus}
443
- `);
444
- return { status: syncStatus, statusFile: relStatusFile, exitCode: 0 };
445
- }
446
- var DOC_SYNC_SNAPSHOT_SUFFIX = "wrapup-doc-sync-snapshot.txt";
447
- var DOC_SYNC_SCOPE_STATUS_SUFFIX = "wrapup-doc-sync-scope.status";
448
- var DOC_SYNC_SUPERSESSION_STATUS_SUFFIX = "wrapup-doc-supersession.status";
449
- var SUPERSESSION_PIN = "repo-wide-tests/adr-supersession.test.ts";
450
- var GIT_UNAVAILABLE_MARKER = "# git-unavailable:";
451
- var SNAPSHOT_MISSING = "snapshot-missing";
452
- function porcelainPath(line) {
453
- const body = line.length >= 3 ? line.slice(3) : line;
454
- const arrow = body.indexOf(" -> ");
455
- const target = arrow >= 0 ? body.slice(arrow + 4) : body;
456
- const trimmed = target.trim();
457
- return trimmed.startsWith('"') && trimmed.endsWith('"') && trimmed.length >= 2 ? trimmed.slice(1, -1) : trimmed;
458
- }
459
- function isDocSyncAllowedPath(path, allowedExtra) {
460
- if (allowedExtra.includes(path))
461
- return true;
462
- if (!path.startsWith("docs/"))
463
- return false;
464
- return !path.startsWith("docs/tasks") && !path.startsWith("docs/features");
465
- }
466
- function docSyncScopeViolations(snapshot, current, allowedExtra) {
467
- const before = new Set(snapshot.split(`
468
- `).filter((line) => line.trim() !== ""));
469
- const paths = new Set;
470
- for (const line of current.split(`
471
- `)) {
472
- if (line.trim() === "" || before.has(line))
473
- continue;
474
- const path = porcelainPath(line);
475
- if (!isDocSyncAllowedPath(path, allowedExtra))
476
- paths.add(path);
477
- }
478
- return [...paths].sort();
479
- }
480
- function gitPorcelain(cwd) {
481
- const result = spawnSync("git", ["status", "--porcelain", "-uall"], {
482
- cwd,
483
- encoding: "utf8"
484
- });
485
- if (result.error !== undefined)
486
- return { ok: false, listing: "", detail: String(result.error) };
487
- if (result.status !== 0)
488
- return { ok: false, listing: "", detail: (result.stderr ?? "").trim() };
489
- return { ok: true, listing: result.stdout ?? "", detail: "" };
490
- }
491
- function wrapupRunPaths(env, cwd) {
492
- const runId = env.__runId ?? "";
493
- const rel = (suffix) => join(".spur", "run", `${runId}-${suffix}`);
494
- return { rel, abs: (suffix) => cwd ? join(cwd, rel(suffix)) : rel(suffix) };
495
- }
496
- function docSyncAllowedExtra(env) {
497
- const runId = env.__runId ?? "";
498
- return [
499
- join(".spur", "run", `${runId}-wrapup-learnings.md`),
500
- join(".spur", "run", `${runId}-${DOC_SYNC_SNAPSHOT_SUFFIX}`),
501
- join(".spur", "run", `${runId}-${DOC_SYNC_SCOPE_STATUS_SUFFIX}`)
502
- ];
503
- }
504
- function runDocSyncSnapshot(env, options = {}) {
505
- const cwd = options.cwd;
506
- const paths = wrapupRunPaths(env, cwd);
507
- mkdirSync(cwd ? join(cwd, ".spur", "run") : join(".spur", "run"), { recursive: true });
508
- writeFileSync(paths.abs(DOC_SYNC_SNAPSHOT_SUFFIX), "");
509
- const listing = gitPorcelain(cwd);
510
- if (!listing.ok) {
511
- writeFileSync(paths.abs(DOC_SYNC_SNAPSHOT_SUFFIX), `${GIT_UNAVAILABLE_MARKER} ${listing.detail}
512
- `);
513
- process.stderr.write(`doc-sync-snapshot: git status unavailable \u2014 scope check skips (${listing.detail})
514
- `);
515
- return { statusFile: paths.rel(DOC_SYNC_SNAPSHOT_SUFFIX), captured: false, exitCode: 0 };
516
- }
517
- writeFileSync(paths.abs(DOC_SYNC_SNAPSHOT_SUFFIX), listing.listing);
518
- return { statusFile: paths.rel(DOC_SYNC_SNAPSHOT_SUFFIX), captured: true, exitCode: 0 };
519
- }
520
- function runDocSyncScope(env, options = {}) {
521
- const cwd = options.cwd;
522
- const paths = wrapupRunPaths(env, cwd);
523
- mkdirSync(cwd ? join(cwd, ".spur", "run") : join(".spur", "run"), { recursive: true });
524
- const statusFile = paths.rel(DOC_SYNC_SCOPE_STATUS_SUFFIX);
525
- const write = (status, detail) => {
526
- writeFileSync(paths.abs(DOC_SYNC_SCOPE_STATUS_SUFFIX), `${detail}
527
- `);
528
- if (status !== "PASS")
529
- process.stderr.write(`doc-sync-scope: ${detail}
530
- `);
531
- return { status, statusFile, violations: status === "PASS" ? [] : [detail], exitCode: 0 };
532
- };
533
- const snapshot = readFileSyncSafe(paths.abs(DOC_SYNC_SNAPSHOT_SUFFIX));
534
- if (snapshot === null) {
535
- return write("scope-violation", `scope-violation: ${SNAPSHOT_MISSING}`);
536
- }
537
- if (snapshot.startsWith(GIT_UNAVAILABLE_MARKER)) {
538
- process.stderr.write(`doc-sync-scope: entry snapshot recorded no git listing \u2014 scope check skipped
539
- `);
540
- return write("PASS", "PASS");
541
- }
542
- const current = gitPorcelain(cwd);
543
- if (!current.ok) {
544
- return write("scope-violation", `scope-violation: git-unavailable (${current.detail})`);
545
- }
546
- const violations = docSyncScopeViolations(snapshot, current.listing, docSyncAllowedExtra(env));
547
- if (violations.length === 0)
548
- return write("PASS", "PASS");
549
- return write("scope-violation", `scope-violation: ${violations.join(" ")}`);
550
- }
551
- function runDocSupersessionCheck(env, options = {}) {
552
- const cwd = options.cwd;
553
- const paths = wrapupRunPaths(env, cwd);
554
- mkdirSync(cwd ? join(cwd, ".spur", "run") : join(".spur", "run"), { recursive: true });
555
- const statusFile = paths.rel(DOC_SYNC_SUPERSESSION_STATUS_SUFFIX);
556
- const pinPath = cwd ? join(cwd, SUPERSESSION_PIN) : SUPERSESSION_PIN;
557
- if (!existsSync(pinPath)) {
558
- writeFileSync(paths.abs(DOC_SYNC_SUPERSESSION_STATUS_SUFFIX), `PASS
559
- `);
560
- return { status: "PASS", statusFile, exitCode: 0 };
561
- }
562
- const pin = spawnSync("bun", ["test", SUPERSESSION_PIN], { cwd, encoding: "utf8" });
563
- if (pin.status === 0) {
564
- writeFileSync(paths.abs(DOC_SYNC_SUPERSESSION_STATUS_SUFFIX), `PASS
565
- `);
566
- return { status: "PASS", statusFile, exitCode: 0 };
567
- }
568
- writeFileSync(paths.abs(DOC_SYNC_SUPERSESSION_STATUS_SUFFIX), `supersession-pin-failed: ${SUPERSESSION_PIN}
569
- `);
570
- const detail = `${pin.stdout ?? ""}${pin.stderr ?? ""}`.trim();
571
- process.stderr.write(`doc-supersession: ${SUPERSESSION_PIN} failed (exit ${String(pin.status ?? 1)}) \u2014 the doc-sync step introduced or kept superseded index content:
572
- ${detail.slice(-2000)}
573
- `);
574
- return { status: "supersession-pin-failed", statusFile, exitCode: 0 };
575
- }
576
- var WRAPUP_STEPS_USAGE = "usage: wrapup-steps.ts <resolve|route-reason|metrics|feature-transition|doc-sync-snapshot|doc-sync-scope|doc-supersession> (env: __runId, tasks, mode, feature, featureGateCmd, spurBin)";
577
- function main(argv, env = getEnvVars(), options = {}) {
578
- const sub = argv[0];
579
- if (sub === "resolve")
580
- return resolveTasks(env, options).exitCode;
581
- if (sub === "route-reason")
582
- return writeRouteReason(env, options).exitCode;
583
- if (sub === "metrics") {
584
- runMetrics(env, options);
585
- return 0;
586
- }
587
- if (sub === "feature-transition")
588
- return runFeatureTransition(env, options).exitCode;
589
- if (sub === "doc-sync-snapshot")
590
- return runDocSyncSnapshot(env, options).exitCode;
591
- if (sub === "doc-sync-scope")
592
- return runDocSyncScope(env, options).exitCode;
593
- if (sub === "doc-supersession")
594
- return runDocSupersessionCheck(env, options).exitCode;
595
- process.stderr.write(`${WRAPUP_STEPS_USAGE}
596
- `);
597
- return 2;
598
- }
599
- {
600
- process.exit(main(process.argv.slice(2)));
601
- }
602
- export {
603
- DOC_SYNC_SCOPE_STATUS_SUFFIX,
604
- DOC_SYNC_SNAPSHOT_SUFFIX,
605
- DOC_SYNC_SUPERSESSION_STATUS_SUFFIX,
606
- GIT_UNAVAILABLE_MARKER,
607
- SNAPSHOT_MISSING,
608
- SUPERSESSION_PIN,
609
- WBS_PATTERN,
610
- WRAPUP_STEPS_USAGE,
611
- classifySync,
612
- docSyncScopeViolations,
613
- isDocSyncAllowedPath,
614
- jqPick,
615
- main,
616
- porcelainPath,
617
- preflightFeature,
618
- resolveTasks,
619
- runDocSupersessionCheck,
620
- runDocSyncScope,
621
- runDocSyncSnapshot,
622
- runFeatureTransition,
623
- runMetrics,
624
- taskStatusOf,
625
- verdictFromTestingSection,
626
- writeRouteReason
627
- };