@gobing-ai/spur 0.3.80 → 0.3.81

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 (158) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/config.example.yaml +29 -18
  3. package/config/config.global.yaml +10 -11
  4. package/config/pipeline-budgets.json +34 -2
  5. package/config/plugin-scripts.json +25 -0
  6. package/config/rules/boundary/config-loading-ownership.yaml +0 -3
  7. package/config/rules/boundary/dao-boundary.yaml +4 -17
  8. package/config/rules/boundary/planning-folder-hardcode.yaml +0 -1
  9. package/config/rules/boundary/sp-no-vendor-refs.yaml +3 -2
  10. package/config/rules/boundary/sp-runtime-path.yaml +3 -14
  11. package/config/rules/quality/coverage-gate.yaml +3 -14
  12. package/config/rules/quality/tsdoc-exports.yaml +4 -7
  13. package/config/rules/strict/http-boundaries.yaml +5 -8
  14. package/config/rules/strict/runtime-boundaries.yaml +1 -5
  15. package/config/rules/structure/protected-files.yaml +9 -3
  16. package/config/rules/structure/test-focus-skip.yaml +0 -2
  17. package/config/rules/structure/test-location.yaml +0 -5
  18. package/config/rules/surface/check-cli-surface.yaml +3 -2
  19. package/config/rules/typescript/bun-tooling.yaml +5 -7
  20. package/config/rules/typescript/guarded-happy-dom-register.yaml +0 -2
  21. package/config/rules/typescript/happy-dom-teardown.yaml +0 -2
  22. package/config/rules/typescript/no-biome-suppressions.yaml +0 -2
  23. package/config/rules/typescript/no-debugger.yaml +0 -2
  24. package/config/rules/typescript/no-eslint-suppressions.yaml +0 -4
  25. package/config/rules/typescript/no-leaky-module-mocks.yaml +6 -13
  26. package/config/rules/typescript/no-module-scope-import-calls.yaml +0 -2
  27. package/config/rules/typescript/no-syscall-emulation-in-boundary-mock.yaml +0 -3
  28. package/config/rules/typescript/no-unmocked-module-eval-side-effects.yaml +0 -3
  29. package/config/rules/typescript/output-boundaries.yaml +0 -3
  30. package/config/rules/typescript/prefer-accessible-role-for-button-queries.yaml +0 -3
  31. package/config/rules/ui/ui-import-boundary.yaml +1 -5
  32. package/config/transition-shims.json +7 -7
  33. package/config/workflows/basic.yaml +4 -0
  34. package/config/workflows/docs-pipeline.yaml +13 -14
  35. package/config/workflows/feature-dev.yaml +20 -65
  36. package/config/workflows/history-anatomy.yaml +22 -1
  37. package/config/workflows/idea-pipeline.yaml +53 -97
  38. package/config/workflows/pr-review.yaml +21 -33
  39. package/config/workflows/task-pipeline.yaml +87 -330
  40. package/config/workflows/wayfinder-resolution.yaml +12 -26
  41. package/config/workflows/wrapup-pipeline.yaml +48 -189
  42. package/package.json +9 -9
  43. package/plugins/sp/README.md +10 -1
  44. package/plugins/sp/agents/expert-spur.md +41 -19
  45. package/plugins/sp/lib/idea-handoff.generated.d.mts +17 -0
  46. package/plugins/sp/lib/idea-handoff.generated.mjs +1301 -0
  47. package/plugins/sp/plugin.json +1 -1
  48. package/plugins/sp/scripts/feature-dev-precheck.mjs +146 -0
  49. package/plugins/sp/scripts/feature-dev-precheck.ts +238 -0
  50. package/plugins/sp/scripts/idea-handoff.mjs +27 -0
  51. package/plugins/sp/scripts/idea-handoff.ts +44 -0
  52. package/plugins/sp/scripts/quality-gate.mjs +165 -0
  53. package/plugins/sp/scripts/quality-gate.ts +217 -0
  54. package/plugins/sp/scripts/workflow-step-profile.mjs +319 -0
  55. package/plugins/sp/scripts/workflow-step-profile.ts +456 -0
  56. package/plugins/sp/scripts/wrapup-steps.mjs +350 -0
  57. package/plugins/sp/scripts/wrapup-steps.ts +466 -0
  58. package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +1 -1
  59. package/plugins/sp/skills/spec-decomposition/references/decomposition.md +29 -0
  60. package/plugins/sp/skills/spur-cli/references/agent.md +56 -14
  61. package/plugins/sp/skills/spur-cli/references/message.md +30 -3
  62. package/plugins/sp/skills/spur-cli/references/projects.md +45 -1
  63. package/plugins/sp/skills/spur-cli/references/self.md +5 -4
  64. package/plugins/sp/skills/spur-cli/references/serve.md +5 -4
  65. package/plugins/sp/skills/spur-cli/references/tasks.md +1 -1
  66. package/plugins/sp/skills/spur-cli/references/team.md +21 -1
  67. package/plugins/sp/skills/spur-cli/references/workflows/operations.md +6 -3
  68. package/plugins/sp/skills/spur-cli/references/workflows/workflow-fit-and-tuning.md +57 -18
  69. package/plugins/sp/skills/spur-composer/SKILL.md +145 -0
  70. package/plugins/sp/skills/spur-dev/references/planning-workflow.md +24 -0
  71. package/plugins/sp/skills/spur-doctor/SKILL.md +138 -0
  72. package/plugins/sp/skills/taste-refactoring-api/README.md +43 -0
  73. package/plugins/sp/skills/taste-refactoring-api/SKILL.md +334 -0
  74. package/plugins/sp/skills/taste-refactoring-api/checklists/daily-api-review.md +71 -0
  75. package/plugins/sp/skills/taste-refactoring-api/examples/refactor-example.md +72 -0
  76. package/plugins/sp/skills/taste-refactoring-api/examples/review-template.md +93 -0
  77. package/plugins/sp/skills/taste-refactoring-api/references/api-refactoring-playbook.md +253 -0
  78. package/plugins/sp/skills/taste-refactoring-api/references/protocol-modes.md +79 -0
  79. package/plugins/sp/skills/taste-refactoring-api/references/research-basis.md +58 -0
  80. package/plugins/sp/skills/taste-refactoring-architect/README.md +26 -0
  81. package/plugins/sp/skills/taste-refactoring-architect/SKILL.md +471 -0
  82. package/plugins/sp/skills/taste-refactoring-architect/checklists/daily-architecture-review.md +48 -0
  83. package/plugins/sp/skills/taste-refactoring-architect/examples/refactor-example.md +55 -0
  84. package/plugins/sp/skills/taste-refactoring-architect/examples/review-template.md +51 -0
  85. package/plugins/sp/skills/taste-refactoring-architect/references/architecture-refactoring-playbook.md +173 -0
  86. package/plugins/sp/skills/taste-refactoring-architect/references/research-basis.md +28 -0
  87. package/plugins/sp/skills/taste-refactoring-tests/README.md +28 -0
  88. package/plugins/sp/skills/taste-refactoring-tests/SKILL.md +482 -0
  89. package/plugins/sp/skills/taste-refactoring-tests/checklists/daily-test-review.md +39 -0
  90. package/plugins/sp/skills/taste-refactoring-tests/examples/refactor-example.md +85 -0
  91. package/plugins/sp/skills/taste-refactoring-tests/examples/review-template.md +59 -0
  92. package/plugins/sp/skills/taste-refactoring-tests/references/research-basis.md +47 -0
  93. package/plugins/sp/skills/taste-refactoring-tests/references/test-refactoring-playbook.md +222 -0
  94. package/plugins/sp/skills/taste-refactoring-ui/README.md +12 -0
  95. package/plugins/sp/skills/taste-refactoring-ui/SKILL.md +290 -0
  96. package/plugins/sp/skills/taste-refactoring-ui/checklists/daily-ui-review.md +72 -0
  97. package/plugins/sp/skills/taste-refactoring-ui/examples/review-template.md +51 -0
  98. package/plugins/sp/skills/taste-refactoring-ui/references/refactoring-ui-playbook.md +170 -0
  99. package/plugins/sp/skills/wayfinder/SKILL.md +2 -2
  100. package/plugins/sp/skills/wayfinder/references/pipeline-resolution.md +30 -0
  101. package/schemas/spur-config.schema.json +49 -0
  102. package/spur.js +46754 -44121
  103. package/web/_astro/{BoardApp.CHQ1lycZ.js → BoardApp.B1U26g3I.js} +97 -95
  104. package/web/_astro/BoardApp.Csgyg-lS.js +1 -0
  105. package/web/_astro/{TaskDetail.GKfQJ60c.js → TaskDetail.DwPqpq7v.js} +1 -1
  106. package/web/_astro/{arc.DWEtA3Tx.js → arc.CweZEjN2.js} +1 -1
  107. package/web/_astro/{architectureDiagram-3BPJPVTR.DB42oWmP.js → architectureDiagram-3BPJPVTR.D89pbDuv.js} +1 -1
  108. package/web/_astro/{blockDiagram-GPEHLZMM.rhv-zNQV.js → blockDiagram-GPEHLZMM.BOuTeEpX.js} +1 -1
  109. package/web/_astro/{c4Diagram-AAUBKEIU.Ci4-4VvY.js → c4Diagram-AAUBKEIU.CASbkWZF.js} +1 -1
  110. package/web/_astro/channel.Cx6sXxhq.js +1 -0
  111. package/web/_astro/{chunk-2J33WTMH.Cc9veUgf.js → chunk-2J33WTMH.BKQYtOvY.js} +1 -1
  112. package/web/_astro/{chunk-4BX2VUAB.Bec9c4eI.js → chunk-4BX2VUAB.9sHLdMtG.js} +1 -1
  113. package/web/_astro/{chunk-55IACEB6.DoV8S1iB.js → chunk-55IACEB6.wOLXWlPs.js} +1 -1
  114. package/web/_astro/{chunk-727SXJPM.DwR-Qlyj.js → chunk-727SXJPM.DovFbwg3.js} +1 -1
  115. package/web/_astro/{chunk-AQP2D5EJ.ND_a81WY.js → chunk-AQP2D5EJ.B1Weod1X.js} +1 -1
  116. package/web/_astro/{chunk-FMBD7UC4.Wv_jwG48.js → chunk-FMBD7UC4.TEMS04st.js} +1 -1
  117. package/web/_astro/{chunk-ND2GUHAM.CXKXCMmp.js → chunk-ND2GUHAM.Cp8VT1wQ.js} +1 -1
  118. package/web/_astro/{chunk-QZHKN3VN.nkaoNYQq.js → chunk-QZHKN3VN.BzATdEcP.js} +1 -1
  119. package/web/_astro/{classDiagram-4FO5ZUOK.cMQcVlQu.js → classDiagram-4FO5ZUOK.C9BOCfAO.js} +1 -1
  120. package/web/_astro/{classDiagram-v2-Q7XG4LA2.cMQcVlQu.js → classDiagram-v2-Q7XG4LA2.C9BOCfAO.js} +1 -1
  121. package/web/_astro/{cose-bilkent-S5V4N54A.OaDJ7Mr2.js → cose-bilkent-S5V4N54A.DUnr4UAw.js} +1 -1
  122. package/web/_astro/{cynefin-OW5HDTMX.Chi8IphF.js → cynefin-OW5HDTMX.rYq5uM3D.js} +1 -1
  123. package/web/_astro/{cytoscape.esm.DzSz-X2X.js → cytoscape.esm.BB4DxJjf.js} +1 -1
  124. package/web/_astro/{dagre-BM42HDAG.CzK2t_Fp.js → dagre-BM42HDAG.CWeNKe3I.js} +1 -1
  125. package/web/_astro/{diagram-2AECGRRQ.DRvxlVS7.js → diagram-2AECGRRQ.DCkfls10.js} +1 -1
  126. package/web/_astro/{diagram-5GNKFQAL.CnYvNdwA.js → diagram-5GNKFQAL.D5U4JCka.js} +1 -1
  127. package/web/_astro/{diagram-KO2AKTUF.CpLpMw5R.js → diagram-KO2AKTUF.BZJgqaqG.js} +1 -1
  128. package/web/_astro/{diagram-LMA3HP47.JTb78qUA.js → diagram-LMA3HP47.DoMeHvPR.js} +1 -1
  129. package/web/_astro/{diagram-OG6HWLK6.Bk-1jDIb.js → diagram-OG6HWLK6.B50qwwWX.js} +1 -1
  130. package/web/_astro/{erDiagram-TEJ5UH35.D8hN9GZq.js → erDiagram-TEJ5UH35.DdGPG6LK.js} +1 -1
  131. package/web/_astro/{flowDiagram-I6XJVG4X.-6zQr6m5.js → flowDiagram-I6XJVG4X.QP2MJ12u.js} +1 -1
  132. package/web/_astro/{ganttDiagram-6RSMTGT7.DboLQ9ca.js → ganttDiagram-6RSMTGT7.BI6LgKSy.js} +1 -1
  133. package/web/_astro/{gitGraphDiagram-PVQCEYII.4tYvJKGR.js → gitGraphDiagram-PVQCEYII.npPZiC2G.js} +1 -1
  134. package/web/_astro/index.DayyIngm.css +1 -0
  135. package/web/_astro/{infoDiagram-5YYISTIA.Bd9rXpsB.js → infoDiagram-5YYISTIA.DCJCBVbp.js} +1 -1
  136. package/web/_astro/{ishikawaDiagram-YF4QCWOH.CvMoaf67.js → ishikawaDiagram-YF4QCWOH.BMLV-3I1.js} +1 -1
  137. package/web/_astro/{journeyDiagram-JHISSGLW.Ccy1CA7y.js → journeyDiagram-JHISSGLW.LE58crde.js} +1 -1
  138. package/web/_astro/{kanban-definition-UN3LZRKU.0MaMqHNS.js → kanban-definition-UN3LZRKU.BPbz8rH9.js} +1 -1
  139. package/web/_astro/{linear.CHXgcIbN.js → linear.DhZaBtYh.js} +1 -1
  140. package/web/_astro/{mermaid.core.Ca-kcelG.js → mermaid.core.BD5-jXum.js} +6 -6
  141. package/web/_astro/{mindmap-definition-RKZ34NQL.BUIDlHa0.js → mindmap-definition-RKZ34NQL.MTJyrQ65.js} +1 -1
  142. package/web/_astro/ordinal.BYWQX77i.js +1 -0
  143. package/web/_astro/{pieDiagram-4H26LBE5.2dX3CU1s.js → pieDiagram-4H26LBE5.BrDhDvIS.js} +1 -1
  144. package/web/_astro/{quadrantDiagram-W4KKPZXB.B3LBlRiv.js → quadrantDiagram-W4KKPZXB.71d73_5N.js} +1 -1
  145. package/web/_astro/{requirementDiagram-4Y6WPE33.X12I2uNx.js → requirementDiagram-4Y6WPE33.Bga6UF-z.js} +1 -1
  146. package/web/_astro/{sankeyDiagram-5OEKKPKP.BXohIHqx.js → sankeyDiagram-5OEKKPKP.BnHs4K82.js} +1 -1
  147. package/web/_astro/{sequenceDiagram-3UESZ5HK.C37ZIUzg.js → sequenceDiagram-3UESZ5HK.DsfY2gnj.js} +1 -1
  148. package/web/_astro/{stateDiagram-AJRCARHV.BRgz317z.js → stateDiagram-AJRCARHV.DvsTSc9a.js} +1 -1
  149. package/web/_astro/{stateDiagram-v2-BHNVJYJU.7VYSXN9-.js → stateDiagram-v2-BHNVJYJU.DxzzmHUR.js} +1 -1
  150. package/web/_astro/{timeline-definition-PNZ67QCA.BVNz_HiN.js → timeline-definition-PNZ67QCA.4ZuQmOTt.js} +1 -1
  151. package/web/_astro/{vennDiagram-CIIHVFJN.CHVDkPX4.js → vennDiagram-CIIHVFJN.Ck5Q86SG.js} +1 -1
  152. package/web/_astro/{wardleyDiagram-YWT4CUSO.EQQ_qT9v.js → wardleyDiagram-YWT4CUSO.BK7k2hXr.js} +1 -1
  153. package/web/_astro/{xychartDiagram-2RQKCTM6.DrAT9WoP.js → xychartDiagram-2RQKCTM6.DfCrgauK.js} +1 -1
  154. package/web/index.html +2 -2
  155. package/web/_astro/BoardApp.DV9kx0wo.js +0 -1
  156. package/web/_astro/channel.BAI6xLeV.js +0 -1
  157. package/web/_astro/index.Dcr_8fiK.css +0 -1
  158. package/web/_astro/ordinal.DBvzRdQf.js +0 -1
