@gobing-ai/spur 0.3.99 → 0.4.1
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/config.global.yaml +17 -0
- package/config/decisions/gates.yaml +36 -0
- package/config/decisions/history-anatomy.yaml +35 -0
- package/config/decisions/idea-pipeline.yaml +38 -0
- package/config/decisions/task-pipeline.yaml +55 -0
- package/config/plugin-scripts.json +25 -0
- package/config/rules/boundary/sp-runtime-path.yaml +2 -0
- package/config/rules/docs/sourced-api-claims.yaml +60 -0
- package/config/rules/quality/coverage-gate.yaml +5 -3
- package/config/rules/recommended-pre-check.yaml +8 -3
- package/config/rules/strict/runtime-boundaries.yaml +11 -0
- package/config/rules/structure/protected-files.yaml +3 -0
- package/config/rules/structure/scratch-evidence-pointer.yaml +35 -0
- package/config/script-placement-baseline.json +2 -2
- package/config/workflows/feature-lifecycle.yaml +9 -2
- package/config/workflows/history-anatomy.yaml +22 -0
- package/config/workflows/idea-pipeline.yaml +75 -0
- package/config/workflows/task-pipeline.yaml +368 -49
- package/config/workflows/wrapup-pipeline.yaml +106 -13
- package/package.json +9 -9
- package/plugins/sp/README.md +25 -2
- package/plugins/sp/agents/super-planner.md +11 -0
- package/plugins/sp/agents/super-reviewer.md +39 -11
- package/plugins/sp/bunfig.toml +8 -0
- package/plugins/sp/commands/dev-brainstorm.md +29 -16
- package/plugins/sp/commands/dev-changelog.md +5 -4
- package/plugins/sp/commands/dev-find-way.md +80 -0
- package/plugins/sp/commands/dev-idea.md +6 -0
- package/plugins/sp/commands/dev-parallel.md +8 -2
- package/plugins/sp/commands/dev-plan.md +6 -0
- package/plugins/sp/commands/dev-review-session.md +14 -7
- package/plugins/sp/commands/dev-run.md +17 -4
- package/plugins/sp/commands/dev-runall.md +21 -10
- package/plugins/sp/hooks/agent-lifecycle.ts +155 -0
- package/plugins/sp/hooks/context-session-start.ts +1 -1
- package/plugins/sp/hooks/fleet-guest-stop.ts +180 -0
- package/plugins/sp/hooks/hooks.json +45 -1
- package/plugins/sp/hooks/pi/guard-extension.ts +21 -0
- package/plugins/sp/lib/history-anatomy.generated.mjs +5 -0
- package/plugins/sp/lib/idea-handoff.generated.mjs +72 -11
- package/plugins/sp/lib/inline-run-app.ts +28 -0
- package/plugins/sp/lib/inline-run.generated.d.mts +6 -0
- package/plugins/sp/lib/inline-run.generated.mjs +364 -46
- package/plugins/sp/lib/plan-projection.generated.d.mts +28 -0
- package/plugins/sp/lib/plan-projection.generated.mjs +255 -0
- package/plugins/sp/lib/quality-gate.generated.d.mts +47 -1
- package/plugins/sp/lib/quality-gate.generated.mjs +30489 -82
- package/plugins/sp/lib/residual-scan.generated.d.mts +19 -0
- package/plugins/sp/lib/residual-scan.generated.mjs +66 -0
- package/plugins/sp/lib/run-summary-core.ts +561 -0
- package/plugins/sp/lib/transcript.ts +275 -0
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/references/roles.md +1 -1
- package/plugins/sp/scripts/batch-plan.mjs +335 -0
- package/plugins/sp/scripts/batch-plan.ts +100 -0
- package/plugins/sp/scripts/batch-preflight.mjs +51 -3
- package/plugins/sp/scripts/batch-preflight.ts +72 -1
- package/plugins/sp/scripts/commit-guard.mjs +216 -0
- package/plugins/sp/scripts/commit-guard.ts +249 -0
- package/plugins/sp/scripts/history-anatomy-cache.mjs +5 -0
- package/plugins/sp/scripts/inline-run-dispatch.mjs +130 -0
- package/plugins/sp/scripts/inline-run-dispatch.ts +135 -0
- package/plugins/sp/scripts/inline-run-setup.mjs +563 -91
- package/plugins/sp/scripts/inline-run-setup.ts +65 -97
- package/plugins/sp/scripts/persist-out-check.mjs +90 -41
- package/plugins/sp/scripts/persist-out-check.ts +112 -57
- package/plugins/sp/scripts/quality-gate.mjs +30407 -73
- package/plugins/sp/scripts/quality-gate.ts +21 -5
- package/plugins/sp/scripts/residual-scan.mjs +88 -38
- package/plugins/sp/scripts/residual-scan.ts +35 -38
- package/plugins/sp/scripts/run-summary.mjs +693 -0
- package/plugins/sp/scripts/run-summary.ts +105 -0
- package/plugins/sp/scripts/session-timeline.mjs +331 -0
- package/plugins/sp/scripts/session-timeline.ts +204 -0
- package/plugins/sp/scripts/wrapup-steps.mjs +150 -2
- package/plugins/sp/scripts/wrapup-steps.ts +203 -1
- package/plugins/sp/skills/brainstorm/SKILL.md +9 -47
- package/plugins/sp/skills/branch-workflow/SKILL.md +1 -0
- package/plugins/sp/skills/branch-workflow/references/worktree-patterns.md +1 -0
- package/plugins/sp/skills/code-implementation/SKILL.md +48 -1
- package/plugins/sp/skills/code-verification/SKILL.md +8 -4
- package/plugins/sp/skills/code-verification/references/secu-review.md +3 -0
- package/plugins/sp/skills/code-verification/references/verdict-schema.md +68 -0
- package/plugins/sp/skills/fleet-join/SKILL.md +63 -0
- package/plugins/sp/skills/session-review/SKILL.md +77 -24
- package/plugins/sp/skills/source-driven-development/SKILL.md +5 -0
- package/plugins/sp/skills/spec-decomposition/references/decomposition.md +5 -3
- package/plugins/sp/skills/spur-cli/SKILL.md +6 -2
- package/plugins/sp/skills/spur-cli/references/agent.md +124 -17
- package/plugins/sp/skills/spur-cli/references/decision.md +98 -0
- package/plugins/sp/skills/spur-cli/references/message.md +7 -7
- package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +41 -3
- package/plugins/sp/skills/spur-cli/references/tasks.md +10 -7
- package/plugins/sp/skills/spur-cli/references/workflows/authoring-workflows.md +7 -0
- package/plugins/sp/skills/spur-cli/references/workflows.md +7 -2
- package/plugins/sp/skills/spur-composer/SKILL.md +28 -2
- package/plugins/sp/skills/spur-dev/SKILL.md +18 -14
- package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +6 -3
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +49 -14
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +51 -18
- package/plugins/sp/skills/spur-dev/references/execution-batch-continuation.md +68 -0
- package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +194 -0
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +113 -963
- package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +130 -0
- package/plugins/sp/skills/spur-dev/references/execution-workflow.md +20 -0
- package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +471 -0
- package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +379 -0
- package/plugins/sp/skills/spur-dev/references/flag-glossary.md +51 -9
- package/plugins/sp/skills/spur-dev/references/glossary.md +2 -2
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +361 -156
- package/plugins/sp/skills/spur-dev/references/planning-workflow.md +6 -0
- package/plugins/sp/skills/spur-dev/references/structured-trace-emission.md +178 -0
- package/plugins/sp/skills/wayfinder/SKILL.md +10 -14
- package/schemas/decision-catalog.schema.json +226 -0
- package/schemas/spur-config.schema.json +25 -0
- package/schemas/state-machine-workflow.schema.json +23 -0
- package/spur.js +28165 -19051
- package/web/_astro/{BoardApp.i4eh_fHA.js → BoardApp.D66k_s_T.js} +1 -1
- package/web/_astro/BoardApp.D6sjAJr_.js +324 -0
- package/web/_astro/{TaskDetail.C5Cu8LdB.js → TaskDetail.BynLnMNc.js} +1 -1
- package/web/_astro/{arc.Bu5XpBEz.js → arc._BUadrdH.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.9vHQTvhF.js → architectureDiagram-3BPJPVTR.Dh7xS1Pp.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.Cn2Xl7D6.js → blockDiagram-GPEHLZMM.DFDVpfXm.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.7o0P_QDo.js → c4Diagram-AAUBKEIU.V7_3_glh.js} +1 -1
- package/web/_astro/channel.DZVfto-r.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.C3zZFsNt.js → chunk-2J33WTMH.De1yx7Jz.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.CNy_M7fO.js → chunk-4BX2VUAB.BDhHFuxx.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.2TiPnLBL.js → chunk-55IACEB6.DDLkWnmC.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.5CGSyKAp.js → chunk-727SXJPM.CwfiLnA7.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.w-ZIgfsK.js → chunk-AQP2D5EJ.CtGDmUnM.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.BkcEyhKa.js → chunk-FMBD7UC4.BOLjpw73.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.mxKKbBG4.js → chunk-ND2GUHAM.BDhQls6O.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.cyA0z9aT.js → chunk-QZHKN3VN.76qBynDF.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.CqtUzwhX.js → classDiagram-4FO5ZUOK.N0mpPHT0.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.CqtUzwhX.js → classDiagram-v2-Q7XG4LA2.N0mpPHT0.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.BExPTopI.js → cose-bilkent-S5V4N54A.CKknFfjB.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.Bq2i10Cn.js → cynefin-OW5HDTMX.Cw3ZWBAh.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.By_HsOuv.js → dagre-BM42HDAG.D3yIKCRD.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.BtU0b7hz.js → diagram-2AECGRRQ.D51dO83Z.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.D00ztMdT.js → diagram-5GNKFQAL.Bb-FViDj.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.DMVJWaUN.js → diagram-KO2AKTUF.D3mN8S6x.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.Cxqrjg5-.js → diagram-LMA3HP47.dMMyPcbJ.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.2lz_1B5h.js → diagram-OG6HWLK6.AzbvzY4C.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.BVQEuJ3T.js → erDiagram-TEJ5UH35.BR5I4OZb.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.Qlyo90ky.js → flowDiagram-I6XJVG4X.DFqvhGkn.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.DRwb8HHZ.js → ganttDiagram-6RSMTGT7.ByJbhH6J.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.YfoPthfS.js → gitGraphDiagram-PVQCEYII.CInHjQR3.js} +1 -1
- package/web/_astro/index.COh_kBxN.css +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.DvFREck1.js → infoDiagram-5YYISTIA.Cm0dGVjJ.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.BzCj9Q0-.js → ishikawaDiagram-YF4QCWOH.C3nOP6Sj.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.BDMT_AFb.js → journeyDiagram-JHISSGLW.4IvLa2MW.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.D6Fh3gj9.js → kanban-definition-UN3LZRKU.DS87Cl-k.js} +1 -1
- package/web/_astro/{linear.Dxw00fGQ.js → linear.MzmRrNER.js} +1 -1
- package/web/_astro/{mermaid.core.CsoxUWa0.js → mermaid.core.fK-WNswS.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.CN8oeHLu.js → mindmap-definition-RKZ34NQL.BqipOHhY.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.C7ADqQ_7.js → pieDiagram-4H26LBE5.0nBiPqFG.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.Cw9lKyHP.js → quadrantDiagram-W4KKPZXB.Szj7Zpx5.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.B1ni-z4q.js → requirementDiagram-4Y6WPE33.D4UR6TWo.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.BhexdjVr.js → sankeyDiagram-5OEKKPKP.DAE7BdOs.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.C3Rm7dpQ.js → sequenceDiagram-3UESZ5HK.BP9ZVq25.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.CkuMt27u.js → stateDiagram-AJRCARHV.BQW7mIxk.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.Dc_aazUA.js → stateDiagram-v2-BHNVJYJU.PbK4nDih.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.ja7sUFhH.js → timeline-definition-PNZ67QCA.Ci_GvUpo.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.BxIshX_K.js → vennDiagram-CIIHVFJN.DNw1Y5qx.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.DfHlC0sh.js → wardleyDiagram-YWT4CUSO.CwlkaaNe.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.DSnhHwCh.js → xychartDiagram-2RQKCTM6.MI6ZuiH_.js} +1 -1
- package/web/board-runtime.json +1 -1
- package/web/index.html +2 -2
- package/web/_astro/BoardApp.BVp-LYi6.js +0 -322
- package/web/_astro/channel.6IFIKBhu.js +0 -1
- package/web/_astro/index.CbNz17Sx.css +0 -1
|
@@ -17,12 +17,12 @@ import { appendFileSync } from 'node:fs';
|
|
|
17
17
|
import { join } from 'node:path';
|
|
18
18
|
import { getEnvVars } from '../lib/env';
|
|
19
19
|
import type { QualityGateEnv, QualityGateOptions } from '../lib/quality-gate.generated.mjs';
|
|
20
|
-
import {
|
|
20
|
+
import { resolveReceiptReuse, runDeferredGate, runLightGate, runQualityGate } from '../lib/quality-gate.generated.mjs';
|
|
21
21
|
|
|
22
22
|
export * from '../lib/quality-gate.generated.mjs';
|
|
23
23
|
|
|
24
24
|
export const QUALITY_GATE_USAGE =
|
|
25
|
-
'usage: quality-gate.ts <run|recheck|light|status> (env: wbs, qualityGateCmd, gateProbeCmd, proofDigest, runId)';
|
|
25
|
+
'usage: quality-gate.ts <run|recheck|light|deferred|status> (env: wbs, qualityGateCmd, gateProbeCmd, proofDigest, runId)';
|
|
26
26
|
|
|
27
27
|
export function main(
|
|
28
28
|
argv: string[],
|
|
@@ -30,7 +30,7 @@ export function main(
|
|
|
30
30
|
options: QualityGateOptions = {},
|
|
31
31
|
): number {
|
|
32
32
|
const mode = argv[0];
|
|
33
|
-
if (mode !== 'run' && mode !== 'recheck' && mode !== 'light' && mode !== 'status') {
|
|
33
|
+
if (mode !== 'run' && mode !== 'recheck' && mode !== 'light' && mode !== 'deferred' && mode !== 'status') {
|
|
34
34
|
process.stderr.write(`${QUALITY_GATE_USAGE}\n`);
|
|
35
35
|
return 2;
|
|
36
36
|
}
|
|
@@ -42,10 +42,26 @@ export function main(
|
|
|
42
42
|
const env: QualityGateEnv = { ...rawEnv, wbs };
|
|
43
43
|
if (mode === 'light') {
|
|
44
44
|
runLightGate(env);
|
|
45
|
+
} else if (mode === 'deferred') {
|
|
46
|
+
// 1111 R1: light tier for per-task feedback, `DEFERRED`/`FAIL` for the status token —
|
|
47
|
+
// exit 0 on DEFERRED so the YAML's `; exit 0` wrapper and the action result agree.
|
|
48
|
+
return runDeferredGate(env, options).status === 'DEFERRED' ? 0 : 1;
|
|
45
49
|
} else if (mode === 'status') {
|
|
46
50
|
const runDir = join(options.cwd ?? '.', '.spur', 'run');
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
// Task 1136 R3: the gate owns its reuse identity — recompute the proof-input fingerprint
|
|
52
|
+
// before consulting the receipt, so a caller-copied digest can never trigger reuse.
|
|
53
|
+
const decision = resolveReceiptReuse(
|
|
54
|
+
join(runDir, `${env.wbs}-check-receipt.json`),
|
|
55
|
+
env,
|
|
56
|
+
options.cwd ?? process.cwd(),
|
|
57
|
+
options.recomputeFingerprint,
|
|
58
|
+
);
|
|
59
|
+
const verdict = decision.receiptStatus;
|
|
60
|
+
if (decision.refusal !== undefined) {
|
|
61
|
+
const line = `check.reuse-refused — ${decision.refusal}\n`;
|
|
62
|
+
process.stdout.write(line); // tee: stdout and the log
|
|
63
|
+
appendFileSync(join(runDir, `${env.wbs}-test-gate.log`), line);
|
|
64
|
+
} else if (verdict.reuse) {
|
|
49
65
|
// 0940 R3: reuse is observable in the gate log and on stdout (the action result
|
|
50
66
|
// `data`); the `{reuse, reason}` JSON stays the last stdout line for machine readers.
|
|
51
67
|
const line = `check.reused — full-tier receipt reused for input digest ${env.proofDigest ?? ''}\n`;
|
|
@@ -164,6 +164,43 @@ function scanResiduals(inputs) {
|
|
|
164
164
|
counts
|
|
165
165
|
};
|
|
166
166
|
}
|
|
167
|
+
function parseDeferralEntries(raw) {
|
|
168
|
+
const isDeferral = (e) => typeof e === "object" && e !== null && typeof e.id === "string" && typeof e.reason === "string" && e.reason.trim().length > 0;
|
|
169
|
+
return (Array.isArray(raw) ? raw : []).filter(isDeferral);
|
|
170
|
+
}
|
|
171
|
+
function blockingReviewFindings(taskContent, deferrals) {
|
|
172
|
+
const rows = parseReviewFindings(taskContent);
|
|
173
|
+
const tableDeferrals = rows.flatMap((r) => r.deferral === undefined ? [] : [{ id: makeItemId("review-finding", r.location, r.text), reason: r.deferral }]);
|
|
174
|
+
const items = rows.map((r) => ({
|
|
175
|
+
category: "review-finding",
|
|
176
|
+
priority: r.priority,
|
|
177
|
+
location: r.location,
|
|
178
|
+
text: r.text
|
|
179
|
+
}));
|
|
180
|
+
return classify(items, [...tableDeferrals, ...deferrals]);
|
|
181
|
+
}
|
|
182
|
+
function buildReviewGateArtifact(wbs, taskContent, deferrals) {
|
|
183
|
+
const items = blockingReviewFindings(taskContent, deferrals);
|
|
184
|
+
const blocking = items.filter((i) => i.class === "blocking");
|
|
185
|
+
const counts = { blocking: 0, deferrable: 0, advisory: 0, housekeeping: 0 };
|
|
186
|
+
for (const item of items)
|
|
187
|
+
counts[item.class]++;
|
|
188
|
+
const artifact = {
|
|
189
|
+
wbs,
|
|
190
|
+
base: null,
|
|
191
|
+
scanned: {
|
|
192
|
+
"review-finding": true,
|
|
193
|
+
"diff-marker": false,
|
|
194
|
+
"unchecked-box": false,
|
|
195
|
+
"staging-residue": false
|
|
196
|
+
},
|
|
197
|
+
items,
|
|
198
|
+
counts
|
|
199
|
+
};
|
|
200
|
+
const note = `residual-review-gate: ${wbs} blocking=${counts.blocking}` + (blocking.length > 0 ? ` ids=${blocking.map((i) => i.id).join(",")} anchors=${blockingAnchors(blocking).join(",")}` : "") + `
|
|
201
|
+
`;
|
|
202
|
+
return { artifact, note };
|
|
203
|
+
}
|
|
167
204
|
function blockingAnchors(items) {
|
|
168
205
|
const anchors = new Set;
|
|
169
206
|
for (const item of items) {
|
|
@@ -262,6 +299,31 @@ function verdictDisagreementNote(runDir, wbs, fs) {
|
|
|
262
299
|
const winner = mtimeOf(fs, durable) > mtimeOf(fs, run) ? "durable" : "run";
|
|
263
300
|
return `residual-fold: ${wbs} verdict copies disagree — run=${run} (${value(run)}) durable=${durable} (${value(durable)})` + ` → chose ${winner} (newer mtime)`;
|
|
264
301
|
}
|
|
302
|
+
function parseScanArgs(argv, cwd, defaultTmpDir) {
|
|
303
|
+
let mode = "";
|
|
304
|
+
let wbs = "";
|
|
305
|
+
let spurBin;
|
|
306
|
+
let root = cwd;
|
|
307
|
+
let tmpDir = defaultTmpDir;
|
|
308
|
+
for (let i = 0;i < argv.length; i++) {
|
|
309
|
+
const a = argv[i];
|
|
310
|
+
if (a === undefined)
|
|
311
|
+
break;
|
|
312
|
+
if (a === "--spur-bin")
|
|
313
|
+
spurBin = argv[++i];
|
|
314
|
+
else if (a === "--root")
|
|
315
|
+
root = argv[++i] ?? root;
|
|
316
|
+
else if (a === "--tmp-dir")
|
|
317
|
+
tmpDir = argv[++i] ?? tmpDir;
|
|
318
|
+
else if (a === "--help" || a === "-h")
|
|
319
|
+
return null;
|
|
320
|
+
else if (mode === "")
|
|
321
|
+
mode = a;
|
|
322
|
+
else if (wbs === "")
|
|
323
|
+
wbs = a;
|
|
324
|
+
}
|
|
325
|
+
return mode === "" || wbs === "" ? null : { mode, wbs, spurBin, root, tmpDir };
|
|
326
|
+
}
|
|
265
327
|
|
|
266
328
|
// plugins/sp/lib/spur-bin.ts
|
|
267
329
|
function spurCommand(spurBin) {
|
|
@@ -270,7 +332,7 @@ function spurCommand(spurBin) {
|
|
|
270
332
|
}
|
|
271
333
|
|
|
272
334
|
// plugins/sp/scripts/residual-scan.ts
|
|
273
|
-
var RESIDUAL_SCAN_USAGE = "usage: residual-scan.ts <scan|fold|settle|report> <wbs> [--spur-bin <bin>] [--root <dir>] [--tmp-dir <dir>]";
|
|
335
|
+
var RESIDUAL_SCAN_USAGE = "usage: residual-scan.ts <scan|fold|settle|report|review-gate> <wbs> [--spur-bin <bin>] [--root <dir>] [--tmp-dir <dir>]";
|
|
274
336
|
function run(cmd, args, cwd) {
|
|
275
337
|
const result = spawnSync(cmd, args, { cwd, encoding: "utf8" });
|
|
276
338
|
if (result.error !== undefined)
|
|
@@ -326,10 +388,8 @@ function readDeferrals(runDir, wbs) {
|
|
|
326
388
|
const path = join2(runDir, `${wbs}-residual-deferrals.json`);
|
|
327
389
|
if (!fs.existsSync(path))
|
|
328
390
|
return [];
|
|
329
|
-
const isDeferral = (e) => typeof e === "object" && e !== null && typeof e.id === "string" && typeof e.reason === "string" && e.reason.trim().length > 0;
|
|
330
391
|
try {
|
|
331
|
-
|
|
332
|
-
return (Array.isArray(parsed) ? parsed : []).filter(isDeferral);
|
|
392
|
+
return parseDeferralEntries(JSON.parse(fs.readFileSync(path, "utf8")));
|
|
333
393
|
} catch {
|
|
334
394
|
return [];
|
|
335
395
|
}
|
|
@@ -343,31 +403,6 @@ function scanResiduals2(root, wbs, tmpDir, taskContent, _env) {
|
|
|
343
403
|
const addedLines = base === null ? [] : collectAddedLines(root, base);
|
|
344
404
|
return scanResiduals({ wbs, base, taskContent, addedLines, stagingResidue, deferrals });
|
|
345
405
|
}
|
|
346
|
-
function parseArgs(argv) {
|
|
347
|
-
let mode = "";
|
|
348
|
-
let wbs = "";
|
|
349
|
-
let spurBin;
|
|
350
|
-
let root = process.cwd();
|
|
351
|
-
let tmpDir = tmpdir();
|
|
352
|
-
for (let i = 0;i < argv.length; i++) {
|
|
353
|
-
const a = argv[i];
|
|
354
|
-
if (a === undefined)
|
|
355
|
-
break;
|
|
356
|
-
if (a === "--spur-bin")
|
|
357
|
-
spurBin = argv[++i];
|
|
358
|
-
else if (a === "--root")
|
|
359
|
-
root = argv[++i] ?? root;
|
|
360
|
-
else if (a === "--tmp-dir")
|
|
361
|
-
tmpDir = argv[++i] ?? tmpDir;
|
|
362
|
-
else if (a === "--help" || a === "-h")
|
|
363
|
-
return null;
|
|
364
|
-
else if (mode === "")
|
|
365
|
-
mode = a;
|
|
366
|
-
else if (wbs === "")
|
|
367
|
-
wbs = a;
|
|
368
|
-
}
|
|
369
|
-
return mode === "" || wbs === "" ? null : { mode, wbs, spurBin, root, tmpDir };
|
|
370
|
-
}
|
|
371
406
|
function loadTask(env, spurBinFlag, wbs, root) {
|
|
372
407
|
const res = spur(env, spurBinFlag, ["task", "show", wbs, "--json"], root);
|
|
373
408
|
if (res.status !== 0)
|
|
@@ -388,6 +423,16 @@ function scanMode(opts, env, io) {
|
|
|
388
423
|
`);
|
|
389
424
|
return 0;
|
|
390
425
|
}
|
|
426
|
+
function reviewGateMode(opts, env, io) {
|
|
427
|
+
const runDir = join2(opts.root, ".spur", "run");
|
|
428
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
429
|
+
const { content } = loadTask(env, opts.spurBin, opts.wbs, opts.root);
|
|
430
|
+
const gate = buildReviewGateArtifact(opts.wbs, content, readDeferrals(runDir, opts.wbs));
|
|
431
|
+
fs.writeFileSync(join2(runDir, `${opts.wbs}-residuals.json`), `${JSON.stringify(gate.artifact, null, 2)}
|
|
432
|
+
`);
|
|
433
|
+
io.out(gate.note);
|
|
434
|
+
return gate.artifact.counts.blocking > 0 ? 1 : 0;
|
|
435
|
+
}
|
|
391
436
|
function foldMode(opts, _env, io) {
|
|
392
437
|
const runDir = join2(opts.root, ".spur", "run");
|
|
393
438
|
const scan = JSON.parse(fs.readFileSync(join2(runDir, `${opts.wbs}-residuals.json`), "utf8"));
|
|
@@ -427,12 +472,7 @@ function settleMode(opts, env, io) {
|
|
|
427
472
|
prior = JSON.parse(fs.readFileSync(residualsPath, "utf8"));
|
|
428
473
|
const deferred = scan.items.filter((i) => i.class === "deferrable");
|
|
429
474
|
if (deferred.length > 0 && prior.followUp === undefined) {
|
|
430
|
-
|
|
431
|
-
io.err(`residual-settle: ${wbs} deferrals pending but feature_id unknown; re-run: residual-scan settle ${wbs}
|
|
432
|
-
`);
|
|
433
|
-
return 0;
|
|
434
|
-
}
|
|
435
|
-
const args = ["task", "create", `Residuals from ${wbs}`, "--feature", task.featureId, "--skip-ready", "--json"];
|
|
475
|
+
const args = ["task", "create", `Residuals from ${wbs}`, "--skip-ready", "--json"];
|
|
436
476
|
const created = spur(env, opts.spurBin, args, opts.root);
|
|
437
477
|
let wbsNew = "";
|
|
438
478
|
try {
|
|
@@ -448,7 +488,7 @@ function settleMode(opts, env, io) {
|
|
|
448
488
|
return 0;
|
|
449
489
|
}
|
|
450
490
|
const rows = deferred.map((i) => `- ${i.id} \u2014 ${i.location}: ${i.text}`);
|
|
451
|
-
const head = `Source task: ${wbs} (feature ${task.featureId}) \u2014 deferred residuals filed by residual-scan settle.`;
|
|
491
|
+
const head = `Source task: ${wbs}${task.featureId === "" ? "" : ` (feature ${task.featureId})`} \u2014 deferred residuals filed by residual-scan settle (unlinked: a deferral must not hold the completing feature open).`;
|
|
452
492
|
const bgFile = join2(runDir, `${wbs}-residual-background.md`);
|
|
453
493
|
fs.writeFileSync(bgFile, `${[head, "", ...rows].join(`
|
|
454
494
|
`)}
|
|
@@ -497,8 +537,14 @@ function main(argv, env = getEnvVars(), options = {}) {
|
|
|
497
537
|
err: (line) => process.stderr.write(line)
|
|
498
538
|
};
|
|
499
539
|
const cwd = options.cwd ?? process.cwd();
|
|
500
|
-
const opts =
|
|
501
|
-
const modes = {
|
|
540
|
+
const opts = parseScanArgs(argv, cwd, tmpdir());
|
|
541
|
+
const modes = {
|
|
542
|
+
scan: scanMode,
|
|
543
|
+
fold: foldMode,
|
|
544
|
+
settle: settleMode,
|
|
545
|
+
report: reportMode,
|
|
546
|
+
"review-gate": reviewGateMode
|
|
547
|
+
};
|
|
502
548
|
const modeFn = opts === null ? undefined : modes[opts.mode];
|
|
503
549
|
if (opts === null || modeFn === undefined) {
|
|
504
550
|
io.err(`${RESIDUAL_SCAN_USAGE}
|
|
@@ -514,8 +560,10 @@ export {
|
|
|
514
560
|
scanResiduals2 as scanResiduals,
|
|
515
561
|
renderReport,
|
|
516
562
|
recordedVerdictPath,
|
|
563
|
+
parseScanArgs,
|
|
517
564
|
parseReviewFindings,
|
|
518
565
|
parseDiffMarkers,
|
|
566
|
+
parseDeferralEntries,
|
|
519
567
|
normalizeAnchor,
|
|
520
568
|
makeItemId,
|
|
521
569
|
main,
|
|
@@ -525,6 +573,8 @@ export {
|
|
|
525
573
|
findUncheckedBoxes,
|
|
526
574
|
collectAddedLines,
|
|
527
575
|
classify,
|
|
576
|
+
buildReviewGateArtifact,
|
|
577
|
+
blockingReviewFindings,
|
|
528
578
|
blockingAnchors,
|
|
529
579
|
RESIDUAL_SCAN_USAGE,
|
|
530
580
|
ALLOW_PRAGMA
|
|
@@ -10,12 +10,12 @@ import { spurCommand } from '../lib/spur-bin';
|
|
|
10
10
|
|
|
11
11
|
export * from '../lib/residual-scan.generated.mjs';
|
|
12
12
|
export const RESIDUAL_SCAN_USAGE =
|
|
13
|
-
'usage: residual-scan.ts <scan|fold|settle|report> <wbs> [--spur-bin <bin>] [--root <dir>] [--tmp-dir <dir>]';
|
|
13
|
+
'usage: residual-scan.ts <scan|fold|settle|report|review-gate> <wbs> [--spur-bin <bin>] [--root <dir>] [--tmp-dir <dir>]';
|
|
14
14
|
export type ScanEnv = { spurBin?: string } & Record<string, string | undefined>;
|
|
15
15
|
export type ScanIo = { out: (line: string) => void; err: (line: string) => void };
|
|
16
16
|
export type ScanOptions = { cwd?: string; io?: ScanIo };
|
|
17
17
|
type RunResult = { status: number; stdout: string };
|
|
18
|
-
type ParsedArgs =
|
|
18
|
+
type ParsedArgs = core.ParsedScanArgs;
|
|
19
19
|
type ModeFn = (o: ParsedArgs, e: ScanEnv, i: ScanIo) => number;
|
|
20
20
|
type ScanTask = { content: string; featureId: string };
|
|
21
21
|
type VerdictFile = { verdict: string; checks: Array<{ name: string; status: string; evidence: string }> };
|
|
@@ -66,15 +66,8 @@ export function listStagingResidue(tmpDir: string, wbs: string): string[] {
|
|
|
66
66
|
function readDeferrals(runDir: string, wbs: string): core.Deferral[] {
|
|
67
67
|
const path = join(runDir, `${wbs}-residual-deferrals.json`);
|
|
68
68
|
if (!fs.existsSync(path)) return [];
|
|
69
|
-
const isDeferral = (e: unknown): e is core.Deferral =>
|
|
70
|
-
typeof e === 'object' &&
|
|
71
|
-
e !== null &&
|
|
72
|
-
typeof (e as core.Deferral).id === 'string' &&
|
|
73
|
-
typeof (e as core.Deferral).reason === 'string' &&
|
|
74
|
-
(e as core.Deferral).reason.trim().length > 0;
|
|
75
69
|
try {
|
|
76
|
-
|
|
77
|
-
return (Array.isArray(parsed) ? parsed : []).filter(isDeferral);
|
|
70
|
+
return core.parseDeferralEntries(JSON.parse(fs.readFileSync(path, 'utf8')));
|
|
78
71
|
} catch {
|
|
79
72
|
return [];
|
|
80
73
|
}
|
|
@@ -88,24 +81,6 @@ export function scanResiduals(root: string, wbs: string, tmpDir: string, taskCon
|
|
|
88
81
|
const addedLines = base === null ? [] : collectAddedLines(root, base);
|
|
89
82
|
return core.scanResiduals({ wbs, base, taskContent, addedLines, stagingResidue, deferrals });
|
|
90
83
|
}
|
|
91
|
-
function parseArgs(argv: string[]): ParsedArgs | null {
|
|
92
|
-
let mode = '';
|
|
93
|
-
let wbs = '';
|
|
94
|
-
let spurBin: string | undefined;
|
|
95
|
-
let root = process.cwd();
|
|
96
|
-
let tmpDir = tmpdir();
|
|
97
|
-
for (let i = 0; i < argv.length; i++) {
|
|
98
|
-
const a = argv[i];
|
|
99
|
-
if (a === undefined) break;
|
|
100
|
-
if (a === '--spur-bin') spurBin = argv[++i];
|
|
101
|
-
else if (a === '--root') root = argv[++i] ?? root;
|
|
102
|
-
else if (a === '--tmp-dir') tmpDir = argv[++i] ?? tmpDir;
|
|
103
|
-
else if (a === '--help' || a === '-h') return null;
|
|
104
|
-
else if (mode === '') mode = a;
|
|
105
|
-
else if (wbs === '') wbs = a;
|
|
106
|
-
}
|
|
107
|
-
return mode === '' || wbs === '' ? null : { mode, wbs, spurBin, root, tmpDir };
|
|
108
|
-
}
|
|
109
84
|
function loadTask(env: ScanEnv, spurBinFlag: string | undefined, wbs: string, root: string): ScanTask {
|
|
110
85
|
const res = spur(env, spurBinFlag, ['task', 'show', wbs, '--json'], root);
|
|
111
86
|
if (res.status !== 0) throw new Error(`task show ${wbs} failed`);
|
|
@@ -128,6 +103,23 @@ function scanMode(opts: ParsedArgs, env: ScanEnv, io: ScanIo): number {
|
|
|
128
103
|
);
|
|
129
104
|
return 0;
|
|
130
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Task 1122 R2: the review-time gate — the record sweep's review-finding slice with the same
|
|
108
|
+
* parse/classify/deferrals (core.buildReviewGateArtifact), applied at the review PASS edges
|
|
109
|
+
* before verify spends the cycle. Writes the same `<wbs>-residuals.json` artifact the test-fix
|
|
110
|
+
* hop already appends to the gate log (its remediation input); the record sweep stays the final
|
|
111
|
+
* authority and overwrites it. Exit 1 on an open P1-P3 finding fails the review PASS edge closed
|
|
112
|
+
* into review-fail-triage.
|
|
113
|
+
*/
|
|
114
|
+
function reviewGateMode(opts: ParsedArgs, env: ScanEnv, io: ScanIo): number {
|
|
115
|
+
const runDir = join(opts.root, '.spur', 'run');
|
|
116
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
117
|
+
const { content } = loadTask(env, opts.spurBin, opts.wbs, opts.root);
|
|
118
|
+
const gate = core.buildReviewGateArtifact(opts.wbs, content, readDeferrals(runDir, opts.wbs));
|
|
119
|
+
fs.writeFileSync(join(runDir, `${opts.wbs}-residuals.json`), `${JSON.stringify(gate.artifact, null, 2)}\n`);
|
|
120
|
+
io.out(gate.note);
|
|
121
|
+
return gate.artifact.counts.blocking > 0 ? 1 : 0;
|
|
122
|
+
}
|
|
131
123
|
function foldMode(opts: ParsedArgs, _env: ScanEnv, io: ScanIo): number {
|
|
132
124
|
const runDir = join(opts.root, '.spur', 'run');
|
|
133
125
|
const scan = JSON.parse(
|
|
@@ -170,13 +162,12 @@ function settleMode(opts: ParsedArgs, env: ScanEnv, io: ScanIo): number {
|
|
|
170
162
|
if (fs.existsSync(residualsPath)) prior = JSON.parse(fs.readFileSync(residualsPath, 'utf8'));
|
|
171
163
|
const deferred = scan.items.filter((i) => i.class === 'deferrable');
|
|
172
164
|
if (deferred.length > 0 && prior.followUp === undefined) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
const args = ['task', 'create', `Residuals from ${wbs}`, '--feature', task.featureId, '--skip-ready', '--json'];
|
|
165
|
+
// The follow-up is filed UNLINKED: carrying a feature edge to the feature whose run produced
|
|
166
|
+
// it made that feature's done-gate fail with `L4.verifying-incomplete-tasks` the moment a
|
|
167
|
+
// residual was deferred (2026-10-04 session, E72/1086→1087), so a deferral must not be able
|
|
168
|
+
// to block the completing feature. The source task and its feature stay named in the
|
|
169
|
+
// Background, which keeps the traceability without the lifecycle coupling.
|
|
170
|
+
const args = ['task', 'create', `Residuals from ${wbs}`, '--skip-ready', '--json'];
|
|
180
171
|
const created = spur(env, opts.spurBin, args, opts.root);
|
|
181
172
|
let wbsNew = '';
|
|
182
173
|
try {
|
|
@@ -191,7 +182,7 @@ function settleMode(opts: ParsedArgs, env: ScanEnv, io: ScanIo): number {
|
|
|
191
182
|
return 0;
|
|
192
183
|
}
|
|
193
184
|
const rows = deferred.map((i) => `- ${i.id} — ${i.location}: ${i.text}`);
|
|
194
|
-
const head = `Source task: ${wbs} (feature ${task.featureId}) — deferred residuals filed by residual-scan settle.`;
|
|
185
|
+
const head = `Source task: ${wbs}${task.featureId === '' ? '' : ` (feature ${task.featureId})`} — deferred residuals filed by residual-scan settle (unlinked: a deferral must not hold the completing feature open).`;
|
|
195
186
|
const bgFile = join(runDir, `${wbs}-residual-background.md`);
|
|
196
187
|
fs.writeFileSync(bgFile, `${[head, '', ...rows].join('\n')}\n`);
|
|
197
188
|
const updArgs = ['task', 'update', wbsNew, '--section', 'Background', '--from-file', bgFile];
|
|
@@ -237,8 +228,14 @@ export function main(argv: string[], env: ScanEnv = getEnvVars(), options: ScanO
|
|
|
237
228
|
err: (line) => process.stderr.write(line),
|
|
238
229
|
};
|
|
239
230
|
const cwd = options.cwd ?? process.cwd();
|
|
240
|
-
const opts =
|
|
241
|
-
const modes: Record<string, ModeFn> = {
|
|
231
|
+
const opts = core.parseScanArgs(argv, cwd, tmpdir());
|
|
232
|
+
const modes: Record<string, ModeFn> = {
|
|
233
|
+
scan: scanMode,
|
|
234
|
+
fold: foldMode,
|
|
235
|
+
settle: settleMode,
|
|
236
|
+
report: reportMode,
|
|
237
|
+
'review-gate': reviewGateMode,
|
|
238
|
+
};
|
|
242
239
|
const modeFn = opts === null ? undefined : modes[opts.mode];
|
|
243
240
|
if (opts === null || modeFn === undefined) {
|
|
244
241
|
io.err(`${RESIDUAL_SCAN_USAGE}\n`);
|