@gobing-ai/spur 0.3.53 → 0.3.55
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.
- package/.claude-plugin/marketplace.json +1 -1
- package/config/corpus-baseline.json +2783 -2783
- package/config/pipeline-budgets.json +42 -0
- package/config/plugin-scripts.json +73 -0
- package/config/workflow-composition-baseline.json +561 -0
- package/config/workflows/docs-pipeline.yaml +29 -13
- package/config/workflows/feature-dev.yaml +54 -2
- package/config/workflows/idea-pipeline.yaml +15 -0
- package/config/workflows/task-pipeline.yaml +77 -65
- package/config/workflows/wrapup-pipeline.yaml +40 -45
- package/package.json +9 -9
- package/plugins/README.md +1 -1
- package/plugins/sp/README.md +3 -3
- package/plugins/sp/agents/super-planner.md +1 -1
- package/plugins/sp/commands/dev-daily.md +1 -1
- package/plugins/sp/commands/dev-history-load.md +1 -1
- package/plugins/sp/commands/dev-idea.md +10 -2
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/scripts/batch-preflight.mjs +160 -0
- package/plugins/sp/scripts/batch-preflight.ts +1 -1
- package/plugins/sp/scripts/daily-summary/daily-summary.mjs +479 -0
- package/plugins/sp/scripts/daily-summary/daily-summary.ts +75 -37
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +204 -0
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +1 -1
- package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +141 -0
- package/plugins/sp/scripts/dogfood-testing/validate-report.ts +1 -1
- package/plugins/sp/scripts/feature-sync-bounded.mjs +290 -0
- package/plugins/sp/scripts/feature-sync-bounded.ts +9 -8
- package/plugins/sp/scripts/history-load.mjs +268 -0
- package/plugins/sp/scripts/pr-reviewing.mjs +685 -0
- package/plugins/sp/scripts/pr-reviewing.ts +1 -1
- package/plugins/sp/scripts/script-contract-check.ts +319 -0
- package/plugins/sp/skills/daily-summary/SKILL.md +3 -3
- package/plugins/sp/skills/dogfood-testing/SKILL.md +5 -5
- package/plugins/sp/skills/dogfood-testing/references/report-template.md +1 -1
- package/plugins/sp/skills/pr-reviewing/SKILL.md +3 -3
- package/plugins/sp/skills/spur-dev/SKILL.md +2 -1
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +3 -4
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +1 -1
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +3 -3
- package/plugins/sp/skills/spur-dev/references/gate-checklists.md +4 -4
- package/spur.js +4270 -1323
- package/web/_astro/{BoardApp.plVXYkKo.js → BoardApp.DCLSB3Zs.js} +63 -63
- package/web/_astro/BoardApp.Dx5gzAhb.js +1 -0
- package/web/_astro/{TaskDetail.5DIyVQ_N.js → TaskDetail.DN_RxS-2.js} +1 -1
- package/web/_astro/{arc.BnUgL7ho.js → arc.CldTHRg-.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.B9YMJau-.js → architectureDiagram-3BPJPVTR.DQ4T9oeU.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.Dw2qgvWJ.js → blockDiagram-GPEHLZMM.CGDaXH8J.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.jTbU0-Nk.js → c4Diagram-AAUBKEIU.Cqsc2iub.js} +1 -1
- package/web/_astro/channel.CrBJYpxo.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.Di46EtkU.js → chunk-2J33WTMH.CQSKsRmq.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.DweHBwqQ.js → chunk-4BX2VUAB.Pv1HCtp8.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.rRjTKaa7.js → chunk-55IACEB6.-9MseOIk.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.BkEQIkSV.js → chunk-727SXJPM.M3OI1DF7.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.Cfe9IvAd.js → chunk-AQP2D5EJ.NfHka5Ca.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.fI6tLbeN.js → chunk-FMBD7UC4.B2suYe3s.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.BIjgpq0O.js → chunk-ND2GUHAM.BfJ7aucP.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.ZwLnVxyz.js → chunk-QZHKN3VN._582hZVc.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.BJAGYidE.js → classDiagram-4FO5ZUOK._tttU_jk.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.BJAGYidE.js → classDiagram-v2-Q7XG4LA2._tttU_jk.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.B5y3nz26.js → cose-bilkent-S5V4N54A.C4rVATLQ.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.DHmw0ZBC.js → dagre-BM42HDAG.8BLRi7f9.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.10gapJfY.js → diagram-2AECGRRQ.D6lwUZos.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.Gh33_drM.js → diagram-5GNKFQAL.Cl_rMiVT.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.8ROWlfcX.js → diagram-KO2AKTUF.DhMExHLp.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.BsP3tKmg.js → diagram-LMA3HP47.3OAxzH_o.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.DM2WFDhU.js → diagram-OG6HWLK6.Uh9RLucG.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.royaC_lH.js → erDiagram-TEJ5UH35.izzElG0c.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.C9GEvIie.js → flowDiagram-I6XJVG4X.BlGwp5qM.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.CXO4T2U0.js → ganttDiagram-6RSMTGT7.je7Vf8dN.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.Q02KzYH7.js → gitGraphDiagram-PVQCEYII.DNk0Ycop.js} +1 -1
- package/web/_astro/{index.DAxu50UF.css → index.V6Q7nhed.css} +1 -1
- package/web/_astro/{infoDiagram-5YYISTIA.DNfSDKJt.js → infoDiagram-5YYISTIA.K2HhGyWH.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.BFttvtgJ.js → ishikawaDiagram-YF4QCWOH.BbbOW4IN.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.Cfm-2SbG.js → journeyDiagram-JHISSGLW.B1UaZKqN.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.BWGNW6fV.js → kanban-definition-UN3LZRKU.9tsw5QFd.js} +1 -1
- package/web/_astro/{linear.BprdUk2Y.js → linear.I9dvtu-j.js} +1 -1
- package/web/_astro/{mermaid.core.B2zRNxtX.js → mermaid.core.5bKJsMgf.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.g9jij8sw.js → mindmap-definition-RKZ34NQL.BC5MqSn6.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.CbzR4n5b.js → pieDiagram-4H26LBE5.B8GIMyxD.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.DxV3viWt.js → quadrantDiagram-W4KKPZXB.K_aZSM_u.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.DvAb90JS.js → requirementDiagram-4Y6WPE33.B_V5kzfT.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.CIFz5t4D.js → sankeyDiagram-5OEKKPKP.DyV2quLS.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.DzlEQynd.js → sequenceDiagram-3UESZ5HK.BlKvPnBv.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.Dph2V-ts.js → stateDiagram-AJRCARHV.C1lYUMdS.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.wJhfrLAy.js → stateDiagram-v2-BHNVJYJU.DJWjr3QV.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.BrSiyubX.js → timeline-definition-PNZ67QCA.CYYiXVlv.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.COobPrUX.js → vennDiagram-CIIHVFJN.zYjQKa_S.js} +1 -1
- package/web/_astro/{wardley-L42UT6IY.CrjeTx8A.js → wardley-L42UT6IY.BGrWTY3D.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.B8_fTQ8E.js → wardleyDiagram-YWT4CUSO.CkXxtcI1.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.BPpokgVv.js → xychartDiagram-2RQKCTM6.BhKH3KG3.js} +1 -1
- package/web/index.html +2 -2
- package/config/workflows/planning-pipeline.yaml +0 -249
- package/config/workflows/task-pipeline2.yaml +0 -778
- package/web/_astro/BoardApp.pdtYu-yW.js +0 -1
- package/web/_astro/channel.DEqvaz-I.js +0 -1
|
@@ -0,0 +1,290 @@
|
|
|
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
|
+
function classifySyncResult(result) {
|
|
12
|
+
if (result.proposal.gateBlocked === true)
|
|
13
|
+
return "blocked";
|
|
14
|
+
if (result.applied === true)
|
|
15
|
+
return "applied";
|
|
16
|
+
if (result.proposal.from !== result.proposal.to)
|
|
17
|
+
return "blocked";
|
|
18
|
+
return "no-op";
|
|
19
|
+
}
|
|
20
|
+
function computeFingerprint(input) {
|
|
21
|
+
const material = [
|
|
22
|
+
input.featureContentHash,
|
|
23
|
+
...[...input.taskStatusVector].sort(),
|
|
24
|
+
...[...input.verdictMtimeVector].sort()
|
|
25
|
+
].join(`
|
|
26
|
+
`);
|
|
27
|
+
return createHash("sha256").update(material).digest("hex").slice(0, 32);
|
|
28
|
+
}
|
|
29
|
+
var blockedStateFile = (featureId, runDir) => `${runDir.replace(/\/$/, "")}/feature-sync-blocked-${featureId}.json`;
|
|
30
|
+
function serializeBlockedState(state) {
|
|
31
|
+
return `${JSON.stringify(state)}
|
|
32
|
+
`;
|
|
33
|
+
}
|
|
34
|
+
function parseBlockedState(raw) {
|
|
35
|
+
const trimmed = raw.trim();
|
|
36
|
+
if (trimmed.length === 0)
|
|
37
|
+
return null;
|
|
38
|
+
try {
|
|
39
|
+
const parsed = JSON.parse(trimmed);
|
|
40
|
+
if (typeof parsed.featureId !== "string" || typeof parsed.inputFingerprint !== "string" || typeof parsed.proposal !== "object" || parsed.proposal === null) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return parsed;
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function shouldSuppressBlocked(prior, currentFingerprint) {
|
|
49
|
+
if (!prior)
|
|
50
|
+
return { suppress: false };
|
|
51
|
+
if (prior.inputFingerprint === currentFingerprint) {
|
|
52
|
+
return { suppress: true, replay: prior.result };
|
|
53
|
+
}
|
|
54
|
+
return { suppress: false };
|
|
55
|
+
}
|
|
56
|
+
function decideBoundedSync(prior, currentFingerprint) {
|
|
57
|
+
const decision = shouldSuppressBlocked(prior, currentFingerprint);
|
|
58
|
+
if (decision.suppress && decision.replay) {
|
|
59
|
+
return { kind: "suppress", replay: decision.replay };
|
|
60
|
+
}
|
|
61
|
+
return { kind: "invoke" };
|
|
62
|
+
}
|
|
63
|
+
function processSyncResult(result, currentFingerprint, persistedAt, wasSuppressed) {
|
|
64
|
+
const classification = classifySyncResult(result);
|
|
65
|
+
if (classification === "blocked") {
|
|
66
|
+
return {
|
|
67
|
+
classification,
|
|
68
|
+
emit: result,
|
|
69
|
+
persist: {
|
|
70
|
+
featureId: result.proposal.featureId,
|
|
71
|
+
inputFingerprint: currentFingerprint,
|
|
72
|
+
proposal: result.proposal,
|
|
73
|
+
classification,
|
|
74
|
+
result,
|
|
75
|
+
persistedAt
|
|
76
|
+
},
|
|
77
|
+
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}`
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
classification,
|
|
82
|
+
emit: result,
|
|
83
|
+
annotation: ""
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
var BOUNDED_SYNC_CLI_USAGE = `Usage:
|
|
87
|
+
bun plugins/sp/scripts/feature-sync-bounded.ts <feature-id> \\
|
|
88
|
+
--spur-bin <spur|bun apps/cli/src/index.ts> \\
|
|
89
|
+
[--run-dir .spur/run] [--json]
|
|
90
|
+
|
|
91
|
+
Wraps 'spur feature sync <id> --json' with bounded retry-suppression: an identical
|
|
92
|
+
blocked proposal is reported once and suppressed until feature file content, linked
|
|
93
|
+
task statuses, or verdict artifact mtimes change. Applied and no-op results pass
|
|
94
|
+
through unchanged.
|
|
95
|
+
|
|
96
|
+
Exit: 0 = sync handled (applied / no-op / suppressed-blocked / live-blocked).`;
|
|
97
|
+
function defaultSpurBin() {
|
|
98
|
+
if (process.env.SPUR_BIN)
|
|
99
|
+
return process.env.SPUR_BIN;
|
|
100
|
+
const local = fileURLToPath(new URL("../../../apps/cli/src/index.ts", import.meta.url));
|
|
101
|
+
if (existsSync(local))
|
|
102
|
+
return `bun ${local}`;
|
|
103
|
+
return "spur";
|
|
104
|
+
}
|
|
105
|
+
function parseBoundedSyncCliArgs(argv) {
|
|
106
|
+
let featureId = "";
|
|
107
|
+
let spurBin = defaultSpurBin();
|
|
108
|
+
let runDir = ".spur/run";
|
|
109
|
+
let json = false;
|
|
110
|
+
let help = false;
|
|
111
|
+
for (let i = 0;i < argv.length; i++) {
|
|
112
|
+
const a = argv[i];
|
|
113
|
+
if (a === "--help" || a === "-h")
|
|
114
|
+
help = true;
|
|
115
|
+
else if (a === "--json")
|
|
116
|
+
json = true;
|
|
117
|
+
else if (a === "--spur-bin")
|
|
118
|
+
spurBin = argv[++i] ?? spurBin;
|
|
119
|
+
else if (a === "--run-dir")
|
|
120
|
+
runDir = argv[++i] ?? runDir;
|
|
121
|
+
else if (!a.startsWith("--") && featureId === "")
|
|
122
|
+
featureId = a;
|
|
123
|
+
}
|
|
124
|
+
return { featureId, spurBin, runDir, json, help };
|
|
125
|
+
}
|
|
126
|
+
function runSpurJson(spurBin, args) {
|
|
127
|
+
const binParts = spurBin.split(/\s+/).filter(Boolean);
|
|
128
|
+
const cmd = binParts[0] ?? "spur";
|
|
129
|
+
const cmdArgs = [...binParts.slice(1), ...args];
|
|
130
|
+
const r = spawnSync(cmd, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], encoding: "utf8" });
|
|
131
|
+
const decode = (b) => typeof b === "string" ? b : Buffer.from(b ?? []).toString("utf8");
|
|
132
|
+
return {
|
|
133
|
+
stdout: typeof r.stdout === "string" ? r.stdout : decode(r.stdout),
|
|
134
|
+
stderr: typeof r.stderr === "string" ? r.stderr : decode(r.stderr),
|
|
135
|
+
exitCode: r.status ?? (r.error ? 1 : 0),
|
|
136
|
+
ok: (r.status ?? (r.error ? 1 : 0)) === 0
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function readFeatureContentHash(spurBin, featureId) {
|
|
140
|
+
const r = runSpurJson(spurBin, ["feature", "show", featureId, "--json"]);
|
|
141
|
+
if (!r.ok)
|
|
142
|
+
return null;
|
|
143
|
+
try {
|
|
144
|
+
const parsed = JSON.parse(r.stdout);
|
|
145
|
+
if (typeof parsed.content !== "string")
|
|
146
|
+
return null;
|
|
147
|
+
return createHash("sha256").update(parsed.content).digest("hex");
|
|
148
|
+
} catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function readTaskStatusVector(spurBin, featureId) {
|
|
153
|
+
const r = runSpurJson(spurBin, ["task", "list", "--feature", featureId, "--json"]);
|
|
154
|
+
if (!r.ok)
|
|
155
|
+
return null;
|
|
156
|
+
try {
|
|
157
|
+
const parsed = JSON.parse(r.stdout);
|
|
158
|
+
return parsed.filter((t) => typeof t.wbs === "string" && typeof t.status === "string").map((t) => `${t.wbs}:${t.status}`).sort();
|
|
159
|
+
} catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function readVerdictMtimeVector(runDir) {
|
|
164
|
+
const dir = runDir.replace(/\/$/, "");
|
|
165
|
+
let entries;
|
|
166
|
+
try {
|
|
167
|
+
entries = readdirSync(dir).filter((f) => f.endsWith("-verdict.json"));
|
|
168
|
+
} catch {
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
const vector = [];
|
|
172
|
+
for (const entry of entries) {
|
|
173
|
+
try {
|
|
174
|
+
const mtime = statSync(`${dir}/${entry}`).mtimeMs;
|
|
175
|
+
vector.push(`${entry.replace("-verdict.json", "")}:${mtime}`);
|
|
176
|
+
} catch {}
|
|
177
|
+
}
|
|
178
|
+
return vector.sort();
|
|
179
|
+
}
|
|
180
|
+
function writeBlockedState(state, path) {
|
|
181
|
+
try {
|
|
182
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
183
|
+
writeFileSync(path, serializeBlockedState(state));
|
|
184
|
+
} catch (err) {
|
|
185
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
186
|
+
process.stderr.write(`feature-sync-bounded: could not persist blocked state to ${path} — ${reason}
|
|
187
|
+
`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function readBlockedState(path) {
|
|
191
|
+
try {
|
|
192
|
+
if (!existsSync(path))
|
|
193
|
+
return null;
|
|
194
|
+
const text = readFileSync(path, "utf8");
|
|
195
|
+
return parseBlockedState(text);
|
|
196
|
+
} catch {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function runBoundedCli(argv) {
|
|
201
|
+
const args = parseBoundedSyncCliArgs(argv);
|
|
202
|
+
if (args.help)
|
|
203
|
+
return { exitCode: 0, stdout: "", stderr: BOUNDED_SYNC_CLI_USAGE };
|
|
204
|
+
if (!args.featureId)
|
|
205
|
+
return { exitCode: 1, stdout: "", stderr: BOUNDED_SYNC_CLI_USAGE };
|
|
206
|
+
const statePath = blockedStateFile(args.featureId, args.runDir);
|
|
207
|
+
const prior = readBlockedState(statePath);
|
|
208
|
+
const featureContentHash = readFeatureContentHash(args.spurBin, args.featureId);
|
|
209
|
+
const taskStatusVector = readTaskStatusVector(args.spurBin, args.featureId);
|
|
210
|
+
if (featureContentHash === null || taskStatusVector === null) {
|
|
211
|
+
return invokeLiveSync(args, statePath);
|
|
212
|
+
}
|
|
213
|
+
const verdictMtimeVector = readVerdictMtimeVector(args.runDir);
|
|
214
|
+
const currentFingerprint = computeFingerprint({
|
|
215
|
+
featureContentHash,
|
|
216
|
+
taskStatusVector,
|
|
217
|
+
verdictMtimeVector
|
|
218
|
+
});
|
|
219
|
+
const decision = decideBoundedSync(prior, currentFingerprint);
|
|
220
|
+
if (decision.kind === "suppress") {
|
|
221
|
+
const processed = processSyncResult(decision.replay, currentFingerprint, new Date().toISOString(), true);
|
|
222
|
+
emitResult(processed.emit, processed.annotation, args.json);
|
|
223
|
+
return { exitCode: 0, stdout: "", stderr: "" };
|
|
224
|
+
}
|
|
225
|
+
return invokeLiveSync(args, statePath, currentFingerprint);
|
|
226
|
+
}
|
|
227
|
+
function invokeLiveSync(args, statePath, fingerprint) {
|
|
228
|
+
const r = runSpurJson(args.spurBin, ["feature", "sync", args.featureId, "--json"]);
|
|
229
|
+
if (!r.ok) {
|
|
230
|
+
return { exitCode: r.exitCode, stdout: r.stdout, stderr: r.stderr };
|
|
231
|
+
}
|
|
232
|
+
let result;
|
|
233
|
+
try {
|
|
234
|
+
result = JSON.parse(r.stdout);
|
|
235
|
+
} catch {
|
|
236
|
+
return { exitCode: r.exitCode, stdout: r.stdout, stderr: "" };
|
|
237
|
+
}
|
|
238
|
+
if (!result || typeof result !== "object" || !result.proposal) {
|
|
239
|
+
return {
|
|
240
|
+
exitCode: r.exitCode,
|
|
241
|
+
stdout: r.stdout,
|
|
242
|
+
stderr: "unrecognized feature sync envelope: missing proposal"
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
const currentFingerprint = fingerprint ?? computeFingerprint({
|
|
246
|
+
featureContentHash: readFeatureContentHash(args.spurBin, args.featureId) ?? "",
|
|
247
|
+
taskStatusVector: readTaskStatusVector(args.spurBin, args.featureId) ?? [],
|
|
248
|
+
verdictMtimeVector: readVerdictMtimeVector(args.runDir)
|
|
249
|
+
});
|
|
250
|
+
const processed = processSyncResult(result, currentFingerprint, new Date().toISOString(), false);
|
|
251
|
+
if (processed.persist)
|
|
252
|
+
writeBlockedState(processed.persist, statePath);
|
|
253
|
+
emitResult(processed.emit, processed.annotation, args.json);
|
|
254
|
+
return { exitCode: 0, stdout: "", stderr: "" };
|
|
255
|
+
}
|
|
256
|
+
function emitResult(result, annotation, json) {
|
|
257
|
+
if (json) {
|
|
258
|
+
process.stdout.write(`${JSON.stringify(result)}
|
|
259
|
+
`);
|
|
260
|
+
} else {
|
|
261
|
+
process.stdout.write(`${result.proposal.featureId}: ${result.proposal.from} → ${result.proposal.to} (applied=${result.applied})
|
|
262
|
+
`);
|
|
263
|
+
}
|
|
264
|
+
if (annotation.length > 0)
|
|
265
|
+
process.stderr.write(`${annotation}
|
|
266
|
+
`);
|
|
267
|
+
}
|
|
268
|
+
{
|
|
269
|
+
const { exitCode, stdout, stderr } = runBoundedCli(process.argv.slice(2));
|
|
270
|
+
if (stdout)
|
|
271
|
+
process.stdout.write(stdout);
|
|
272
|
+
if (stderr)
|
|
273
|
+
process.stderr.write(`${stderr}
|
|
274
|
+
`);
|
|
275
|
+
process.exit(exitCode);
|
|
276
|
+
}
|
|
277
|
+
export {
|
|
278
|
+
shouldSuppressBlocked,
|
|
279
|
+
serializeBlockedState,
|
|
280
|
+
runBoundedCli,
|
|
281
|
+
processSyncResult,
|
|
282
|
+
parseBoundedSyncCliArgs,
|
|
283
|
+
parseBlockedState,
|
|
284
|
+
defaultSpurBin,
|
|
285
|
+
decideBoundedSync,
|
|
286
|
+
computeFingerprint,
|
|
287
|
+
classifySyncResult,
|
|
288
|
+
blockedStateFile,
|
|
289
|
+
BOUNDED_SYNC_CLI_USAGE
|
|
290
|
+
};
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* to avoid importing service packages into the plugin tree.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
+
import { spawnSync } from 'node:child_process';
|
|
22
23
|
import { createHash } from 'node:crypto';
|
|
23
24
|
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
24
25
|
import { dirname } from 'node:path';
|
|
@@ -280,14 +281,14 @@ function runSpurJson(spurBin: string, args: string[]): SpawnResult {
|
|
|
280
281
|
const binParts = spurBin.split(/\s+/).filter(Boolean);
|
|
281
282
|
const cmd = binParts[0] ?? 'spur';
|
|
282
283
|
const cmdArgs = [...binParts.slice(1), ...args];
|
|
283
|
-
const r =
|
|
284
|
+
const r = spawnSync(cmd, cmdArgs, { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' });
|
|
284
285
|
const decode = (b: unknown): string =>
|
|
285
286
|
typeof b === 'string' ? b : Buffer.from((b as Uint8Array) ?? []).toString('utf8');
|
|
286
287
|
return {
|
|
287
|
-
stdout: decode(r.stdout),
|
|
288
|
-
stderr: decode(r.stderr),
|
|
289
|
-
exitCode: r.
|
|
290
|
-
ok: (r.
|
|
288
|
+
stdout: typeof r.stdout === 'string' ? r.stdout : decode(r.stdout),
|
|
289
|
+
stderr: typeof r.stderr === 'string' ? r.stderr : decode(r.stderr),
|
|
290
|
+
exitCode: r.status ?? (r.error ? 1 : 0),
|
|
291
|
+
ok: (r.status ?? (r.error ? 1 : 0)) === 0,
|
|
291
292
|
};
|
|
292
293
|
}
|
|
293
294
|
|
|
@@ -361,8 +362,8 @@ function writeBlockedState(state: BlockedState, path: string): void {
|
|
|
361
362
|
|
|
362
363
|
function readBlockedState(path: string): BlockedState | null {
|
|
363
364
|
try {
|
|
364
|
-
|
|
365
|
-
const text =
|
|
365
|
+
if (!existsSync(path)) return null;
|
|
366
|
+
const text = readFileSync(path, 'utf8');
|
|
366
367
|
return parseBlockedState(text);
|
|
367
368
|
} catch {
|
|
368
369
|
return null;
|
|
@@ -470,7 +471,7 @@ function emitResult(result: FeatureSyncResult, annotation: string, json: boolean
|
|
|
470
471
|
}
|
|
471
472
|
|
|
472
473
|
if (import.meta.main) {
|
|
473
|
-
const { exitCode, stdout, stderr } = runBoundedCli(
|
|
474
|
+
const { exitCode, stdout, stderr } = runBoundedCli(process.argv.slice(2));
|
|
474
475
|
if (stdout) process.stdout.write(stdout);
|
|
475
476
|
if (stderr) process.stderr.write(`${stderr}\n`);
|
|
476
477
|
process.exit(exitCode);
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// plugins/sp/scripts/history-load.ts
|
|
5
|
+
import { spawnSync } from "child_process";
|
|
6
|
+
import { existsSync, realpathSync } from "fs";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
var VALUE_FLAGS = {
|
|
10
|
+
"--source": true,
|
|
11
|
+
"--session": true,
|
|
12
|
+
"--task": true,
|
|
13
|
+
"--since": true,
|
|
14
|
+
"--until": true
|
|
15
|
+
};
|
|
16
|
+
var BOOL_FLAGS = {
|
|
17
|
+
"--report": true,
|
|
18
|
+
"--dry-run": true,
|
|
19
|
+
"--json": true
|
|
20
|
+
};
|
|
21
|
+
var ALL_FLAGS = { ...VALUE_FLAGS, ...BOOL_FLAGS };
|
|
22
|
+
function usage() {
|
|
23
|
+
console.error("Usage: history-load.ts [--source <name>] [--session <id>] [--task <wbs>] " + "[--since <iso>] [--until <iso>] [--report] [--dry-run] [--json]");
|
|
24
|
+
process.exit(2);
|
|
25
|
+
}
|
|
26
|
+
var FLAG_KEY = {
|
|
27
|
+
"--source": "source",
|
|
28
|
+
"--session": "session",
|
|
29
|
+
"--task": "task",
|
|
30
|
+
"--since": "since",
|
|
31
|
+
"--until": "until",
|
|
32
|
+
"--report": "report",
|
|
33
|
+
"--dry-run": "dryRun",
|
|
34
|
+
"--json": "json"
|
|
35
|
+
};
|
|
36
|
+
function parseArgs(argv) {
|
|
37
|
+
const out = { report: false, dryRun: false, json: false };
|
|
38
|
+
for (let i = 0;i < argv.length; i++) {
|
|
39
|
+
const arg = argv[i];
|
|
40
|
+
if (!arg.startsWith("--") || ALL_FLAGS[arg] !== true)
|
|
41
|
+
usage();
|
|
42
|
+
if (BOOL_FLAGS[arg] === true) {
|
|
43
|
+
out[FLAG_KEY[arg]] = true;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const value = argv[++i];
|
|
47
|
+
if (value === undefined || value.startsWith("--"))
|
|
48
|
+
usage();
|
|
49
|
+
out[FLAG_KEY[arg]] = value;
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
function defaultSpurBin() {
|
|
54
|
+
if (process.env.SPUR_BIN)
|
|
55
|
+
return process.env.SPUR_BIN;
|
|
56
|
+
const local = fileURLToPath(new URL("../../../apps/cli/src/index.ts", import.meta.url));
|
|
57
|
+
if (existsSync(local))
|
|
58
|
+
return `bun ${local}`;
|
|
59
|
+
return "spur";
|
|
60
|
+
}
|
|
61
|
+
function runSpur(spurBin, args) {
|
|
62
|
+
const [file = "spur", ...lead] = spurBin.split(/\s+/).filter(Boolean);
|
|
63
|
+
const result = spawnSync(file, [...lead, ...args], { encoding: "utf-8" });
|
|
64
|
+
return {
|
|
65
|
+
status: result.status ?? 1,
|
|
66
|
+
stdout: result.stdout ?? "",
|
|
67
|
+
stderr: result.stderr ?? ""
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function latestArtifactPath(cwd) {
|
|
71
|
+
const pointer = join(cwd, ".spur", "reports", "history", "latest.json");
|
|
72
|
+
if (!existsSync(pointer))
|
|
73
|
+
return null;
|
|
74
|
+
try {
|
|
75
|
+
return realpathSync(pointer);
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function parseImportJson(stdout) {
|
|
81
|
+
try {
|
|
82
|
+
const parsed = JSON.parse(stdout);
|
|
83
|
+
return parsed && Array.isArray(parsed.entries) ? parsed : null;
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function buildAnalyzeArgs(args) {
|
|
89
|
+
const out = ["history", "analyze", "--json"];
|
|
90
|
+
if (args.source)
|
|
91
|
+
out.push("--source", args.source);
|
|
92
|
+
if (args.session)
|
|
93
|
+
out.push("--session", args.session);
|
|
94
|
+
if (args.task)
|
|
95
|
+
out.push("--task", args.task);
|
|
96
|
+
if (args.since)
|
|
97
|
+
out.push("--since", args.since);
|
|
98
|
+
if (args.until)
|
|
99
|
+
out.push("--until", args.until);
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
function emitJson(obj) {
|
|
103
|
+
process.stdout.write(`${JSON.stringify(obj)}
|
|
104
|
+
`);
|
|
105
|
+
}
|
|
106
|
+
function buildDegradedWarnings(imp) {
|
|
107
|
+
const detailFor = (source) => imp?.warnings?.find((w) => w.source === source)?.detail ?? "no warning detail reported by import";
|
|
108
|
+
return (imp?.entries ?? []).filter((e) => e.status === "degraded" || e.status === "failed").map((e) => ({
|
|
109
|
+
source: e.source,
|
|
110
|
+
status: e.status,
|
|
111
|
+
parseErrors: typeof e.parseErrors === "number" ? e.parseErrors : 0,
|
|
112
|
+
validationErrors: typeof e.validationErrors === "number" ? e.validationErrors : 0,
|
|
113
|
+
detail: detailFor(e.source)
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
function withWarnings(payload, degraded) {
|
|
117
|
+
return degraded.length > 0 ? { ...payload, warnings: degraded } : payload;
|
|
118
|
+
}
|
|
119
|
+
function main() {
|
|
120
|
+
const args = parseArgs(process.argv.slice(2));
|
|
121
|
+
const spurBin = defaultSpurBin();
|
|
122
|
+
const cwd = process.cwd();
|
|
123
|
+
const importArgs = ["history", "import", "--json"];
|
|
124
|
+
if (args.source)
|
|
125
|
+
importArgs.push("--source", args.source);
|
|
126
|
+
if (args.dryRun)
|
|
127
|
+
importArgs.push("--dry-run");
|
|
128
|
+
const importResult = runSpur(spurBin, importArgs);
|
|
129
|
+
const imp = parseImportJson(importResult.stdout);
|
|
130
|
+
if (importResult.status !== 0 && importResult.status !== 2) {
|
|
131
|
+
const failed = (imp?.entries ?? []).filter((e) => e.status !== "ok" && e.status !== "empty").map((e) => e.source);
|
|
132
|
+
const warning = imp?.warnings?.find((w) => w.code === "source-failed" || w.code === "source-degraded");
|
|
133
|
+
const detail = warning?.detail || importResult.stderr.trim() || "import exited non-zero";
|
|
134
|
+
const message = failed.length > 0 ? `import failed for source(s): ${failed.join(", ")} \u2014 ${detail}` : detail;
|
|
135
|
+
if (args.json) {
|
|
136
|
+
emitJson({
|
|
137
|
+
import: imp ?? { entries: [], exitCode: importResult.status },
|
|
138
|
+
artifact: null,
|
|
139
|
+
reported: false,
|
|
140
|
+
status: "error",
|
|
141
|
+
message
|
|
142
|
+
});
|
|
143
|
+
} else {
|
|
144
|
+
console.error(message);
|
|
145
|
+
}
|
|
146
|
+
process.exit(importResult.status);
|
|
147
|
+
}
|
|
148
|
+
const degraded = importResult.status === 2 ? buildDegradedWarnings(imp) : [];
|
|
149
|
+
if (degraded.length > 0 && !args.json) {
|
|
150
|
+
console.error("WARNING: import fan-out degraded \u2014 proceeding with the healthy sources:");
|
|
151
|
+
for (const w of degraded) {
|
|
152
|
+
console.error(` ${w.source}: status=${w.status} parseErrors=${w.parseErrors} ` + `validationErrors=${w.validationErrors} \u2014 ${w.detail}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (args.dryRun) {
|
|
156
|
+
const analyzeArgs = buildAnalyzeArgs(args);
|
|
157
|
+
const sequence = [`spur history import --json${args.source ? ` --source ${args.source}` : ""} --dry-run`];
|
|
158
|
+
sequence.push(`spur ${analyzeArgs.join(" ")}`);
|
|
159
|
+
if (args.report)
|
|
160
|
+
sequence.push("spur history report --mode forensics <artifact-path>");
|
|
161
|
+
if (args.json) {
|
|
162
|
+
emitJson(withWarnings({
|
|
163
|
+
import: imp ?? { entries: [], exitCode: importResult.status },
|
|
164
|
+
artifact: null,
|
|
165
|
+
reported: false,
|
|
166
|
+
status: "dry-run",
|
|
167
|
+
wouldRun: sequence
|
|
168
|
+
}, degraded));
|
|
169
|
+
} else {
|
|
170
|
+
console.log("[dry-run] would run:");
|
|
171
|
+
for (const line of sequence)
|
|
172
|
+
console.log(` ${line}`);
|
|
173
|
+
}
|
|
174
|
+
process.exit(0);
|
|
175
|
+
}
|
|
176
|
+
const analyzeResult = runSpur(spurBin, buildAnalyzeArgs(args));
|
|
177
|
+
if (analyzeResult.status !== 0) {
|
|
178
|
+
const message = analyzeResult.stderr.trim() || `history analyze exited non-zero (${analyzeResult.status})`;
|
|
179
|
+
if (args.json) {
|
|
180
|
+
emitJson({
|
|
181
|
+
import: imp ?? { entries: [], exitCode: 0 },
|
|
182
|
+
artifact: null,
|
|
183
|
+
reported: false,
|
|
184
|
+
status: "error",
|
|
185
|
+
message
|
|
186
|
+
});
|
|
187
|
+
} else {
|
|
188
|
+
console.error(message);
|
|
189
|
+
}
|
|
190
|
+
process.exit(analyzeResult.status);
|
|
191
|
+
}
|
|
192
|
+
let artifact = null;
|
|
193
|
+
try {
|
|
194
|
+
artifact = JSON.parse(analyzeResult.stdout);
|
|
195
|
+
} catch {}
|
|
196
|
+
const artifactPath = latestArtifactPath(cwd);
|
|
197
|
+
const messages = artifact?.totals?.messages;
|
|
198
|
+
if (typeof messages === "number" && messages === 0) {
|
|
199
|
+
const message = "history analyze: window matched zero messages \u2014 nothing to report";
|
|
200
|
+
if (args.json) {
|
|
201
|
+
emitJson({
|
|
202
|
+
import: imp ?? { entries: [], exitCode: 0 },
|
|
203
|
+
artifact: artifactPath,
|
|
204
|
+
reported: false,
|
|
205
|
+
status: "empty-window",
|
|
206
|
+
message
|
|
207
|
+
});
|
|
208
|
+
} else {
|
|
209
|
+
console.error(message);
|
|
210
|
+
}
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
if (artifactPath === null) {
|
|
214
|
+
const message = "history analyze completed but no artifact pointer (.spur/reports/history/latest.json) was found";
|
|
215
|
+
if (args.json) {
|
|
216
|
+
emitJson({
|
|
217
|
+
import: imp ?? { entries: [], exitCode: 0 },
|
|
218
|
+
artifact: null,
|
|
219
|
+
reported: false,
|
|
220
|
+
status: "error",
|
|
221
|
+
message
|
|
222
|
+
});
|
|
223
|
+
} else {
|
|
224
|
+
console.error(message);
|
|
225
|
+
}
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
let reported = false;
|
|
229
|
+
let reportText = "";
|
|
230
|
+
if (args.report) {
|
|
231
|
+
const reportResult = runSpur(spurBin, ["history", "report", "--mode", "forensics", artifactPath]);
|
|
232
|
+
reported = reportResult.status === 0;
|
|
233
|
+
reportText = reportResult.stdout;
|
|
234
|
+
if (reportResult.status !== 0) {
|
|
235
|
+
const message = reportResult.stderr.trim() || `history report exited non-zero (${reportResult.status})`;
|
|
236
|
+
if (args.json) {
|
|
237
|
+
emitJson({
|
|
238
|
+
import: imp ?? { entries: [], exitCode: 0 },
|
|
239
|
+
artifact: artifactPath,
|
|
240
|
+
reported: false,
|
|
241
|
+
status: "error",
|
|
242
|
+
message
|
|
243
|
+
});
|
|
244
|
+
} else {
|
|
245
|
+
console.error(message);
|
|
246
|
+
}
|
|
247
|
+
process.exit(reportResult.status);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const count = (imp?.entries ?? []).reduce((sum, e) => sum + (typeof e.messages === "number" ? e.messages : 0), 0);
|
|
251
|
+
if (args.json) {
|
|
252
|
+
const payload = withWarnings({
|
|
253
|
+
import: imp ?? { entries: [], exitCode: 0 },
|
|
254
|
+
artifact: artifactPath,
|
|
255
|
+
reported,
|
|
256
|
+
status: "ok"
|
|
257
|
+
}, degraded);
|
|
258
|
+
if (args.report)
|
|
259
|
+
payload.report = reportText;
|
|
260
|
+
emitJson(payload);
|
|
261
|
+
} else {
|
|
262
|
+
console.log(`history import: ${count} records`);
|
|
263
|
+
console.log(`artifact: ${artifactPath}`);
|
|
264
|
+
if (args.report)
|
|
265
|
+
process.stdout.write(reportText);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
main();
|