@@ -0,0 +1,350 @@
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
+ function jqPick(...values) {
9
+ for (const value of values) {
10
+ if (value !== null && value !== undefined && value !== false)
11
+ return value;
12
+ }
13
+ return values[values.length - 1];
14
+ }
15
+ function jqText(value) {
16
+ return typeof value === "string" ? value : JSON.stringify(value);
17
+ }
18
+ var WBS_PATTERN = /^[0-9]{4}$/;
19
+ function spurCommand(spurBin) {
20
+ const parts = (spurBin ?? "spur").trim().split(/\s+/).filter((p) => p.length > 0);
21
+ return { cmd: parts[0] ?? "spur", prefix: parts.slice(1) };
22
+ }
23
+ function spur(env, args, options = {}) {
24
+ const { cmd, prefix } = spurCommand(env.spurBin);
25
+ const result = spawnSync(cmd, [...prefix, ...args], {
26
+ cwd: options.cwd,
27
+ encoding: "utf8",
28
+ ...options.stderr === "inherit" ? { stdio: ["ignore", "pipe", "inherit"] } : {}
29
+ });
30
+ if (result.error !== undefined)
31
+ return { status: result.status ?? 1, stdout: "" };
32
+ return { status: result.status ?? 1, stdout: result.stdout ?? "" };
33
+ }
34
+ function taskStatusOf(taskJson) {
35
+ let parsed;
36
+ try {
37
+ parsed = JSON.parse(taskJson);
38
+ } catch {
39
+ return { resolved: null, present: false };
40
+ }
41
+ if (parsed === null || typeof parsed !== "object")
42
+ return { resolved: null, present: false };
43
+ const frontmatter = parsed.frontmatter;
44
+ const fmStatus = frontmatter !== null && typeof frontmatter === "object" ? frontmatter.status : undefined;
45
+ const status = parsed.status;
46
+ if (fmStatus === null || fmStatus === undefined) {
47
+ return { resolved: status ?? null, present: status !== undefined && status !== null };
48
+ }
49
+ return { resolved: fmStatus, present: true };
50
+ }
51
+ function statusDisplay(taskJson) {
52
+ const { resolved } = taskStatusOf(taskJson);
53
+ if (resolved === null || resolved === undefined || resolved === false)
54
+ return "unresolved";
55
+ return jqText(resolved);
56
+ }
57
+ function resolveTasks(env, options = {}) {
58
+ const cwd = options.cwd;
59
+ const runId = env.__runId ?? "";
60
+ if (runId.length === 0) {
61
+ process.stderr.write(`task-resolve: __runId is empty \u2014 refusing the legacy fixed-path fallback
62
+ `);
63
+ return { status: "FAIL", statusFile: "", tasksFile: "", exitCode: 1 };
64
+ }
65
+ mkdirSync(cwd ? join(cwd, ".spur", "run") : join(".spur", "run"), { recursive: true });
66
+ const relTasksFile = join(".spur", "run", `${runId}-wrapup-tasks.json`);
67
+ const relReasonFile = join(".spur", "run", `${runId}-route-reason.txt`);
68
+ const relStatusFile = join(".spur", "run", `${runId}-wrapup-resolve.status`);
69
+ const abs = (p) => cwd ? join(cwd, p) : p;
70
+ const writeFail = (reason) => {
71
+ writeFileSync(abs(relReasonFile), reason);
72
+ writeFileSync(abs(relStatusFile), `FAIL
73
+ `);
74
+ return { status: "FAIL", statusFile: relStatusFile, tasksFile: relTasksFile, exitCode: 0 };
75
+ };
76
+ let parsedTasks;
77
+ try {
78
+ parsedTasks = JSON.parse(env.tasks ?? "");
79
+ } catch {
80
+ parsedTasks = undefined;
81
+ }
82
+ const validArray = Array.isArray(parsedTasks) && parsedTasks.every((w) => typeof w === "string" && WBS_PATTERN.test(w));
83
+ if (!validArray) {
84
+ process.stderr.write(`task-resolve: tasks must be a JSON array of canonical four-digit WBS strings (whitespace is rejected, not trimmed)
85
+ `);
86
+ return writeFail("failed:tasks is not a JSON array of canonical four-digit WBS strings");
87
+ }
88
+ const deduped = [];
89
+ for (const wbs of parsedTasks) {
90
+ if (!deduped.includes(wbs))
91
+ deduped.push(wbs);
92
+ }
93
+ writeFileSync(abs(relTasksFile), `${JSON.stringify(deduped)}
94
+ `);
95
+ let unresolved = false;
96
+ for (const wbs of deduped) {
97
+ const shown = spur(env, ["task", "show", wbs, "--json"], { cwd });
98
+ const status = shown.status === 0 ? statusDisplay(shown.stdout) : "unresolved";
99
+ if (status !== "done" && status !== "cancelled") {
100
+ process.stderr.write(`task-resolve: task ${wbs} did not resolve to a completed status (status=${status})
101
+ `);
102
+ unresolved = true;
103
+ }
104
+ }
105
+ if (unresolved) {
106
+ return writeFail(`failed:unresolved or non-completed task (see ${relTasksFile})`);
107
+ }
108
+ writeFileSync(abs(relStatusFile), `PASS
109
+ `);
110
+ return { status: "PASS", statusFile: relStatusFile, tasksFile: relTasksFile, exitCode: 0 };
111
+ }
112
+ function runMetrics(env, options = {}) {
113
+ const cwd = options.cwd;
114
+ const runId = env.__runId ?? "";
115
+ mkdirSync(cwd ? join(cwd, ".spur", "run") : join(".spur", "run"), { recursive: true });
116
+ mkdirSync(cwd ? join(cwd, ".spur", "memory") : join(".spur", "memory"), { recursive: true });
117
+ const relStatusFile = join(".spur", "run", `${runId}-wrapup-metrics.status`);
118
+ const relTasksFile = join(".spur", "run", `${runId}-wrapup-tasks.json`);
119
+ const relMetricsFile = join(".spur", "memory", "wrapup-metrics.jsonl");
120
+ const abs = (p) => cwd ? join(cwd, p) : p;
121
+ const fail = () => {
122
+ writeFileSync(abs(relStatusFile), `FAIL
123
+ `);
124
+ return { status: "FAIL", statusFile: relStatusFile };
125
+ };
126
+ let captured;
127
+ try {
128
+ captured = JSON.parse(readFileSync(abs(relTasksFile), "utf8"));
129
+ } catch {
130
+ captured = undefined;
131
+ }
132
+ const validCapture = Array.isArray(captured) && captured.every((w) => typeof w === "string" && WBS_PATTERN.test(w));
133
+ if (!validCapture) {
134
+ process.stderr.write(`metrics-record: run-scoped task capture missing, corrupted or non-canonical \u2014 refusing to record metrics
135
+ `);
136
+ return fail();
137
+ }
138
+ let metricsRc = 0;
139
+ for (const wbs of captured) {
140
+ const shown = spur(env, ["task", "show", wbs, "--json"], { cwd });
141
+ const lookup = shown.status === 0 ? taskStatusOf(shown.stdout) : { resolved: null, present: false };
142
+ if (!lookup.present) {
143
+ process.stderr.write(`metrics-record: task ${wbs} lookup failed or was malformed \u2014 recording FAIL instead of silently omitting its metrics row
144
+ `);
145
+ metricsRc = 1;
146
+ continue;
147
+ }
148
+ const parsed = JSON.parse(shown.stdout);
149
+ const frontmatter = parsed.frontmatter !== null && typeof parsed.frontmatter === "object" ? parsed.frontmatter : {};
150
+ const featureId = String(jqPick(frontmatter.feature_id, parsed.feature_id, ""));
151
+ const status2 = String(jqPick(frontmatter.status, parsed.status, "unknown"));
152
+ let verdict = "UNKNOWN";
153
+ const verdictPath = join(".spur", "run", `${wbs}-verdict.json`);
154
+ if (existsSync(abs(verdictPath))) {
155
+ try {
156
+ const raw = jqPick(JSON.parse(readFileSync(abs(verdictPath), "utf8")).verdict, "UNKNOWN");
157
+ const text = raw === "UNKNOWN" ? "UNKNOWN" : jqText(raw);
158
+ if (text.length > 0)
159
+ verdict = text;
160
+ } catch {}
161
+ }
162
+ const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
163
+ const row = { wbs, feature_id: featureId, status: status2, verdict, timestamp };
164
+ try {
165
+ appendFileSync(abs(relMetricsFile), `${JSON.stringify(row)}
166
+ `);
167
+ } catch {
168
+ process.stderr.write(`metrics-record: metrics append failed for task ${wbs} \u2014 recording FAIL instead of claiming the row landed
169
+ `);
170
+ metricsRc = 1;
171
+ }
172
+ }
173
+ const status = metricsRc === 0 ? "PASS" : "FAIL";
174
+ writeFileSync(abs(relStatusFile), `${status}
175
+ `);
176
+ return { status, statusFile: relStatusFile };
177
+ }
178
+ function classifySync(syncOutput, syncRc, feature, observed) {
179
+ if (syncRc !== 0) {
180
+ return { reason: `sync exited nonzero (rc=${syncRc})`, applied: "unreadable", syncOk: false };
181
+ }
182
+ let parsed;
183
+ try {
184
+ parsed = JSON.parse(syncOutput);
185
+ } catch {
186
+ parsed = undefined;
187
+ }
188
+ const obj = parsed !== null && typeof parsed === "object" ? parsed : undefined;
189
+ const proposal = obj?.proposal !== null && typeof obj?.proposal === "object" ? obj.proposal : undefined;
190
+ const shapeOk = obj !== undefined && proposal !== undefined && typeof proposal.featureId === "string" && typeof proposal.from === "string" && typeof proposal.to === "string" && typeof obj.applied === "boolean";
191
+ if (!shapeOk) {
192
+ return { reason: "malformed or unreadable sync result", applied: "unreadable", syncOk: false };
193
+ }
194
+ const applied = obj.applied === true ? "true" : "false";
195
+ const pFrom = String(jqPick(proposal.from, ""));
196
+ const pTo = String(jqPick(proposal.to, ""));
197
+ if (String(jqPick(proposal.featureId, "")) !== feature) {
198
+ return { reason: `sync proposal does not match feature ${feature}`, applied, syncOk: false };
199
+ }
200
+ if (proposal.gateBlocked === true) {
201
+ return {
202
+ reason: "sync proposal is gate-blocked \u2014 a blocked sync is not a no-change success",
203
+ applied,
204
+ syncOk: false
205
+ };
206
+ }
207
+ if (proposal.requiresConfirm === true) {
208
+ return { reason: "sync proposal requires operator confirmation", applied, syncOk: false };
209
+ }
210
+ if (applied === "true" && observed !== pTo) {
211
+ return {
212
+ reason: `applied sync did not land on the proposal target (observed=${observed}, to=${pTo})`,
213
+ applied,
214
+ syncOk: false
215
+ };
216
+ }
217
+ if (applied === "false" && (pFrom !== pTo || observed !== pTo)) {
218
+ return {
219
+ reason: `sync applied nothing without a from==to observed no-op (from=${pFrom}, to=${pTo}, observed=${observed})`,
220
+ applied,
221
+ syncOk: false
222
+ };
223
+ }
224
+ return { reason: "", applied, syncOk: true };
225
+ }
226
+ function runFeatureTransition(env, options = {}) {
227
+ const cwd = options.cwd;
228
+ const runId = env.__runId ?? "";
229
+ const feature = env.feature ?? "";
230
+ mkdirSync(cwd ? join(cwd, ".spur", "run") : join(".spur", "run"), { recursive: true });
231
+ const relStatusFile = join(".spur", "run", `${runId}-wrapup-sync.status`);
232
+ const abs = (p) => cwd ? join(cwd, p) : p;
233
+ if (feature.length === 0) {
234
+ process.stderr.write(`feature-transition: vars.feature is empty \u2014 refusing no-op feature sync (mis-invocation, not a blocked sync)
235
+ `);
236
+ return { status: "FAIL", statusFile: relStatusFile, exitCode: 1 };
237
+ }
238
+ let syncOutput = "";
239
+ let syncRc = 1;
240
+ const boundedTs = join("plugins", "sp", "scripts", "feature-sync-bounded.ts");
241
+ const boundedArgs = [feature, "--spur-bin", env.spurBin ?? "spur", "--json"];
242
+ if (existsSync(cwd ? join(cwd, boundedTs) : boundedTs)) {
243
+ const result = spawnSync("bun", [boundedTs, ...boundedArgs], { cwd, encoding: "utf8" });
244
+ syncOutput = result.stdout ?? "";
245
+ syncRc = result.status ?? 1;
246
+ if (result.stderr !== null && result.stderr.length > 0)
247
+ process.stderr.write(result.stderr);
248
+ } else {
249
+ const probe = spawnSync("superskill", ["script", "path", "sp", "feature-sync-bounded.mjs"], {
250
+ cwd,
251
+ encoding: "utf8"
252
+ });
253
+ const twin = probe.status === 0 ? (probe.stdout ?? "").trim() : "";
254
+ if (twin.length > 0 && existsSync(twin)) {
255
+ const result = spawnSync("node", [twin, ...boundedArgs], { cwd, encoding: "utf8" });
256
+ syncOutput = result.stdout ?? "";
257
+ syncRc = result.status ?? 1;
258
+ if (result.stderr !== null && result.stderr.length > 0)
259
+ process.stderr.write(result.stderr);
260
+ } else {
261
+ const result = spur(env, ["feature", "sync", feature, "--json"], { cwd, stderr: "inherit" });
262
+ syncOutput = result.stdout;
263
+ syncRc = result.status;
264
+ }
265
+ }
266
+ process.stdout.write(`${syncOutput}
267
+ `);
268
+ const shown = spur(env, ["feature", "show", feature, "--json"], { cwd });
269
+ let observed = "";
270
+ if (shown.status === 0) {
271
+ try {
272
+ const parsed = JSON.parse(shown.stdout);
273
+ const frontmatter = parsed.frontmatter !== null && typeof parsed.frontmatter === "object" ? parsed.frontmatter : {};
274
+ const picked = jqPick(parsed.status, frontmatter.status, "");
275
+ observed = picked === "" ? "" : jqText(picked);
276
+ } catch {
277
+ observed = "";
278
+ }
279
+ }
280
+ if (observed.length === 0)
281
+ observed = "unreadable";
282
+ const classified = classifySync(syncOutput, syncRc, feature, observed);
283
+ const { reason, applied } = classified;
284
+ const syncOk = classified.syncOk;
285
+ let gate = "skipped";
286
+ if (applied === "true" || syncRc !== 0) {
287
+ process.stdout.write(`feature-transition: sync applied or failed after a possible partial transition for ${feature} \u2014 running feature gate: ${env.featureGateCmd ?? ""}
288
+ `);
289
+ const gateResult = spawnSync("sh", ["-c", env.featureGateCmd ?? ""], { cwd, stdio: "inherit" });
290
+ if ((gateResult.status ?? 1) === 0) {
291
+ gate = "PASS";
292
+ process.stdout.write(`feature-transition: feature gate PASS for feature ${feature}
293
+ `);
294
+ } else {
295
+ gate = "FAIL";
296
+ process.stderr.write(`feature-transition: feature gate FAIL for feature ${feature} \u2014 inspect findings before reporting the transition complete
297
+ `);
298
+ }
299
+ } else {
300
+ process.stdout.write(`feature-transition: sync did not apply a transition (rc=${syncRc}, applied=${applied}) \u2014 feature gate skipped
301
+ `);
302
+ }
303
+ let syncStatus;
304
+ if (!syncOk || gate === "FAIL") {
305
+ syncStatus = "FAIL";
306
+ process.stderr.write(`feature-transition: required synchronization failed for ${feature} \u2014 ${reason}; gate=${gate}
307
+ `);
308
+ } else if (applied === "false") {
309
+ syncStatus = "PASS";
310
+ process.stdout.write(`feature-transition: feature sync verified for ${feature} (from==to observed at ${observed}, gate=${gate}) \u2014 explicit no-change
311
+ `);
312
+ } else {
313
+ syncStatus = "PASS";
314
+ process.stdout.write(`feature-transition: feature sync verified for ${feature} (applied, observed=${observed}, gate=${gate})
315
+ `);
316
+ }
317
+ writeFileSync(abs(relStatusFile), `${syncStatus}
318
+ `);
319
+ return { status: syncStatus, statusFile: relStatusFile, exitCode: 0 };
320
+ }
321
+ var WRAPUP_STEPS_USAGE = "usage: wrapup-steps.ts <resolve|metrics|feature-transition> (env: __runId, tasks, feature, featureGateCmd, spurBin)";
322
+ function main(argv, env = process.env, options = {}) {
323
+ const sub = argv[0];
324
+ if (sub === "resolve")
325
+ return resolveTasks(env, options).exitCode;
326
+ if (sub === "metrics") {
327
+ runMetrics(env, options);
328
+ return 0;
329
+ }
330
+ if (sub === "feature-transition")
331
+ return runFeatureTransition(env, options).exitCode;
332
+ process.stderr.write(`${WRAPUP_STEPS_USAGE}
333
+ `);
334
+ return 2;
335
+ }
336
+ {
337
+ process.exit(main(process.argv.slice(2)));
338
+ }
339
+ export {
340
+ taskStatusOf,
341
+ spurCommand,
342
+ runMetrics,
343
+ runFeatureTransition,
344
+ resolveTasks,
345
+ main,
346
+ jqPick,
347
+ classifySync,
348
+ WRAPUP_STEPS_USAGE,
349
+ WBS_PATTERN
350
+ };