@gobing-ai/spur 0.3.93 → 0.3.95
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/README.md +42 -0
- package/board/index.d.ts +38 -0
- package/config/plugin-scripts.json +5 -0
- package/config/rules/boundary/test-subpath-boundary.yaml +48 -0
- package/config/rules/strict/runtime-boundaries.yaml +2 -1
- package/config/rules/structure/protected-files.yaml +4 -0
- package/config/workflows/feature-verification.yaml +7 -4
- package/config/workflows/idea-pipeline.yaml +10 -4
- package/config/workflows/task-pipeline.yaml +71 -52
- package/config/workflows/wrapup-pipeline.yaml +92 -17
- package/package.json +15 -11
- package/plugins/sp/README.md +1 -1
- package/plugins/sp/commands/dev-review-session.md +4 -4
- package/plugins/sp/commands/dev-review.md +16 -5
- package/plugins/sp/commands/dev-verify.md +7 -4
- package/plugins/sp/commands/dev-verifyall.md +4 -3
- package/plugins/sp/commands/dev-wrap.md +1 -1
- package/plugins/sp/hooks/context-post-tool.ts +2 -2
- package/plugins/sp/hooks/context-session-start.ts +2 -1
- package/plugins/sp/hooks/context-session-stop.ts +22 -14
- package/plugins/sp/hooks/pi/guard-extension.ts +112 -172
- package/plugins/sp/lib/idea-handoff.generated.mjs +2 -2
- package/plugins/sp/lib/inline-run.generated.d.mts +4 -2
- package/plugins/sp/lib/inline-run.generated.mjs +35 -14
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/scripts/daily-summary/daily-summary.mjs +3 -3
- package/plugins/sp/scripts/daily-summary/daily-summary.ts +3 -3
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +2 -0
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +1 -0
- package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +9 -7
- package/plugins/sp/scripts/dogfood-testing/validate-report.ts +8 -7
- package/plugins/sp/scripts/feature-sync-bounded.mjs +5 -2
- package/plugins/sp/scripts/feature-sync-bounded.ts +3 -1
- package/plugins/sp/scripts/feature-verification-steps.ts +6 -6
- package/plugins/sp/scripts/history-anatomy-cache.mjs +3 -3
- package/plugins/sp/scripts/history-anatomy-cache.ts +4 -4
- package/plugins/sp/scripts/inline-run-setup.mjs +60 -12
- package/plugins/sp/scripts/inline-run-setup.ts +122 -15
- package/plugins/sp/scripts/pr-reviewing.mjs +2 -0
- package/plugins/sp/scripts/pr-reviewing.ts +1 -0
- package/plugins/sp/scripts/quality-gate.mjs +15 -6
- package/plugins/sp/scripts/quality-gate.ts +30 -9
- package/plugins/sp/scripts/residual-scan.mjs +4 -4
- package/plugins/sp/scripts/residual-scan.ts +5 -4
- package/plugins/sp/scripts/script-contract-check.ts +175 -46
- package/plugins/sp/scripts/script-root.mjs +125 -0
- package/plugins/sp/scripts/script-root.ts +195 -0
- package/plugins/sp/scripts/stage-registry-adapter.ts +1 -1
- package/plugins/sp/scripts/task-evidence-precheck.ts +4 -2
- package/plugins/sp/scripts/task-size-precheck.ts +3 -1
- package/plugins/sp/scripts/validate-commands.ts +12 -12
- package/plugins/sp/scripts/validate-flag-contracts.ts +42 -30
- package/plugins/sp/scripts/verify-answer-lint.ts +3 -1
- package/plugins/sp/scripts/workflow-step-profile.mjs +5 -2
- package/plugins/sp/scripts/workflow-step-profile.ts +3 -1
- package/plugins/sp/scripts/wrapup-drift-probe.mjs +5 -2
- package/plugins/sp/scripts/wrapup-drift-probe.ts +4 -2
- package/plugins/sp/scripts/wrapup-steps.mjs +32 -8
- package/plugins/sp/scripts/wrapup-steps.ts +56 -10
- package/plugins/sp/skills/code-verification/SKILL.md +12 -7
- package/plugins/sp/skills/code-verification/references/verdict-schema.md +11 -0
- package/plugins/sp/skills/functional-review/SKILL.md +1 -1
- package/plugins/sp/skills/next-router/references/routing-table.md +1 -1
- package/plugins/sp/skills/session-review/SKILL.md +11 -9
- package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +9 -1
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +8 -2
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +65 -17
- package/plugins/sp/skills/spur-dev/references/flag-glossary.md +16 -3
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +22 -5
- package/plugins/sp/tsconfig.json +8 -0
- package/spur.js +20934 -21087
- package/web/_astro/{BoardApp.CerSBgis.js → BoardApp.BxJuwD7I.js} +151 -152
- package/web/_astro/BoardApp.Cpxntzad.js +1 -0
- package/web/_astro/TaskDetail.C5bW4WGV.js +1 -0
- package/web/_astro/_commonjsHelpers.CqkleIqs.js +1 -0
- package/web/_astro/arc.CyjRvNMY.js +1 -0
- package/web/_astro/architectureDiagram-3BPJPVTR.B4lWRzJA.js +36 -0
- package/web/_astro/{blockDiagram-GPEHLZMM.DXVIiv0p.js → blockDiagram-GPEHLZMM.Be38USQ2.js} +1 -1
- package/web/_astro/board-facade-react-dom-client.B2U5wMvk.js +1 -0
- package/web/_astro/board-facade-react-dom.Cd2MuMhF.js +1 -0
- package/web/_astro/board-facade-react-jsx-dev-runtime.B_L3JzfC.js +1 -0
- package/web/_astro/board-facade-react-jsx-runtime.BY2yfHRi.js +1 -0
- package/web/_astro/board-facade-react-router-dom.Dy5pjzOw.js +1 -0
- package/web/_astro/board-facade-react-router.DjjqU27_.js +1 -0
- package/web/_astro/board-facade-react.Dgf4GRyj.js +1 -0
- package/web/_astro/{c4Diagram-AAUBKEIU.BbF_zCxW.js → c4Diagram-AAUBKEIU.DT9Fj5Qx.js} +1 -1
- package/web/_astro/channel.CI6N_tCg.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.CAgQHpPC.js → chunk-2J33WTMH.SeSBWLg5.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.BN-5tpw4.js → chunk-4BX2VUAB.DoO4VE14.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.CnPkEEr0.js → chunk-55IACEB6.DrmTFA53.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.BQzQeMVm.js → chunk-727SXJPM.Bi-TUb_V.js} +4 -4
- package/web/_astro/{chunk-AQP2D5EJ.B6xNyDnL.js → chunk-AQP2D5EJ.B-xp8Uhi.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.C7f9Ih78.js → chunk-FMBD7UC4.DGWrDnUU.js} +1 -1
- package/web/_astro/chunk-JMJ3UQ3L.BQJEJHu4.js +34 -0
- package/web/_astro/{chunk-ND2GUHAM.CNV1dFXT.js → chunk-ND2GUHAM._BagvPDy.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.Cudn2TkJ.js → chunk-QZHKN3VN.VOboYWQ0.js} +1 -1
- package/web/_astro/chunk-YNUBSHFH.CxeAygfi.js +7 -0
- package/web/_astro/{classDiagram-4FO5ZUOK.D1NwP50q.js → classDiagram-4FO5ZUOK.D_apfHS7.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.D1NwP50q.js → classDiagram-v2-Q7XG4LA2.D_apfHS7.js} +1 -1
- package/web/_astro/client.BhluLQqe.js +1 -0
- package/web/_astro/client.DBpSRLnx.js +9 -0
- package/web/_astro/cose-bilkent-S5V4N54A.DNLU_L-x.js +1 -0
- package/web/_astro/{cynefin-OW5HDTMX.BmK52w8G.js → cynefin-OW5HDTMX.D8borhV-.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.Bfy5CTDT.js → dagre-BM42HDAG.Uc0lvjcV.js} +2 -2
- package/web/_astro/diagram-2AECGRRQ.DDIxtDBo.js +43 -0
- package/web/_astro/diagram-5GNKFQAL.DVoDnD3H.js +10 -0
- package/web/_astro/{diagram-KO2AKTUF.CW_vMJ4z.js → diagram-KO2AKTUF.CRubTJ-0.js} +3 -3
- package/web/_astro/{diagram-LMA3HP47.B_8ZGF67.js → diagram-LMA3HP47.m9SeYcXQ.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.BppnHsdS.js → diagram-OG6HWLK6.C6s2ZjV5.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.BEuHXcjJ.js → erDiagram-TEJ5UH35.nSzr5KTj.js} +5 -5
- package/web/_astro/{flowDiagram-I6XJVG4X.CH-UlnGr.js → flowDiagram-I6XJVG4X.DrZcys16.js} +4 -4
- package/web/_astro/ganttDiagram-6RSMTGT7.Do4M1b-K.js +292 -0
- package/web/_astro/{gitGraphDiagram-PVQCEYII.XnPxPPZN.js → gitGraphDiagram-PVQCEYII.BWc9oJ4l.js} +1 -1
- package/web/_astro/index.5dn-Zm-2.js +1 -0
- package/web/_astro/index.C-IqPpW2.js +1 -0
- package/web/_astro/index.CENnIEqT.css +1 -0
- package/web/_astro/index.Cyc_GA4f.js +1 -0
- package/web/_astro/index.tZfqwSut.js +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.JyjYRu_T.js → infoDiagram-5YYISTIA.BIdOctFk.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.BBRBF-Fo.js → ishikawaDiagram-YF4QCWOH.CV5SuG2t.js} +5 -5
- package/web/_astro/{journeyDiagram-JHISSGLW.C_iymSyp.js → journeyDiagram-JHISSGLW.CxE-rEJ-.js} +1 -1
- package/web/_astro/jsx-runtime.DWOiEOII.js +1 -0
- package/web/_astro/{kanban-definition-UN3LZRKU.DdfW-Oqt.js → kanban-definition-UN3LZRKU.BPRdKWs_.js} +7 -7
- package/web/_astro/{linear.C2_IkbZT.js → linear.zRsuuDTE.js} +1 -1
- package/web/_astro/mermaid.core.jAJTcMKc.js +301 -0
- package/web/_astro/{mindmap-definition-RKZ34NQL.DAZIxQSK.js → mindmap-definition-RKZ34NQL.BffJxfCr.js} +2 -2
- package/web/_astro/{pieDiagram-4H26LBE5.CN8sIhKM.js → pieDiagram-4H26LBE5.CacwWaE3.js} +3 -3
- package/web/_astro/{quadrantDiagram-W4KKPZXB.3dGcX5GP.js → quadrantDiagram-W4KKPZXB.hXELD06-.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.BV2y4dd6.js → requirementDiagram-4Y6WPE33.DdEbPcqT.js} +3 -3
- package/web/_astro/{sankeyDiagram-5OEKKPKP.Cqo15Tvo.js → sankeyDiagram-5OEKKPKP.DgaDocKv.js} +4 -4
- package/web/_astro/{sequenceDiagram-3UESZ5HK.CROCPMJB.js → sequenceDiagram-3UESZ5HK.B0KScnAu.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.RfXZrkFE.js → stateDiagram-AJRCARHV.CV9M_WNc.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.CPXmbBs9.js → stateDiagram-v2-BHNVJYJU.BOAo74Et.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.DdgKTiO8.js → timeline-definition-PNZ67QCA.CqqepUMe.js} +3 -3
- package/web/_astro/{vennDiagram-CIIHVFJN.CPNVSHF1.js → vennDiagram-CIIHVFJN.DDqe4dOG.js} +5 -5
- package/web/_astro/{wardleyDiagram-YWT4CUSO.CQhA0Jyr.js → wardleyDiagram-YWT4CUSO.B3R9OPdO.js} +3 -3
- package/web/_astro/{xychartDiagram-2RQKCTM6.n61BWyy4.js → xychartDiagram-2RQKCTM6.B3SvgXpQ.js} +1 -1
- package/web/board-runtime.json +69 -0
- package/web/index.html +2 -2
- package/web/_astro/BoardApp.eoTz0pZs.js +0 -1
- package/web/_astro/TaskDetail.DCqiC-OZ.js +0 -1
- package/web/_astro/arc.CPwg6Rw0.js +0 -1
- package/web/_astro/architectureDiagram-3BPJPVTR.DM_vp_hO.js +0 -36
- package/web/_astro/channel.MYZLKNwy.js +0 -1
- package/web/_astro/client.yhYJvxCU.js +0 -9
- package/web/_astro/cose-bilkent-S5V4N54A.B1wSL-Xb.js +0 -1
- package/web/_astro/diagram-2AECGRRQ.DhNnvUvX.js +0 -43
- package/web/_astro/diagram-5GNKFQAL.lTX5KwnS.js +0 -10
- package/web/_astro/ganttDiagram-6RSMTGT7.BO81S85v.js +0 -292
- package/web/_astro/index.De90oHcH.js +0 -1
- package/web/_astro/index.Hjbr15fG.css +0 -1
- package/web/_astro/mermaid.core.GAOYeSR0.js +0 -303
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* (task 0600, feature I, ADR-065).
|
|
5
5
|
*
|
|
6
6
|
* Enforces superskill's standard plugin script contract across plugins/sp:
|
|
7
|
-
* 1. A 'standard' entry must have a valid .mjs twin
|
|
7
|
+
* 1. A 'standard' entry must have a valid .mjs twin byte-identical to a fresh convert of its .ts source.
|
|
8
8
|
* 2. A .mjs twin on disk must be registered under a 'standard' entry (never 'repo-only' or unlisted).
|
|
9
9
|
* 3. Every script file under plugins/sp/scripts/ must have a manifest entry (two-sided).
|
|
10
10
|
* 4. No shipped surface (commands/, skills/, agents/, README.md) may reference 'bun plugins/sp/scripts/'.
|
|
@@ -18,8 +18,10 @@
|
|
|
18
18
|
* Exit code: 0 on success, 1 on any violation.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
21
|
+
import { spawnSync } from 'node:child_process';
|
|
22
|
+
import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync } from 'node:fs';
|
|
23
|
+
import { tmpdir } from 'node:os';
|
|
24
|
+
import { dirname, join, resolve } from 'node:path';
|
|
23
25
|
|
|
24
26
|
export type ScriptContractType = 'standard' | 'repo-only';
|
|
25
27
|
|
|
@@ -42,6 +44,8 @@ export interface Violation {
|
|
|
42
44
|
| 'unexpected_twin'
|
|
43
45
|
| 'unregistered_script'
|
|
44
46
|
| 'forbidden_invocation'
|
|
47
|
+
| 'converter_unavailable'
|
|
48
|
+
| 'gobing_ai_import'
|
|
45
49
|
| 'incomplete';
|
|
46
50
|
target: string;
|
|
47
51
|
message: string;
|
|
@@ -116,12 +120,24 @@ export function listDiskScripts(scriptsDir: string): { tsFiles: string[]; mjsFil
|
|
|
116
120
|
|
|
117
121
|
export function scanShippedSurfaces(pluginDir: string): Array<{ file: string; line: number; content: string }> {
|
|
118
122
|
const matches: Array<{ file: string; line: number; content: string }> = [];
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
+
const guardMarker = 'config/plugin-scripts.json';
|
|
124
|
+
|
|
125
|
+
// 0960 R5: the probe shapes the guard idiom introduces, plus the legacy literal. A hit is
|
|
126
|
+
// legal only when its line (or, for a YAML command block, its folded block) names the
|
|
127
|
+
// source-repo marker — otherwise it is a project-first probe that shadows the installed
|
|
128
|
+
// twin in a consumer.
|
|
129
|
+
const projectFirstPatterns = [
|
|
130
|
+
/\[ -f "?plugins\/sp\/scripts\//,
|
|
131
|
+
/\b[A-Z_]*S(?:CRIPT)?="?plugins\/sp\/scripts\//,
|
|
132
|
+
/\bbun plugins\/sp\/scripts\//,
|
|
133
|
+
];
|
|
134
|
+
const isHit = (line: string): boolean => projectFirstPatterns.some((pattern) => pattern.test(line));
|
|
135
|
+
|
|
136
|
+
function push(file: string, line: number, content: string): void {
|
|
137
|
+
matches.push({ file, line, content: content.trim() });
|
|
138
|
+
}
|
|
123
139
|
|
|
124
|
-
function
|
|
140
|
+
function scanLines(filePath: string): void {
|
|
125
141
|
if (!existsSync(filePath)) return;
|
|
126
142
|
let text: string;
|
|
127
143
|
try {
|
|
@@ -132,13 +148,72 @@ export function scanShippedSurfaces(pluginDir: string): Array<{ file: string; li
|
|
|
132
148
|
const lines = text.split('\n');
|
|
133
149
|
for (let i = 0; i < lines.length; i++) {
|
|
134
150
|
const line = lines[i] ?? '';
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
151
|
+
const trimmed = line.trim();
|
|
152
|
+
if (trimmed.startsWith('#')) continue; // comment lines are not executable probes
|
|
153
|
+
if (!isHit(line)) continue;
|
|
154
|
+
if (line.includes(guardMarker)) continue; // same-line source-repo guard
|
|
155
|
+
push(filePath, i + 1, trimmed);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 0960 R5: the repo-root workflow definitions also ship, and their shell lives in folded
|
|
161
|
+
* `command:` blocks. A guard on one physical line legalizes the invocation on the next, so
|
|
162
|
+
* the block — not the line — is the unit of judgment. Markdown prose references (backticked
|
|
163
|
+
* paths with no `[ -f` / assignment / `bun ` prefix) never match the patterns, so they stay
|
|
164
|
+
* legal.
|
|
165
|
+
*/
|
|
166
|
+
function scanYamlCommands(filePath: string): void {
|
|
167
|
+
if (!existsSync(filePath)) return;
|
|
168
|
+
let text: string;
|
|
169
|
+
try {
|
|
170
|
+
text = readFileSync(filePath, 'utf8');
|
|
171
|
+
} catch {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const lines = text.split('\n');
|
|
175
|
+
let i = 0;
|
|
176
|
+
while (i < lines.length) {
|
|
177
|
+
const line = lines[i] ?? '';
|
|
178
|
+
const blockKey = /^(\s*)command:\s*(?:>[+-]?|\|[+-]?)?\s*$/.exec(line);
|
|
179
|
+
const inlineKey = /^(\s*)command:\s+(.+)$/.exec(line);
|
|
180
|
+
if (blockKey) {
|
|
181
|
+
const indent = (blockKey[1] ?? '').length;
|
|
182
|
+
const block: Array<{ no: number; text: string }> = [];
|
|
183
|
+
let j = i + 1;
|
|
184
|
+
while (j < lines.length) {
|
|
185
|
+
const next = lines[j] ?? '';
|
|
186
|
+
if (next.trim() === '') {
|
|
187
|
+
block.push({ no: j + 1, text: next });
|
|
188
|
+
j++;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const nextIndent = (next.match(/^\s*/)?.[0] ?? '').length;
|
|
192
|
+
if (nextIndent <= indent) break;
|
|
193
|
+
block.push({ no: j + 1, text: next });
|
|
194
|
+
j++;
|
|
195
|
+
}
|
|
196
|
+
if (!block.some((b) => b.text.includes(guardMarker))) {
|
|
197
|
+
for (const b of block) {
|
|
198
|
+
const trimmed = b.text.trim();
|
|
199
|
+
if (trimmed.startsWith('#')) continue;
|
|
200
|
+
if (!isHit(b.text)) continue;
|
|
201
|
+
if (b.text.includes(guardMarker)) continue;
|
|
202
|
+
push(filePath, b.no, trimmed);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
i = j;
|
|
206
|
+
continue;
|
|
141
207
|
}
|
|
208
|
+
if (inlineKey) {
|
|
209
|
+
const value = inlineKey[2] ?? '';
|
|
210
|
+
if (!line.trim().startsWith('#') && isHit(value) && !value.includes(guardMarker)) {
|
|
211
|
+
push(filePath, i + 1, line.trim());
|
|
212
|
+
}
|
|
213
|
+
i++;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
i++;
|
|
142
217
|
}
|
|
143
218
|
}
|
|
144
219
|
|
|
@@ -163,16 +238,27 @@ export function scanShippedSurfaces(pluginDir: string): Array<{ file: string; li
|
|
|
163
238
|
st.isFile() &&
|
|
164
239
|
(entry.endsWith('.md') || entry.endsWith('.json') || entry.endsWith('.yaml') || entry.endsWith('.yml'))
|
|
165
240
|
) {
|
|
166
|
-
|
|
241
|
+
scanLines(fullPath);
|
|
167
242
|
}
|
|
168
243
|
}
|
|
169
244
|
}
|
|
170
245
|
|
|
171
|
-
for (const d of
|
|
246
|
+
for (const d of ['commands', 'skills', 'agents']) {
|
|
172
247
|
walk(join(pluginDir, d));
|
|
173
248
|
}
|
|
174
|
-
|
|
175
|
-
|
|
249
|
+
scanLines(join(pluginDir, 'README.md'));
|
|
250
|
+
|
|
251
|
+
// 0960 R5: the workflow tree is repo-only (resolved from pluginDir/../..); a consumer's own
|
|
252
|
+
// workflow definitions are not this rule's subject.
|
|
253
|
+
const workflowsDir = resolve(pluginDir, '..', '..', 'config', 'workflows');
|
|
254
|
+
let workflowFiles: string[] = [];
|
|
255
|
+
try {
|
|
256
|
+
workflowFiles = readdirSync(workflowsDir).filter((name) => name.endsWith('.yaml') || name.endsWith('.yml'));
|
|
257
|
+
} catch {
|
|
258
|
+
workflowFiles = [];
|
|
259
|
+
}
|
|
260
|
+
for (const name of workflowFiles.sort()) {
|
|
261
|
+
scanYamlCommands(join(workflowsDir, name));
|
|
176
262
|
}
|
|
177
263
|
|
|
178
264
|
return matches;
|
|
@@ -206,8 +292,9 @@ function findGobingAiValueImports(dir: string): { file: string; line: number }[]
|
|
|
206
292
|
const lines = readFileSync(full, 'utf8').split('\n');
|
|
207
293
|
let stmtStart = 0;
|
|
208
294
|
for (let i = 0; i < lines.length; i++) {
|
|
209
|
-
|
|
210
|
-
if (
|
|
295
|
+
const line = lines[i] ?? '';
|
|
296
|
+
if (/^\s*(?:import|export)\b/.test(line)) stmtStart = i;
|
|
297
|
+
if (/(?:from\s*|import\s*|import\s*\(\s*)['"]@gobing-ai\//.test(line)) {
|
|
211
298
|
const opener = lines.slice(stmtStart, i + 1).join(' ');
|
|
212
299
|
if (!/^\s*(?:import|export)\s+type\b/.test(opener)) {
|
|
213
300
|
hits.push({ file: full, line: i + 1 });
|
|
@@ -219,7 +306,35 @@ function findGobingAiValueImports(dir: string): { file: string; line: number }[]
|
|
|
219
306
|
return hits;
|
|
220
307
|
}
|
|
221
308
|
|
|
222
|
-
|
|
309
|
+
/** Regenerates a standard script's twin into `outPath`; false when conversion could not run. */
|
|
310
|
+
export type ConvertTwin = (rel: string, outPath: string) => boolean;
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* The real converter: `superskill script convert sp <rel> --out <outPath>` run from the repo
|
|
314
|
+
* root, because `convert` resolves `plugins/<plugin>/scripts/<rel>` against the process cwd.
|
|
315
|
+
* Returns false — never throws — on spawn error, non-zero exit, or a missing output file, and
|
|
316
|
+
* records the command plus stderr in `lastError` for the `converter_unavailable` violation.
|
|
317
|
+
*/
|
|
318
|
+
export function spawnConvertTwin(repoRoot: string, bin = 'superskill'): ConvertTwin & { lastError?: string } {
|
|
319
|
+
const convert: ConvertTwin & { lastError?: string } = (rel, outPath) => {
|
|
320
|
+
const args = ['script', 'convert', 'sp', rel, '--out', outPath];
|
|
321
|
+
const res = spawnSync(bin, args, { cwd: repoRoot, encoding: 'utf-8' });
|
|
322
|
+
if (res.error || res.status !== 0 || !existsSync(outPath)) {
|
|
323
|
+
const detail = (res.stderr ?? '').trim() || res.error?.message || `exit ${res.status}`;
|
|
324
|
+
convert.lastError = `${bin} ${args.join(' ')}: ${detail}`;
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
return true;
|
|
328
|
+
};
|
|
329
|
+
return convert;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export function validateContract(
|
|
333
|
+
manifest: ScriptManifest,
|
|
334
|
+
scriptsDir: string,
|
|
335
|
+
pluginDir: string,
|
|
336
|
+
opts: { convertTwin?: ConvertTwin } = {},
|
|
337
|
+
): Violation[] {
|
|
223
338
|
const violations: Violation[] = [];
|
|
224
339
|
const { tsFiles, mjsFiles } = listDiskScripts(scriptsDir);
|
|
225
340
|
|
|
@@ -236,37 +351,51 @@ export function validateContract(manifest: ScriptManifest, scriptsDir: string, p
|
|
|
236
351
|
manifestMap.set(entry.rel, entry);
|
|
237
352
|
}
|
|
238
353
|
|
|
239
|
-
// Rule 1: standard entries must have
|
|
240
|
-
// mtime is
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
kind: 'missing_twin',
|
|
255
|
-
target: entry.rel,
|
|
256
|
-
message: `standard script ${entry.rel} is missing its .mjs twin (${expectedTwinRel})`,
|
|
257
|
-
});
|
|
258
|
-
} else if (existsSync(tsPath)) {
|
|
259
|
-
const tsStat = statSync(tsPath);
|
|
260
|
-
const twinStat = statSync(twinPath);
|
|
261
|
-
if (twinStat.mtimeMs < tsStat.mtimeMs - STALE_TWIN_TOLERANCE_MS) {
|
|
354
|
+
// Rule 1: standard entries must have a .mjs twin byte-identical to a fresh convert.
|
|
355
|
+
// mtime is never consulted: git does not store mtimes, so a committed stale twin looks
|
|
356
|
+
// freshly checked-out in every clone and worktree. Only regenerating the twin proves it
|
|
357
|
+
// matches its source (task 0970; replaces the 0606 sub-second mtime tolerance).
|
|
358
|
+
const twinTmpDir = mkdtempSync(join(tmpdir(), 'script-twin-'));
|
|
359
|
+
try {
|
|
360
|
+
const convertTwin = opts.convertTwin ?? spawnConvertTwin(resolve(pluginDir, '..', '..'));
|
|
361
|
+
let converterDown = false;
|
|
362
|
+
for (const entry of manifest.entries) {
|
|
363
|
+
if (entry.rel && entry.contract === 'standard') {
|
|
364
|
+
const expectedTwinRel = entry.twin ?? entry.rel.replace(/\.ts$/, '.mjs');
|
|
365
|
+
const twinPath = join(scriptsDir, expectedTwinRel);
|
|
366
|
+
const tsPath = join(scriptsDir, entry.rel);
|
|
367
|
+
|
|
368
|
+
if (!existsSync(twinPath)) {
|
|
262
369
|
violations.push({
|
|
263
|
-
kind: '
|
|
370
|
+
kind: 'missing_twin',
|
|
264
371
|
target: entry.rel,
|
|
265
|
-
message: `standard script
|
|
372
|
+
message: `standard script ${entry.rel} is missing its .mjs twin (${expectedTwinRel})`,
|
|
266
373
|
});
|
|
374
|
+
} else if (existsSync(tsPath) && !converterDown) {
|
|
375
|
+
const outPath = join(twinTmpDir, expectedTwinRel);
|
|
376
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
377
|
+
if (!convertTwin(entry.rel, outPath)) {
|
|
378
|
+
// One violation, then stop: a broken converter is one environment failure,
|
|
379
|
+
// not 17 stale twins. It never silently passes (task 0970 R3).
|
|
380
|
+
const detail = (convertTwin as { lastError?: string }).lastError;
|
|
381
|
+
violations.push({
|
|
382
|
+
kind: 'converter_unavailable',
|
|
383
|
+
target: entry.rel,
|
|
384
|
+
message: `could not regenerate ${expectedTwinRel} from ${entry.rel}${detail ? ` — ${detail}` : ''}; twin content cannot be verified`,
|
|
385
|
+
});
|
|
386
|
+
converterDown = true;
|
|
387
|
+
} else if (!readFileSync(outPath).equals(readFileSync(twinPath))) {
|
|
388
|
+
violations.push({
|
|
389
|
+
kind: 'stale_twin',
|
|
390
|
+
target: entry.rel,
|
|
391
|
+
message: `standard script .mjs twin ${expectedTwinRel} does not match a fresh convert of ${entry.rel} — run bun run build:scripts`,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
267
394
|
}
|
|
268
395
|
}
|
|
269
396
|
}
|
|
397
|
+
} finally {
|
|
398
|
+
rmSync(twinTmpDir, { recursive: true, force: true });
|
|
270
399
|
}
|
|
271
400
|
|
|
272
401
|
// Rule 2: committed .mjs files must belong to a 'standard' entry
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// plugins/sp/scripts/script-root.ts
|
|
5
|
+
import { spawnSync } from "child_process";
|
|
6
|
+
import { createHash } from "crypto";
|
|
7
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
8
|
+
import { dirname, join } from "path";
|
|
9
|
+
|
|
10
|
+
// plugins/sp/lib/env.ts
|
|
11
|
+
function getEnvVars() {
|
|
12
|
+
return process.env;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// plugins/sp/scripts/script-root.ts
|
|
16
|
+
var SOURCE_REPO_MARKER = join("config", "plugin-scripts.json");
|
|
17
|
+
var PROJECT_SCRIPTS_DIR = join("plugins", "sp", "scripts");
|
|
18
|
+
var SCRIPT_ROOT_TWIN_REL = "script-root.mjs";
|
|
19
|
+
function defaultResolveScript(cwd) {
|
|
20
|
+
return (rel) => {
|
|
21
|
+
const run = spawnSync("superskill", ["script", "path", "sp", rel, "--json"], {
|
|
22
|
+
cwd,
|
|
23
|
+
encoding: "utf8",
|
|
24
|
+
env: getEnvVars()
|
|
25
|
+
});
|
|
26
|
+
if (run.status !== 0 || !run.stdout)
|
|
27
|
+
return null;
|
|
28
|
+
const lastLine = run.stdout.trim().split(`
|
|
29
|
+
`).pop() ?? "";
|
|
30
|
+
try {
|
|
31
|
+
const parsed = JSON.parse(lastLine);
|
|
32
|
+
return typeof parsed.path === "string" && parsed.path.length > 0 ? parsed : null;
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function digestScriptSet(dir, cwd) {
|
|
39
|
+
const abs = cwd ? join(cwd, dir) : dir;
|
|
40
|
+
const names = readdirSync(abs).filter((name) => {
|
|
41
|
+
try {
|
|
42
|
+
return statSync(join(abs, name)).isFile();
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}).sort();
|
|
47
|
+
const list = names.map((name) => `${name}:${createHash("sha256").update(readFileSync(join(abs, name))).digest("hex")}`);
|
|
48
|
+
return `sha256:${createHash("sha256").update(list.join(`
|
|
49
|
+
`)).digest("hex")}`;
|
|
50
|
+
}
|
|
51
|
+
function runScriptRoot(env, options = {}) {
|
|
52
|
+
const cwd = options.cwd;
|
|
53
|
+
const runId = env.__runId ?? env.runId ?? "";
|
|
54
|
+
const relResult = join(".spur", "run", `${runId}-script-root.json`);
|
|
55
|
+
const abs = (p) => cwd ? join(cwd, p) : p;
|
|
56
|
+
const warn = options.warn ?? ((message) => void process.stderr.write(`${message}
|
|
57
|
+
`));
|
|
58
|
+
mkdirSync(abs(join(".spur", "run")), { recursive: true });
|
|
59
|
+
const write = (row) => {
|
|
60
|
+
writeFileSync(abs(relResult), `${JSON.stringify(row)}
|
|
61
|
+
`);
|
|
62
|
+
return { ...row, resultFile: relResult, exitCode: 0 };
|
|
63
|
+
};
|
|
64
|
+
if (existsSync(abs(SOURCE_REPO_MARKER))) {
|
|
65
|
+
try {
|
|
66
|
+
return write({
|
|
67
|
+
mode: "source-repo",
|
|
68
|
+
source: "project",
|
|
69
|
+
dir: PROJECT_SCRIPTS_DIR,
|
|
70
|
+
scriptSetDigest: digestScriptSet(PROJECT_SCRIPTS_DIR, cwd)
|
|
71
|
+
});
|
|
72
|
+
} catch (error) {
|
|
73
|
+
return write({ mode: "unresolved", error: `source-repo digest failed: ${String(error)}` });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (existsSync(abs(PROJECT_SCRIPTS_DIR))) {
|
|
77
|
+
warn(`script-root: ignoring vendored ${PROJECT_SCRIPTS_DIR}/ (no ${SOURCE_REPO_MARKER} marker) \u2014 ` + `steps resolve the installed twin via 'superskill script path sp <rel>'`);
|
|
78
|
+
}
|
|
79
|
+
const resolve = options.resolveScript ?? defaultResolveScript(cwd);
|
|
80
|
+
const resolved = resolve(SCRIPT_ROOT_TWIN_REL);
|
|
81
|
+
if (!resolved?.path) {
|
|
82
|
+
return write({
|
|
83
|
+
mode: "unresolved",
|
|
84
|
+
error: `'superskill script path sp ${SCRIPT_ROOT_TWIN_REL}' resolved nothing`
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const dir = dirname(resolved.path);
|
|
88
|
+
try {
|
|
89
|
+
return write({
|
|
90
|
+
mode: "installed",
|
|
91
|
+
source: resolved.source ?? "unknown",
|
|
92
|
+
dir,
|
|
93
|
+
scriptSetDigest: digestScriptSet(dir)
|
|
94
|
+
});
|
|
95
|
+
} catch (error) {
|
|
96
|
+
return write({ mode: "unresolved", error: `installed digest failed: ${String(error)}` });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
var SCRIPT_ROOT_USAGE = "usage: script-root --run-id <id>";
|
|
100
|
+
function main(argv, env = getEnvVars(), options = {}) {
|
|
101
|
+
let runId = env.__runId ?? "";
|
|
102
|
+
for (let i = 0;i < argv.length; i++) {
|
|
103
|
+
if (argv[i] === "--run-id") {
|
|
104
|
+
runId = argv[i + 1] ?? "";
|
|
105
|
+
i++;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
process.stderr.write(`${SCRIPT_ROOT_USAGE}
|
|
109
|
+
`);
|
|
110
|
+
return 2;
|
|
111
|
+
}
|
|
112
|
+
return runScriptRoot({ ...env, __runId: runId }, options).exitCode;
|
|
113
|
+
}
|
|
114
|
+
{
|
|
115
|
+
process.exit(main(process.argv.slice(2)));
|
|
116
|
+
}
|
|
117
|
+
export {
|
|
118
|
+
runScriptRoot,
|
|
119
|
+
main,
|
|
120
|
+
digestScriptSet,
|
|
121
|
+
SOURCE_REPO_MARKER,
|
|
122
|
+
SCRIPT_ROOT_USAGE,
|
|
123
|
+
SCRIPT_ROOT_TWIN_REL,
|
|
124
|
+
PROJECT_SCRIPTS_DIR
|
|
125
|
+
};
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* script-root — run-start script-resolution identity for the sp pipelines (task 0960 R2).
|
|
4
|
+
*
|
|
5
|
+
* WHY: the pipeline YAML ships to every consumer, but its script probes resolved
|
|
6
|
+
* `plugins/sp/scripts/` **project-first**. A consumer that hand-vendored a stale copy then ran
|
|
7
|
+
* a different revision than the installed twin, mixing revisions across steps that write and
|
|
8
|
+
* read the same cross-step artifacts (status files, verdicts, proof digests). The probe is
|
|
9
|
+
* dogfooding-only, so it is now gated on `config/plugin-scripts.json` — a marker that lives
|
|
10
|
+
* outside `plugins/sp/`, so copying the plugin tree cannot reproduce it.
|
|
11
|
+
*
|
|
12
|
+
* This script records *which* tree a run resolved, once, before any step executes:
|
|
13
|
+
* `.spur/run/<runId>-script-root.json` `{ mode, source, dir, scriptSetDigest }`
|
|
14
|
+
*
|
|
15
|
+
* - `source-repo` — the marker is present; the project's live `plugins/sp/scripts/` is the
|
|
16
|
+
* resolved tree (the dogfood path).
|
|
17
|
+
* - `installed` — no marker; the tree is `dirname(superskill script path sp script-root.mjs
|
|
18
|
+
* --json .path)`, with `source` from the same probe. One stderr warning names a dead
|
|
19
|
+
* vendored `plugins/sp/scripts/` dir that is no longer executed.
|
|
20
|
+
* - `unresolved` — nothing resolved. Written with an `error` and **exit 0**: a missing
|
|
21
|
+
* optional script must stay a per-step fail-closed outcome, never a run-wide abort (R3).
|
|
22
|
+
*
|
|
23
|
+
* `scriptSetDigest` is one sha256 over the sorted `name:sha256` list of the resolved dir's
|
|
24
|
+
* regular files (non-recursive) — the installed staging has no version file, so this answers
|
|
25
|
+
* "which revision produced this run?" without a Superskill change.
|
|
26
|
+
*
|
|
27
|
+
* Node/bun builtins and the vendored `../lib/env` gateway only — bundled standalone by
|
|
28
|
+
* `superskill install`, so no `@gobing-ai/*` value imports (task 0669).
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { spawnSync } from 'node:child_process';
|
|
32
|
+
import { createHash } from 'node:crypto';
|
|
33
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
34
|
+
import { dirname, join } from 'node:path';
|
|
35
|
+
import { getEnvVars } from '../lib/env';
|
|
36
|
+
|
|
37
|
+
export type ScriptRootMode = 'source-repo' | 'installed' | 'unresolved';
|
|
38
|
+
|
|
39
|
+
export interface ScriptRoot {
|
|
40
|
+
mode: ScriptRootMode;
|
|
41
|
+
source?: string;
|
|
42
|
+
dir?: string;
|
|
43
|
+
scriptSetDigest?: string;
|
|
44
|
+
error?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ScriptRootResult extends ScriptRoot {
|
|
48
|
+
resultFile: string;
|
|
49
|
+
exitCode: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ScriptRootEnv {
|
|
53
|
+
__runId?: string;
|
|
54
|
+
[key: string]: string | undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Resolved script descriptor from `superskill script path sp <rel> --json`. */
|
|
58
|
+
export interface ResolvedScript {
|
|
59
|
+
path?: string;
|
|
60
|
+
source?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface ScriptRootOptions {
|
|
64
|
+
/** Base directory for `.spur/run` and the project marker; defaults to process cwd. */
|
|
65
|
+
cwd?: string;
|
|
66
|
+
/** Test seam: run `superskill script path sp script-root.mjs --json`. */
|
|
67
|
+
resolveScript?: (rel: string) => ResolvedScript | null;
|
|
68
|
+
/** Test seam: the one-warning sink. */
|
|
69
|
+
warn?: (message: string) => void;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Marker that identifies the Spur source repo (owned by script-contract-check, outside plugins/sp). */
|
|
73
|
+
export const SOURCE_REPO_MARKER = join('config', 'plugin-scripts.json');
|
|
74
|
+
/** The project-first tree the dogfood probes execute. */
|
|
75
|
+
export const PROJECT_SCRIPTS_DIR = join('plugins', 'sp', 'scripts');
|
|
76
|
+
/** The rel the installed twin resolves under (its own staging name). */
|
|
77
|
+
export const SCRIPT_ROOT_TWIN_REL = 'script-root.mjs';
|
|
78
|
+
|
|
79
|
+
function defaultResolveScript(cwd?: string): (rel: string) => ResolvedScript | null {
|
|
80
|
+
return (rel: string): ResolvedScript | null => {
|
|
81
|
+
const run = spawnSync('superskill', ['script', 'path', 'sp', rel, '--json'], {
|
|
82
|
+
cwd,
|
|
83
|
+
encoding: 'utf8',
|
|
84
|
+
env: getEnvVars(),
|
|
85
|
+
});
|
|
86
|
+
if (run.status !== 0 || !run.stdout) return null;
|
|
87
|
+
const lastLine = run.stdout.trim().split('\n').pop() ?? '';
|
|
88
|
+
try {
|
|
89
|
+
const parsed = JSON.parse(lastLine) as ResolvedScript;
|
|
90
|
+
return typeof parsed.path === 'string' && parsed.path.length > 0 ? parsed : null;
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** sha256 over the sorted `name:sha256` list of `dir`'s regular files (non-recursive). */
|
|
98
|
+
export function digestScriptSet(dir: string, cwd?: string): string {
|
|
99
|
+
const abs = cwd ? join(cwd, dir) : dir;
|
|
100
|
+
const names = readdirSync(abs)
|
|
101
|
+
.filter((name) => {
|
|
102
|
+
try {
|
|
103
|
+
return statSync(join(abs, name)).isFile();
|
|
104
|
+
} catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
.sort();
|
|
109
|
+
const list = names.map(
|
|
110
|
+
(name) =>
|
|
111
|
+
`${name}:${createHash('sha256')
|
|
112
|
+
.update(readFileSync(join(abs, name)))
|
|
113
|
+
.digest('hex')}`,
|
|
114
|
+
);
|
|
115
|
+
return `sha256:${createHash('sha256').update(list.join('\n')).digest('hex')}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Resolve the run's script root and write `.spur/run/<runId>-script-root.json`.
|
|
120
|
+
* Never aborts: every failure is recorded as `mode: "unresolved"` with exit 0 (R3).
|
|
121
|
+
*/
|
|
122
|
+
export function runScriptRoot(env: ScriptRootEnv, options: ScriptRootOptions = {}): ScriptRootResult {
|
|
123
|
+
const cwd = options.cwd;
|
|
124
|
+
const runId = env.__runId ?? env.runId ?? '';
|
|
125
|
+
const relResult = join('.spur', 'run', `${runId}-script-root.json`);
|
|
126
|
+
const abs = (p: string): string => (cwd ? join(cwd, p) : p);
|
|
127
|
+
const warn = options.warn ?? ((message: string): void => void process.stderr.write(`${message}\n`));
|
|
128
|
+
mkdirSync(abs(join('.spur', 'run')), { recursive: true });
|
|
129
|
+
|
|
130
|
+
const write = (row: ScriptRoot): ScriptRootResult => {
|
|
131
|
+
writeFileSync(abs(relResult), `${JSON.stringify(row)}\n`);
|
|
132
|
+
return { ...row, resultFile: relResult, exitCode: 0 };
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// Source repo: the project's live tree is the resolved tree (R6).
|
|
136
|
+
if (existsSync(abs(SOURCE_REPO_MARKER))) {
|
|
137
|
+
try {
|
|
138
|
+
return write({
|
|
139
|
+
mode: 'source-repo',
|
|
140
|
+
source: 'project',
|
|
141
|
+
dir: PROJECT_SCRIPTS_DIR,
|
|
142
|
+
scriptSetDigest: digestScriptSet(PROJECT_SCRIPTS_DIR, cwd),
|
|
143
|
+
});
|
|
144
|
+
} catch (error) {
|
|
145
|
+
return write({ mode: 'unresolved', error: `source-repo digest failed: ${String(error)}` });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Installed: resolve the twin, and name a dead vendored dir that is no longer executed.
|
|
150
|
+
if (existsSync(abs(PROJECT_SCRIPTS_DIR))) {
|
|
151
|
+
warn(
|
|
152
|
+
`script-root: ignoring vendored ${PROJECT_SCRIPTS_DIR}/ (no ${SOURCE_REPO_MARKER} marker) — ` +
|
|
153
|
+
`steps resolve the installed twin via 'superskill script path sp <rel>'`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
const resolve = options.resolveScript ?? defaultResolveScript(cwd);
|
|
157
|
+
const resolved = resolve(SCRIPT_ROOT_TWIN_REL);
|
|
158
|
+
if (!resolved?.path) {
|
|
159
|
+
return write({
|
|
160
|
+
mode: 'unresolved',
|
|
161
|
+
error: `'superskill script path sp ${SCRIPT_ROOT_TWIN_REL}' resolved nothing`,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
const dir = dirname(resolved.path);
|
|
165
|
+
try {
|
|
166
|
+
return write({
|
|
167
|
+
mode: 'installed',
|
|
168
|
+
source: resolved.source ?? 'unknown',
|
|
169
|
+
dir,
|
|
170
|
+
scriptSetDigest: digestScriptSet(dir),
|
|
171
|
+
});
|
|
172
|
+
} catch (error) {
|
|
173
|
+
return write({ mode: 'unresolved', error: `installed digest failed: ${String(error)}` });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export const SCRIPT_ROOT_USAGE = 'usage: script-root --run-id <id>';
|
|
178
|
+
|
|
179
|
+
export function main(argv: string[], env: ScriptRootEnv = getEnvVars(), options: ScriptRootOptions = {}): number {
|
|
180
|
+
let runId = env.__runId ?? '';
|
|
181
|
+
for (let i = 0; i < argv.length; i++) {
|
|
182
|
+
if (argv[i] === '--run-id') {
|
|
183
|
+
runId = argv[i + 1] ?? '';
|
|
184
|
+
i++;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
process.stderr.write(`${SCRIPT_ROOT_USAGE}\n`);
|
|
188
|
+
return 2;
|
|
189
|
+
}
|
|
190
|
+
return runScriptRoot({ ...env, __runId: runId }, options).exitCode;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (import.meta.main) {
|
|
194
|
+
process.exit(main(process.argv.slice(2)));
|
|
195
|
+
}
|
|
@@ -1281,7 +1281,7 @@ function resolveFeature(input: ResolutionInput): StageResolution {
|
|
|
1281
1281
|
reasonKind: 'dispatch',
|
|
1282
1282
|
requiresConfirmation: row.requiresConfirmation,
|
|
1283
1283
|
chain: false,
|
|
1284
|
-
dispatchCommand: dispatchCmd,
|
|
1284
|
+
dispatchCommand: dispatchCmd ?? null,
|
|
1285
1285
|
nextObservableOutcome: row.rowId === 'B6' ? 'feature transition or wrap' : undefined,
|
|
1286
1286
|
};
|
|
1287
1287
|
}
|
|
@@ -59,7 +59,8 @@ function usage(): never {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
function defaultSpurBin(): string {
|
|
62
|
-
|
|
62
|
+
const fromEnv = getEnvVar('SPUR_BIN');
|
|
63
|
+
if (fromEnv) return fromEnv;
|
|
63
64
|
const local = fileURLToPath(new URL('../../../apps/cli/src/index.ts', import.meta.url));
|
|
64
65
|
if (existsSync(local)) return `bun ${local}`;
|
|
65
66
|
return 'spur';
|
|
@@ -71,6 +72,7 @@ function parseArgs(argv: string[]): { wbs: string; spurBin: string } {
|
|
|
71
72
|
let i = 0;
|
|
72
73
|
while (i < argv.length) {
|
|
73
74
|
const arg = argv[i];
|
|
75
|
+
if (arg === undefined) break;
|
|
74
76
|
if (arg === '--spur-bin') {
|
|
75
77
|
spurBin = argv[i + 1] ?? defaultSpurBin();
|
|
76
78
|
i += 2;
|
|
@@ -109,7 +111,7 @@ function writeStatus(wbs: string, status: 'PASS' | 'FAIL'): void {
|
|
|
109
111
|
writeFileSync(join(statusDir, `${wbs}-precheck-evidence.status`), `${status}\n`);
|
|
110
112
|
}
|
|
111
113
|
|
|
112
|
-
function fail(wbs: string, reasons: string[]):
|
|
114
|
+
function fail(wbs: string, reasons: string[]): never {
|
|
113
115
|
writeStatus(wbs, 'FAIL');
|
|
114
116
|
console.error(`task-evidence-precheck: FAIL`);
|
|
115
117
|
for (const r of reasons) {
|
|
@@ -89,7 +89,8 @@ function usage(): never {
|
|
|
89
89
|
* keeps ad-hoc invocations from silently hitting a stale PATH install.
|
|
90
90
|
*/
|
|
91
91
|
function defaultSpurBin(): string {
|
|
92
|
-
|
|
92
|
+
const fromEnv = getEnvVar('SPUR_BIN');
|
|
93
|
+
if (fromEnv) return fromEnv;
|
|
93
94
|
// scripts/ -> plugins/sp/ -> <repo>/apps/cli/src/index.ts (fileURLToPath — raw pathname breaks
|
|
94
95
|
// on %-encoded paths, e.g. spaces in the checkout directory)
|
|
95
96
|
const local = fileURLToPath(new URL('../../../apps/cli/src/index.ts', import.meta.url));
|
|
@@ -115,6 +116,7 @@ function parseArgs(argv: string[]): {
|
|
|
115
116
|
let i = 0;
|
|
116
117
|
while (i < argv.length) {
|
|
117
118
|
const arg = argv[i];
|
|
119
|
+
if (arg === undefined) break;
|
|
118
120
|
if (arg === '--spur-bin') {
|
|
119
121
|
spurBin = argv[i + 1] ?? defaultSpurBin();
|
|
120
122
|
i += 2;
|