@gobing-ai/spur 0.3.94 → 0.3.96

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.
Files changed (198) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +42 -0
  3. package/board/index.d.ts +38 -0
  4. package/config/plugin-scripts.json +3 -52
  5. package/config/rules/boundary/sp-script-placement.yaml +19 -0
  6. package/config/rules/strict/runtime-boundaries.yaml +5 -0
  7. package/config/rules/structure/test-location.yaml +2 -0
  8. package/config/rules/typescript/no-syscall-emulation-in-boundary-mock.yaml +1 -1
  9. package/config/rules/typescript/output-boundaries.yaml +15 -2
  10. package/config/script-placement-baseline.json +51 -0
  11. package/config/workflows/feature-verification.yaml +7 -4
  12. package/config/workflows/history-anatomy.yaml +23 -11
  13. package/config/workflows/idea-pipeline.yaml +87 -86
  14. package/config/workflows/pr-review.yaml +18 -9
  15. package/config/workflows/task-pipeline.yaml +84 -109
  16. package/config/workflows/wrapup-pipeline.yaml +173 -46
  17. package/package.json +15 -11
  18. package/plugins/sp/README.md +8 -10
  19. package/plugins/sp/agents/super-reviewer.md +43 -18
  20. package/plugins/sp/commands/dev-fixgha.md +83 -0
  21. package/plugins/sp/commands/dev-gitmsg.md +8 -6
  22. package/plugins/sp/commands/dev-gtd.md +2 -2
  23. package/plugins/sp/commands/dev-idea.md +22 -12
  24. package/plugins/sp/commands/dev-plan.md +8 -9
  25. package/plugins/sp/commands/dev-review.md +22 -13
  26. package/plugins/sp/commands/dev-verify.md +7 -4
  27. package/plugins/sp/commands/dev-verifyall.md +5 -4
  28. package/plugins/sp/commands/dev-wrap.md +1 -1
  29. package/plugins/sp/commands/spur-init.md +2 -2
  30. package/plugins/sp/hooks/context-post-tool.ts +2 -2
  31. package/plugins/sp/hooks/context-session-start.ts +2 -1
  32. package/plugins/sp/hooks/context-session-stop.ts +22 -14
  33. package/plugins/sp/hooks/pi/guard-extension.ts +109 -150
  34. package/plugins/sp/lib/history-anatomy.generated.d.mts +112 -0
  35. package/plugins/sp/lib/history-anatomy.generated.mjs +686 -0
  36. package/plugins/sp/lib/idea-handoff.generated.mjs +6 -5
  37. package/plugins/sp/lib/inline-run.generated.d.mts +13 -2
  38. package/plugins/sp/lib/inline-run.generated.mjs +33 -16
  39. package/plugins/sp/lib/quality-gate.generated.d.mts +104 -0
  40. package/plugins/sp/lib/quality-gate.generated.mjs +438 -0
  41. package/plugins/sp/lib/residual-scan.generated.d.mts +62 -0
  42. package/plugins/sp/lib/residual-scan.generated.mjs +210 -0
  43. package/plugins/sp/lib/spur-bin.ts +36 -0
  44. package/plugins/sp/lib/step-profile.generated.d.mts +71 -0
  45. package/plugins/sp/lib/step-profile.generated.mjs +174 -0
  46. package/plugins/sp/plugin.json +1 -1
  47. package/plugins/sp/references/roles.md +1 -1
  48. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +3 -3
  49. package/plugins/sp/scripts/daily-summary/daily-summary.ts +3 -3
  50. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +2 -0
  51. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +1 -0
  52. package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +9 -7
  53. package/plugins/sp/scripts/dogfood-testing/validate-report.ts +8 -7
  54. package/plugins/sp/scripts/feature-verification-steps.ts +6 -6
  55. package/plugins/sp/scripts/history-anatomy-cache.mjs +23 -22
  56. package/plugins/sp/scripts/history-anatomy-cache.ts +23 -928
  57. package/plugins/sp/scripts/inline-run-setup.mjs +90 -315
  58. package/plugins/sp/scripts/inline-run-setup.ts +109 -639
  59. package/plugins/sp/scripts/pr-reviewing.mjs +2 -0
  60. package/plugins/sp/scripts/pr-reviewing.ts +1 -0
  61. package/plugins/sp/scripts/quality-gate.mjs +38 -21
  62. package/plugins/sp/scripts/quality-gate.ts +20 -660
  63. package/plugins/sp/scripts/residual-scan.mjs +138 -159
  64. package/plugins/sp/scripts/residual-scan.ts +102 -499
  65. package/plugins/sp/scripts/script-root.mjs +129 -0
  66. package/plugins/sp/scripts/script-root.ts +199 -0
  67. package/plugins/sp/scripts/workflow-step-profile.mjs +27 -16
  68. package/plugins/sp/scripts/workflow-step-profile.ts +22 -314
  69. package/plugins/sp/scripts/wrapup-drift-probe.mjs +12 -3
  70. package/plugins/sp/scripts/wrapup-drift-probe.ts +6 -3
  71. package/plugins/sp/scripts/wrapup-steps.mjs +42 -40
  72. package/plugins/sp/scripts/wrapup-steps.ts +64 -46
  73. package/plugins/sp/skills/code-improvement/SKILL.md +5 -4
  74. package/plugins/sp/skills/code-verification/SKILL.md +46 -16
  75. package/plugins/sp/skills/code-verification/references/verdict-schema.md +13 -2
  76. package/plugins/sp/skills/functional-review/SKILL.md +8 -5
  77. package/plugins/sp/skills/functional-review/references/verdict-schema.md +1 -1
  78. package/plugins/sp/skills/history-anatomy/references/modes.md +2 -1
  79. package/plugins/sp/skills/next-feature/references/handoff-routing.md +1 -1
  80. package/plugins/sp/skills/next-router/references/routing-table.md +2 -2
  81. package/plugins/sp/skills/spur-cli/SKILL.md +3 -3
  82. package/plugins/sp/skills/spur-cli/references/agent.md +10 -10
  83. package/plugins/sp/skills/spur-cli/references/features.md +17 -6
  84. package/plugins/sp/skills/spur-cli/references/init.md +17 -16
  85. package/plugins/sp/skills/spur-cli/references/self.md +3 -2
  86. package/plugins/sp/skills/spur-cli/references/serve.md +10 -10
  87. package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +28 -5
  88. package/plugins/sp/skills/spur-cli/references/tasks.md +14 -8
  89. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +4 -3
  90. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +11 -9
  91. package/plugins/sp/skills/spur-dev/references/decision-brief.md +1 -1
  92. package/plugins/sp/skills/spur-dev/references/dev-operations.md +86 -70
  93. package/plugins/sp/skills/spur-dev/references/done-housekeeping.md +3 -3
  94. package/plugins/sp/skills/spur-dev/references/execution-batch.md +107 -44
  95. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +5 -9
  96. package/plugins/sp/skills/spur-dev/references/feature-link-helper.md +3 -3
  97. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +31 -21
  98. package/plugins/sp/skills/spur-dev/references/gate-checklists.md +19 -19
  99. package/plugins/sp/skills/spur-dev/references/idea-evaluation.md +4 -3
  100. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +35 -6
  101. package/plugins/sp/skills/spur-doctor/SKILL.md +1 -1
  102. package/plugins/sp/skills/sys-architecture/SKILL.md +3 -2
  103. package/plugins/sp/tsconfig.json +8 -0
  104. package/spur.js +6638 -4775
  105. package/web/_astro/BoardApp.BIjMatT1.js +1 -0
  106. package/web/_astro/{BoardApp.FTEs3-N8.js → BoardApp.GvjIe9Z6.js} +150 -153
  107. package/web/_astro/TaskDetail.BZM3EAFx.js +1 -0
  108. package/web/_astro/_commonjsHelpers.CqkleIqs.js +1 -0
  109. package/web/_astro/{arc.uuAf51IT.js → arc.HkRiZnoI.js} +1 -1
  110. package/web/_astro/architectureDiagram-3BPJPVTR.BDdK-tgZ.js +36 -0
  111. package/web/_astro/{blockDiagram-GPEHLZMM.D1mGCq3p.js → blockDiagram-GPEHLZMM.CZ9kOBxx.js} +1 -1
  112. package/web/_astro/board-facade-react-dom-client.B2U5wMvk.js +1 -0
  113. package/web/_astro/board-facade-react-dom.Cd2MuMhF.js +1 -0
  114. package/web/_astro/board-facade-react-jsx-dev-runtime.B_L3JzfC.js +1 -0
  115. package/web/_astro/board-facade-react-jsx-runtime.BY2yfHRi.js +1 -0
  116. package/web/_astro/board-facade-react-router-dom.Dy5pjzOw.js +1 -0
  117. package/web/_astro/board-facade-react-router.DjjqU27_.js +1 -0
  118. package/web/_astro/board-facade-react.Dgf4GRyj.js +1 -0
  119. package/web/_astro/{c4Diagram-AAUBKEIU.CMsolcde.js → c4Diagram-AAUBKEIU.DXK9qeRD.js} +1 -1
  120. package/web/_astro/channel.DlhX2MEt.js +1 -0
  121. package/web/_astro/{chunk-2J33WTMH.CrGA3fik.js → chunk-2J33WTMH.BTD2WeX5.js} +1 -1
  122. package/web/_astro/{chunk-4BX2VUAB.DsLVVla0.js → chunk-4BX2VUAB.Ci6Qbcvk.js} +1 -1
  123. package/web/_astro/{chunk-55IACEB6.Dxc59Tfi.js → chunk-55IACEB6.yl3zsj7p.js} +1 -1
  124. package/web/_astro/{chunk-727SXJPM.CaEVE2Wy.js → chunk-727SXJPM.ClXZsfyR.js} +1 -1
  125. package/web/_astro/{chunk-AQP2D5EJ.BiJ4HXeI.js → chunk-AQP2D5EJ.BmKWZBcP.js} +1 -1
  126. package/web/_astro/{chunk-FMBD7UC4.Mxf1fru5.js → chunk-FMBD7UC4.By30fcb7.js} +1 -1
  127. package/web/_astro/chunk-JMJ3UQ3L.BQJEJHu4.js +34 -0
  128. package/web/_astro/{chunk-ND2GUHAM.pyOWQixH.js → chunk-ND2GUHAM.24NmrD-k.js} +1 -1
  129. package/web/_astro/{chunk-QZHKN3VN.BmEsg4vr.js → chunk-QZHKN3VN.BN4sKdcS.js} +1 -1
  130. package/web/_astro/chunk-YNUBSHFH.CxeAygfi.js +7 -0
  131. package/web/_astro/{classDiagram-4FO5ZUOK.BHhhMFTO.js → classDiagram-4FO5ZUOK.CJpoMPb5.js} +1 -1
  132. package/web/_astro/{classDiagram-v2-Q7XG4LA2.BHhhMFTO.js → classDiagram-v2-Q7XG4LA2.CJpoMPb5.js} +1 -1
  133. package/web/_astro/client.BhluLQqe.js +1 -0
  134. package/web/_astro/client.DBpSRLnx.js +9 -0
  135. package/web/_astro/cose-bilkent-S5V4N54A.BraxQ2Nt.js +1 -0
  136. package/web/_astro/{cynefin-OW5HDTMX.C2j1_lKL.js → cynefin-OW5HDTMX.gzoU73oL.js} +1 -1
  137. package/web/_astro/{dagre-BM42HDAG.hZ2NCTdT.js → dagre-BM42HDAG.wyDfWySU.js} +1 -1
  138. package/web/_astro/{diagram-2AECGRRQ.Bfw5_EzK.js → diagram-2AECGRRQ.h2_EbMxV.js} +1 -1
  139. package/web/_astro/{diagram-5GNKFQAL.Bt1V_Tmk.js → diagram-5GNKFQAL.Db6dcs89.js} +1 -1
  140. package/web/_astro/{diagram-KO2AKTUF.DEk-YFwp.js → diagram-KO2AKTUF.NVwzxu_U.js} +1 -1
  141. package/web/_astro/{diagram-LMA3HP47.CDjndtQm.js → diagram-LMA3HP47.B-eNgDQk.js} +1 -1
  142. package/web/_astro/{diagram-OG6HWLK6.cFnUHScG.js → diagram-OG6HWLK6.Bsa8ZRbJ.js} +1 -1
  143. package/web/_astro/{erDiagram-TEJ5UH35.DlhYp7NV.js → erDiagram-TEJ5UH35.BospRE5Q.js} +1 -1
  144. package/web/_astro/{flowDiagram-I6XJVG4X.DNTpsxfx.js → flowDiagram-I6XJVG4X.DMzvHwAA.js} +1 -1
  145. package/web/_astro/ganttDiagram-6RSMTGT7.Dmdxgd-M.js +292 -0
  146. package/web/_astro/{gitGraphDiagram-PVQCEYII.DkEqNI0P.js → gitGraphDiagram-PVQCEYII.CaqdO0rL.js} +1 -1
  147. package/web/_astro/index.5dn-Zm-2.js +1 -0
  148. package/web/_astro/index.C-IqPpW2.js +1 -0
  149. package/web/_astro/index.Cyc_GA4f.js +1 -0
  150. package/web/_astro/index.EoZlzLK-.css +1 -0
  151. package/web/_astro/index.tZfqwSut.js +1 -0
  152. package/web/_astro/{infoDiagram-5YYISTIA.CrjioCTG.js → infoDiagram-5YYISTIA.DDVD9Wn7.js} +1 -1
  153. package/web/_astro/{ishikawaDiagram-YF4QCWOH.BcK0CN8n.js → ishikawaDiagram-YF4QCWOH.EKyCUFsL.js} +1 -1
  154. package/web/_astro/{journeyDiagram-JHISSGLW.p0CbBDX1.js → journeyDiagram-JHISSGLW.By2-RkYB.js} +1 -1
  155. package/web/_astro/jsx-runtime.DWOiEOII.js +1 -0
  156. package/web/_astro/{kanban-definition-UN3LZRKU.puHIFt6J.js → kanban-definition-UN3LZRKU.ar-WmlyR.js} +1 -1
  157. package/web/_astro/{linear.Bb-3a1d7.js → linear.BnzMBgo_.js} +1 -1
  158. package/web/_astro/{mermaid.core.CJDgXOJs.js → mermaid.core.DSqeFu2Y.js} +4 -4
  159. package/web/_astro/{mindmap-definition-RKZ34NQL.BoAZEI9t.js → mindmap-definition-RKZ34NQL.1XcF8Wh2.js} +1 -1
  160. package/web/_astro/{pieDiagram-4H26LBE5.CJNSdqY7.js → pieDiagram-4H26LBE5.DnItHOdK.js} +1 -1
  161. package/web/_astro/{quadrantDiagram-W4KKPZXB.DtTy7_0Z.js → quadrantDiagram-W4KKPZXB.BQQyrnBt.js} +1 -1
  162. package/web/_astro/{requirementDiagram-4Y6WPE33.BNYV98Xg.js → requirementDiagram-4Y6WPE33.Dmzxun82.js} +1 -1
  163. package/web/_astro/{sankeyDiagram-5OEKKPKP.DRq9DGHp.js → sankeyDiagram-5OEKKPKP.BBIeRKYB.js} +1 -1
  164. package/web/_astro/{sequenceDiagram-3UESZ5HK.B7KdBFCm.js → sequenceDiagram-3UESZ5HK.CUNVdBBf.js} +1 -1
  165. package/web/_astro/{stateDiagram-AJRCARHV.CD62ZJ2H.js → stateDiagram-AJRCARHV.DlFf2sJm.js} +1 -1
  166. package/web/_astro/{stateDiagram-v2-BHNVJYJU.CeVk1TAj.js → stateDiagram-v2-BHNVJYJU.DXR7I1H5.js} +1 -1
  167. package/web/_astro/{timeline-definition-PNZ67QCA.C_ZPA5tT.js → timeline-definition-PNZ67QCA.BcO-GyX2.js} +1 -1
  168. package/web/_astro/{vennDiagram-CIIHVFJN.CrThO_FQ.js → vennDiagram-CIIHVFJN.ChCECmmW.js} +1 -1
  169. package/web/_astro/{wardleyDiagram-YWT4CUSO.DSZCA5nl.js → wardleyDiagram-YWT4CUSO.CRaT12mD.js} +1 -1
  170. package/web/_astro/{xychartDiagram-2RQKCTM6.KI7baTuk.js → xychartDiagram-2RQKCTM6.B3lsHVpj.js} +1 -1
  171. package/web/board-runtime.json +69 -0
  172. package/web/index.html +2 -2
  173. package/plugins/sp/lib/artifact-digest.generated.d.mts +0 -7
  174. package/plugins/sp/lib/artifact-digest.generated.mjs +0 -48
  175. package/plugins/sp/scripts/feature-sync-bounded.mjs +0 -298
  176. package/plugins/sp/scripts/feature-sync-bounded.ts +0 -479
  177. package/plugins/sp/scripts/idea-coverage-check.ts +0 -168
  178. package/plugins/sp/scripts/inline-pipeline-parity-check.ts +0 -298
  179. package/plugins/sp/scripts/record-feature-sync.mjs +0 -63
  180. package/plugins/sp/scripts/record-feature-sync.ts +0 -84
  181. package/plugins/sp/scripts/script-contract-check.ts +0 -377
  182. package/plugins/sp/scripts/stage-registry-adapter.ts +0 -1533
  183. package/plugins/sp/scripts/surface-drift-inventory.ts +0 -989
  184. package/plugins/sp/scripts/task-evidence-precheck.ts +0 -187
  185. package/plugins/sp/scripts/task-size-precheck.ts +0 -210
  186. package/plugins/sp/scripts/transition-shim-check.ts +0 -238
  187. package/plugins/sp/scripts/validate-commands.ts +0 -689
  188. package/plugins/sp/scripts/validate-flag-contracts.ts +0 -878
  189. package/plugins/sp/scripts/verify-answer-lint.ts +0 -547
  190. package/web/_astro/BoardApp.C02hAHPO.js +0 -1
  191. package/web/_astro/TaskDetail.C-GdsS-t.js +0 -1
  192. package/web/_astro/architectureDiagram-3BPJPVTR.CGe629A1.js +0 -36
  193. package/web/_astro/channel.fsgl7o5j.js +0 -1
  194. package/web/_astro/client.yhYJvxCU.js +0 -9
  195. package/web/_astro/cose-bilkent-S5V4N54A.2fH4YOlp.js +0 -1
  196. package/web/_astro/ganttDiagram-6RSMTGT7.DC_p36PI.js +0 -292
  197. package/web/_astro/index.De90oHcH.js +0 -1
  198. package/web/_astro/index.Hjbr15fG.css +0 -1
