@gobing-ai/spur 0.3.95 → 0.3.97

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 (165) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/plugin-scripts.json +0 -54
  3. package/config/rules/boundary/sp-script-placement.yaml +19 -0
  4. package/config/rules/strict/runtime-boundaries.yaml +6 -0
  5. package/config/rules/structure/test-location.yaml +2 -0
  6. package/config/rules/typescript/no-syscall-emulation-in-boundary-mock.yaml +1 -1
  7. package/config/rules/typescript/output-boundaries.yaml +15 -2
  8. package/config/script-placement-baseline.json +51 -0
  9. package/config/templates/AGENTS.md +3 -3
  10. package/config/workflows/feature-verification.yaml +1 -1
  11. package/config/workflows/history-anatomy.yaml +23 -11
  12. package/config/workflows/idea-pipeline.yaml +80 -85
  13. package/config/workflows/pr-review.yaml +18 -9
  14. package/config/workflows/task-pipeline.yaml +42 -95
  15. package/config/workflows/wrapup-pipeline.yaml +91 -38
  16. package/package.json +2 -2
  17. package/plugins/sp/README.md +14 -10
  18. package/plugins/sp/agents/super-reviewer.md +43 -18
  19. package/plugins/sp/commands/dev-fixgha.md +83 -0
  20. package/plugins/sp/commands/dev-gitmsg.md +8 -6
  21. package/plugins/sp/commands/dev-gtd.md +2 -2
  22. package/plugins/sp/commands/dev-idea.md +22 -12
  23. package/plugins/sp/commands/dev-job-dump.md +29 -0
  24. package/plugins/sp/commands/dev-job-resume.md +29 -0
  25. package/plugins/sp/commands/dev-plan.md +8 -9
  26. package/plugins/sp/commands/dev-review.md +22 -13
  27. package/plugins/sp/commands/dev-verifyall.md +1 -1
  28. package/plugins/sp/commands/spur-init.md +2 -2
  29. package/plugins/sp/lib/history-anatomy.generated.d.mts +112 -0
  30. package/plugins/sp/lib/history-anatomy.generated.mjs +686 -0
  31. package/plugins/sp/lib/idea-handoff.generated.mjs +5 -4
  32. package/plugins/sp/lib/inline-run.generated.d.mts +11 -0
  33. package/plugins/sp/lib/inline-run.generated.mjs +39 -13
  34. package/plugins/sp/lib/quality-gate.generated.d.mts +104 -0
  35. package/plugins/sp/lib/quality-gate.generated.mjs +445 -0
  36. package/plugins/sp/lib/residual-scan.generated.d.mts +63 -0
  37. package/plugins/sp/lib/residual-scan.generated.mjs +226 -0
  38. package/plugins/sp/lib/spur-bin.ts +36 -0
  39. package/plugins/sp/lib/step-profile.generated.d.mts +71 -0
  40. package/plugins/sp/lib/step-profile.generated.mjs +174 -0
  41. package/plugins/sp/plugin.json +1 -1
  42. package/plugins/sp/references/roles.md +3 -3
  43. package/plugins/sp/scripts/feature-verification-steps.mjs +14 -1
  44. package/plugins/sp/scripts/feature-verification-steps.ts +14 -1
  45. package/plugins/sp/scripts/history-anatomy-cache.mjs +20 -19
  46. package/plugins/sp/scripts/history-anatomy-cache.ts +23 -928
  47. package/plugins/sp/scripts/inline-run-setup.mjs +85 -320
  48. package/plugins/sp/scripts/inline-run-setup.ts +104 -667
  49. package/plugins/sp/scripts/quality-gate.mjs +46 -24
  50. package/plugins/sp/scripts/quality-gate.ts +13 -659
  51. package/plugins/sp/scripts/residual-scan.mjs +179 -174
  52. package/plugins/sp/scripts/residual-scan.ts +125 -517
  53. package/plugins/sp/scripts/script-root.mjs +5 -1
  54. package/plugins/sp/scripts/script-root.ts +5 -1
  55. package/plugins/sp/scripts/workflow-step-profile.mjs +25 -17
  56. package/plugins/sp/scripts/workflow-step-profile.ts +21 -315
  57. package/plugins/sp/scripts/wrapup-drift-probe.mjs +8 -2
  58. package/plugins/sp/scripts/wrapup-drift-probe.ts +3 -2
  59. package/plugins/sp/scripts/wrapup-steps.mjs +72 -33
  60. package/plugins/sp/scripts/wrapup-steps.ts +128 -37
  61. package/plugins/sp/skills/code-improvement/SKILL.md +5 -4
  62. package/plugins/sp/skills/code-verification/SKILL.md +34 -9
  63. package/plugins/sp/skills/code-verification/references/verdict-schema.md +3 -3
  64. package/plugins/sp/skills/functional-review/SKILL.md +7 -4
  65. package/plugins/sp/skills/functional-review/references/verdict-schema.md +1 -1
  66. package/plugins/sp/skills/history-anatomy/references/modes.md +2 -1
  67. package/plugins/sp/skills/next-feature/references/handoff-routing.md +1 -1
  68. package/plugins/sp/skills/next-router/references/routing-table.md +1 -1
  69. package/plugins/sp/skills/source-driven-development/SKILL.md +11 -0
  70. package/plugins/sp/skills/spur-cli/SKILL.md +3 -3
  71. package/plugins/sp/skills/spur-cli/references/agent.md +10 -10
  72. package/plugins/sp/skills/spur-cli/references/features.md +17 -6
  73. package/plugins/sp/skills/spur-cli/references/init.md +17 -16
  74. package/plugins/sp/skills/spur-cli/references/self.md +3 -2
  75. package/plugins/sp/skills/spur-cli/references/serve.md +10 -10
  76. package/plugins/sp/skills/spur-cli/references/tasks/section-editing.md +9 -5
  77. package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +21 -6
  78. package/plugins/sp/skills/spur-cli/references/tasks.md +14 -8
  79. package/plugins/sp/skills/spur-cli/references/workflows.md +17 -14
  80. package/plugins/sp/skills/spur-dev/SKILL.md +2 -0
  81. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +4 -3
  82. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +14 -10
  83. package/plugins/sp/skills/spur-dev/references/decision-brief.md +1 -1
  84. package/plugins/sp/skills/spur-dev/references/dev-operations.md +165 -75
  85. package/plugins/sp/skills/spur-dev/references/done-housekeeping.md +7 -5
  86. package/plugins/sp/skills/spur-dev/references/execution-batch.md +84 -39
  87. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +8 -12
  88. package/plugins/sp/skills/spur-dev/references/feature-link-helper.md +3 -3
  89. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +40 -21
  90. package/plugins/sp/skills/spur-dev/references/gate-checklists.md +19 -19
  91. package/plugins/sp/skills/spur-dev/references/idea-evaluation.md +4 -3
  92. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +72 -15
  93. package/plugins/sp/skills/spur-doctor/SKILL.md +1 -1
  94. package/plugins/sp/skills/sys-architecture/SKILL.md +3 -2
  95. package/spur.js +4461 -2085
  96. package/web/_astro/{BoardApp.Cpxntzad.js → BoardApp.Bmv5WkJ9.js} +1 -1
  97. package/web/_astro/BoardApp.vveLOdDq.js +322 -0
  98. package/web/_astro/{TaskDetail.C5bW4WGV.js → TaskDetail.IKc3uDYV.js} +1 -1
  99. package/web/_astro/{arc.CyjRvNMY.js → arc.C63Ufg35.js} +1 -1
  100. package/web/_astro/{architectureDiagram-3BPJPVTR.B4lWRzJA.js → architectureDiagram-3BPJPVTR.CP8Hyldk.js} +1 -1
  101. package/web/_astro/{blockDiagram-GPEHLZMM.Be38USQ2.js → blockDiagram-GPEHLZMM.Bo_48MRA.js} +1 -1
  102. package/web/_astro/{c4Diagram-AAUBKEIU.DT9Fj5Qx.js → c4Diagram-AAUBKEIU.Ch61sw-O.js} +1 -1
  103. package/web/_astro/channel.BKCmJpWM.js +1 -0
  104. package/web/_astro/{chunk-2J33WTMH.SeSBWLg5.js → chunk-2J33WTMH.CHpzS7xf.js} +1 -1
  105. package/web/_astro/{chunk-4BX2VUAB.DoO4VE14.js → chunk-4BX2VUAB.DzMNNWIw.js} +1 -1
  106. package/web/_astro/{chunk-55IACEB6.DrmTFA53.js → chunk-55IACEB6.BqPrDw_Q.js} +1 -1
  107. package/web/_astro/{chunk-727SXJPM.Bi-TUb_V.js → chunk-727SXJPM.ZzBHhNw-.js} +1 -1
  108. package/web/_astro/{chunk-AQP2D5EJ.B-xp8Uhi.js → chunk-AQP2D5EJ.1_O_LLkx.js} +1 -1
  109. package/web/_astro/{chunk-FMBD7UC4.DGWrDnUU.js → chunk-FMBD7UC4.GS4UHg7_.js} +1 -1
  110. package/web/_astro/{chunk-ND2GUHAM._BagvPDy.js → chunk-ND2GUHAM.BnqotN4g.js} +1 -1
  111. package/web/_astro/{chunk-QZHKN3VN.VOboYWQ0.js → chunk-QZHKN3VN.Ghuh8zoJ.js} +1 -1
  112. package/web/_astro/{classDiagram-4FO5ZUOK.D_apfHS7.js → classDiagram-4FO5ZUOK.BW9vC3zg.js} +1 -1
  113. package/web/_astro/{classDiagram-v2-Q7XG4LA2.D_apfHS7.js → classDiagram-v2-Q7XG4LA2.BW9vC3zg.js} +1 -1
  114. package/web/_astro/{cose-bilkent-S5V4N54A.DNLU_L-x.js → cose-bilkent-S5V4N54A.D6jNC0ti.js} +1 -1
  115. package/web/_astro/{cynefin-OW5HDTMX.D8borhV-.js → cynefin-OW5HDTMX.BOGoCs4L.js} +1 -1
  116. package/web/_astro/{dagre-BM42HDAG.Uc0lvjcV.js → dagre-BM42HDAG.8iU5VcWi.js} +1 -1
  117. package/web/_astro/{diagram-2AECGRRQ.DDIxtDBo.js → diagram-2AECGRRQ.CVt88HwH.js} +1 -1
  118. package/web/_astro/{diagram-5GNKFQAL.DVoDnD3H.js → diagram-5GNKFQAL.C9HxUUMl.js} +1 -1
  119. package/web/_astro/{diagram-KO2AKTUF.CRubTJ-0.js → diagram-KO2AKTUF.4ywkQN3t.js} +1 -1
  120. package/web/_astro/{diagram-LMA3HP47.m9SeYcXQ.js → diagram-LMA3HP47.C48A6gpz.js} +1 -1
  121. package/web/_astro/{diagram-OG6HWLK6.C6s2ZjV5.js → diagram-OG6HWLK6.DxFieQW4.js} +1 -1
  122. package/web/_astro/{erDiagram-TEJ5UH35.nSzr5KTj.js → erDiagram-TEJ5UH35.CtjtQqYs.js} +1 -1
  123. package/web/_astro/{flowDiagram-I6XJVG4X.DrZcys16.js → flowDiagram-I6XJVG4X.C5zAiahU.js} +1 -1
  124. package/web/_astro/{ganttDiagram-6RSMTGT7.Do4M1b-K.js → ganttDiagram-6RSMTGT7.D_L6zsny.js} +1 -1
  125. package/web/_astro/{gitGraphDiagram-PVQCEYII.BWc9oJ4l.js → gitGraphDiagram-PVQCEYII.Ignnrk3V.js} +1 -1
  126. package/web/_astro/index.CbNz17Sx.css +1 -0
  127. package/web/_astro/{infoDiagram-5YYISTIA.BIdOctFk.js → infoDiagram-5YYISTIA.DLv8-P5u.js} +1 -1
  128. package/web/_astro/{ishikawaDiagram-YF4QCWOH.CV5SuG2t.js → ishikawaDiagram-YF4QCWOH.p0OTpwFO.js} +1 -1
  129. package/web/_astro/{journeyDiagram-JHISSGLW.CxE-rEJ-.js → journeyDiagram-JHISSGLW.CIaBt-DD.js} +1 -1
  130. package/web/_astro/{kanban-definition-UN3LZRKU.BPRdKWs_.js → kanban-definition-UN3LZRKU.DGySIntq.js} +1 -1
  131. package/web/_astro/{linear.zRsuuDTE.js → linear.BATV4RXu.js} +1 -1
  132. package/web/_astro/{mermaid.core.jAJTcMKc.js → mermaid.core.DzkwM3VX.js} +4 -4
  133. package/web/_astro/{mindmap-definition-RKZ34NQL.BffJxfCr.js → mindmap-definition-RKZ34NQL.3piBRRzA.js} +1 -1
  134. package/web/_astro/{pieDiagram-4H26LBE5.CacwWaE3.js → pieDiagram-4H26LBE5.DUNueLub.js} +1 -1
  135. package/web/_astro/{quadrantDiagram-W4KKPZXB.hXELD06-.js → quadrantDiagram-W4KKPZXB.9bihVbrt.js} +1 -1
  136. package/web/_astro/{requirementDiagram-4Y6WPE33.DdEbPcqT.js → requirementDiagram-4Y6WPE33.CkjsC-YT.js} +1 -1
  137. package/web/_astro/{sankeyDiagram-5OEKKPKP.DgaDocKv.js → sankeyDiagram-5OEKKPKP.Dbq4vaoQ.js} +1 -1
  138. package/web/_astro/{sequenceDiagram-3UESZ5HK.B0KScnAu.js → sequenceDiagram-3UESZ5HK.BivrH99P.js} +1 -1
  139. package/web/_astro/{stateDiagram-AJRCARHV.CV9M_WNc.js → stateDiagram-AJRCARHV.CqLQ1Vzz.js} +1 -1
  140. package/web/_astro/{stateDiagram-v2-BHNVJYJU.BOAo74Et.js → stateDiagram-v2-BHNVJYJU.DvvoLD3b.js} +1 -1
  141. package/web/_astro/{timeline-definition-PNZ67QCA.CqqepUMe.js → timeline-definition-PNZ67QCA.B-KR78gv.js} +1 -1
  142. package/web/_astro/{vennDiagram-CIIHVFJN.DDqe4dOG.js → vennDiagram-CIIHVFJN.Ck0Eieb5.js} +1 -1
  143. package/web/_astro/{wardleyDiagram-YWT4CUSO.B3R9OPdO.js → wardleyDiagram-YWT4CUSO.CRfdd_do.js} +1 -1
  144. package/web/_astro/{xychartDiagram-2RQKCTM6.B3SvgXpQ.js → xychartDiagram-2RQKCTM6.Ck_Jxk9C.js} +1 -1
  145. package/web/index.html +2 -2
  146. package/plugins/sp/lib/artifact-digest.generated.d.mts +0 -7
  147. package/plugins/sp/lib/artifact-digest.generated.mjs +0 -48
  148. package/plugins/sp/scripts/feature-sync-bounded.mjs +0 -301
  149. package/plugins/sp/scripts/feature-sync-bounded.ts +0 -481
  150. package/plugins/sp/scripts/idea-coverage-check.ts +0 -168
  151. package/plugins/sp/scripts/inline-pipeline-parity-check.ts +0 -298
  152. package/plugins/sp/scripts/record-feature-sync.mjs +0 -63
  153. package/plugins/sp/scripts/record-feature-sync.ts +0 -84
  154. package/plugins/sp/scripts/script-contract-check.ts +0 -506
  155. package/plugins/sp/scripts/stage-registry-adapter.ts +0 -1533
  156. package/plugins/sp/scripts/surface-drift-inventory.ts +0 -989
  157. package/plugins/sp/scripts/task-evidence-precheck.ts +0 -189
  158. package/plugins/sp/scripts/task-size-precheck.ts +0 -212
  159. package/plugins/sp/scripts/transition-shim-check.ts +0 -238
  160. package/plugins/sp/scripts/validate-commands.ts +0 -689
  161. package/plugins/sp/scripts/validate-flag-contracts.ts +0 -890
  162. package/plugins/sp/scripts/verify-answer-lint.ts +0 -549
  163. package/web/_astro/BoardApp.BxJuwD7I.js +0 -191
  164. package/web/_astro/channel.CI6N_tCg.js +0 -1
  165. package/web/_astro/index.CENnIEqT.css +0 -1
