@gobing-ai/spur 0.3.95 → 0.3.97
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/plugin-scripts.json +0 -54
- package/config/rules/boundary/sp-script-placement.yaml +19 -0
- package/config/rules/strict/runtime-boundaries.yaml +6 -0
- package/config/rules/structure/test-location.yaml +2 -0
- package/config/rules/typescript/no-syscall-emulation-in-boundary-mock.yaml +1 -1
- package/config/rules/typescript/output-boundaries.yaml +15 -2
- package/config/script-placement-baseline.json +51 -0
- package/config/templates/AGENTS.md +3 -3
- package/config/workflows/feature-verification.yaml +1 -1
- package/config/workflows/history-anatomy.yaml +23 -11
- package/config/workflows/idea-pipeline.yaml +80 -85
- package/config/workflows/pr-review.yaml +18 -9
- package/config/workflows/task-pipeline.yaml +42 -95
- package/config/workflows/wrapup-pipeline.yaml +91 -38
- package/package.json +2 -2
- package/plugins/sp/README.md +14 -10
- package/plugins/sp/agents/super-reviewer.md +43 -18
- package/plugins/sp/commands/dev-fixgha.md +83 -0
- package/plugins/sp/commands/dev-gitmsg.md +8 -6
- package/plugins/sp/commands/dev-gtd.md +2 -2
- package/plugins/sp/commands/dev-idea.md +22 -12
- package/plugins/sp/commands/dev-job-dump.md +29 -0
- package/plugins/sp/commands/dev-job-resume.md +29 -0
- package/plugins/sp/commands/dev-plan.md +8 -9
- package/plugins/sp/commands/dev-review.md +22 -13
- package/plugins/sp/commands/dev-verifyall.md +1 -1
- package/plugins/sp/commands/spur-init.md +2 -2
- package/plugins/sp/lib/history-anatomy.generated.d.mts +112 -0
- package/plugins/sp/lib/history-anatomy.generated.mjs +686 -0
- package/plugins/sp/lib/idea-handoff.generated.mjs +5 -4
- package/plugins/sp/lib/inline-run.generated.d.mts +11 -0
- package/plugins/sp/lib/inline-run.generated.mjs +39 -13
- package/plugins/sp/lib/quality-gate.generated.d.mts +104 -0
- package/plugins/sp/lib/quality-gate.generated.mjs +445 -0
- package/plugins/sp/lib/residual-scan.generated.d.mts +63 -0
- package/plugins/sp/lib/residual-scan.generated.mjs +226 -0
- package/plugins/sp/lib/spur-bin.ts +36 -0
- package/plugins/sp/lib/step-profile.generated.d.mts +71 -0
- package/plugins/sp/lib/step-profile.generated.mjs +174 -0
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/references/roles.md +3 -3
- package/plugins/sp/scripts/feature-verification-steps.mjs +14 -1
- package/plugins/sp/scripts/feature-verification-steps.ts +14 -1
- package/plugins/sp/scripts/history-anatomy-cache.mjs +20 -19
- package/plugins/sp/scripts/history-anatomy-cache.ts +23 -928
- package/plugins/sp/scripts/inline-run-setup.mjs +85 -320
- package/plugins/sp/scripts/inline-run-setup.ts +104 -667
- package/plugins/sp/scripts/quality-gate.mjs +46 -24
- package/plugins/sp/scripts/quality-gate.ts +13 -659
- package/plugins/sp/scripts/residual-scan.mjs +179 -174
- package/plugins/sp/scripts/residual-scan.ts +125 -517
- package/plugins/sp/scripts/script-root.mjs +5 -1
- package/plugins/sp/scripts/script-root.ts +5 -1
- package/plugins/sp/scripts/workflow-step-profile.mjs +25 -17
- package/plugins/sp/scripts/workflow-step-profile.ts +21 -315
- package/plugins/sp/scripts/wrapup-drift-probe.mjs +8 -2
- package/plugins/sp/scripts/wrapup-drift-probe.ts +3 -2
- package/plugins/sp/scripts/wrapup-steps.mjs +72 -33
- package/plugins/sp/scripts/wrapup-steps.ts +128 -37
- package/plugins/sp/skills/code-improvement/SKILL.md +5 -4
- package/plugins/sp/skills/code-verification/SKILL.md +34 -9
- package/plugins/sp/skills/code-verification/references/verdict-schema.md +3 -3
- package/plugins/sp/skills/functional-review/SKILL.md +7 -4
- package/plugins/sp/skills/functional-review/references/verdict-schema.md +1 -1
- package/plugins/sp/skills/history-anatomy/references/modes.md +2 -1
- package/plugins/sp/skills/next-feature/references/handoff-routing.md +1 -1
- package/plugins/sp/skills/next-router/references/routing-table.md +1 -1
- package/plugins/sp/skills/source-driven-development/SKILL.md +11 -0
- package/plugins/sp/skills/spur-cli/SKILL.md +3 -3
- package/plugins/sp/skills/spur-cli/references/agent.md +10 -10
- package/plugins/sp/skills/spur-cli/references/features.md +17 -6
- package/plugins/sp/skills/spur-cli/references/init.md +17 -16
- package/plugins/sp/skills/spur-cli/references/self.md +3 -2
- package/plugins/sp/skills/spur-cli/references/serve.md +10 -10
- package/plugins/sp/skills/spur-cli/references/tasks/section-editing.md +9 -5
- package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +21 -6
- package/plugins/sp/skills/spur-cli/references/tasks.md +14 -8
- package/plugins/sp/skills/spur-cli/references/workflows.md +17 -14
- package/plugins/sp/skills/spur-dev/SKILL.md +2 -0
- package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +4 -3
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +14 -10
- package/plugins/sp/skills/spur-dev/references/decision-brief.md +1 -1
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +165 -75
- package/plugins/sp/skills/spur-dev/references/done-housekeeping.md +7 -5
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +84 -39
- package/plugins/sp/skills/spur-dev/references/execution-workflow.md +8 -12
- package/plugins/sp/skills/spur-dev/references/feature-link-helper.md +3 -3
- package/plugins/sp/skills/spur-dev/references/flag-glossary.md +40 -21
- package/plugins/sp/skills/spur-dev/references/gate-checklists.md +19 -19
- package/plugins/sp/skills/spur-dev/references/idea-evaluation.md +4 -3
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +72 -15
- package/plugins/sp/skills/spur-doctor/SKILL.md +1 -1
- package/plugins/sp/skills/sys-architecture/SKILL.md +3 -2
- package/spur.js +4461 -2085
- package/web/_astro/{BoardApp.Cpxntzad.js → BoardApp.Bmv5WkJ9.js} +1 -1
- package/web/_astro/BoardApp.vveLOdDq.js +322 -0
- package/web/_astro/{TaskDetail.C5bW4WGV.js → TaskDetail.IKc3uDYV.js} +1 -1
- package/web/_astro/{arc.CyjRvNMY.js → arc.C63Ufg35.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.B4lWRzJA.js → architectureDiagram-3BPJPVTR.CP8Hyldk.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.Be38USQ2.js → blockDiagram-GPEHLZMM.Bo_48MRA.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.DT9Fj5Qx.js → c4Diagram-AAUBKEIU.Ch61sw-O.js} +1 -1
- package/web/_astro/channel.BKCmJpWM.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.SeSBWLg5.js → chunk-2J33WTMH.CHpzS7xf.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.DoO4VE14.js → chunk-4BX2VUAB.DzMNNWIw.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.DrmTFA53.js → chunk-55IACEB6.BqPrDw_Q.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.Bi-TUb_V.js → chunk-727SXJPM.ZzBHhNw-.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.B-xp8Uhi.js → chunk-AQP2D5EJ.1_O_LLkx.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.DGWrDnUU.js → chunk-FMBD7UC4.GS4UHg7_.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM._BagvPDy.js → chunk-ND2GUHAM.BnqotN4g.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.VOboYWQ0.js → chunk-QZHKN3VN.Ghuh8zoJ.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.D_apfHS7.js → classDiagram-4FO5ZUOK.BW9vC3zg.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.D_apfHS7.js → classDiagram-v2-Q7XG4LA2.BW9vC3zg.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.DNLU_L-x.js → cose-bilkent-S5V4N54A.D6jNC0ti.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.D8borhV-.js → cynefin-OW5HDTMX.BOGoCs4L.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.Uc0lvjcV.js → dagre-BM42HDAG.8iU5VcWi.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.DDIxtDBo.js → diagram-2AECGRRQ.CVt88HwH.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.DVoDnD3H.js → diagram-5GNKFQAL.C9HxUUMl.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.CRubTJ-0.js → diagram-KO2AKTUF.4ywkQN3t.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.m9SeYcXQ.js → diagram-LMA3HP47.C48A6gpz.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.C6s2ZjV5.js → diagram-OG6HWLK6.DxFieQW4.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.nSzr5KTj.js → erDiagram-TEJ5UH35.CtjtQqYs.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.DrZcys16.js → flowDiagram-I6XJVG4X.C5zAiahU.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.Do4M1b-K.js → ganttDiagram-6RSMTGT7.D_L6zsny.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.BWc9oJ4l.js → gitGraphDiagram-PVQCEYII.Ignnrk3V.js} +1 -1
- package/web/_astro/index.CbNz17Sx.css +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.BIdOctFk.js → infoDiagram-5YYISTIA.DLv8-P5u.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.CV5SuG2t.js → ishikawaDiagram-YF4QCWOH.p0OTpwFO.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.CxE-rEJ-.js → journeyDiagram-JHISSGLW.CIaBt-DD.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.BPRdKWs_.js → kanban-definition-UN3LZRKU.DGySIntq.js} +1 -1
- package/web/_astro/{linear.zRsuuDTE.js → linear.BATV4RXu.js} +1 -1
- package/web/_astro/{mermaid.core.jAJTcMKc.js → mermaid.core.DzkwM3VX.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.BffJxfCr.js → mindmap-definition-RKZ34NQL.3piBRRzA.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.CacwWaE3.js → pieDiagram-4H26LBE5.DUNueLub.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.hXELD06-.js → quadrantDiagram-W4KKPZXB.9bihVbrt.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.DdEbPcqT.js → requirementDiagram-4Y6WPE33.CkjsC-YT.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.DgaDocKv.js → sankeyDiagram-5OEKKPKP.Dbq4vaoQ.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.B0KScnAu.js → sequenceDiagram-3UESZ5HK.BivrH99P.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.CV9M_WNc.js → stateDiagram-AJRCARHV.CqLQ1Vzz.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.BOAo74Et.js → stateDiagram-v2-BHNVJYJU.DvvoLD3b.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.CqqepUMe.js → timeline-definition-PNZ67QCA.B-KR78gv.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.DDqe4dOG.js → vennDiagram-CIIHVFJN.Ck0Eieb5.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.B3R9OPdO.js → wardleyDiagram-YWT4CUSO.CRfdd_do.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.B3SvgXpQ.js → xychartDiagram-2RQKCTM6.Ck_Jxk9C.js} +1 -1
- package/web/index.html +2 -2
- package/plugins/sp/lib/artifact-digest.generated.d.mts +0 -7
- package/plugins/sp/lib/artifact-digest.generated.mjs +0 -48
- package/plugins/sp/scripts/feature-sync-bounded.mjs +0 -301
- package/plugins/sp/scripts/feature-sync-bounded.ts +0 -481
- package/plugins/sp/scripts/idea-coverage-check.ts +0 -168
- package/plugins/sp/scripts/inline-pipeline-parity-check.ts +0 -298
- package/plugins/sp/scripts/record-feature-sync.mjs +0 -63
- package/plugins/sp/scripts/record-feature-sync.ts +0 -84
- package/plugins/sp/scripts/script-contract-check.ts +0 -506
- package/plugins/sp/scripts/stage-registry-adapter.ts +0 -1533
- package/plugins/sp/scripts/surface-drift-inventory.ts +0 -989
- package/plugins/sp/scripts/task-evidence-precheck.ts +0 -189
- package/plugins/sp/scripts/task-size-precheck.ts +0 -212
- package/plugins/sp/scripts/transition-shim-check.ts +0 -238
- package/plugins/sp/scripts/validate-commands.ts +0 -689
- package/plugins/sp/scripts/validate-flag-contracts.ts +0 -890
- package/plugins/sp/scripts/verify-answer-lint.ts +0 -549
- package/web/_astro/BoardApp.BxJuwD7I.js +0 -191
- package/web/_astro/channel.CI6N_tCg.js +0 -1
- package/web/_astro/index.CENnIEqT.css +0 -1
|
@@ -1,506 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
/**
|
|
3
|
-
* script-contract-check — two-sided gate over plugins/sp script entrypoint contracts
|
|
4
|
-
* (task 0600, feature I, ADR-065).
|
|
5
|
-
*
|
|
6
|
-
* Enforces superskill's standard plugin script contract across plugins/sp:
|
|
7
|
-
* 1. A 'standard' entry must have a valid .mjs twin byte-identical to a fresh convert of its .ts source.
|
|
8
|
-
* 2. A .mjs twin on disk must be registered under a 'standard' entry (never 'repo-only' or unlisted).
|
|
9
|
-
* 3. Every script file under plugins/sp/scripts/ must have a manifest entry (two-sided).
|
|
10
|
-
* 4. No shipped surface (commands/, skills/, agents/, README.md) may reference 'bun plugins/sp/scripts/'.
|
|
11
|
-
*
|
|
12
|
-
* Usage:
|
|
13
|
-
* bun plugins/sp/scripts/script-contract-check.ts
|
|
14
|
-
* [--manifest <path>] default: config/plugin-scripts.json
|
|
15
|
-
* [--scripts-dir <path>] default: plugins/sp/scripts
|
|
16
|
-
* [--plugin-dir <path>] default: plugins/sp
|
|
17
|
-
*
|
|
18
|
-
* Exit code: 0 on success, 1 on any violation.
|
|
19
|
-
*/
|
|
20
|
-
|
|
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';
|
|
25
|
-
|
|
26
|
-
export type ScriptContractType = 'standard' | 'repo-only';
|
|
27
|
-
|
|
28
|
-
export interface ScriptManifestEntry {
|
|
29
|
-
rel: string;
|
|
30
|
-
contract: ScriptContractType;
|
|
31
|
-
twin?: string;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface ScriptManifest {
|
|
35
|
-
schema_version?: number;
|
|
36
|
-
description?: string;
|
|
37
|
-
entries: ScriptManifestEntry[];
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface Violation {
|
|
41
|
-
kind:
|
|
42
|
-
| 'missing_twin'
|
|
43
|
-
| 'stale_twin'
|
|
44
|
-
| 'unexpected_twin'
|
|
45
|
-
| 'unregistered_script'
|
|
46
|
-
| 'forbidden_invocation'
|
|
47
|
-
| 'converter_unavailable'
|
|
48
|
-
| 'gobing_ai_import'
|
|
49
|
-
| 'incomplete';
|
|
50
|
-
target: string;
|
|
51
|
-
message: string;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function parseArgs(argv: string[]): {
|
|
55
|
-
manifest: string;
|
|
56
|
-
scriptsDir: string;
|
|
57
|
-
pluginDir: string;
|
|
58
|
-
cwd: string;
|
|
59
|
-
} {
|
|
60
|
-
let manifest = 'config/plugin-scripts.json';
|
|
61
|
-
let scriptsDir = 'plugins/sp/scripts';
|
|
62
|
-
let pluginDir = 'plugins/sp';
|
|
63
|
-
for (let i = 0; i < argv.length; i++) {
|
|
64
|
-
if (argv[i] === '--manifest') manifest = argv[++i] ?? manifest;
|
|
65
|
-
else if (argv[i] === '--scripts-dir') scriptsDir = argv[++i] ?? scriptsDir;
|
|
66
|
-
else if (argv[i] === '--plugin-dir') pluginDir = argv[++i] ?? pluginDir;
|
|
67
|
-
}
|
|
68
|
-
return { manifest, scriptsDir, pluginDir, cwd: process.cwd() };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function loadManifest(path: string): { manifest: ScriptManifest | null; error: string | null } {
|
|
72
|
-
if (!existsSync(path)) return { manifest: null, error: `manifest not found at ${path}` };
|
|
73
|
-
try {
|
|
74
|
-
const raw = readFileSync(path, 'utf8');
|
|
75
|
-
const parsed = JSON.parse(raw) as ScriptManifest;
|
|
76
|
-
if (!Array.isArray(parsed.entries)) {
|
|
77
|
-
return { manifest: null, error: `${path}: missing "entries" array` };
|
|
78
|
-
}
|
|
79
|
-
return { manifest: parsed, error: null };
|
|
80
|
-
} catch (err) {
|
|
81
|
-
return { manifest: null, error: `malformed JSON at ${path}: ${String(err)}` };
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function listDiskScripts(scriptsDir: string): { tsFiles: string[]; mjsFiles: string[] } {
|
|
86
|
-
const tsFiles: string[] = [];
|
|
87
|
-
const mjsFiles: string[] = [];
|
|
88
|
-
|
|
89
|
-
function walk(dir: string, base: string): void {
|
|
90
|
-
let entries: string[];
|
|
91
|
-
try {
|
|
92
|
-
entries = readdirSync(dir);
|
|
93
|
-
} catch {
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
for (const entry of entries) {
|
|
97
|
-
const fullPath = join(dir, entry);
|
|
98
|
-
let st: ReturnType<typeof statSync>;
|
|
99
|
-
try {
|
|
100
|
-
st = statSync(fullPath);
|
|
101
|
-
} catch {
|
|
102
|
-
continue;
|
|
103
|
-
}
|
|
104
|
-
if (st.isDirectory()) {
|
|
105
|
-
walk(fullPath, base ? `${base}/${entry}` : entry);
|
|
106
|
-
} else if (st.isFile()) {
|
|
107
|
-
const rel = base ? `${base}/${entry}` : entry;
|
|
108
|
-
if (entry.endsWith('.ts')) {
|
|
109
|
-
tsFiles.push(rel);
|
|
110
|
-
} else if (entry.endsWith('.mjs')) {
|
|
111
|
-
mjsFiles.push(rel);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
walk(scriptsDir, '');
|
|
118
|
-
return { tsFiles: tsFiles.sort(), mjsFiles: mjsFiles.sort() };
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export function scanShippedSurfaces(pluginDir: string): Array<{ file: string; line: number; content: string }> {
|
|
122
|
-
const matches: Array<{ file: string; line: number; content: string }> = [];
|
|
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
|
-
}
|
|
139
|
-
|
|
140
|
-
function scanLines(filePath: string): void {
|
|
141
|
-
if (!existsSync(filePath)) return;
|
|
142
|
-
let text: string;
|
|
143
|
-
try {
|
|
144
|
-
text = readFileSync(filePath, 'utf8');
|
|
145
|
-
} catch {
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
const lines = text.split('\n');
|
|
149
|
-
for (let i = 0; i < lines.length; i++) {
|
|
150
|
-
const line = lines[i] ?? '';
|
|
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;
|
|
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++;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function walk(dir: string): void {
|
|
221
|
-
let entries: string[];
|
|
222
|
-
try {
|
|
223
|
-
entries = readdirSync(dir);
|
|
224
|
-
} catch {
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
for (const entry of entries) {
|
|
228
|
-
const fullPath = join(dir, entry);
|
|
229
|
-
let st: ReturnType<typeof statSync>;
|
|
230
|
-
try {
|
|
231
|
-
st = statSync(fullPath);
|
|
232
|
-
} catch {
|
|
233
|
-
continue;
|
|
234
|
-
}
|
|
235
|
-
if (st.isDirectory()) {
|
|
236
|
-
walk(fullPath);
|
|
237
|
-
} else if (
|
|
238
|
-
st.isFile() &&
|
|
239
|
-
(entry.endsWith('.md') || entry.endsWith('.json') || entry.endsWith('.yaml') || entry.endsWith('.yml'))
|
|
240
|
-
) {
|
|
241
|
-
scanLines(fullPath);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
for (const d of ['commands', 'skills', 'agents']) {
|
|
247
|
-
walk(join(pluginDir, d));
|
|
248
|
-
}
|
|
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));
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
return matches;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Bare-specifier value imports in bundled surfaces break `superskill install`, which
|
|
268
|
-
// bundles hooks/scripts on targets with no node_modules (task 0669; releases
|
|
269
|
-
// 0.3.81–0.3.88 failed with "Bundle failed" when scripts imported @gobing-ai/ts-utils).
|
|
270
|
-
// Type-only imports are erased before bundling, so they stay legal. Line-based
|
|
271
|
-
// statement walk (import/export statements are column 0 under biome) — lookahead
|
|
272
|
-
// regexes misbehave in Bun 1.3.x (JSC) when grouped with lazy quantifiers.
|
|
273
|
-
function findGobingAiValueImports(dir: string): { file: string; line: number }[] {
|
|
274
|
-
const hits: { file: string; line: number }[] = [];
|
|
275
|
-
let entries: string[];
|
|
276
|
-
try {
|
|
277
|
-
entries = readdirSync(dir);
|
|
278
|
-
} catch {
|
|
279
|
-
return hits;
|
|
280
|
-
}
|
|
281
|
-
for (const entry of entries) {
|
|
282
|
-
const full = join(dir, entry);
|
|
283
|
-
let st: ReturnType<typeof statSync>;
|
|
284
|
-
try {
|
|
285
|
-
st = statSync(full);
|
|
286
|
-
} catch {
|
|
287
|
-
continue;
|
|
288
|
-
}
|
|
289
|
-
if (st.isDirectory()) {
|
|
290
|
-
hits.push(...findGobingAiValueImports(full));
|
|
291
|
-
} else if (entry.endsWith('.ts')) {
|
|
292
|
-
const lines = readFileSync(full, 'utf8').split('\n');
|
|
293
|
-
let stmtStart = 0;
|
|
294
|
-
for (let i = 0; i < lines.length; i++) {
|
|
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)) {
|
|
298
|
-
const opener = lines.slice(stmtStart, i + 1).join(' ');
|
|
299
|
-
if (!/^\s*(?:import|export)\s+type\b/.test(opener)) {
|
|
300
|
-
hits.push({ file: full, line: i + 1 });
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
return hits;
|
|
307
|
-
}
|
|
308
|
-
|
|
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[] {
|
|
338
|
-
const violations: Violation[] = [];
|
|
339
|
-
const { tsFiles, mjsFiles } = listDiskScripts(scriptsDir);
|
|
340
|
-
|
|
341
|
-
const manifestMap = new Map<string, ScriptManifestEntry>();
|
|
342
|
-
for (const entry of manifest.entries) {
|
|
343
|
-
if (!entry.rel || !entry.contract) {
|
|
344
|
-
violations.push({
|
|
345
|
-
kind: 'incomplete',
|
|
346
|
-
target: entry.rel ?? '<missing>',
|
|
347
|
-
message: `manifest entry missing required fields (rel, contract)`,
|
|
348
|
-
});
|
|
349
|
-
continue;
|
|
350
|
-
}
|
|
351
|
-
manifestMap.set(entry.rel, entry);
|
|
352
|
-
}
|
|
353
|
-
|
|
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)) {
|
|
369
|
-
violations.push({
|
|
370
|
-
kind: 'missing_twin',
|
|
371
|
-
target: entry.rel,
|
|
372
|
-
message: `standard script ${entry.rel} is missing its .mjs twin (${expectedTwinRel})`,
|
|
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
|
-
}
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
} finally {
|
|
398
|
-
rmSync(twinTmpDir, { recursive: true, force: true });
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// Rule 2: committed .mjs files must belong to a 'standard' entry
|
|
402
|
-
for (const mjsRel of mjsFiles) {
|
|
403
|
-
const correspondingTsRel = mjsRel.replace(/\.mjs$/, '.ts');
|
|
404
|
-
const entry = manifestMap.get(correspondingTsRel);
|
|
405
|
-
if (!entry) {
|
|
406
|
-
violations.push({
|
|
407
|
-
kind: 'unexpected_twin',
|
|
408
|
-
target: mjsRel,
|
|
409
|
-
message: `.mjs twin ${mjsRel} exists on disk but has no manifest entry for ${correspondingTsRel}`,
|
|
410
|
-
});
|
|
411
|
-
} else if (entry.contract === 'repo-only') {
|
|
412
|
-
violations.push({
|
|
413
|
-
kind: 'unexpected_twin',
|
|
414
|
-
target: mjsRel,
|
|
415
|
-
message: `repo-only script ${correspondingTsRel} must not have a .mjs twin (${mjsRel})`,
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
// Rule 3: all .ts files on disk must have a manifest entry
|
|
421
|
-
const diskTsSet = new Set(tsFiles);
|
|
422
|
-
for (const tsRel of tsFiles) {
|
|
423
|
-
if (!manifestMap.has(tsRel)) {
|
|
424
|
-
violations.push({
|
|
425
|
-
kind: 'unregistered_script',
|
|
426
|
-
target: tsRel,
|
|
427
|
-
message: `script ${tsRel} exists in scripts dir but is not registered in manifest`,
|
|
428
|
-
});
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
// Also verify manifest entries exist on disk
|
|
433
|
-
for (const [rel] of manifestMap) {
|
|
434
|
-
if (!diskTsSet.has(rel)) {
|
|
435
|
-
violations.push({
|
|
436
|
-
kind: 'unregistered_script',
|
|
437
|
-
target: rel,
|
|
438
|
-
message: `manifest entry ${rel} does not exist on disk in ${scriptsDir}`,
|
|
439
|
-
});
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// Rule 4: scan shipped surfaces for forbidden invocation
|
|
444
|
-
const forbiddenHits = scanShippedSurfaces(pluginDir);
|
|
445
|
-
for (const hit of forbiddenHits) {
|
|
446
|
-
violations.push({
|
|
447
|
-
kind: 'forbidden_invocation',
|
|
448
|
-
target: `${hit.file}:${hit.line}`,
|
|
449
|
-
message: `shipped surface ${hit.file}:${hit.line} contains forbidden invocation 'bun plugins/sp/scripts/': ${hit.content}`,
|
|
450
|
-
});
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// Rule 5: bundled surfaces (scripts, hooks, lib) must not value-import @gobing-ai/*.
|
|
454
|
-
// superskill install bundles them where no node_modules exists; resolution then
|
|
455
|
-
// depends on the host superskill's private dep tree (0.3.28+ happens to carry
|
|
456
|
-
// ts-utils — an accident we do not depend on). Vendor into plugins/sp/lib/ instead.
|
|
457
|
-
for (const dir of [scriptsDir, join(pluginDir, 'hooks'), join(pluginDir, 'lib')]) {
|
|
458
|
-
for (const hit of findGobingAiValueImports(dir)) {
|
|
459
|
-
violations.push({
|
|
460
|
-
kind: 'gobing_ai_import',
|
|
461
|
-
target: `${hit.file}:${hit.line}`,
|
|
462
|
-
message:
|
|
463
|
-
`bundled surface ${hit.file}:${hit.line} value-imports @gobing-ai/* — ` +
|
|
464
|
-
'superskill install bundles it with no node_modules ("Bundle failed", task 0669). ' +
|
|
465
|
-
'Vendor into plugins/sp/lib/ or use a relative import; `import type` is exempt.',
|
|
466
|
-
});
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
return violations;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
export function run(argv: string[] = process.argv.slice(2)): number {
|
|
474
|
-
const { manifest, scriptsDir, pluginDir, cwd } = parseArgs(argv);
|
|
475
|
-
const manifestPath = resolve(cwd, manifest);
|
|
476
|
-
const absScriptsDir = resolve(cwd, scriptsDir);
|
|
477
|
-
const absPluginDir = resolve(cwd, pluginDir);
|
|
478
|
-
|
|
479
|
-
const { manifest: parsed, error } = loadManifest(manifestPath);
|
|
480
|
-
if (error || !parsed) {
|
|
481
|
-
console.error(`script-contract-check: FAIL - ${error}`);
|
|
482
|
-
return 1;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
const violations = validateContract(parsed, absScriptsDir, absPluginDir);
|
|
486
|
-
const ok = violations.length === 0;
|
|
487
|
-
|
|
488
|
-
const standardCount = parsed.entries.filter((e) => e.contract === 'standard').length;
|
|
489
|
-
const repoOnlyCount = parsed.entries.filter((e) => e.contract === 'repo-only').length;
|
|
490
|
-
|
|
491
|
-
console.log(
|
|
492
|
-
`script-contract-check: ${parsed.entries.length} script(s) baselined (${standardCount} standard, ${repoOnlyCount} repo-only), ` +
|
|
493
|
-
`${violations.length} violation(s) — ${ok ? 'PASS' : 'FAIL'}`,
|
|
494
|
-
);
|
|
495
|
-
|
|
496
|
-
if (ok) return 0;
|
|
497
|
-
|
|
498
|
-
for (const v of violations) {
|
|
499
|
-
console.error(`script-contract-check: FAIL (${v.kind}) - ${v.message}`);
|
|
500
|
-
}
|
|
501
|
-
return 1;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
if (import.meta.main) {
|
|
505
|
-
process.exit(run());
|
|
506
|
-
}
|