@gobing-ai/spur 0.3.80 → 0.3.82
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.example.yaml +31 -18
- package/config/config.global.yaml +13 -11
- package/config/pipeline-budgets.json +34 -2
- package/config/plugin-scripts.json +25 -0
- package/config/rules/boundary/config-loading-ownership.yaml +0 -3
- package/config/rules/boundary/dao-boundary.yaml +4 -17
- package/config/rules/boundary/planning-folder-hardcode.yaml +0 -1
- package/config/rules/boundary/sp-no-vendor-refs.yaml +3 -2
- package/config/rules/boundary/sp-runtime-path.yaml +3 -14
- package/config/rules/quality/coverage-gate.yaml +3 -14
- package/config/rules/quality/tsdoc-exports.yaml +4 -7
- package/config/rules/strict/http-boundaries.yaml +5 -8
- package/config/rules/strict/runtime-boundaries.yaml +1 -5
- package/config/rules/structure/protected-files.yaml +9 -3
- package/config/rules/structure/test-focus-skip.yaml +0 -2
- package/config/rules/structure/test-location.yaml +0 -5
- package/config/rules/surface/check-cli-surface.yaml +3 -2
- package/config/rules/typescript/bun-tooling.yaml +5 -7
- package/config/rules/typescript/guarded-happy-dom-register.yaml +0 -2
- package/config/rules/typescript/happy-dom-teardown.yaml +0 -2
- package/config/rules/typescript/no-biome-suppressions.yaml +0 -2
- package/config/rules/typescript/no-debugger.yaml +0 -2
- package/config/rules/typescript/no-eslint-suppressions.yaml +0 -4
- package/config/rules/typescript/no-leaky-module-mocks.yaml +6 -13
- package/config/rules/typescript/no-module-scope-import-calls.yaml +0 -2
- package/config/rules/typescript/no-syscall-emulation-in-boundary-mock.yaml +0 -3
- package/config/rules/typescript/no-unmocked-module-eval-side-effects.yaml +0 -3
- package/config/rules/typescript/output-boundaries.yaml +0 -3
- package/config/rules/typescript/prefer-accessible-role-for-button-queries.yaml +0 -3
- package/config/rules/ui/ui-import-boundary.yaml +1 -5
- package/config/templates/docs/99_PROJECT_CONSTITUTION.md +75 -13
- package/config/transition-shims.json +0 -7
- package/config/workflows/basic.yaml +4 -0
- package/config/workflows/docs-pipeline.yaml +13 -14
- package/config/workflows/feature-dev.yaml +20 -65
- package/config/workflows/history-anatomy.yaml +22 -1
- package/config/workflows/idea-pipeline.yaml +53 -97
- package/config/workflows/pr-review.yaml +21 -33
- package/config/workflows/task-pipeline.yaml +87 -330
- package/config/workflows/wayfinder-resolution.yaml +12 -26
- package/config/workflows/wrapup-pipeline.yaml +48 -189
- package/package.json +9 -9
- package/plugins/sp/README.md +12 -3
- package/plugins/sp/agents/expert-spur.md +41 -19
- package/plugins/sp/lib/idea-handoff.generated.d.mts +17 -0
- package/plugins/sp/lib/idea-handoff.generated.mjs +1301 -0
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/scripts/feature-dev-precheck.mjs +146 -0
- package/plugins/sp/scripts/feature-dev-precheck.ts +238 -0
- package/plugins/sp/scripts/idea-handoff.mjs +27 -0
- package/plugins/sp/scripts/idea-handoff.ts +44 -0
- package/plugins/sp/scripts/inline-run-setup.ts +69 -1
- package/plugins/sp/scripts/quality-gate.mjs +216 -0
- package/plugins/sp/scripts/quality-gate.ts +287 -0
- package/plugins/sp/scripts/surface-drift-inventory.ts +0 -2
- package/plugins/sp/scripts/task-size-precheck.ts +44 -16
- package/plugins/sp/scripts/verify-answer-lint.ts +17 -1
- package/plugins/sp/scripts/workflow-step-profile.mjs +319 -0
- package/plugins/sp/scripts/workflow-step-profile.ts +456 -0
- package/plugins/sp/scripts/wrapup-steps.mjs +350 -0
- package/plugins/sp/scripts/wrapup-steps.ts +466 -0
- package/plugins/sp/skills/code-review/references/review-lenses.md +3 -0
- package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +1 -1
- package/plugins/sp/skills/spec-decomposition/references/decomposition.md +29 -0
- package/plugins/sp/skills/spur-cli/SKILL.md +4 -8
- package/plugins/sp/skills/spur-cli/references/agent.md +44 -69
- package/plugins/sp/skills/spur-cli/references/message.md +30 -3
- package/plugins/sp/skills/spur-cli/references/projects.md +25 -1
- package/plugins/sp/skills/spur-cli/references/self.md +6 -5
- package/plugins/sp/skills/spur-cli/references/serve.md +8 -7
- package/plugins/sp/skills/spur-cli/references/tasks.md +1 -1
- package/plugins/sp/skills/spur-cli/references/workflows/operations.md +6 -3
- package/plugins/sp/skills/spur-cli/references/workflows/workflow-fit-and-tuning.md +57 -18
- package/plugins/sp/skills/spur-composer/SKILL.md +145 -0
- package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +10 -0
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +1 -1
- package/plugins/sp/skills/spur-dev/references/execution-workflow.md +4 -6
- package/plugins/sp/skills/spur-dev/references/glossary.md +1 -1
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +11 -1
- package/plugins/sp/skills/spur-dev/references/planning-workflow.md +24 -0
- package/plugins/sp/skills/spur-doctor/SKILL.md +138 -0
- package/plugins/sp/skills/taste-refactoring-api/README.md +43 -0
- package/plugins/sp/skills/taste-refactoring-api/SKILL.md +334 -0
- package/plugins/sp/skills/taste-refactoring-api/checklists/daily-api-review.md +71 -0
- package/plugins/sp/skills/taste-refactoring-api/examples/refactor-example.md +72 -0
- package/plugins/sp/skills/taste-refactoring-api/examples/review-template.md +93 -0
- package/plugins/sp/skills/taste-refactoring-api/references/api-refactoring-playbook.md +253 -0
- package/plugins/sp/skills/taste-refactoring-api/references/protocol-modes.md +79 -0
- package/plugins/sp/skills/taste-refactoring-api/references/research-basis.md +58 -0
- package/plugins/sp/skills/taste-refactoring-architect/README.md +26 -0
- package/plugins/sp/skills/taste-refactoring-architect/SKILL.md +471 -0
- package/plugins/sp/skills/taste-refactoring-architect/checklists/daily-architecture-review.md +48 -0
- package/plugins/sp/skills/taste-refactoring-architect/examples/refactor-example.md +55 -0
- package/plugins/sp/skills/taste-refactoring-architect/examples/review-template.md +51 -0
- package/plugins/sp/skills/taste-refactoring-architect/references/architecture-refactoring-playbook.md +173 -0
- package/plugins/sp/skills/taste-refactoring-architect/references/research-basis.md +28 -0
- package/plugins/sp/skills/taste-refactoring-tests/README.md +28 -0
- package/plugins/sp/skills/taste-refactoring-tests/SKILL.md +482 -0
- package/plugins/sp/skills/taste-refactoring-tests/checklists/daily-test-review.md +39 -0
- package/plugins/sp/skills/taste-refactoring-tests/examples/refactor-example.md +85 -0
- package/plugins/sp/skills/taste-refactoring-tests/examples/review-template.md +59 -0
- package/plugins/sp/skills/taste-refactoring-tests/references/research-basis.md +47 -0
- package/plugins/sp/skills/taste-refactoring-tests/references/test-refactoring-playbook.md +222 -0
- package/plugins/sp/skills/taste-refactoring-ui/README.md +12 -0
- package/plugins/sp/skills/taste-refactoring-ui/SKILL.md +290 -0
- package/plugins/sp/skills/taste-refactoring-ui/checklists/daily-ui-review.md +72 -0
- package/plugins/sp/skills/taste-refactoring-ui/examples/review-template.md +51 -0
- package/plugins/sp/skills/taste-refactoring-ui/references/refactoring-ui-playbook.md +170 -0
- package/plugins/sp/skills/wayfinder/SKILL.md +2 -2
- package/plugins/sp/skills/wayfinder/references/pipeline-resolution.md +30 -0
- package/schemas/spur-config.schema.json +105 -85
- package/spur.js +44616 -43320
- package/web/_astro/BoardApp.D-WlxiN2.js +1 -0
- package/web/_astro/{BoardApp.CHQ1lycZ.js → BoardApp.D8bM9pKL.js} +97 -95
- package/web/_astro/{TaskDetail.GKfQJ60c.js → TaskDetail.BPRqgVUE.js} +1 -1
- package/web/_astro/{arc.DWEtA3Tx.js → arc.BPrPES3z.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.DB42oWmP.js → architectureDiagram-3BPJPVTR.qX_7q02P.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.rhv-zNQV.js → blockDiagram-GPEHLZMM.CUZfj5V7.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.Ci4-4VvY.js → c4Diagram-AAUBKEIU.CTaOr8hH.js} +1 -1
- package/web/_astro/channel.DGZaFHZx.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.Cc9veUgf.js → chunk-2J33WTMH.Dt-9wf3h.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.Bec9c4eI.js → chunk-4BX2VUAB.CTC2sdoN.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.DoV8S1iB.js → chunk-55IACEB6.DQcxt2_g.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.DwR-Qlyj.js → chunk-727SXJPM.DXFPSn-a.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.ND_a81WY.js → chunk-AQP2D5EJ.BCx3U4bT.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.Wv_jwG48.js → chunk-FMBD7UC4.DL2tJkdO.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.CXKXCMmp.js → chunk-ND2GUHAM.DZyflMro.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.nkaoNYQq.js → chunk-QZHKN3VN.CUI2mT09.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.cMQcVlQu.js → classDiagram-4FO5ZUOK.g4rX4Fr1.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.cMQcVlQu.js → classDiagram-v2-Q7XG4LA2.g4rX4Fr1.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.OaDJ7Mr2.js → cose-bilkent-S5V4N54A.CzWJLqp0.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.Chi8IphF.js → cynefin-OW5HDTMX.WgsvQeCR.js} +1 -1
- package/web/_astro/{cytoscape.esm.DzSz-X2X.js → cytoscape.esm.BB4DxJjf.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.CzK2t_Fp.js → dagre-BM42HDAG.Dzv6ngql.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.DRvxlVS7.js → diagram-2AECGRRQ.CpJ4a9rU.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.CnYvNdwA.js → diagram-5GNKFQAL.CzPlF_dq.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.CpLpMw5R.js → diagram-KO2AKTUF.TAkZNTcQ.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.JTb78qUA.js → diagram-LMA3HP47.uCjoKSag.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.Bk-1jDIb.js → diagram-OG6HWLK6.eMplIjoK.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.D8hN9GZq.js → erDiagram-TEJ5UH35.Bf7zoXGz.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.-6zQr6m5.js → flowDiagram-I6XJVG4X.B_bHj3gN.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.DboLQ9ca.js → ganttDiagram-6RSMTGT7.BasrHRMj.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.4tYvJKGR.js → gitGraphDiagram-PVQCEYII.C6iphq1x.js} +1 -1
- package/web/_astro/index.DayyIngm.css +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.Bd9rXpsB.js → infoDiagram-5YYISTIA.HXmDMhW4.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.CvMoaf67.js → ishikawaDiagram-YF4QCWOH.BSmW8NiU.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.Ccy1CA7y.js → journeyDiagram-JHISSGLW.DEQow5fo.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.0MaMqHNS.js → kanban-definition-UN3LZRKU.IVm9cTdc.js} +1 -1
- package/web/_astro/{linear.CHXgcIbN.js → linear.CrsM73_9.js} +1 -1
- package/web/_astro/{mermaid.core.Ca-kcelG.js → mermaid.core.CfBeDJls.js} +6 -6
- package/web/_astro/{mindmap-definition-RKZ34NQL.BUIDlHa0.js → mindmap-definition-RKZ34NQL.C3j60Y-0.js} +1 -1
- package/web/_astro/ordinal.BYWQX77i.js +1 -0
- package/web/_astro/{pieDiagram-4H26LBE5.2dX3CU1s.js → pieDiagram-4H26LBE5.B-aCMeEA.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.B3LBlRiv.js → quadrantDiagram-W4KKPZXB.Cib965yq.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.X12I2uNx.js → requirementDiagram-4Y6WPE33.D61cS4O-.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.BXohIHqx.js → sankeyDiagram-5OEKKPKP.GKF2qVPy.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.C37ZIUzg.js → sequenceDiagram-3UESZ5HK.DZnq8F2h.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.BRgz317z.js → stateDiagram-AJRCARHV.DXUFmdgM.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.7VYSXN9-.js → stateDiagram-v2-BHNVJYJU.BtHmhLEz.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.BVNz_HiN.js → timeline-definition-PNZ67QCA.Cy-WW2ln.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.CHVDkPX4.js → vennDiagram-CIIHVFJN.SLp5b9KI.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.EQQ_qT9v.js → wardleyDiagram-YWT4CUSO.Bww45mWV.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.DrAT9WoP.js → xychartDiagram-2RQKCTM6.DR4swI6a.js} +1 -1
- package/web/apple-touch-icon.png +0 -0
- package/web/favicon.ico +0 -0
- package/web/favicon.svg +17 -4
- package/web/icon-192.png +0 -0
- package/web/icon-512.png +0 -0
- package/web/index.html +2 -2
- package/web/site.webmanifest +31 -0
- package/web/spur_logo.svg +1 -0
- package/plugins/sp/skills/spur-cli/references/team.md +0 -145
- package/web/_astro/BoardApp.DV9kx0wo.js +0 -1
- package/web/_astro/channel.BAI6xLeV.js +0 -1
- package/web/_astro/index.Dcr_8fiK.css +0 -1
- package/web/_astro/ordinal.DBvzRdQf.js +0 -1
package/plugins/sp/plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.82",
|
|
4
4
|
"description": "Spur — a local-first harness engineering toolkit that wraps mainstream coding agents with constraint checking, workflow orchestration, and history analytics.",
|
|
5
5
|
"extensions": {
|
|
6
6
|
"pi": ["./hooks/pi/guard-extension.ts"]
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// plugins/sp/scripts/feature-dev-precheck.ts
|
|
5
|
+
import { spawnSync } from "child_process";
|
|
6
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
var BLOCKING_STATUSES = ["backlog", "wip", "testing", "blocked"];
|
|
9
|
+
var KNOWN_STATUSES = ["todo", "done", "cancelled", ...BLOCKING_STATUSES];
|
|
10
|
+
function identityRc(featureId, runId) {
|
|
11
|
+
return featureId && runId ? 0 : 1;
|
|
12
|
+
}
|
|
13
|
+
function featureIdentityMatches(raw, featureId) {
|
|
14
|
+
let parsed;
|
|
15
|
+
try {
|
|
16
|
+
parsed = JSON.parse(raw);
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) && parsed.id === featureId;
|
|
21
|
+
}
|
|
22
|
+
function rosterIsNonEmptyArray(raw) {
|
|
23
|
+
let parsed;
|
|
24
|
+
try {
|
|
25
|
+
parsed = JSON.parse(raw);
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
return Array.isArray(parsed) && parsed.length > 0;
|
|
30
|
+
}
|
|
31
|
+
function rosterContractHolds(rows) {
|
|
32
|
+
const seen = new Set;
|
|
33
|
+
for (const row of rows) {
|
|
34
|
+
if (row === null || typeof row !== "object")
|
|
35
|
+
return false;
|
|
36
|
+
if (typeof row.wbs !== "string" || row.wbs.length === 0)
|
|
37
|
+
return false;
|
|
38
|
+
if (seen.has(row.wbs))
|
|
39
|
+
return false;
|
|
40
|
+
seen.add(row.wbs);
|
|
41
|
+
if (typeof row.status !== "string" || !KNOWN_STATUSES.includes(row.status)) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
function hasBlockingStatus(rows) {
|
|
48
|
+
return rows.some((row) => typeof row.status === "string" && BLOCKING_STATUSES.includes(row.status));
|
|
49
|
+
}
|
|
50
|
+
function freezeTodoList(rows) {
|
|
51
|
+
return rows.filter((row) => row.status === "todo" && typeof row.wbs === "string").map((row) => row.wbs).sort().join(",");
|
|
52
|
+
}
|
|
53
|
+
function spurCommand(spurBin) {
|
|
54
|
+
const parts = (spurBin ?? "spur").trim().split(/\s+/).filter((p) => p.length > 0);
|
|
55
|
+
return { cmd: parts[0] ?? "spur", prefix: parts.slice(1) };
|
|
56
|
+
}
|
|
57
|
+
function readFileSyncRaw(path) {
|
|
58
|
+
try {
|
|
59
|
+
return readFileSync(path, "utf8");
|
|
60
|
+
} catch {
|
|
61
|
+
return "";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function runFeatureDevPrecheck(env, options = {}) {
|
|
65
|
+
const cwd = options.cwd;
|
|
66
|
+
const runDir = join(".spur", "run");
|
|
67
|
+
mkdirSync(cwd ? join(cwd, runDir) : runDir, { recursive: true });
|
|
68
|
+
const abs = (p) => cwd ? join(cwd, p) : p;
|
|
69
|
+
const featureFile = join(runDir, `${env.__runId ?? ""}-feature-dev-feature.json`);
|
|
70
|
+
const rosterFile = join(runDir, `${env.__runId ?? ""}-feature-dev-roster.json`);
|
|
71
|
+
const tasksFile = join(runDir, `${env.__runId ?? ""}-feature-dev-tasks.txt`);
|
|
72
|
+
const statusFile = join(runDir, `${env.__runId ?? ""}-feature-dev-precheck.status`);
|
|
73
|
+
const featureId = env.featureId ?? "";
|
|
74
|
+
rmSync(abs(statusFile), { force: true });
|
|
75
|
+
const { cmd, prefix } = spurCommand(env.spurBin);
|
|
76
|
+
const read = (args, target) => {
|
|
77
|
+
const result = spawnSync(cmd, [...prefix, ...args], {
|
|
78
|
+
cwd,
|
|
79
|
+
encoding: "utf8",
|
|
80
|
+
...options.env ? { env: { ...process.env, ...options.env } } : {}
|
|
81
|
+
});
|
|
82
|
+
if (result.error !== undefined) {
|
|
83
|
+
writeFileSync(abs(target), `spawn failed: ${result.error.message}
|
|
84
|
+
`);
|
|
85
|
+
return 127;
|
|
86
|
+
}
|
|
87
|
+
writeFileSync(abs(target), `${result.stdout ?? ""}${result.stderr ?? ""}`);
|
|
88
|
+
return result.status ?? 1;
|
|
89
|
+
};
|
|
90
|
+
const idRc = identityRc(featureId, env.__runId);
|
|
91
|
+
const showRc = idRc === 0 ? read(["feature", "show", featureId, "--json"], featureFile) : 1;
|
|
92
|
+
const listRc = idRc === 0 && showRc === 0 && featureIdentityMatches(readFileSyncRaw(abs(featureFile)), featureId) ? read(["task", "list", "--feature", featureId, "--json"], rosterFile) : 1;
|
|
93
|
+
const fail = (message) => {
|
|
94
|
+
process.stderr.write(`${message}
|
|
95
|
+
`);
|
|
96
|
+
writeFileSync(abs(statusFile), `FAIL
|
|
97
|
+
`);
|
|
98
|
+
return { status: "FAIL", featureFile, rosterFile, tasksFile, statusFile, tasks: "" };
|
|
99
|
+
};
|
|
100
|
+
if (idRc !== 0 || showRc !== 0 || listRc !== 0) {
|
|
101
|
+
return fail(`feature-dev precheck: missing featureId/runId, unknown feature '${featureId}', or unreadable roster (rc ${idRc}/${showRc}/${listRc}) \u2014 supply an existing planned feature via /sp:dev-plan or /sp:dev-idea; nothing was auto-created or re-planned`);
|
|
102
|
+
}
|
|
103
|
+
const rosterRaw = readFileSyncRaw(abs(rosterFile));
|
|
104
|
+
if (!rosterIsNonEmptyArray(rosterRaw)) {
|
|
105
|
+
return fail(`feature-dev precheck: roster at ${rosterFile} is malformed, not an array, or empty \u2014 plan the feature first via /sp:dev-plan; refusing to replan or run an empty batch`);
|
|
106
|
+
}
|
|
107
|
+
const rows = JSON.parse(rosterRaw);
|
|
108
|
+
if (!rosterContractHolds(rows)) {
|
|
109
|
+
return fail(`feature-dev precheck: roster has empty/duplicate/mismatched WBS identities or unknown statuses at ${rosterFile} \u2014 repair the task corpus; refusing to batch a broken roster`);
|
|
110
|
+
}
|
|
111
|
+
if (hasBlockingStatus(rows)) {
|
|
112
|
+
return fail("feature-dev precheck: linked task(s) are backlog/wip/testing/blocked \u2014 refine or resume them through their own task pipelines before batching; refusing to launch overlapping work");
|
|
113
|
+
}
|
|
114
|
+
const tasks = freezeTodoList(rows);
|
|
115
|
+
writeFileSync(abs(tasksFile), tasks);
|
|
116
|
+
writeFileSync(abs(statusFile), `PASS
|
|
117
|
+
`);
|
|
118
|
+
return { status: "PASS", featureFile, rosterFile, tasksFile, statusFile, tasks };
|
|
119
|
+
}
|
|
120
|
+
var FEATURE_DEV_PRECHECK_USAGE = "usage: feature-dev-precheck.ts (env: featureId, __runId, optional spurBin) \u2014 no subcommands";
|
|
121
|
+
function main(argv, env = process.env) {
|
|
122
|
+
if (argv.length > 0) {
|
|
123
|
+
process.stderr.write(`${FEATURE_DEV_PRECHECK_USAGE}
|
|
124
|
+
`);
|
|
125
|
+
return 2;
|
|
126
|
+
}
|
|
127
|
+
runFeatureDevPrecheck(env);
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
{
|
|
131
|
+
process.exit(main(process.argv.slice(2)));
|
|
132
|
+
}
|
|
133
|
+
export {
|
|
134
|
+
spurCommand,
|
|
135
|
+
runFeatureDevPrecheck,
|
|
136
|
+
rosterIsNonEmptyArray,
|
|
137
|
+
rosterContractHolds,
|
|
138
|
+
main,
|
|
139
|
+
identityRc,
|
|
140
|
+
hasBlockingStatus,
|
|
141
|
+
freezeTodoList,
|
|
142
|
+
featureIdentityMatches,
|
|
143
|
+
KNOWN_STATUSES,
|
|
144
|
+
FEATURE_DEV_PRECHECK_USAGE,
|
|
145
|
+
BLOCKING_STATUSES
|
|
146
|
+
};
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* feature-dev-precheck — portable identity/roster gate behind the feature-dev pipeline
|
|
4
|
+
* precheck (task 0825, feature I21, governance §1.1 composition budgets).
|
|
5
|
+
*
|
|
6
|
+
* Ports the former `feature-dev.yaml` `precheck:onEnter:0` shell program one-for-one so the
|
|
7
|
+
* workflow stays inside the shell-program caps while writing the same `.spur/run` artifacts:
|
|
8
|
+
* - `<runId>-feature-dev-feature.json` merged `feature show` capture (stdout + stderr)
|
|
9
|
+
* - `<runId>-feature-dev-roster.json` merged `task list --feature` capture
|
|
10
|
+
* - `<runId>-feature-dev-tasks.txt` frozen sorted todo WBS list, comma-joined
|
|
11
|
+
* - `<runId>-feature-dev-precheck.status` `PASS`/`FAIL`
|
|
12
|
+
*
|
|
13
|
+
* Failure ladder (first failing check wins, message to stderr, FAIL status):
|
|
14
|
+
* 1. identity/roster read failure — missing featureId/runId, unknown feature, or a
|
|
15
|
+
* non-zero rc from any of the three reads (message carries the rc triple);
|
|
16
|
+
* 2. roster not a non-empty JSON array;
|
|
17
|
+
* 3. roster contract breach — empty/duplicate/non-string WBS identities or a status
|
|
18
|
+
* outside todo/done/cancelled/backlog/wip/testing/blocked;
|
|
19
|
+
* 4. refinement-blocking statuses present (backlog/wip/testing/blocked).
|
|
20
|
+
*
|
|
21
|
+
* Environment: `featureId`, `__runId` (required), `spurBin` (split on whitespace into a
|
|
22
|
+
* command plus prefix args). Soft-fail contract: the process always exits 0; the verdict
|
|
23
|
+
* lives in the status file. Node-builtin imports only; pure helpers are exported for unit
|
|
24
|
+
* testing (ADR-065).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { spawnSync } from 'node:child_process';
|
|
28
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
29
|
+
import { join } from 'node:path';
|
|
30
|
+
|
|
31
|
+
/** Statuses that must be refined/resumed through their own pipelines before batching. */
|
|
32
|
+
export const BLOCKING_STATUSES = ['backlog', 'wip', 'testing', 'blocked'] as const;
|
|
33
|
+
|
|
34
|
+
/** Every status a roster row may carry (anything else fails the structural contract). */
|
|
35
|
+
export const KNOWN_STATUSES = ['todo', 'done', 'cancelled', ...BLOCKING_STATUSES] as const;
|
|
36
|
+
|
|
37
|
+
export interface FeatureDevPrecheckEnv {
|
|
38
|
+
featureId?: string;
|
|
39
|
+
__runId?: string;
|
|
40
|
+
spurBin?: string;
|
|
41
|
+
[key: string]: string | undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface FeatureDevPrecheckOptions {
|
|
45
|
+
/** Base directory for `.spur/run`; defaults to the process cwd (CLI behavior). */
|
|
46
|
+
cwd?: string;
|
|
47
|
+
/** Extra environment for the spawned CLI reads (merged over process.env). */
|
|
48
|
+
env?: Record<string, string>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface FeatureDevPrecheckResult {
|
|
52
|
+
status: 'PASS' | 'FAIL';
|
|
53
|
+
featureFile: string;
|
|
54
|
+
rosterFile: string;
|
|
55
|
+
tasksFile: string;
|
|
56
|
+
statusFile: string;
|
|
57
|
+
/** Frozen todo list; empty string on FAIL. */
|
|
58
|
+
tasks: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface RosterRow {
|
|
62
|
+
wbs: unknown;
|
|
63
|
+
status: unknown;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Identity gate: rc 0 when featureId and __runId are both non-empty, else 1. */
|
|
67
|
+
export function identityRc(featureId: string | undefined, runId: string | undefined): number {
|
|
68
|
+
return featureId && runId ? 0 : 1;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* jq `type == "object" and .id == $id` port: parsed JSON must be a non-array object whose
|
|
73
|
+
* `.id` equals the supplied feature id.
|
|
74
|
+
*/
|
|
75
|
+
export function featureIdentityMatches(raw: string, featureId: string): boolean {
|
|
76
|
+
let parsed: unknown;
|
|
77
|
+
try {
|
|
78
|
+
parsed = JSON.parse(raw);
|
|
79
|
+
} catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
return (
|
|
83
|
+
typeof parsed === 'object' &&
|
|
84
|
+
parsed !== null &&
|
|
85
|
+
!Array.isArray(parsed) &&
|
|
86
|
+
(parsed as { id?: unknown }).id === featureId
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** jq roster-shape port: parsed JSON must be a non-empty array. */
|
|
91
|
+
export function rosterIsNonEmptyArray(raw: string): boolean {
|
|
92
|
+
let parsed: unknown;
|
|
93
|
+
try {
|
|
94
|
+
parsed = JSON.parse(raw);
|
|
95
|
+
} catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
return Array.isArray(parsed) && parsed.length > 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** jq WBS-identity/status contract port: non-empty string wbs, unique, known statuses. */
|
|
102
|
+
export function rosterContractHolds(rows: RosterRow[]): boolean {
|
|
103
|
+
const seen = new Set<string>();
|
|
104
|
+
for (const row of rows) {
|
|
105
|
+
if (row === null || typeof row !== 'object') return false;
|
|
106
|
+
if (typeof row.wbs !== 'string' || row.wbs.length === 0) return false;
|
|
107
|
+
if (seen.has(row.wbs)) return false;
|
|
108
|
+
seen.add(row.wbs);
|
|
109
|
+
if (typeof row.status !== 'string' || !(KNOWN_STATUSES as readonly string[]).includes(row.status)) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** jq blocking-status port: any backlog/wip/testing/blocked member. */
|
|
117
|
+
export function hasBlockingStatus(rows: RosterRow[]): boolean {
|
|
118
|
+
return rows.some(
|
|
119
|
+
(row) => typeof row.status === 'string' && (BLOCKING_STATUSES as readonly string[]).includes(row.status),
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** jq `sort | join(",")` port over the todo subset: codepoint-sorted, comma-joined, no newline. */
|
|
124
|
+
export function freezeTodoList(rows: RosterRow[]): string {
|
|
125
|
+
return rows
|
|
126
|
+
.filter((row) => row.status === 'todo' && typeof row.wbs === 'string')
|
|
127
|
+
.map((row) => row.wbs as string)
|
|
128
|
+
.sort()
|
|
129
|
+
.join(',');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** `spurBin` splits on whitespace into a command plus prefix args (PATH-independent runs). */
|
|
133
|
+
export function spurCommand(spurBin: string | undefined): { cmd: string; prefix: string[] } {
|
|
134
|
+
const parts = (spurBin ?? 'spur')
|
|
135
|
+
.trim()
|
|
136
|
+
.split(/\s+/)
|
|
137
|
+
.filter((p) => p.length > 0);
|
|
138
|
+
return { cmd: parts[0] ?? 'spur', prefix: parts.slice(1) };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function readFileSyncRaw(path: string): string {
|
|
142
|
+
try {
|
|
143
|
+
return readFileSync(path, 'utf8');
|
|
144
|
+
} catch {
|
|
145
|
+
return '';
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function runFeatureDevPrecheck(
|
|
150
|
+
env: FeatureDevPrecheckEnv,
|
|
151
|
+
options: FeatureDevPrecheckOptions = {},
|
|
152
|
+
): FeatureDevPrecheckResult {
|
|
153
|
+
const cwd = options.cwd;
|
|
154
|
+
const runDir = join('.spur', 'run');
|
|
155
|
+
mkdirSync(cwd ? join(cwd, runDir) : runDir, { recursive: true });
|
|
156
|
+
const abs = (p: string): string => (cwd ? join(cwd, p) : p);
|
|
157
|
+
const featureFile = join(runDir, `${env.__runId ?? ''}-feature-dev-feature.json`);
|
|
158
|
+
const rosterFile = join(runDir, `${env.__runId ?? ''}-feature-dev-roster.json`);
|
|
159
|
+
const tasksFile = join(runDir, `${env.__runId ?? ''}-feature-dev-tasks.txt`);
|
|
160
|
+
const statusFile = join(runDir, `${env.__runId ?? ''}-feature-dev-precheck.status`);
|
|
161
|
+
const featureId = env.featureId ?? '';
|
|
162
|
+
|
|
163
|
+
rmSync(abs(statusFile), { force: true });
|
|
164
|
+
|
|
165
|
+
const { cmd, prefix } = spurCommand(env.spurBin);
|
|
166
|
+
/** Spawn one CLI read; merged output into the artifact, 127 on a spawn error (exec loss). */
|
|
167
|
+
const read = (args: string[], target: string): number => {
|
|
168
|
+
const result = spawnSync(cmd, [...prefix, ...args], {
|
|
169
|
+
cwd,
|
|
170
|
+
encoding: 'utf8',
|
|
171
|
+
...(options.env ? { env: { ...process.env, ...options.env } } : {}),
|
|
172
|
+
});
|
|
173
|
+
if (result.error !== undefined) {
|
|
174
|
+
writeFileSync(abs(target), `spawn failed: ${result.error.message}\n`);
|
|
175
|
+
return 127;
|
|
176
|
+
}
|
|
177
|
+
writeFileSync(abs(target), `${result.stdout ?? ''}${result.stderr ?? ''}`);
|
|
178
|
+
return result.status ?? 1;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const idRc = identityRc(featureId, env.__runId);
|
|
182
|
+
const showRc = idRc === 0 ? read(['feature', 'show', featureId, '--json'], featureFile) : 1;
|
|
183
|
+
const listRc =
|
|
184
|
+
idRc === 0 && showRc === 0 && featureIdentityMatches(readFileSyncRaw(abs(featureFile)), featureId)
|
|
185
|
+
? read(['task', 'list', '--feature', featureId, '--json'], rosterFile)
|
|
186
|
+
: 1;
|
|
187
|
+
|
|
188
|
+
const fail = (message: string): FeatureDevPrecheckResult => {
|
|
189
|
+
process.stderr.write(`${message}\n`);
|
|
190
|
+
writeFileSync(abs(statusFile), 'FAIL\n');
|
|
191
|
+
return { status: 'FAIL', featureFile, rosterFile, tasksFile, statusFile, tasks: '' };
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
if (idRc !== 0 || showRc !== 0 || listRc !== 0) {
|
|
195
|
+
return fail(
|
|
196
|
+
`feature-dev precheck: missing featureId/runId, unknown feature '${featureId}', or unreadable roster (rc ${idRc}/${showRc}/${listRc}) — supply an existing planned feature via /sp:dev-plan or /sp:dev-idea; nothing was auto-created or re-planned`,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const rosterRaw = readFileSyncRaw(abs(rosterFile));
|
|
201
|
+
if (!rosterIsNonEmptyArray(rosterRaw)) {
|
|
202
|
+
return fail(
|
|
203
|
+
`feature-dev precheck: roster at ${rosterFile} is malformed, not an array, or empty — plan the feature first via /sp:dev-plan; refusing to replan or run an empty batch`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
const rows = JSON.parse(rosterRaw) as RosterRow[];
|
|
207
|
+
if (!rosterContractHolds(rows)) {
|
|
208
|
+
return fail(
|
|
209
|
+
`feature-dev precheck: roster has empty/duplicate/mismatched WBS identities or unknown statuses at ${rosterFile} — repair the task corpus; refusing to batch a broken roster`,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
if (hasBlockingStatus(rows)) {
|
|
213
|
+
return fail(
|
|
214
|
+
'feature-dev precheck: linked task(s) are backlog/wip/testing/blocked — refine or resume them through their own task pipelines before batching; refusing to launch overlapping work',
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const tasks = freezeTodoList(rows);
|
|
219
|
+
writeFileSync(abs(tasksFile), tasks);
|
|
220
|
+
writeFileSync(abs(statusFile), 'PASS\n');
|
|
221
|
+
return { status: 'PASS', featureFile, rosterFile, tasksFile, statusFile, tasks };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export const FEATURE_DEV_PRECHECK_USAGE =
|
|
225
|
+
'usage: feature-dev-precheck.ts (env: featureId, __runId, optional spurBin) — no subcommands';
|
|
226
|
+
|
|
227
|
+
export function main(argv: string[], env: FeatureDevPrecheckEnv = process.env): number {
|
|
228
|
+
if (argv.length > 0) {
|
|
229
|
+
process.stderr.write(`${FEATURE_DEV_PRECHECK_USAGE}\n`);
|
|
230
|
+
return 2;
|
|
231
|
+
}
|
|
232
|
+
runFeatureDevPrecheck(env);
|
|
233
|
+
return 0;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (import.meta.main) {
|
|
237
|
+
process.exit(main(process.argv.slice(2)));
|
|
238
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// plugins/sp/scripts/idea-handoff.ts
|
|
5
|
+
var libUrl = new URL("../lib/idea-handoff.generated.mjs", import.meta.url).href;
|
|
6
|
+
async function loadHandoffLib() {
|
|
7
|
+
return await import(libUrl);
|
|
8
|
+
}
|
|
9
|
+
var IDEA_HANDOFF_USAGE = "usage: idea-handoff.ts (env: __runId, featureId, optional spurBin) \u2014 no subcommands";
|
|
10
|
+
async function main(argv, env = process.env) {
|
|
11
|
+
if (argv.length > 0) {
|
|
12
|
+
process.stderr.write(`${IDEA_HANDOFF_USAGE}
|
|
13
|
+
`);
|
|
14
|
+
return 2;
|
|
15
|
+
}
|
|
16
|
+
const { runIdeaHandoffCli } = await loadHandoffLib();
|
|
17
|
+
const outcome = await runIdeaHandoffCli(env);
|
|
18
|
+
return outcome.exitCode;
|
|
19
|
+
}
|
|
20
|
+
{
|
|
21
|
+
process.exit(await main(process.argv.slice(2)));
|
|
22
|
+
}
|
|
23
|
+
export {
|
|
24
|
+
main,
|
|
25
|
+
loadHandoffLib,
|
|
26
|
+
IDEA_HANDOFF_USAGE
|
|
27
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* idea-handoff — portable entrypoint for the idea-pipeline finalization (task 0824, feature I21).
|
|
4
|
+
*
|
|
5
|
+
* ADR-065 standard plugin script: the workflow wrapper prefers the monorepo writer
|
|
6
|
+
* (`bun packages/app/src/workflow/idea-handoff-cli.ts`) and falls back to this script's
|
|
7
|
+
* registered `.mjs` twin under bare `node`, so a seeded project with no monorepo checkout
|
|
8
|
+
* still finalizes the handoff (run-scoped report + task-deps ordering + roster refresh) and
|
|
9
|
+
* fails closed (exit 1) when neither writer resolves.
|
|
10
|
+
*
|
|
11
|
+
* The behavior lives in the generated bundle `../lib/idea-handoff.generated.mjs`, produced by
|
|
12
|
+
* `bun run build:plugin-lib` from `packages/app/src/workflow/idea-handoff-cli.ts` — the same
|
|
13
|
+
* source the monorepo branch executes, so both branches share one contract (0604/0824).
|
|
14
|
+
* Declarations: `../lib/idea-handoff.generated.d.mts`. The bundle is imported by computed URL
|
|
15
|
+
* on purpose: `superskill script convert` must NOT inline it (its tree carries one
|
|
16
|
+
* `Bun.spawn` site in a ts-runtime streaming path this workflow never exercises), so the
|
|
17
|
+
* twin stays a thin node-runnable loader over the committed runtime asset.
|
|
18
|
+
*
|
|
19
|
+
* Environment: `__runId`, `featureId`, optional `spurBin`. Missing required vars exit 1
|
|
20
|
+
* (mis-invocation is failed-closed, never a silent skip).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const libUrl = new URL('../lib/idea-handoff.generated.mjs', import.meta.url).href;
|
|
24
|
+
|
|
25
|
+
export async function loadHandoffLib(): Promise<typeof import('../lib/idea-handoff.generated.mjs')> {
|
|
26
|
+
return (await import(libUrl)) as typeof import('../lib/idea-handoff.generated.mjs');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const IDEA_HANDOFF_USAGE =
|
|
30
|
+
'usage: idea-handoff.ts (env: __runId, featureId, optional spurBin) — no subcommands';
|
|
31
|
+
|
|
32
|
+
export async function main(argv: string[], env: NodeJS.ProcessEnv = process.env): Promise<number> {
|
|
33
|
+
if (argv.length > 0) {
|
|
34
|
+
process.stderr.write(`${IDEA_HANDOFF_USAGE}\n`);
|
|
35
|
+
return 2;
|
|
36
|
+
}
|
|
37
|
+
const { runIdeaHandoffCli } = await loadHandoffLib();
|
|
38
|
+
const outcome = await runIdeaHandoffCli(env as { __runId?: string; featureId?: string; spurBin?: string });
|
|
39
|
+
return outcome.exitCode;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (import.meta.main) {
|
|
43
|
+
process.exit(await main(process.argv.slice(2)));
|
|
44
|
+
}
|
|
@@ -29,6 +29,10 @@
|
|
|
29
29
|
*
|
|
30
30
|
* Usage:
|
|
31
31
|
* bun plugins/sp/scripts/inline-run-setup.ts --run-id <id> --file <definition> [--spur-bin <path>]
|
|
32
|
+
* bun plugins/sp/scripts/inline-run-setup.ts --fingerprint --task-file <path> [--feature-file <path>] [--spur-bin <path>]
|
|
33
|
+
*
|
|
34
|
+
* The `--fingerprint` mode prints the engine's proof-input digest for the given spec files and
|
|
35
|
+
* creates nothing (task 0862 R5).
|
|
32
36
|
*
|
|
33
37
|
* Env: SPUR_BIN
|
|
34
38
|
*/
|
|
@@ -56,7 +60,10 @@ function usage(): never {
|
|
|
56
60
|
console.error(
|
|
57
61
|
'Usage: bun plugins/sp/scripts/inline-run-setup.ts --run-id <id> --file <definition> [--spur-bin <path>]',
|
|
58
62
|
);
|
|
59
|
-
|
|
63
|
+
console.error(
|
|
64
|
+
' bun plugins/sp/scripts/inline-run-setup.ts --fingerprint --task-file <path> [--feature-file <path>] [--spur-bin <path>]',
|
|
65
|
+
);
|
|
66
|
+
process.exit(2);
|
|
60
67
|
}
|
|
61
68
|
|
|
62
69
|
/**
|
|
@@ -122,16 +129,77 @@ function writeOutcome(runId: string, outcome: SetupOutcome): void {
|
|
|
122
129
|
writeFileSync(join(runDir, `${runId}-inline-setup.json`), `${JSON.stringify(outcome, null, 4)}\n`);
|
|
123
130
|
}
|
|
124
131
|
|
|
132
|
+
/**
|
|
133
|
+
* `--fingerprint` mode (task 0862 R5): print the engine's proof-input digest for a task file
|
|
134
|
+
* (plus optional feature file) instead of creating a run, so the inline driver can capture the
|
|
135
|
+
* fresh digest bound `run.artifact` registration checks against.
|
|
136
|
+
*
|
|
137
|
+
* The digest itself comes from the exported app functions — never reimplemented here — and the
|
|
138
|
+
* options mirror `ProofFingerprintActionRunner` (cwd, spec contents, fileSystem; no `--expect`).
|
|
139
|
+
* Returns the process exit code: 0 printed, 1 read/resolve failure.
|
|
140
|
+
*/
|
|
141
|
+
async function printFingerprint(taskFile: string, featureFile: string, spurBin: string): Promise<number> {
|
|
142
|
+
const { entry, repoRoot, chain } = resolveAppEntry(spurBin);
|
|
143
|
+
if (entry === null || repoRoot === null) {
|
|
144
|
+
console.error(
|
|
145
|
+
`inline-run-setup: FAIL — no monorepo checkout of spur is reachable via ${chain}. ` +
|
|
146
|
+
'The proof-input digest must be computed by the app service; a bundle-only install cannot do it.',
|
|
147
|
+
);
|
|
148
|
+
return 1;
|
|
149
|
+
}
|
|
150
|
+
const app = (await import(entry)) as {
|
|
151
|
+
computeProofInputFingerprint: (options: Record<string, unknown>) => Promise<string>;
|
|
152
|
+
readProofInputContents: (
|
|
153
|
+
fileSystem: unknown,
|
|
154
|
+
workdir: string,
|
|
155
|
+
options: { taskFile?: unknown; featureFile?: unknown },
|
|
156
|
+
) => Promise<{ ok: true; taskContent?: string; featureContent?: string } | { ok: false; error: string }>;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const workdir = process.cwd();
|
|
160
|
+
// `undefined` fs takes readProofInputContents' node-filesystem default — the same default its
|
|
161
|
+
// sibling createGitAlternateTree applies, and the same Node FS the CLI's runner injects.
|
|
162
|
+
const inputs = await app.readProofInputContents(undefined, workdir, {
|
|
163
|
+
taskFile,
|
|
164
|
+
...(featureFile.trim() !== '' ? { featureFile } : {}),
|
|
165
|
+
});
|
|
166
|
+
if (!inputs.ok) {
|
|
167
|
+
console.error(`inline-run-setup: FAIL — ${inputs.error}`);
|
|
168
|
+
return 1;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const digest = await app.computeProofInputFingerprint({
|
|
172
|
+
cwd: workdir,
|
|
173
|
+
...(inputs.taskContent !== undefined ? { taskContent: inputs.taskContent } : {}),
|
|
174
|
+
...(inputs.featureContent !== undefined ? { featureContent: inputs.featureContent } : {}),
|
|
175
|
+
});
|
|
176
|
+
process.stdout.write(`${digest}\n`);
|
|
177
|
+
return 0;
|
|
178
|
+
}
|
|
179
|
+
|
|
125
180
|
async function main(): Promise<void> {
|
|
126
181
|
let runId = '';
|
|
127
182
|
let file = '';
|
|
183
|
+
let fingerprint = false;
|
|
184
|
+
let taskFile = '';
|
|
185
|
+
let featureFile = '';
|
|
128
186
|
let spurBin = process.env.SPUR_BIN ?? '';
|
|
129
187
|
const argv = process.argv.slice(2);
|
|
130
188
|
for (let i = 0; i < argv.length; i++) {
|
|
131
189
|
if (argv[i] === '--run-id') runId = argv[++i] ?? '';
|
|
132
190
|
else if (argv[i] === '--file') file = argv[++i] ?? '';
|
|
191
|
+
else if (argv[i] === '--fingerprint') fingerprint = true;
|
|
192
|
+
else if (argv[i] === '--task-file') taskFile = argv[++i] ?? '';
|
|
193
|
+
else if (argv[i] === '--feature-file') featureFile = argv[++i] ?? '';
|
|
133
194
|
else if (argv[i] === '--spur-bin') spurBin = argv[++i] ?? spurBin;
|
|
134
195
|
}
|
|
196
|
+
|
|
197
|
+
// Two mutually exclusive modes share this entry point: create/attach a run (run-id + file), or
|
|
198
|
+
// print the proof digest for the inline driver (task 0862 R5). Mixing them is a usage error.
|
|
199
|
+
if (fingerprint) {
|
|
200
|
+
if (runId !== '' || file !== '' || taskFile.trim() === '') usage();
|
|
201
|
+
process.exit(await printFingerprint(taskFile, featureFile, spurBin));
|
|
202
|
+
}
|
|
135
203
|
if (runId.trim() === '' || file.trim() === '') usage();
|
|
136
204
|
if (!SAFE_RUN_ID_RE.test(runId)) refuseUnsafeRunId(runId);
|
|
137
205
|
|