@@ -1,377 +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 not older than 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 { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
22
- import { join, resolve } from 'node:path';
23
-
24
- export type ScriptContractType = 'standard' | 'repo-only';
25
-
26
- export interface ScriptManifestEntry {
27
- rel: string;
28
- contract: ScriptContractType;
29
- twin?: string;
30
- }
31
-
32
- export interface ScriptManifest {
33
- schema_version?: number;
34
- description?: string;
35
- entries: ScriptManifestEntry[];
36
- }
37
-
38
- export interface Violation {
39
- kind:
40
- | 'missing_twin'
41
- | 'stale_twin'
42
- | 'unexpected_twin'
43
- | 'unregistered_script'
44
- | 'forbidden_invocation'
45
- | 'incomplete';
46
- target: string;
47
- message: string;
48
- }
49
-
50
- export function parseArgs(argv: string[]): {
51
- manifest: string;
52
- scriptsDir: string;
53
- pluginDir: string;
54
- cwd: string;
55
- } {
56
- let manifest = 'config/plugin-scripts.json';
57
- let scriptsDir = 'plugins/sp/scripts';
58
- let pluginDir = 'plugins/sp';
59
- for (let i = 0; i < argv.length; i++) {
60
- if (argv[i] === '--manifest') manifest = argv[++i] ?? manifest;
61
- else if (argv[i] === '--scripts-dir') scriptsDir = argv[++i] ?? scriptsDir;
62
- else if (argv[i] === '--plugin-dir') pluginDir = argv[++i] ?? pluginDir;
63
- }
64
- return { manifest, scriptsDir, pluginDir, cwd: process.cwd() };
65
- }
66
-
67
- export function loadManifest(path: string): { manifest: ScriptManifest | null; error: string | null } {
68
- if (!existsSync(path)) return { manifest: null, error: `manifest not found at ${path}` };
69
- try {
70
- const raw = readFileSync(path, 'utf8');
71
- const parsed = JSON.parse(raw) as ScriptManifest;
72
- if (!Array.isArray(parsed.entries)) {
73
- return { manifest: null, error: `${path}: missing "entries" array` };
74
- }
75
- return { manifest: parsed, error: null };
76
- } catch (err) {
77
- return { manifest: null, error: `malformed JSON at ${path}: ${String(err)}` };
78
- }
79
- }
80
-
81
- export function listDiskScripts(scriptsDir: string): { tsFiles: string[]; mjsFiles: string[] } {
82
- const tsFiles: string[] = [];
83
- const mjsFiles: string[] = [];
84
-
85
- function walk(dir: string, base: string): void {
86
- let entries: string[];
87
- try {
88
- entries = readdirSync(dir);
89
- } catch {
90
- return;
91
- }
92
- for (const entry of entries) {
93
- const fullPath = join(dir, entry);
94
- let st: ReturnType<typeof statSync>;
95
- try {
96
- st = statSync(fullPath);
97
- } catch {
98
- continue;
99
- }
100
- if (st.isDirectory()) {
101
- walk(fullPath, base ? `${base}/${entry}` : entry);
102
- } else if (st.isFile()) {
103
- const rel = base ? `${base}/${entry}` : entry;
104
- if (entry.endsWith('.ts')) {
105
- tsFiles.push(rel);
106
- } else if (entry.endsWith('.mjs')) {
107
- mjsFiles.push(rel);
108
- }
109
- }
110
- }
111
- }
112
-
113
- walk(scriptsDir, '');
114
- return { tsFiles: tsFiles.sort(), mjsFiles: mjsFiles.sort() };
115
- }
116
-
117
- export function scanShippedSurfaces(pluginDir: string): Array<{ file: string; line: number; content: string }> {
118
- const matches: Array<{ file: string; line: number; content: string }> = [];
119
- const forbiddenPattern = 'bun plugins/sp/scripts/';
120
-
121
- const searchDirs = ['commands', 'skills', 'agents'];
122
- const singleFiles = ['README.md'];
123
-
124
- function scanFile(filePath: string): void {
125
- if (!existsSync(filePath)) return;
126
- let text: string;
127
- try {
128
- text = readFileSync(filePath, 'utf8');
129
- } catch {
130
- return;
131
- }
132
- const lines = text.split('\n');
133
- for (let i = 0; i < lines.length; i++) {
134
- const line = lines[i] ?? '';
135
- if (line.includes(forbiddenPattern)) {
136
- matches.push({
137
- file: filePath,
138
- line: i + 1,
139
- content: line.trim(),
140
- });
141
- }
142
- }
143
- }
144
-
145
- function walk(dir: string): void {
146
- let entries: string[];
147
- try {
148
- entries = readdirSync(dir);
149
- } catch {
150
- return;
151
- }
152
- for (const entry of entries) {
153
- const fullPath = join(dir, entry);
154
- let st: ReturnType<typeof statSync>;
155
- try {
156
- st = statSync(fullPath);
157
- } catch {
158
- continue;
159
- }
160
- if (st.isDirectory()) {
161
- walk(fullPath);
162
- } else if (
163
- st.isFile() &&
164
- (entry.endsWith('.md') || entry.endsWith('.json') || entry.endsWith('.yaml') || entry.endsWith('.yml'))
165
- ) {
166
- scanFile(fullPath);
167
- }
168
- }
169
- }
170
-
171
- for (const d of searchDirs) {
172
- walk(join(pluginDir, d));
173
- }
174
- for (const f of singleFiles) {
175
- scanFile(join(pluginDir, f));
176
- }
177
-
178
- return matches;
179
- }
180
-
181
- // Bare-specifier value imports in bundled surfaces break `superskill install`, which
182
- // bundles hooks/scripts on targets with no node_modules (task 0669; releases
183
- // 0.3.81–0.3.88 failed with "Bundle failed" when scripts imported @gobing-ai/ts-utils).
184
- // Type-only imports are erased before bundling, so they stay legal. Line-based
185
- // statement walk (import/export statements are column 0 under biome) — lookahead
186
- // regexes misbehave in Bun 1.3.x (JSC) when grouped with lazy quantifiers.
187
- function findGobingAiValueImports(dir: string): { file: string; line: number }[] {
188
- const hits: { file: string; line: number }[] = [];
189
- let entries: string[];
190
- try {
191
- entries = readdirSync(dir);
192
- } catch {
193
- return hits;
194
- }
195
- for (const entry of entries) {
196
- const full = join(dir, entry);
197
- let st: ReturnType<typeof statSync>;
198
- try {
199
- st = statSync(full);
200
- } catch {
201
- continue;
202
- }
203
- if (st.isDirectory()) {
204
- hits.push(...findGobingAiValueImports(full));
205
- } else if (entry.endsWith('.ts')) {
206
- const lines = readFileSync(full, 'utf8').split('\n');
207
- let stmtStart = 0;
208
- for (let i = 0; i < lines.length; i++) {
209
- if (/^\s*(?:import|export)\b/.test(lines[i])) stmtStart = i;
210
- if (/(?:from\s*|import\s*|import\s*\(\s*)['"]@gobing-ai\//.test(lines[i])) {
211
- const opener = lines.slice(stmtStart, i + 1).join(' ');
212
- if (!/^\s*(?:import|export)\s+type\b/.test(opener)) {
213
- hits.push({ file: full, line: i + 1 });
214
- }
215
- }
216
- }
217
- }
218
- }
219
- return hits;
220
- }
221
-
222
- export function validateContract(manifest: ScriptManifest, scriptsDir: string, pluginDir: string): Violation[] {
223
- const violations: Violation[] = [];
224
- const { tsFiles, mjsFiles } = listDiskScripts(scriptsDir);
225
-
226
- const manifestMap = new Map<string, ScriptManifestEntry>();
227
- for (const entry of manifest.entries) {
228
- if (!entry.rel || !entry.contract) {
229
- violations.push({
230
- kind: 'incomplete',
231
- target: entry.rel ?? '<missing>',
232
- message: `manifest entry missing required fields (rel, contract)`,
233
- });
234
- continue;
235
- }
236
- manifestMap.set(entry.rel, entry);
237
- }
238
-
239
- // Rule 1: standard entries must have valid .mjs twins not older than the .ts source.
240
- // mtime is only meaningful when a build genuinely ran: a `git worktree add` stamps
241
- // every checked-out file within the same millisecond, and the lexicographic write
242
- // order (`tool.mjs` < `tool.ts`) makes the twin spuriously "older" by <1ms. Tolerate
243
- // sub-second deltas so a fresh checkout is never flagged; real build staleness is
244
- // seconds-to-minutes, far beyond this window (task 0606 R1 eval-worktree blocker).
245
- const STALE_TWIN_TOLERANCE_MS = 1000;
246
- for (const entry of manifest.entries) {
247
- if (entry.rel && entry.contract === 'standard') {
248
- const expectedTwinRel = entry.twin ?? entry.rel.replace(/\.ts$/, '.mjs');
249
- const twinPath = join(scriptsDir, expectedTwinRel);
250
- const tsPath = join(scriptsDir, entry.rel);
251
-
252
- if (!existsSync(twinPath)) {
253
- violations.push({
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) {
262
- violations.push({
263
- kind: 'stale_twin',
264
- target: entry.rel,
265
- message: `standard script .mjs twin ${expectedTwinRel} is older than source ${entry.rel}`,
266
- });
267
- }
268
- }
269
- }
270
- }
271
-
272
- // Rule 2: committed .mjs files must belong to a 'standard' entry
273
- for (const mjsRel of mjsFiles) {
274
- const correspondingTsRel = mjsRel.replace(/\.mjs$/, '.ts');
275
- const entry = manifestMap.get(correspondingTsRel);
276
- if (!entry) {
277
- violations.push({
278
- kind: 'unexpected_twin',
279
- target: mjsRel,
280
- message: `.mjs twin ${mjsRel} exists on disk but has no manifest entry for ${correspondingTsRel}`,
281
- });
282
- } else if (entry.contract === 'repo-only') {
283
- violations.push({
284
- kind: 'unexpected_twin',
285
- target: mjsRel,
286
- message: `repo-only script ${correspondingTsRel} must not have a .mjs twin (${mjsRel})`,
287
- });
288
- }
289
- }
290
-
291
- // Rule 3: all .ts files on disk must have a manifest entry
292
- const diskTsSet = new Set(tsFiles);
293
- for (const tsRel of tsFiles) {
294
- if (!manifestMap.has(tsRel)) {
295
- violations.push({
296
- kind: 'unregistered_script',
297
- target: tsRel,
298
- message: `script ${tsRel} exists in scripts dir but is not registered in manifest`,
299
- });
300
- }
301
- }
302
-
303
- // Also verify manifest entries exist on disk
304
- for (const [rel] of manifestMap) {
305
- if (!diskTsSet.has(rel)) {
306
- violations.push({
307
- kind: 'unregistered_script',
308
- target: rel,
309
- message: `manifest entry ${rel} does not exist on disk in ${scriptsDir}`,
310
- });
311
- }
312
- }
313
-
314
- // Rule 4: scan shipped surfaces for forbidden invocation
315
- const forbiddenHits = scanShippedSurfaces(pluginDir);
316
- for (const hit of forbiddenHits) {
317
- violations.push({
318
- kind: 'forbidden_invocation',
319
- target: `${hit.file}:${hit.line}`,
320
- message: `shipped surface ${hit.file}:${hit.line} contains forbidden invocation 'bun plugins/sp/scripts/': ${hit.content}`,
321
- });
322
- }
323
-
324
- // Rule 5: bundled surfaces (scripts, hooks, lib) must not value-import @gobing-ai/*.
325
- // superskill install bundles them where no node_modules exists; resolution then
326
- // depends on the host superskill's private dep tree (0.3.28+ happens to carry
327
- // ts-utils — an accident we do not depend on). Vendor into plugins/sp/lib/ instead.
328
- for (const dir of [scriptsDir, join(pluginDir, 'hooks'), join(pluginDir, 'lib')]) {
329
- for (const hit of findGobingAiValueImports(dir)) {
330
- violations.push({
331
- kind: 'gobing_ai_import',
332
- target: `${hit.file}:${hit.line}`,
333
- message:
334
- `bundled surface ${hit.file}:${hit.line} value-imports @gobing-ai/* — ` +
335
- 'superskill install bundles it with no node_modules ("Bundle failed", task 0669). ' +
336
- 'Vendor into plugins/sp/lib/ or use a relative import; `import type` is exempt.',
337
- });
338
- }
339
- }
340
-
341
- return violations;
342
- }
343
-
344
- export function run(argv: string[] = process.argv.slice(2)): number {
345
- const { manifest, scriptsDir, pluginDir, cwd } = parseArgs(argv);
346
- const manifestPath = resolve(cwd, manifest);
347
- const absScriptsDir = resolve(cwd, scriptsDir);
348
- const absPluginDir = resolve(cwd, pluginDir);
349
-
350
- const { manifest: parsed, error } = loadManifest(manifestPath);
351
- if (error || !parsed) {
352
- console.error(`script-contract-check: FAIL - ${error}`);
353
- return 1;
354
- }
355
-
356
- const violations = validateContract(parsed, absScriptsDir, absPluginDir);
357
- const ok = violations.length === 0;
358
-
359
- const standardCount = parsed.entries.filter((e) => e.contract === 'standard').length;
360
- const repoOnlyCount = parsed.entries.filter((e) => e.contract === 'repo-only').length;
361
-
362
- console.log(
363
- `script-contract-check: ${parsed.entries.length} script(s) baselined (${standardCount} standard, ${repoOnlyCount} repo-only), ` +
364
- `${violations.length} violation(s) — ${ok ? 'PASS' : 'FAIL'}`,
365
- );
366
-
367
- if (ok) return 0;
368
-
369
- for (const v of violations) {
370
- console.error(`script-contract-check: FAIL (${v.kind}) - ${v.message}`);
371
- }
372
- return 1;
373
- }
374
-
375
- if (import.meta.main) {
376
- process.exit(run());
377
- }