@gobing-ai/spur 0.4.0 → 0.4.2
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 +4 -2
- 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/plugin-scripts.json +15 -0
- package/config/rules/docs/sourced-api-claims.yaml +60 -0
- package/config/rules/recommended-pre-check.yaml +8 -3
- package/config/rules/structure/scratch-evidence-pointer.yaml +35 -0
- package/config/script-placement-baseline.json +2 -2
- package/config/templates/AGENTS.md +1 -1
- 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 +216 -57
- package/config/workflows/wrapup-pipeline.yaml +107 -14
- package/package.json +10 -10
- package/plugins/sp/README.md +12 -3
- package/plugins/sp/agents/super-planner.md +16 -6
- package/plugins/sp/agents/super-reviewer.md +14 -0
- package/plugins/sp/bunfig.toml +8 -0
- package/plugins/sp/commands/dev-brainstorm.md +29 -16
- package/plugins/sp/commands/dev-find-way.md +80 -0
- package/plugins/sp/commands/dev-idea.md +6 -0
- package/plugins/sp/commands/dev-plan.md +6 -0
- package/plugins/sp/commands/dev-review-session.md +4 -1
- package/plugins/sp/commands/dev-run.md +12 -2
- package/plugins/sp/commands/dev-runall.md +32 -15
- package/plugins/sp/hooks/context-owner.ts +56 -0
- package/plugins/sp/hooks/context-post-tool.ts +23 -0
- package/plugins/sp/hooks/context-session-start.ts +49 -27
- package/plugins/sp/hooks/context-session-stop.ts +131 -30
- package/plugins/sp/hooks/pi/guard-extension.ts +21 -8
- package/plugins/sp/lib/idea-handoff.generated.mjs +43 -5
- package/plugins/sp/lib/inline-run-app.ts +28 -0
- package/plugins/sp/lib/inline-run.generated.d.mts +5 -0
- package/plugins/sp/lib/inline-run.generated.mjs +351 -46
- package/plugins/sp/lib/plan-projection.generated.d.mts +47 -0
- package/plugins/sp/lib/plan-projection.generated.mjs +368 -0
- package/plugins/sp/lib/quality-gate.generated.d.mts +35 -1
- package/plugins/sp/lib/quality-gate.generated.mjs +30296 -71
- 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 +150 -7
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/references/roles.md +1 -1
- package/plugins/sp/scripts/batch-plan.mjs +488 -0
- package/plugins/sp/scripts/batch-plan.ts +145 -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/inline-run-dispatch.mjs +130 -0
- package/plugins/sp/scripts/inline-run-dispatch.ts +135 -0
- package/plugins/sp/scripts/inline-run-setup.mjs +562 -80
- package/plugins/sp/scripts/inline-run-setup.ts +52 -78
- package/plugins/sp/scripts/persist-out-check.mjs +90 -41
- package/plugins/sp/scripts/persist-out-check.ts +112 -57
- package/plugins/sp/scripts/pr-reviewing.mjs +19 -5
- package/plugins/sp/scripts/pr-reviewing.ts +34 -9
- package/plugins/sp/scripts/quality-gate.mjs +30213 -61
- package/plugins/sp/scripts/quality-gate.ts +21 -5
- package/plugins/sp/scripts/residual-scan.mjs +86 -31
- package/plugins/sp/scripts/residual-scan.ts +28 -30
- package/plugins/sp/scripts/run-summary.mjs +432 -14
- package/plugins/sp/scripts/run-summary.ts +41 -177
- package/plugins/sp/scripts/session-timeline.mjs +109 -11
- package/plugins/sp/scripts/session-timeline.ts +57 -19
- package/plugins/sp/scripts/task-diffstat.mjs +32 -29
- package/plugins/sp/scripts/task-diffstat.ts +34 -34
- 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 +3 -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 +1 -1
- package/plugins/sp/skills/indexed-context/SKILL.md +14 -7
- package/plugins/sp/skills/parallel-execution/SKILL.md +2 -2
- package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +1 -1
- package/plugins/sp/skills/parallel-execution/references/fan-out-patterns.md +2 -2
- package/plugins/sp/skills/pr-reviewing/SKILL.md +7 -3
- package/plugins/sp/skills/session-review/SKILL.md +40 -7
- 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 +5 -2
- package/plugins/sp/skills/spur-cli/references/agent.md +20 -15
- package/plugins/sp/skills/spur-cli/references/decision.md +16 -8
- package/plugins/sp/skills/spur-cli/references/message.md +7 -7
- package/plugins/sp/skills/spur-cli/references/projects.md +7 -5
- package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +19 -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 +1 -1
- package/plugins/sp/skills/spur-dev/SKILL.md +20 -14
- package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +33 -3
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +49 -14
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +37 -17
- package/plugins/sp/skills/spur-dev/references/execution-batch-continuation.md +68 -0
- package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +199 -0
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +156 -1024
- package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +133 -0
- package/plugins/sp/skills/spur-dev/references/execution-workflow.md +6 -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 +44 -12
- package/plugins/sp/skills/spur-dev/references/glossary.md +2 -2
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +340 -165
- package/plugins/sp/skills/spur-dev/references/structured-trace-emission.md +178 -0
- package/plugins/sp/skills/wayfinder/SKILL.md +10 -14
- package/schemas/state-machine-workflow.schema.json +23 -0
- package/spur.js +42402 -26889
- package/web/_astro/{BoardApp.VeoxxLBy.js → BoardApp.B3GQZvj8.js} +1 -1
- package/web/_astro/BoardApp.BT7L2kSb.js +324 -0
- package/web/_astro/TaskDetail.Cxov1_N_.js +1 -0
- package/web/_astro/{arc.f4e0OYLg.js → arc.vaT7tPOB.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.SSIvxi-Z.js → architectureDiagram-3BPJPVTR.DRMmTjkN.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.DSXIHOFf.js → blockDiagram-GPEHLZMM.cla0LO_q.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.DG9Ubu85.js → c4Diagram-AAUBKEIU.D4YAIBKn.js} +1 -1
- package/web/_astro/channel.D4nG9YLf.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.C7QU88Qh.js → chunk-2J33WTMH.BrSJbMRZ.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.26Z94xBj.js → chunk-4BX2VUAB.CuLMgKF5.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.DYRiBkqw.js → chunk-55IACEB6.zkvgu54F.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.D5Vfv5pe.js → chunk-727SXJPM.DxWkACh8.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.DXHV47bN.js → chunk-AQP2D5EJ.42XrmIlH.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.D1suAfOs.js → chunk-FMBD7UC4.Bao2wMK9.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.D6BmdHEY.js → chunk-ND2GUHAM.BhydojD1.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.rnPU-Ak-.js → chunk-QZHKN3VN.CmZ82g-H.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.CvEfXIo1.js → classDiagram-4FO5ZUOK.Blr83Gt_.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.CvEfXIo1.js → classDiagram-v2-Q7XG4LA2.Blr83Gt_.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.DkMisLSm.js → cose-bilkent-S5V4N54A.qmfv0SPn.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.C77Q-pF6.js → cynefin-OW5HDTMX.BtELhXPR.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.YaV8ce27.js → dagre-BM42HDAG.AWmaAvXo.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.CUyGEKge.js → diagram-2AECGRRQ.DdH34KDi.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.CRgd8dgj.js → diagram-5GNKFQAL.BmNPix1k.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.Bmzys9hq.js → diagram-KO2AKTUF.BNQd_Ppo.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.BvmJY4Lw.js → diagram-LMA3HP47.BAPgDi0e.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.0hEhD30d.js → diagram-OG6HWLK6.DZ4zyLdC.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.Bjt8lrnm.js → erDiagram-TEJ5UH35.Q1TySMmw.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.CJZTjRDV.js → flowDiagram-I6XJVG4X.BXhFBicG.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7._VqoVafc.js → ganttDiagram-6RSMTGT7.vHSgcuo2.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.CEDb7tVo.js → gitGraphDiagram-PVQCEYII.Bi1f73C7.js} +1 -1
- package/web/_astro/index.BhSwN8yY.css +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.CgwifPpK.js → infoDiagram-5YYISTIA.DPvXZEWm.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.B99wx2J1.js → ishikawaDiagram-YF4QCWOH.Cz829fsV.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.2hRwJbEo.js → journeyDiagram-JHISSGLW.enRnB9Me.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.BahJkKNC.js → kanban-definition-UN3LZRKU.DnTNk6d2.js} +1 -1
- package/web/_astro/{linear.DguUUe6V.js → linear.BzGnBwNL.js} +1 -1
- package/web/_astro/{mermaid.core.CvgfmqVr.js → mermaid.core.CSbTpIVz.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.B3qEA3x9.js → mindmap-definition-RKZ34NQL.DGJDy8zT.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.FlWA-_ru.js → pieDiagram-4H26LBE5.HSPdi5_8.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.9bVn35X7.js → quadrantDiagram-W4KKPZXB.6Gv8mz2Y.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.ChykIhct.js → requirementDiagram-4Y6WPE33.CnBCtFwa.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.Z2Y3hkYb.js → sankeyDiagram-5OEKKPKP.5M8VWAu8.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.J7JsHWDt.js → sequenceDiagram-3UESZ5HK.DN41XN-U.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.DAPbiCSS.js → stateDiagram-AJRCARHV.ErOO3W8j.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.D2jvSi0q.js → stateDiagram-v2-BHNVJYJU.B_ZHICM-.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.psW2ci29.js → timeline-definition-PNZ67QCA.CWdosr07.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.DlILYVmg.js → vennDiagram-CIIHVFJN.BHzYjNV5.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.xNWwLJnn.js → wardleyDiagram-YWT4CUSO.h8x4IwPX.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.EU_BuH7V.js → xychartDiagram-2RQKCTM6.RTapW2X8.js} +1 -1
- package/web/index.html +2 -2
- package/plugins/sp/commands/dev-parallel.md +0 -31
- package/web/_astro/BoardApp.Bzr4OXrW.js +0 -324
- package/web/_astro/TaskDetail.LAyR_Pvn.js +0 -1
- package/web/_astro/channel.CUFdtWhm.js +0 -1
- package/web/_astro/index.Dg_3bOaJ.css +0 -1
|
@@ -1,21 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
|
-
* persist-out-check — WT-4 pre-removal assertion for E71 persist-out (task 1067 R1).
|
|
3
|
+
* persist-out-check — WT-4 pre-removal assertion for E71 persist-out (task 1067 R1, 1148 R1).
|
|
4
4
|
*
|
|
5
|
-
* Before `git worktree remove`, verify the worktree's durable evidence would not be
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* never overwrites — reconcile by hand per execution-batch.md). No new public CLI verb:
|
|
11
|
-
* this is a driver-side assertion step, not a corpus tool.
|
|
12
|
-
*
|
|
13
|
-
* ponytail: file-level comparison only — DB run rows and nested-run deep walks stay
|
|
14
|
-
* persist-out's job (`inline-run-setup --persist-out`); this checks the planes E71
|
|
15
|
-
* names as files. Listing caps (64/prefix, 256 evidence) refuse runaways by name.
|
|
16
|
-
*
|
|
17
|
-
* Node-builtin imports only; pure helpers are exported for tests. Exit 0 ok, 1
|
|
18
|
-
* blocked/divergent, 2 usage error. Normal output is the finding lines themselves.
|
|
5
|
+
* Before `git worktree remove`, verify the worktree's OWNED durable evidence would not be abandoned:
|
|
6
|
+
* every `.spur/run/` and `.spur/memory/evidence/` file starting with a forwarded `<wbs>-`/`<runId>-`
|
|
7
|
+
* prefix must exist in the invoking tree with identical bytes. Unowned evidence is counted
|
|
8
|
+
* (`unowned: N`) and does not block (1148 R1). Node builtins only; helpers are exported for tests.
|
|
9
|
+
* Exit 0 ok, 1 blocked/divergent, 2 usage error. Normal output is the finding lines themselves.
|
|
19
10
|
*/
|
|
20
11
|
|
|
21
12
|
import { spawnSync } from 'node:child_process';
|
|
@@ -26,7 +17,6 @@ const EVIDENCE_DIR = join('.spur', 'memory', 'evidence');
|
|
|
26
17
|
const RUN_DIR = join('.spur', 'run');
|
|
27
18
|
const NAMED_CAP = 32;
|
|
28
19
|
const PREFIX_CAP = 64;
|
|
29
|
-
const EVIDENCE_CAP = 256;
|
|
30
20
|
|
|
31
21
|
/** Task files own their `<wbs>-*` evidence: leading digits before `_` in the basename. */
|
|
32
22
|
export function wbsOfTaskFile(path: string): string | null {
|
|
@@ -34,49 +24,64 @@ export function wbsOfTaskFile(path: string): string | null {
|
|
|
34
24
|
return m?.[1] ?? null;
|
|
35
25
|
}
|
|
36
26
|
|
|
37
|
-
|
|
38
|
-
|
|
27
|
+
export interface ListObligationsResult {
|
|
28
|
+
files: string[];
|
|
29
|
+
unowned: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Recursively list files under `dir`, relative to `root`. */
|
|
33
|
+
function walkFiles(dir: string, root: string): string[] {
|
|
39
34
|
if (!existsSync(dir)) return [];
|
|
40
35
|
const out: string[] = [];
|
|
41
|
-
const walk = (cur: string):
|
|
42
|
-
for (const
|
|
43
|
-
const p = join(cur,
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
} else {
|
|
47
|
-
out.push(relative(root, p));
|
|
48
|
-
if (out.length > cap) return false;
|
|
49
|
-
}
|
|
36
|
+
const walk = (cur: string): void => {
|
|
37
|
+
for (const e of readdirSync(cur, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
38
|
+
const p = join(cur, e.name);
|
|
39
|
+
if (e.isDirectory()) walk(p);
|
|
40
|
+
else if (e.isFile()) out.push(relative(root, p));
|
|
50
41
|
}
|
|
51
|
-
return true;
|
|
52
42
|
};
|
|
53
|
-
|
|
43
|
+
walk(dir);
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Owned evidence (prefix-matched) under one plane; null once a prefix exceeds PREFIX_CAP. */
|
|
48
|
+
function collectOwned(rels: string[], prefixes: string[], counts: Map<string, number>, out: string[]): boolean {
|
|
49
|
+
for (const rel of rels) {
|
|
50
|
+
const prefix = prefixes.find((p) => basename(rel).startsWith(`${p}-`));
|
|
51
|
+
if (prefix === undefined) continue;
|
|
52
|
+
const count = (counts.get(prefix) ?? 0) + 1;
|
|
53
|
+
if (count > PREFIX_CAP) return false;
|
|
54
|
+
counts.set(prefix, count);
|
|
55
|
+
out.push(rel);
|
|
56
|
+
}
|
|
57
|
+
return true;
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
/**
|
|
57
|
-
* Evidence
|
|
58
|
-
*
|
|
59
|
-
*
|
|
61
|
+
* Evidence the worktree owns: `.spur/memory/evidence/` (recursive) and `.spur/run/` (flat) files
|
|
62
|
+
* whose basename starts with a forwarded `<wbs>-`/`<runId>-` prefix. Evidence matching no prefix is
|
|
63
|
+
* counted as `unowned` (1148 R1). Null when no prefix is forwarded or a prefix cap is exceeded.
|
|
60
64
|
*/
|
|
61
|
-
export function listObligations(wtRoot: string, wbsList: string[], runIds: string[]):
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
65
|
+
export function listObligations(wtRoot: string, wbsList: string[], runIds: string[]): ListObligationsResult | null {
|
|
66
|
+
const prefixes = [...new Set([...wbsList, ...runIds].filter(Boolean))];
|
|
67
|
+
if (prefixes.length === 0) return null;
|
|
68
|
+
|
|
69
|
+
const evDir = join(wtRoot, EVIDENCE_DIR);
|
|
70
|
+
const runDir = join(wtRoot, RUN_DIR);
|
|
71
|
+
const evFiles = walkFiles(evDir, wtRoot);
|
|
72
|
+
const runFiles = existsSync(runDir)
|
|
73
|
+
? readdirSync(runDir, { withFileTypes: true })
|
|
74
|
+
.filter((e) => e.isFile())
|
|
75
|
+
.map((e) => join(RUN_DIR, e.name))
|
|
76
|
+
.sort()
|
|
77
|
+
: [];
|
|
78
|
+
|
|
79
|
+
const files: string[] = [];
|
|
80
|
+
const counts = new Map<string, number>();
|
|
81
|
+
if (!collectOwned(evFiles, prefixes, counts, files)) return null;
|
|
82
|
+
if (!collectOwned(runFiles, prefixes, counts, files)) return null;
|
|
83
|
+
const unowned = evFiles.length - files.filter((f) => f.startsWith(EVIDENCE_DIR)).length;
|
|
84
|
+
return { files: [...new Set(files)].sort(), unowned };
|
|
80
85
|
}
|
|
81
86
|
|
|
82
87
|
/** Classify obligations against the invoking tree: missing, divergent (byte-differs), or ok. */
|
|
@@ -84,11 +89,15 @@ export function compareTrees(
|
|
|
84
89
|
wtRoot: string,
|
|
85
90
|
invokeRoot: string,
|
|
86
91
|
files: string[],
|
|
92
|
+
foreignDivergent = new Set<string>(),
|
|
87
93
|
): { missing: string[]; divergent: string[]; ok: number } {
|
|
88
94
|
const missing: string[] = [];
|
|
89
95
|
const divergent: string[] = [];
|
|
90
96
|
let ok = 0;
|
|
91
97
|
for (const rel of files) {
|
|
98
|
+
if (foreignDivergent.has(rel) || foreignDivergent.has(basename(rel))) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
92
101
|
const wtPath = join(wtRoot, rel);
|
|
93
102
|
const invPath = join(invokeRoot, rel);
|
|
94
103
|
if (!existsSync(invPath)) {
|
|
@@ -107,7 +116,7 @@ export function compareTrees(
|
|
|
107
116
|
}
|
|
108
117
|
|
|
109
118
|
export function persistOutCheckUsage(): string {
|
|
110
|
-
return 'usage: persist-out-check --from <worktree> [--task-file <path>]… [--run-id <id>]… [--root <invoke-tree>]';
|
|
119
|
+
return 'usage: persist-out-check --from <worktree> [--task-file <path>]… [--run-id <id>]… [--root <invoke-tree>] [--success-json <path>]';
|
|
111
120
|
}
|
|
112
121
|
|
|
113
122
|
/**
|
|
@@ -129,6 +138,7 @@ export function defaultInvokeRoot(cwd: string): string {
|
|
|
129
138
|
|
|
130
139
|
export function main(argv: string[], invokeRoot = defaultInvokeRoot(process.cwd())): number {
|
|
131
140
|
let wtRoot = '';
|
|
141
|
+
let successJsonPath = '';
|
|
132
142
|
const wbsList: string[] = [];
|
|
133
143
|
const runIds: string[] = [];
|
|
134
144
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -145,25 +155,64 @@ export function main(argv: string[], invokeRoot = defaultInvokeRoot(process.cwd(
|
|
|
145
155
|
} else if (arg === '--run-id') {
|
|
146
156
|
const id = argv[++i] ?? '';
|
|
147
157
|
if (id) runIds.push(id);
|
|
158
|
+
} else if (arg === '--success-json') {
|
|
159
|
+
successJsonPath = argv[++i] ?? '';
|
|
148
160
|
} else {
|
|
149
161
|
process.stderr.write(`${persistOutCheckUsage()}\n`);
|
|
150
162
|
return 2;
|
|
151
163
|
}
|
|
152
164
|
}
|
|
165
|
+
if (wbsList.length === 0 && runIds.length === 0) {
|
|
166
|
+
process.stderr.write(
|
|
167
|
+
`${persistOutCheckUsage()}\npersist-out-check: at least one --task-file or --run-id prefix required\n`,
|
|
168
|
+
);
|
|
169
|
+
return 2;
|
|
170
|
+
}
|
|
153
171
|
if (!wtRoot || !existsSync(wtRoot) || !statSync(wtRoot).isDirectory()) {
|
|
154
172
|
process.stderr.write(
|
|
155
173
|
`${persistOutCheckUsage()}\npersist-out-check: --from must be an existing worktree directory\n`,
|
|
156
174
|
);
|
|
157
175
|
return 2;
|
|
158
176
|
}
|
|
159
|
-
const
|
|
160
|
-
|
|
177
|
+
const foreignDivergent = new Set<string>();
|
|
178
|
+
const skips: Array<{ id: string; reason: string }> = [];
|
|
179
|
+
const candidates = [
|
|
180
|
+
successJsonPath,
|
|
181
|
+
join(invokeRoot, '.spur', 'run', 'persist-out.json'),
|
|
182
|
+
join(wtRoot, '.spur', 'run', 'persist-out.json'),
|
|
183
|
+
].filter(Boolean);
|
|
184
|
+
for (const cand of candidates) {
|
|
185
|
+
if (existsSync(cand)) {
|
|
186
|
+
try {
|
|
187
|
+
const parsed = JSON.parse(readFileSync(cand, 'utf8'));
|
|
188
|
+
if (Array.isArray(parsed.evidenceSkipped)) {
|
|
189
|
+
for (const item of parsed.evidenceSkipped) {
|
|
190
|
+
if (item.reason === 'foreign-divergent' && typeof item.name === 'string') {
|
|
191
|
+
foreignDivergent.add(item.name);
|
|
192
|
+
foreignDivergent.add(join(EVIDENCE_DIR, item.name));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (Array.isArray(parsed.skipped)) {
|
|
197
|
+
for (const item of parsed.skipped) {
|
|
198
|
+
if (typeof item.id === 'string' && typeof item.reason === 'string') {
|
|
199
|
+
skips.push({ id: item.id, reason: item.reason });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
break;
|
|
204
|
+
} catch {}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const obligations = listObligations(wtRoot, wbsList, runIds);
|
|
208
|
+
if (obligations === null) {
|
|
161
209
|
process.stderr.write(
|
|
162
|
-
`persist-out-check: BLOCKED — worktree evidence listing failed or exceeded caps (
|
|
210
|
+
`persist-out-check: BLOCKED — worktree evidence listing failed or exceeded caps (${PREFIX_CAP}/prefix) — inspect by hand\n`,
|
|
163
211
|
);
|
|
164
212
|
return 1;
|
|
165
213
|
}
|
|
166
|
-
const {
|
|
214
|
+
const { files, unowned } = obligations;
|
|
215
|
+
const { missing, divergent, ok } = compareTrees(wtRoot, invokeRoot, files, foreignDivergent);
|
|
167
216
|
const findings = [
|
|
168
217
|
...missing.map((f) => `MISSING ${f}`),
|
|
169
218
|
...divergent.map((f) => `DIVERGENT ${f} — reconcile by hand (persist-out never overwrites)`),
|
|
@@ -171,7 +220,13 @@ export function main(argv: string[], invokeRoot = defaultInvokeRoot(process.cwd(
|
|
|
171
220
|
for (const line of findings.slice(0, NAMED_CAP)) process.stdout.write(`${line}\n`);
|
|
172
221
|
if (findings.length > NAMED_CAP) process.stdout.write(`… +${findings.length - NAMED_CAP} more\n`);
|
|
173
222
|
if (findings.length === 0) {
|
|
174
|
-
|
|
223
|
+
for (const skip of skips) {
|
|
224
|
+
process.stdout.write(`SKIP ${skip.id}: ${skip.reason}\n`);
|
|
225
|
+
}
|
|
226
|
+
const abandonedMsg = skips.length > 0 ? `${skips.length} skipped` : 'nothing abandoned';
|
|
227
|
+
process.stdout.write(
|
|
228
|
+
`persist-out-check: ok — ${ok} evidence file(s) persisted, unowned: ${unowned}, ${abandonedMsg}\n`,
|
|
229
|
+
);
|
|
175
230
|
return 0;
|
|
176
231
|
}
|
|
177
232
|
process.stderr.write(
|
|
@@ -480,6 +480,15 @@ ${requestMarker(pr.headRefOid)}`;
|
|
|
480
480
|
writeStatus(args, "REQUESTED");
|
|
481
481
|
emit(args, { ok: true, requested: true, pr: pr.number, url: pr.url, head: pr.headRefOid, requestedAt, body }, `Requested GitHub Codex review on PR #${pr.number} (${pr.url}) at HEAD ${pr.headRefOid.slice(0, 7)}.`);
|
|
482
482
|
}
|
|
483
|
+
function revalidateWaitTarget(args, number) {
|
|
484
|
+
const fresh = viewPr();
|
|
485
|
+
if (!fresh || fresh.number !== number) {
|
|
486
|
+
writeStatus(args, "FAIL");
|
|
487
|
+
fail(args, `PR #${number} is no longer the branch's PR (view reports ${fresh ? `#${fresh.number}` : "none"}) \u2014 request a fresh review`, 2);
|
|
488
|
+
}
|
|
489
|
+
requireExpectedHead(args, fresh);
|
|
490
|
+
return fresh;
|
|
491
|
+
}
|
|
483
492
|
function cmdWait(args) {
|
|
484
493
|
const since = args.flags.get("--since") ?? new Date().toISOString();
|
|
485
494
|
const timeoutSec = Number(args.flags.get("--timeout") ?? "600");
|
|
@@ -494,24 +503,29 @@ function cmdWait(args) {
|
|
|
494
503
|
writeStatus(args, "FAIL");
|
|
495
504
|
fail(args, "no PR for the current branch", 2);
|
|
496
505
|
}
|
|
506
|
+
const number = pr.number;
|
|
497
507
|
requireExpectedHead(args, pr);
|
|
498
508
|
const deadline = Date.now() + timeoutSec * 1000;
|
|
499
509
|
for (;; ) {
|
|
500
|
-
const
|
|
501
|
-
const
|
|
510
|
+
const observed = revalidateWaitTarget(args, number);
|
|
511
|
+
const reviews = fetchReviews(ctx, number);
|
|
512
|
+
const findings = normalizeFindings(reviews, fetchInlineComments(ctx, number), fetchIssueComments(ctx, number), since, observed.headRefOid);
|
|
502
513
|
if (findings.length > 0) {
|
|
514
|
+
revalidateWaitTarget(args, number);
|
|
503
515
|
writeStatus(args, "FOUND");
|
|
504
516
|
emit(args, { ok: true, verdict: "FOUND", findings }, renderFindings(findings));
|
|
505
517
|
return;
|
|
506
518
|
}
|
|
507
|
-
if (hasCurrentCleanReview(reviews, since,
|
|
519
|
+
if (hasCurrentCleanReview(reviews, since, observed.headRefOid)) {
|
|
520
|
+
const fresh = revalidateWaitTarget(args, number);
|
|
508
521
|
writeStatus(args, "CLEAN");
|
|
509
|
-
emit(args, { ok: true, verdict: "CLEAN", pr:
|
|
522
|
+
emit(args, { ok: true, verdict: "CLEAN", pr: fresh.number, url: fresh.url, head: fresh.headRefOid, findings: [] }, `Codex review completed cleanly for HEAD ${fresh.headRefOid.slice(0, 7)}.`);
|
|
510
523
|
return;
|
|
511
524
|
}
|
|
512
525
|
if (Date.now() >= deadline) {
|
|
526
|
+
revalidateWaitTarget(args, number);
|
|
513
527
|
writeStatus(args, "TIMEOUT");
|
|
514
|
-
emit(args, { ok: true, verdict: "TIMEOUT", pr:
|
|
528
|
+
emit(args, { ok: true, verdict: "TIMEOUT", pr: number, url: pr.url }, `No Codex review within ${timeoutSec}s \u2014 still pending. Collect later with /sp:dev-pr-review collect.
|
|
515
529
|
PR: ${pr.url}`);
|
|
516
530
|
throw new ScriptExit("", 3, args.booleans.has("--json"));
|
|
517
531
|
}
|
|
@@ -703,6 +703,26 @@ function cmdRequest(args: ParsedArgs): void {
|
|
|
703
703
|
);
|
|
704
704
|
}
|
|
705
705
|
|
|
706
|
+
/**
|
|
707
|
+
* 1161 R1: re-read the PR and re-assert the pinned identity + expected head. Called at the start of
|
|
708
|
+
* every poll and again immediately before any FOUND/CLEAN/TIMEOUT status or output, because a head
|
|
709
|
+
* captured once at wait start lets a later push return CLEAN for an old commit. A vanished PR, a
|
|
710
|
+
* different PR on the branch, or a moved head is FAIL/exit2 through the existing error channel.
|
|
711
|
+
*/
|
|
712
|
+
function revalidateWaitTarget(args: ParsedArgs, number: number): GhPr {
|
|
713
|
+
const fresh = viewPr();
|
|
714
|
+
if (!fresh || fresh.number !== number) {
|
|
715
|
+
writeStatus(args, 'FAIL');
|
|
716
|
+
fail(
|
|
717
|
+
args,
|
|
718
|
+
`PR #${number} is no longer the branch's PR (view reports ${fresh ? `#${fresh.number}` : 'none'}) — request a fresh review`,
|
|
719
|
+
2,
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
requireExpectedHead(args, fresh);
|
|
723
|
+
return fresh;
|
|
724
|
+
}
|
|
725
|
+
|
|
706
726
|
function cmdWait(args: ParsedArgs): void {
|
|
707
727
|
const since = args.flags.get('--since') ?? new Date().toISOString();
|
|
708
728
|
const timeoutSec = Number(args.flags.get('--timeout') ?? '600');
|
|
@@ -715,36 +735,41 @@ function cmdWait(args: ParsedArgs): void {
|
|
|
715
735
|
writeStatus(args, 'FAIL');
|
|
716
736
|
fail(args, 'no PR for the current branch', 2);
|
|
717
737
|
}
|
|
738
|
+
const number = pr.number; // 1161 R1: pinned — a switched PR number is a failure, not a retarget.
|
|
718
739
|
requireExpectedHead(args, pr);
|
|
719
|
-
const deadline = Date.now() + timeoutSec * 1000;
|
|
740
|
+
const deadline = Date.now() + timeoutSec * 1000; // fixed once; validation never resets it
|
|
720
741
|
for (;;) {
|
|
721
|
-
const
|
|
742
|
+
const observed = revalidateWaitTarget(args, number);
|
|
743
|
+
const reviews = fetchReviews(ctx, number);
|
|
722
744
|
const findings = normalizeFindings(
|
|
723
745
|
reviews,
|
|
724
|
-
fetchInlineComments(ctx,
|
|
725
|
-
fetchIssueComments(ctx,
|
|
746
|
+
fetchInlineComments(ctx, number),
|
|
747
|
+
fetchIssueComments(ctx, number),
|
|
726
748
|
since,
|
|
727
|
-
|
|
749
|
+
observed.headRefOid,
|
|
728
750
|
);
|
|
729
751
|
if (findings.length > 0) {
|
|
752
|
+
revalidateWaitTarget(args, number); // pre-return recheck: never FOUND for a moved head
|
|
730
753
|
writeStatus(args, 'FOUND');
|
|
731
754
|
emit(args, { ok: true, verdict: 'FOUND', findings }, renderFindings(findings));
|
|
732
755
|
return;
|
|
733
756
|
}
|
|
734
|
-
if (hasCurrentCleanReview(reviews, since,
|
|
757
|
+
if (hasCurrentCleanReview(reviews, since, observed.headRefOid)) {
|
|
758
|
+
const fresh = revalidateWaitTarget(args, number); // pre-return recheck: never CLEAN for a moved head
|
|
735
759
|
writeStatus(args, 'CLEAN');
|
|
736
760
|
emit(
|
|
737
761
|
args,
|
|
738
|
-
{ ok: true, verdict: 'CLEAN', pr:
|
|
739
|
-
`Codex review completed cleanly for HEAD ${
|
|
762
|
+
{ ok: true, verdict: 'CLEAN', pr: fresh.number, url: fresh.url, head: fresh.headRefOid, findings: [] },
|
|
763
|
+
`Codex review completed cleanly for HEAD ${fresh.headRefOid.slice(0, 7)}.`,
|
|
740
764
|
);
|
|
741
765
|
return;
|
|
742
766
|
}
|
|
743
767
|
if (Date.now() >= deadline) {
|
|
768
|
+
revalidateWaitTarget(args, number); // a head moved while pending is a failure, not a timeout
|
|
744
769
|
writeStatus(args, 'TIMEOUT');
|
|
745
770
|
emit(
|
|
746
771
|
args,
|
|
747
|
-
{ ok: true, verdict: 'TIMEOUT', pr:
|
|
772
|
+
{ ok: true, verdict: 'TIMEOUT', pr: number, url: pr.url },
|
|
748
773
|
`No Codex review within ${timeoutSec}s — still pending. Collect later with /sp:dev-pr-review collect.\nPR: ${pr.url}`,
|
|
749
774
|
);
|
|
750
775
|
throw new ScriptExit('', 3, args.booleans.has('--json'));
|