@@ -1,301 +0,0 @@
1
- #!/usr/bin/env node
2
- import { createRequire } from "node:module";
3
- var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
-
5
- // plugins/sp/scripts/feature-sync-bounded.ts
6
- import { spawnSync } from "node:child_process";
7
- import { createHash } from "node:crypto";
8
- import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
9
- import { dirname } from "node:path";
10
- import { fileURLToPath } from "node:url";
11
-
12
- // plugins/sp/lib/env.ts
13
- function getEnvVar(name, fallback) {
14
- const raw = process.env[name];
15
- return raw === undefined ? fallback : raw;
16
- }
17
-
18
- // plugins/sp/scripts/feature-sync-bounded.ts
19
- function classifySyncResult(result) {
20
- if (result.proposal.gateBlocked === true)
21
- return "blocked";
22
- if (result.applied === true)
23
- return "applied";
24
- if (result.proposal.from !== result.proposal.to)
25
- return "blocked";
26
- return "no-op";
27
- }
28
- function computeFingerprint(input) {
29
- const material = [
30
- input.featureContentHash,
31
- ...[...input.taskStatusVector].sort(),
32
- ...[...input.verdictMtimeVector].sort()
33
- ].join(`
34
- `);
35
- return createHash("sha256").update(material).digest("hex").slice(0, 32);
36
- }
37
- var blockedStateFile = (featureId, runDir) => `${runDir.replace(/\/$/, "")}/feature-sync-blocked-${featureId}.json`;
38
- function serializeBlockedState(state) {
39
- return `${JSON.stringify(state)}
40
- `;
41
- }
42
- function parseBlockedState(raw) {
43
- const trimmed = raw.trim();
44
- if (trimmed.length === 0)
45
- return null;
46
- try {
47
- const parsed = JSON.parse(trimmed);
48
- if (typeof parsed.featureId !== "string" || typeof parsed.inputFingerprint !== "string" || typeof parsed.proposal !== "object" || parsed.proposal === null) {
49
- return null;
50
- }
51
- return parsed;
52
- } catch {
53
- return null;
54
- }
55
- }
56
- function shouldSuppressBlocked(prior, currentFingerprint) {
57
- if (!prior)
58
- return { suppress: false };
59
- if (prior.inputFingerprint === currentFingerprint) {
60
- return { suppress: true, replay: prior.result };
61
- }
62
- return { suppress: false };
63
- }
64
- function decideBoundedSync(prior, currentFingerprint) {
65
- const decision = shouldSuppressBlocked(prior, currentFingerprint);
66
- if (decision.suppress && decision.replay) {
67
- return { kind: "suppress", replay: decision.replay };
68
- }
69
- return { kind: "invoke" };
70
- }
71
- function processSyncResult(result, currentFingerprint, persistedAt, wasSuppressed) {
72
- const classification = classifySyncResult(result);
73
- if (classification === "blocked") {
74
- return {
75
- classification,
76
- emit: result,
77
- persist: {
78
- featureId: result.proposal.featureId,
79
- inputFingerprint: currentFingerprint,
80
- proposal: result.proposal,
81
- classification,
82
- result,
83
- persistedAt
84
- },
85
- annotation: wasSuppressed ? `feature-sync-bounded: suppressed duplicate blocked sync for ${result.proposal.featureId} (inputs unchanged)` : `feature-sync-bounded: blocked proposal for ${result.proposal.featureId} — ${result.proposal.reason}`
86
- };
87
- }
88
- return {
89
- classification,
90
- emit: result,
91
- annotation: ""
92
- };
93
- }
94
- var BOUNDED_SYNC_CLI_USAGE = `Usage:
95
- bun plugins/sp/scripts/feature-sync-bounded.ts <feature-id> \\
96
- --spur-bin <spur|bun apps/cli/src/index.ts> \\
97
- [--run-dir .spur/run] [--json]
98
-
99
- Wraps 'spur feature sync <id> --json' with bounded retry-suppression: an identical
100
- blocked proposal is reported once and suppressed until feature file content, linked
101
- task statuses, or verdict artifact mtimes change. Applied and no-op results pass
102
- through unchanged.
103
-
104
- Exit: 0 = sync handled (applied / no-op / suppressed-blocked / live-blocked).`;
105
- function defaultSpurBin() {
106
- const fromEnv = getEnvVar("SPUR_BIN");
107
- if (fromEnv)
108
- return fromEnv;
109
- const local = fileURLToPath(new URL("../../../apps/cli/src/index.ts", import.meta.url));
110
- if (existsSync(local))
111
- return `bun ${local}`;
112
- return "spur";
113
- }
114
- function parseBoundedSyncCliArgs(argv) {
115
- let featureId = "";
116
- let spurBin = defaultSpurBin();
117
- let runDir = ".spur/run";
118
- let json = false;
119
- let help = false;
120
- for (let i = 0;i < argv.length; i++) {
121
- const a = argv[i];
122
- if (a === undefined)
123
- continue;
124
- if (a === "--help" || a === "-h")
125
- help = true;
126
- else if (a === "--json")
127
- json = true;
128
- else if (a === "--spur-bin")
129
- spurBin = argv[++i] ?? spurBin;
130
- else if (a === "--run-dir")
131
- runDir = argv[++i] ?? runDir;
132
- else if (!a.startsWith("--") && featureId === "")
133
- featureId = a;
134
- }
135
- return { featureId, spurBin, runDir, json, help };
136
- }
137
- function runSpurJson(spurBin, args) {
138
- const binParts = spurBin.split(/\s+/).filter(Boolean);
139
- const cmd = binParts[0] ?? "spur";
140
- const cmdArgs = [...binParts.slice(1), ...args];
141
- const r = spawnSync(cmd, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], encoding: "utf8" });
142
- const decode = (b) => typeof b === "string" ? b : Buffer.from(b ?? []).toString("utf8");
143
- return {
144
- stdout: typeof r.stdout === "string" ? r.stdout : decode(r.stdout),
145
- stderr: typeof r.stderr === "string" ? r.stderr : decode(r.stderr),
146
- exitCode: r.status ?? (r.error ? 1 : 0),
147
- ok: (r.status ?? (r.error ? 1 : 0)) === 0
148
- };
149
- }
150
- function readFeatureContentHash(spurBin, featureId) {
151
- const r = runSpurJson(spurBin, ["feature", "show", featureId, "--json"]);
152
- if (!r.ok)
153
- return null;
154
- try {
155
- const parsed = JSON.parse(r.stdout);
156
- if (typeof parsed.content !== "string")
157
- return null;
158
- return createHash("sha256").update(parsed.content).digest("hex");
159
- } catch {
160
- return null;
161
- }
162
- }
163
- function readTaskStatusVector(spurBin, featureId) {
164
- const r = runSpurJson(spurBin, ["task", "list", "--feature", featureId, "--json"]);
165
- if (!r.ok)
166
- return null;
167
- try {
168
- const parsed = JSON.parse(r.stdout);
169
- return parsed.filter((t) => typeof t.wbs === "string" && typeof t.status === "string").map((t) => `${t.wbs}:${t.status}`).sort();
170
- } catch {
171
- return null;
172
- }
173
- }
174
- function readVerdictMtimeVector(runDir) {
175
- const dir = runDir.replace(/\/$/, "");
176
- let entries;
177
- try {
178
- entries = readdirSync(dir).filter((f) => f.endsWith("-verdict.json"));
179
- } catch {
180
- return [];
181
- }
182
- const vector = [];
183
- for (const entry of entries) {
184
- try {
185
- const mtime = statSync(`${dir}/${entry}`).mtimeMs;
186
- vector.push(`${entry.replace("-verdict.json", "")}:${mtime}`);
187
- } catch {}
188
- }
189
- return vector.sort();
190
- }
191
- function writeBlockedState(state, path) {
192
- try {
193
- mkdirSync(dirname(path), { recursive: true });
194
- writeFileSync(path, serializeBlockedState(state));
195
- } catch (err) {
196
- const reason = err instanceof Error ? err.message : String(err);
197
- process.stderr.write(`feature-sync-bounded: could not persist blocked state to ${path} — ${reason}
198
- `);
199
- }
200
- }
201
- function readBlockedState(path) {
202
- try {
203
- if (!existsSync(path))
204
- return null;
205
- const text = readFileSync(path, "utf8");
206
- return parseBlockedState(text);
207
- } catch {
208
- return null;
209
- }
210
- }
211
- function runBoundedCli(argv) {
212
- const args = parseBoundedSyncCliArgs(argv);
213
- if (args.help)
214
- return { exitCode: 0, stdout: "", stderr: BOUNDED_SYNC_CLI_USAGE };
215
- if (!args.featureId)
216
- return { exitCode: 1, stdout: "", stderr: BOUNDED_SYNC_CLI_USAGE };
217
- const statePath = blockedStateFile(args.featureId, args.runDir);
218
- const prior = readBlockedState(statePath);
219
- const featureContentHash = readFeatureContentHash(args.spurBin, args.featureId);
220
- const taskStatusVector = readTaskStatusVector(args.spurBin, args.featureId);
221
- if (featureContentHash === null || taskStatusVector === null) {
222
- return invokeLiveSync(args, statePath);
223
- }
224
- const verdictMtimeVector = readVerdictMtimeVector(args.runDir);
225
- const currentFingerprint = computeFingerprint({
226
- featureContentHash,
227
- taskStatusVector,
228
- verdictMtimeVector
229
- });
230
- const decision = decideBoundedSync(prior, currentFingerprint);
231
- if (decision.kind === "suppress") {
232
- const processed = processSyncResult(decision.replay, currentFingerprint, new Date().toISOString(), true);
233
- emitResult(processed.emit, processed.annotation, args.json);
234
- return { exitCode: 0, stdout: "", stderr: "" };
235
- }
236
- return invokeLiveSync(args, statePath, currentFingerprint);
237
- }
238
- function invokeLiveSync(args, statePath, fingerprint) {
239
- const r = runSpurJson(args.spurBin, ["feature", "sync", args.featureId, "--json"]);
240
- if (!r.ok) {
241
- return { exitCode: r.exitCode, stdout: r.stdout, stderr: r.stderr };
242
- }
243
- let result;
244
- try {
245
- result = JSON.parse(r.stdout);
246
- } catch {
247
- return { exitCode: r.exitCode, stdout: r.stdout, stderr: "" };
248
- }
249
- if (!result || typeof result !== "object" || !result.proposal) {
250
- return {
251
- exitCode: r.exitCode,
252
- stdout: r.stdout,
253
- stderr: "unrecognized feature sync envelope: missing proposal"
254
- };
255
- }
256
- const currentFingerprint = fingerprint ?? computeFingerprint({
257
- featureContentHash: readFeatureContentHash(args.spurBin, args.featureId) ?? "",
258
- taskStatusVector: readTaskStatusVector(args.spurBin, args.featureId) ?? [],
259
- verdictMtimeVector: readVerdictMtimeVector(args.runDir)
260
- });
261
- const processed = processSyncResult(result, currentFingerprint, new Date().toISOString(), false);
262
- if (processed.persist)
263
- writeBlockedState(processed.persist, statePath);
264
- emitResult(processed.emit, processed.annotation, args.json);
265
- return { exitCode: 0, stdout: "", stderr: "" };
266
- }
267
- function emitResult(result, annotation, json) {
268
- if (json) {
269
- process.stdout.write(`${JSON.stringify(result)}
270
- `);
271
- } else {
272
- process.stdout.write(`${result.proposal.featureId}: ${result.proposal.from} → ${result.proposal.to} (applied=${result.applied})
273
- `);
274
- }
275
- if (annotation.length > 0)
276
- process.stderr.write(`${annotation}
277
- `);
278
- }
279
- {
280
- const { exitCode, stdout, stderr } = runBoundedCli(process.argv.slice(2));
281
- if (stdout)
282
- process.stdout.write(stdout);
283
- if (stderr)
284
- process.stderr.write(`${stderr}
285
- `);
286
- process.exit(exitCode);
287
- }
288
- export {
289
- shouldSuppressBlocked,
290
- serializeBlockedState,
291
- runBoundedCli,
292
- processSyncResult,
293
- parseBoundedSyncCliArgs,
294
- parseBlockedState,
295
- defaultSpurBin,
296
- decideBoundedSync,
297
- computeFingerprint,
298
- classifySyncResult,
299
- blockedStateFile,
300
- BOUNDED_SYNC_CLI_USAGE
301
- };