@opengsd/gsd-core 1.7.0 → 1.9.0

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 (261) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +45 -1
  4. package/README.md +2 -0
  5. package/agents/gsd-code-fixer.md +1 -1
  6. package/agents/gsd-codebase-mapper.md +1 -1
  7. package/agents/gsd-debug-session-manager.md +78 -4
  8. package/agents/gsd-debugger.md +87 -29
  9. package/agents/gsd-executor.md +49 -9
  10. package/agents/gsd-intel-updater.md +3 -3
  11. package/agents/gsd-phase-researcher.md +4 -2
  12. package/agents/gsd-plan-checker.md +20 -0
  13. package/agents/gsd-planner.md +44 -59
  14. package/agents/gsd-project-researcher.md +2 -2
  15. package/agents/gsd-ui-auditor.md +0 -40
  16. package/agents/gsd-verifier.md +2 -2
  17. package/bin/install.js +1338 -135
  18. package/commands/gsd/ai-integration-phase.md +1 -1
  19. package/commands/gsd/mempalace-capture.md +9 -5
  20. package/commands/gsd/new-milestone.md +1 -1
  21. package/commands/gsd/plan-phase.md +5 -3
  22. package/commands/gsd/plan-review-convergence.md +7 -2
  23. package/gsd-core/bin/gsd-tools.cjs +2690 -2472
  24. package/gsd-core/bin/lib/adapter-imperative.cjs +8 -1
  25. package/gsd-core/bin/lib/agent-command-router.cjs +20 -5
  26. package/gsd-core/bin/lib/api-coverage.cjs +360 -53
  27. package/gsd-core/bin/lib/audit.cjs +8 -8
  28. package/gsd-core/bin/lib/broken-windows.cjs +716 -0
  29. package/gsd-core/bin/lib/capability-command-router.cjs +733 -0
  30. package/gsd-core/bin/lib/capability-consent.cjs +40 -1
  31. package/gsd-core/bin/lib/capability-lifecycle.cjs +58 -0
  32. package/gsd-core/bin/lib/capability-loader.cjs +23 -1
  33. package/gsd-core/bin/lib/capability-registry.cjs +1450 -160
  34. package/gsd-core/bin/lib/capability-trust.cjs +468 -33
  35. package/gsd-core/bin/lib/capability-validator.cjs +882 -6
  36. package/gsd-core/bin/lib/capability-writer.cjs +6 -1
  37. package/gsd-core/bin/lib/check-command-router.cjs +140 -27
  38. package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +15 -0
  39. package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +209 -31
  40. package/gsd-core/bin/lib/claude-orchestration.cjs +203 -25
  41. package/gsd-core/bin/lib/command-aliases.cjs +14 -0
  42. package/gsd-core/bin/lib/commands.cjs +326 -21
  43. package/gsd-core/bin/lib/config-loader.cjs +214 -30
  44. package/gsd-core/bin/lib/config.cjs +158 -22
  45. package/gsd-core/bin/lib/core-utils.cjs +6 -1
  46. package/gsd-core/bin/lib/decisions.cjs +32 -8
  47. package/gsd-core/bin/lib/docs.cjs +6 -0
  48. package/gsd-core/bin/lib/estimate-cli.cjs +336 -0
  49. package/gsd-core/bin/lib/external-descriptor-trust.cjs +14 -2
  50. package/gsd-core/bin/lib/frontmatter.cjs +125 -15
  51. package/gsd-core/bin/lib/gap-checker.cjs +17 -2
  52. package/gsd-core/bin/lib/host-integration.cjs +215 -8
  53. package/gsd-core/bin/lib/init.cjs +155 -66
  54. package/gsd-core/bin/lib/install-engine.cjs +299 -23
  55. package/gsd-core/bin/lib/install-profiles.cjs +239 -1
  56. package/gsd-core/bin/lib/installer-migrations/005-opencode-baseline-commands-dir.cjs +146 -0
  57. package/gsd-core/bin/lib/installer-migrations/006-pi-extension-cjs-to-js.cjs +91 -0
  58. package/gsd-core/bin/lib/installer-migrations.cjs +44 -5
  59. package/gsd-core/bin/lib/markdown-sectionizer.cjs +107 -0
  60. package/gsd-core/bin/lib/milestone.cjs +248 -14
  61. package/gsd-core/bin/lib/model-catalog.cjs +69 -4
  62. package/gsd-core/bin/lib/model-resolver.cjs +189 -7
  63. package/gsd-core/bin/lib/observability/logger.cjs +7 -2
  64. package/gsd-core/bin/lib/onboard-projection.cjs +11 -8
  65. package/gsd-core/bin/lib/phase-command-router.cjs +10 -1
  66. package/gsd-core/bin/lib/phase-estimation.cjs +398 -0
  67. package/gsd-core/bin/lib/phase-id.cjs +304 -9
  68. package/gsd-core/bin/lib/phase.cjs +258 -17
  69. package/gsd-core/bin/lib/plan-drift-guard.cjs +1 -1
  70. package/gsd-core/bin/lib/plan-scan.cjs +70 -2
  71. package/gsd-core/bin/lib/planning-workspace.cjs +9 -2
  72. package/gsd-core/bin/lib/profile-output.cjs +34 -8
  73. package/gsd-core/bin/lib/review-lane-descriptor.cjs +927 -0
  74. package/gsd-core/bin/lib/review-lane-invocation.cjs +348 -0
  75. package/gsd-core/bin/lib/review-lane-runner.cjs +594 -0
  76. package/gsd-core/bin/lib/review-reviewer-selection.cjs +114 -32
  77. package/gsd-core/bin/lib/roadmap-parser.cjs +61 -10
  78. package/gsd-core/bin/lib/roadmap.cjs +23 -7
  79. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +38 -5
  80. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +23 -9
  81. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +156 -0
  82. package/gsd-core/bin/lib/runtime-name-policy.cjs +15 -2
  83. package/gsd-core/bin/lib/smart-entry.cjs +70 -5
  84. package/gsd-core/bin/lib/state-document.cjs +171 -24
  85. package/gsd-core/bin/lib/state-transition.cjs +50 -11
  86. package/gsd-core/bin/lib/state.cjs +206 -32
  87. package/gsd-core/bin/lib/surface.cjs +51 -9
  88. package/gsd-core/bin/lib/uat-predicate.cjs +6 -4
  89. package/gsd-core/bin/lib/uat.cjs +428 -11
  90. package/gsd-core/bin/lib/ui-consideration-probe.cjs +2 -2
  91. package/gsd-core/bin/lib/unusable-input.cjs +216 -0
  92. package/gsd-core/bin/lib/validate.cjs +44 -8
  93. package/gsd-core/bin/lib/verification.cjs +163 -31
  94. package/gsd-core/bin/lib/verify.cjs +348 -42
  95. package/gsd-core/bin/lib/worktree-safety.cjs +360 -15
  96. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  97. package/gsd-core/bin/shared/config-schema.manifest.json +4 -15
  98. package/gsd-core/bin/shared/model-catalog.json +5 -0
  99. package/gsd-core/bin/shared/runtime-aliases.manifest.json +5 -0
  100. package/gsd-core/references/api-coverage.md +37 -7
  101. package/gsd-core/references/checkpoints.md +1 -1
  102. package/gsd-core/references/common-bug-patterns.md +13 -0
  103. package/gsd-core/references/context-budget.md +40 -0
  104. package/gsd-core/references/debugger-bug-taxonomy.md +111 -0
  105. package/gsd-core/references/debugger-fix-acceptance.md +157 -0
  106. package/gsd-core/references/debugger-philosophy.md +1 -0
  107. package/gsd-core/references/debugger-prevention.md +98 -0
  108. package/gsd-core/references/debugger-rca-branching.md +98 -0
  109. package/gsd-core/references/debugger-repro-hardening.md +130 -0
  110. package/gsd-core/references/debugger-sbfl.md +110 -0
  111. package/gsd-core/references/debugger-semantic-recall.md +81 -0
  112. package/gsd-core/references/execute-phase-quota-recovery.md +55 -0
  113. package/gsd-core/references/execute-phase-requirement-revert.md +8 -0
  114. package/gsd-core/references/execute-phase-response-language.md +7 -0
  115. package/gsd-core/references/gate-prompts.md +6 -3
  116. package/gsd-core/references/model-profile-resolution.md +64 -13
  117. package/gsd-core/references/offer-next.md +88 -0
  118. package/gsd-core/references/planner-antipatterns.md +6 -0
  119. package/gsd-core/references/planner-mvp-mode.md +12 -13
  120. package/gsd-core/references/planner-preconditions.md +156 -0
  121. package/gsd-core/references/planner-reversibility.md +132 -0
  122. package/gsd-core/references/planning-config.md +2 -1
  123. package/gsd-core/references/reviewer-instances.md +28 -19
  124. package/gsd-core/references/runtime-aware-dispatch.md +42 -0
  125. package/gsd-core/references/skeleton-template.md +1 -1
  126. package/gsd-core/references/thinking-models-planning.md +3 -1
  127. package/gsd-core/references/ui-consideration-probe.md +2 -2
  128. package/gsd-core/references/worktree-branch-check.md +4 -4
  129. package/gsd-core/templates/DEBUG.md +5 -3
  130. package/gsd-core/templates/summary-minimal.md +4 -0
  131. package/gsd-core/templates/summary-standard.md +4 -0
  132. package/gsd-core/templates/summary.md +7 -0
  133. package/gsd-core/workflows/add-phase.md +2 -0
  134. package/gsd-core/workflows/add-tests.md +3 -1
  135. package/gsd-core/workflows/add-todo.md +32 -1
  136. package/gsd-core/workflows/ai-integration-phase.md +8 -6
  137. package/gsd-core/workflows/audit-fix.md +6 -2
  138. package/gsd-core/workflows/audit-milestone.md +8 -0
  139. package/gsd-core/workflows/autonomous.md +19 -15
  140. package/gsd-core/workflows/check-todos.md +5 -3
  141. package/gsd-core/workflows/cleanup.md +7 -1
  142. package/gsd-core/workflows/code-review-fix.md +14 -6
  143. package/gsd-core/workflows/code-review.md +93 -24
  144. package/gsd-core/workflows/complete-milestone.md +3 -0
  145. package/gsd-core/workflows/debug.md +35 -7
  146. package/gsd-core/workflows/diagnose-issues.md +5 -1
  147. package/gsd-core/workflows/discovery-phase.md +7 -0
  148. package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -4
  149. package/gsd-core/workflows/discuss-phase/modes/auto.md +0 -6
  150. package/gsd-core/workflows/discuss-phase/templates/context.md +16 -2
  151. package/gsd-core/workflows/discuss-phase-assumptions.md +18 -9
  152. package/gsd-core/workflows/discuss-phase.md +2 -2
  153. package/gsd-core/workflows/do.md +7 -1
  154. package/gsd-core/workflows/docs-update.md +9 -0
  155. package/gsd-core/workflows/eval-review.md +4 -1
  156. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +4 -0
  157. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +160 -0
  158. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +4 -4
  159. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -2
  160. package/gsd-core/workflows/execute-phase.md +110 -149
  161. package/gsd-core/workflows/execute-plan.md +20 -8
  162. package/gsd-core/workflows/explore.md +4 -0
  163. package/gsd-core/workflows/extract-learnings.md +21 -0
  164. package/gsd-core/workflows/graduation.md +3 -0
  165. package/gsd-core/workflows/health.md +7 -1
  166. package/gsd-core/workflows/help/modes/full.md +9 -5
  167. package/gsd-core/workflows/import.md +11 -2
  168. package/gsd-core/workflows/inbox.md +7 -0
  169. package/gsd-core/workflows/ingest-docs.md +19 -10
  170. package/gsd-core/workflows/manager.md +3 -1
  171. package/gsd-core/workflows/map-codebase.md +17 -10
  172. package/gsd-core/workflows/mvp-phase.md +3 -0
  173. package/gsd-core/workflows/new-milestone.md +79 -23
  174. package/gsd-core/workflows/new-project.md +28 -19
  175. package/gsd-core/workflows/new-workspace.md +3 -1
  176. package/gsd-core/workflows/next.md +5 -2
  177. package/gsd-core/workflows/onboard.md +3 -0
  178. package/gsd-core/workflows/plan-phase.md +56 -51
  179. package/gsd-core/workflows/plan-review-convergence.md +61 -12
  180. package/gsd-core/workflows/plant-seed.md +3 -0
  181. package/gsd-core/workflows/profile-user.md +7 -1
  182. package/gsd-core/workflows/progress.md +31 -3
  183. package/gsd-core/workflows/quick.md +33 -10
  184. package/gsd-core/workflows/remove-workspace.md +3 -0
  185. package/gsd-core/workflows/review.md +172 -585
  186. package/gsd-core/workflows/scan.md +10 -2
  187. package/gsd-core/workflows/secure-phase.md +13 -2
  188. package/gsd-core/workflows/settings-integrations.md +3 -0
  189. package/gsd-core/workflows/settings.md +3 -0
  190. package/gsd-core/workflows/ship.md +88 -11
  191. package/gsd-core/workflows/sketch.md +3 -0
  192. package/gsd-core/workflows/smart-entry.md +4 -1
  193. package/gsd-core/workflows/spike.md +7 -1
  194. package/gsd-core/workflows/ui-phase.md +11 -2
  195. package/gsd-core/workflows/ui-review.md +11 -1
  196. package/gsd-core/workflows/undo.md +7 -0
  197. package/gsd-core/workflows/update.md +106 -5
  198. package/gsd-core/workflows/validate-phase.md +13 -2
  199. package/gsd-core/workflows/verify-phase.md +2 -2
  200. package/gsd-core/workflows/verify-work.md +15 -4
  201. package/hooks/dist/gsd-context-monitor.js +27 -9
  202. package/hooks/dist/gsd-cursor-session-start.js +6 -2
  203. package/hooks/dist/gsd-cursor-stop.js +6 -2
  204. package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
  205. package/hooks/dist/gsd-graphify-update.sh +9 -0
  206. package/hooks/dist/gsd-phase-boundary.sh +14 -2
  207. package/hooks/dist/gsd-prompt-guard.js +101 -2
  208. package/hooks/dist/gsd-read-guard.js +100 -2
  209. package/hooks/dist/gsd-read-injection-scanner.js +109 -2
  210. package/hooks/dist/gsd-statusline.js +97 -9
  211. package/hooks/dist/gsd-workflow-guard.js +110 -6
  212. package/hooks/dist/gsd-worktree-path-guard.js +132 -8
  213. package/hooks/dist/lib/cursor-workspace.js +74 -0
  214. package/hooks/gsd-context-monitor.js +27 -9
  215. package/hooks/gsd-cursor-session-start.js +6 -2
  216. package/hooks/gsd-cursor-stop.js +6 -2
  217. package/hooks/gsd-cursor-subagent-start.js +6 -2
  218. package/hooks/gsd-graphify-update.sh +9 -0
  219. package/hooks/gsd-phase-boundary.sh +14 -2
  220. package/hooks/gsd-prompt-guard.js +101 -2
  221. package/hooks/gsd-read-guard.js +100 -2
  222. package/hooks/gsd-read-injection-scanner.js +109 -2
  223. package/hooks/gsd-statusline.js +97 -9
  224. package/hooks/gsd-workflow-guard.js +110 -6
  225. package/hooks/gsd-worktree-path-guard.js +132 -8
  226. package/hooks/lib/cursor-workspace.js +74 -0
  227. package/package.json +10 -8
  228. package/pi/gsd.cjs +34 -3
  229. package/scripts/changeset/lint.cjs +1 -0
  230. package/scripts/changeset/parse.cjs +26 -0
  231. package/scripts/check-coverage-gate.cjs +51 -0
  232. package/scripts/check-glossary-refs.cjs +244 -0
  233. package/scripts/ci-rebase-check.cjs +48 -4
  234. package/scripts/ci-test-scope.cjs +67 -17
  235. package/scripts/gen-adr-index.cjs +528 -0
  236. package/scripts/gen-capability-matrix.cjs +26 -2
  237. package/scripts/gen-capability-registry.cjs +132 -34
  238. package/scripts/gen-emitted-baseline.cjs +145 -0
  239. package/scripts/gen-test-timings.cjs +201 -0
  240. package/scripts/lint-compiled-artifact-sync.cjs +146 -0
  241. package/scripts/lint-emitted-drift-ack.cjs +149 -0
  242. package/scripts/lint-fix-has-regression-test.cjs +131 -0
  243. package/scripts/lint-portable-timeout.cjs +140 -0
  244. package/scripts/lint-resolution-provenance.cjs +9 -0
  245. package/scripts/lint-test-file-count.allowlist.json +1 -0
  246. package/scripts/mutation-matrix.cjs +4 -0
  247. package/scripts/prompt-injection-scan.sh +6 -0
  248. package/scripts/registry-schema.cjs +57 -8
  249. package/scripts/release-notes/conventional-title.cjs +19 -1
  250. package/scripts/release-notes/format-github-release-notes.cjs +7 -3
  251. package/scripts/release-tarball-smoke.cjs +18 -11
  252. package/scripts/run-tests.cjs +420 -58
  253. package/scripts/workflow-size.cjs +16 -8
  254. package/skills/gsd-ai-integration-phase/SKILL.md +1 -1
  255. package/skills/gsd-mempalace-capture/SKILL.md +9 -5
  256. package/skills/gsd-new-milestone/SKILL.md +1 -1
  257. package/skills/gsd-plan-phase/SKILL.md +5 -3
  258. package/skills/gsd-plan-review-convergence/SKILL.md +7 -2
  259. package/vscode/package.json +1 -1
  260. package/scripts/gen-golden-install-parity-zcode.cjs +0 -77
  261. package/scripts/update-size-baseline.cjs +0 -68
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lint-compiled-artifact-sync — fail when a *tracked* compiled artifact under
4
+ * gsd-core/bin/lib/ has drifted from its src/*.cts source.
5
+ *
6
+ * ADR-457 compiles src/*.cts to gsd-core/bin/lib/*.cjs at build time and expects
7
+ * those artifacts to be gitignored. Most are (see .gitignore). A handful are
8
+ * still tracked because the migration that moved the module into src/ did not
9
+ * also add the emitted .cjs to .gitignore. While a compiled artifact remains
10
+ * tracked, the committed bytes are what ships to anyone who reads the repo
11
+ * without building — so they must match the source.
12
+ *
13
+ * #2653: gsd-core/bin/lib/api-coverage.cjs sat four days behind its .cts after
14
+ * PR #2551 changed the source without regenerating the artifact, shipping a
15
+ * module that silently lacked the entire #2366 fix while CI stayed green.
16
+ *
17
+ * This check is deliberately regime-agnostic: it asserts a property of whatever
18
+ * is tracked right now. If the remaining artifacts are later untracked and
19
+ * gitignored (the ADR-457 end state), the tracked set becomes empty and this
20
+ * script passes trivially — no edit required.
21
+ *
22
+ * Usage: node scripts/lint-compiled-artifact-sync.cjs [--check]
23
+ * Exit 0 when every tracked artifact matches a fresh compile; 1 otherwise.
24
+ */
25
+
26
+ 'use strict';
27
+
28
+ const { execFileSync } = require('node:child_process');
29
+ const fs = require('node:fs');
30
+ const os = require('node:os');
31
+ const path = require('node:path');
32
+
33
+ const REPO_ROOT = path.resolve(__dirname, '..');
34
+ const LIB_DIR = path.join('gsd-core', 'bin', 'lib');
35
+ const SRC_DIR = 'src';
36
+
37
+ /** Frozen reason codes so tests assert on structure, not prose. */
38
+ const REASON = Object.freeze({
39
+ OK: 'ok_artifacts_in_sync',
40
+ DRIFTED: 'fail_artifact_drifted',
41
+ BUILD_FAILED: 'fail_build_failed',
42
+ MISSING_EMIT: 'fail_missing_emit',
43
+ });
44
+
45
+ function git(args) {
46
+ return execFileSync('git', args, { cwd: REPO_ROOT, encoding: 'utf8' });
47
+ }
48
+
49
+ /**
50
+ * Tracked .cjs files under gsd-core/bin/lib/ that have a matching src/*.cts.
51
+ * Uses `git ls-files` so the set is derived from what git actually tracks
52
+ * rather than a hand-maintained list that could itself drift.
53
+ */
54
+ function trackedCompiledArtifacts() {
55
+ const tracked = git(['ls-files', LIB_DIR]).split('\n').filter((l) => l.endsWith('.cjs'));
56
+ const out = [];
57
+ for (const rel of tracked) {
58
+ const stem = path.basename(rel, '.cjs');
59
+ const subdir = path.dirname(path.relative(LIB_DIR, rel));
60
+ const srcRel = path.join(SRC_DIR, subdir === '.' ? '' : subdir, `${stem}.cts`);
61
+ if (fs.existsSync(path.join(REPO_ROOT, srcRel))) out.push({ artifact: rel, source: srcRel });
62
+ }
63
+ return out.sort((a, b) => (a.artifact < b.artifact ? -1 : a.artifact > b.artifact ? 1 : 0));
64
+ }
65
+
66
+ /** Compile the whole project to a throwaway outDir so the work tree is untouched. */
67
+ function compileToTemp() {
68
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-artifact-sync-'));
69
+ try {
70
+ execFileSync(
71
+ process.execPath,
72
+ [
73
+ path.join(REPO_ROOT, 'node_modules', 'typescript', 'bin', 'tsc'),
74
+ '-p', path.join(REPO_ROOT, 'tsconfig.build.json'),
75
+ '--outDir', tmp,
76
+ // A throwaway outDir must not reuse the in-tree incremental state, or
77
+ // tsc skips emit for files it believes are already current.
78
+ '--incremental', 'false',
79
+ '--tsBuildInfoFile', 'null',
80
+ ],
81
+ { cwd: REPO_ROOT, encoding: 'utf8', stdio: 'pipe' },
82
+ );
83
+ return { ok: true, dir: tmp };
84
+ } catch (err) {
85
+ fs.rmSync(tmp, { recursive: true, force: true });
86
+ const detail = [err.stdout, err.stderr].filter(Boolean).join('\n').trim();
87
+ return { ok: false, detail };
88
+ }
89
+ }
90
+
91
+ function main() {
92
+ const pairs = trackedCompiledArtifacts();
93
+ if (pairs.length === 0) {
94
+ console.log('ok compiled-artifact-sync: no tracked compiled artifacts (ADR-457 end state)');
95
+ return 0;
96
+ }
97
+
98
+ const build = compileToTemp();
99
+ if (!build.ok) {
100
+ console.error(`FAIL compiled-artifact-sync: ${REASON.BUILD_FAILED}`);
101
+ console.error(build.detail);
102
+ return 1;
103
+ }
104
+
105
+ const drifted = [];
106
+ const missing = [];
107
+ try {
108
+ for (const { artifact, source } of pairs) {
109
+ const fresh = path.join(build.dir, path.relative(LIB_DIR, artifact));
110
+ if (!fs.existsSync(fresh)) { missing.push({ artifact, source }); continue; }
111
+ const a = fs.readFileSync(path.join(REPO_ROOT, artifact));
112
+ const b = fs.readFileSync(fresh);
113
+ if (!a.equals(b)) drifted.push({ artifact, source, committed: a.length, expected: b.length });
114
+ }
115
+ } finally {
116
+ fs.rmSync(build.dir, { recursive: true, force: true });
117
+ }
118
+
119
+ if (missing.length > 0) {
120
+ console.error(`FAIL compiled-artifact-sync: ${REASON.MISSING_EMIT}`);
121
+ for (const m of missing) console.error(` ${m.artifact} — no emit produced from ${m.source}`);
122
+ return 1;
123
+ }
124
+
125
+ if (drifted.length > 0) {
126
+ console.error(`FAIL compiled-artifact-sync: ${REASON.DRIFTED}`);
127
+ for (const d of drifted) {
128
+ console.error(` ${d.artifact} (${d.committed} bytes) != compile of ${d.source} (${d.expected} bytes)`);
129
+ }
130
+ console.error('');
131
+ console.error('The committed artifact is what ships to anyone reading the repo without');
132
+ console.error('building, so it must match its source. Fix with:');
133
+ console.error(' npm run build:lib && git add ' + drifted.map((d) => d.artifact).join(' '));
134
+ console.error('');
135
+ console.error('Alternatively, per ADR-457 these artifacts are meant to be gitignored —');
136
+ console.error('untracking them (git rm --cached + .gitignore) also resolves this.');
137
+ return 1;
138
+ }
139
+
140
+ console.log(`ok compiled-artifact-sync: ${pairs.length} tracked artifact(s) match their source`);
141
+ return 0;
142
+ }
143
+
144
+ if (require.main === module) process.exitCode = main();
145
+
146
+ module.exports = { REASON, trackedCompiledArtifacts };
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * lint-emitted-drift-ack — refuse to merge a broken emitted-drift acknowledgment (#2789).
6
+ *
7
+ * The ack document is read from TWO sides: the working tree (`readAckFile`) and the BASE
8
+ * REF (`readAckFileAtRef`), because an entry already present at the base is spent and may
9
+ * no longer clear a delta. The base-side read fails LOUDLY on a document it cannot parse
10
+ * — it has to, since silently inheriting nothing would leave every entry able to consume
11
+ * a delta, which is the pre-#2789 gate.
12
+ *
13
+ * That makes a corrupt document ON THE BASE BRANCH unusually expensive: it reds every PR
14
+ * that carries an ack until someone repairs it. The real-tree test avoids an outright
15
+ * deadlock (a tree with no ack never consults the base, so the repair PR still lands),
16
+ * but the cheaper answer is to never let a broken document reach the base at all. This
17
+ * runs in `lint:ci`, so a PR carrying one cannot go green and cannot merge.
18
+ *
19
+ * This validator is DELIBERATELY STANDALONE. `scripts/` ships in the npm package and
20
+ * `tests/` does not (package.json `files`), so requiring the gate's own `parseAck` from
21
+ * here would be a MODULE_NOT_FOUND in the published package. The duplication is bounded
22
+ * by a parity test — `tests/emitted-attribution.test.cjs` runs both surfaces over one
23
+ * corpus and fails if they ever disagree about what is schema-valid.
24
+ */
25
+
26
+ const fs = require('node:fs');
27
+ const path = require('node:path');
28
+
29
+ const ACK_VERSION = 1;
30
+ const ACK_REPO_PATH = 'tests/emitted-drift-ack.json';
31
+ const REPO_ROOT = path.join(__dirname, '..');
32
+
33
+ const isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
34
+
35
+ /**
36
+ * Validate an ack document's raw text.
37
+ *
38
+ * `schemaErrors` are the ones that must agree with the gate's `parseAck` — the shape
39
+ * contract. `policyErrors` are lint-only rules that `parseAck` deliberately does NOT
40
+ * enforce, because they are about what may be COMMITTED rather than what may be parsed:
41
+ * a present-but-entryless document parses fine and signals nothing, so it must be deleted
42
+ * rather than left behind.
43
+ *
44
+ * @param {string|null} raw file contents, or null when the file is absent
45
+ * @returns {{ schemaErrors: string[], policyErrors: string[], ok: boolean }}
46
+ */
47
+ function validateAckText(raw) {
48
+ const schemaErrors = [];
49
+ const policyErrors = [];
50
+ const done = () => ({ schemaErrors, policyErrors, ok: schemaErrors.length === 0 && policyErrors.length === 0 });
51
+
52
+ if (raw === null) return done(); // absent is the healthy steady state
53
+
54
+ if (raw.trim() === '') {
55
+ schemaErrors.push(`${ACK_REPO_PATH} is present but empty`);
56
+ return done();
57
+ }
58
+
59
+ let doc;
60
+ try {
61
+ doc = JSON.parse(raw);
62
+ } catch (err) {
63
+ schemaErrors.push(`${ACK_REPO_PATH} is not valid JSON: ${err.message}`);
64
+ return done();
65
+ }
66
+
67
+ // A document that is literally `null` is POLICY, not schema. The gate's `parseAck` uses
68
+ // `null` as its "absent == no acks" sentinel, so it reads such a file as legal and
69
+ // harmless — and the parity test holds us to that. It is still not something to commit:
70
+ // it declares nothing, so the remedy is the same as an entryless document.
71
+ if (doc === null) {
72
+ policyErrors.push(
73
+ `${ACK_REPO_PATH} contains "null" and declares no acknowledgments. Delete the file — `
74
+ + 'the healthy steady state is no file at all.',
75
+ );
76
+ return done();
77
+ }
78
+
79
+ if (!isPlainObject(doc)) {
80
+ schemaErrors.push(
81
+ `${ACK_REPO_PATH}: must be a JSON object, got ${Array.isArray(doc) ? 'array' : typeof doc}`,
82
+ );
83
+ return done();
84
+ }
85
+
86
+ if (doc.version !== undefined && doc.version !== ACK_VERSION) {
87
+ schemaErrors.push(
88
+ `${ACK_REPO_PATH}: unsupported version ${JSON.stringify(doc.version)} (expected ${ACK_VERSION})`,
89
+ );
90
+ }
91
+
92
+ const paths = doc.paths;
93
+ if (paths !== undefined && !isPlainObject(paths)) {
94
+ schemaErrors.push(`${ACK_REPO_PATH}: "paths" must be an object of <emitted path> -> { reason }`);
95
+ return done();
96
+ }
97
+
98
+ const entries = paths === undefined ? [] : Object.entries(paths);
99
+ for (const [rel, value] of entries) {
100
+ const reason = isPlainObject(value) ? value.reason : value;
101
+ if (typeof reason !== 'string' || reason.trim() === '') {
102
+ schemaErrors.push(`${ACK_REPO_PATH}: ack for "${rel}" has no non-empty "reason"`);
103
+ }
104
+ }
105
+
106
+ // Lint-only. An entryless document parses cleanly and acknowledges nothing, so it is
107
+ // pure confusion on the base branch — and it is exactly what a contributor leaves
108
+ // behind after removing the last entry by hand.
109
+ if (entries.length === 0) {
110
+ policyErrors.push(
111
+ `${ACK_REPO_PATH} is present but declares no acknowledgments. Delete the file — an `
112
+ + 'empty one signals nothing, and the healthy steady state is no file at all.',
113
+ );
114
+ }
115
+
116
+ return done();
117
+ }
118
+
119
+ function readIfPresent(file) {
120
+ return fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null;
121
+ }
122
+
123
+ function main() {
124
+ const file = path.join(REPO_ROOT, ...ACK_REPO_PATH.split('/'));
125
+ const result = validateAckText(readIfPresent(file));
126
+ const all = [...result.schemaErrors, ...result.policyErrors];
127
+
128
+ if (all.length) {
129
+ console.error(`lint-emitted-drift-ack: ${all.length} problem(s) in ${ACK_REPO_PATH}\n`);
130
+ for (const e of all) console.error(` - ${e}`);
131
+ console.error(
132
+ '\nThis blocks the merge on purpose. The base-side reader fails loudly on a document '
133
+ + 'it cannot parse, so a broken one on the base branch reds every PR that carries an '
134
+ + 'acknowledgment. Fix or delete the file here, where it is cheap.',
135
+ );
136
+ process.exitCode = 1;
137
+ return;
138
+ }
139
+
140
+ console.log(
141
+ fs.existsSync(file)
142
+ ? `ok lint-emitted-drift-ack: ${ACK_REPO_PATH} is well-formed`
143
+ : `ok lint-emitted-drift-ack: ${ACK_REPO_PATH} absent (the healthy steady state)`,
144
+ );
145
+ }
146
+
147
+ if (require.main === module) main();
148
+
149
+ module.exports = { validateAckText, ACK_VERSION, ACK_REPO_PATH };
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * lint-fix-has-regression-test.cjs — gate: every fix(#NNNN) commit must
6
+ * include at least one behavioral test file (tests/*.test.cjs) that is NOT
7
+ * an auto-generated fixture/baseline.
8
+ *
9
+ * ## Why
10
+ *
11
+ * CONTRIBUTING.md:47: "Fix it. Write a test that would have caught the bug."
12
+ * CLAUDE.md: "Regression Protocol: Write the regression test first."
13
+ *
14
+ * Three merged PRs shipped without regression tests because the golden-fixture
15
+ * regeneration (20+ files under tests/) camouflaged the absence of real
16
+ * *.test.cjs changes. This gate makes the rule machine-enforced.
17
+ *
18
+ * ## What this enforces
19
+ *
20
+ * For every commit in `git log origin/next..HEAD` whose subject matches
21
+ * /^fix\(#\d+\)/ or /^feat\(#\d+\)/, the cumulative diff must include at
22
+ * least one file under tests/ matching /\.test\.cjs$/ that is NOT:
23
+ * - under tests/fixtures/
24
+ * - under tests/install-tree/
25
+ * - a *-baseline.json file
26
+ *
27
+ * If the filtered list is empty, the gate fails with a message naming the
28
+ * fix commits and directing the author to add a regression test.
29
+ *
30
+ * ## Overrides
31
+ *
32
+ * Set GSD_SKIP_REGRESSION_TEST_GATE=1 for legitimate exceptions (e.g. a
33
+ * pure-config or pure-refactor PR where no behavioral test is possible).
34
+ * This env var is auditable in CI logs.
35
+ */
36
+
37
+ const { execSync } = require('child_process');
38
+ const { ExitError, runMain } = require('./lib/cli-exit.cjs');
39
+
40
+ const FIX_OR_FEAT_RE = /^(?:fix|feat)\(#(\d+)\)/;
41
+
42
+ const EXCLUDE_PATTERNS = [
43
+ /tests[\\/]fixtures[\\/]/,
44
+ /tests[\\/]install-tree[\\/]/,
45
+ /-baseline\.json$/,
46
+ ];
47
+
48
+ function isRealTestFile(filePath) {
49
+ if (!filePath.includes('tests/')) return false;
50
+ if (!/\.test\.cjs$/.test(filePath)) return false;
51
+ return !EXCLUDE_PATTERNS.some((re) => re.test(filePath));
52
+ }
53
+
54
+ function getFixCommits(baseRef) {
55
+ const log = execSync(
56
+ `git log ${baseRef}..HEAD --format='%H%x09%s' --no-merges`,
57
+ { encoding: 'utf8', timeout: 10000 },
58
+ );
59
+ return log
60
+ .trim()
61
+ .split('\n')
62
+ .filter(Boolean)
63
+ .map((line) => {
64
+ const [sha, subject] = line.split('\t');
65
+ const match = subject.match(FIX_OR_FEAT_RE);
66
+ return match ? { sha: sha.slice(0, 12), subject, issue: match[1] } : null;
67
+ })
68
+ .filter(Boolean);
69
+ }
70
+
71
+ function getChangedTestFiles(baseRef) {
72
+ const diff = execSync(
73
+ `git diff ${baseRef}...HEAD --name-only --no-merges`,
74
+ { encoding: 'utf8', timeout: 10000 },
75
+ );
76
+ return diff
77
+ .trim()
78
+ .split('\n')
79
+ .filter(Boolean)
80
+ .filter(isRealTestFile);
81
+ }
82
+
83
+ function main() {
84
+ if (process.env.GSD_SKIP_REGRESSION_TEST_GATE === '1') {
85
+ console.log('lint-fix-has-regression-test: SKIPPED (GSD_SKIP_REGRESSION_TEST_GATE=1)');
86
+ return;
87
+ }
88
+
89
+ const baseRef = process.env.GSD_REGRESSION_GATE_BASE || 'origin/next';
90
+
91
+ let fixCommits;
92
+ try {
93
+ fixCommits = getFixCommits(baseRef);
94
+ } catch {
95
+ console.log(`lint-fix-has-regression-test: no fix/feat commits found vs ${baseRef}, skipping`);
96
+ return;
97
+ }
98
+
99
+ if (fixCommits.length === 0) {
100
+ console.log('lint-fix-has-regression-test: no fix/feat commits, passing');
101
+ return;
102
+ }
103
+
104
+ let testFiles;
105
+ try {
106
+ testFiles = getChangedTestFiles(baseRef);
107
+ } catch {
108
+ testFiles = [];
109
+ }
110
+
111
+ if (testFiles.length === 0) {
112
+ const commitList = fixCommits
113
+ .map((c) => ` ${c.sha} ${c.subject}`)
114
+ .join('\n');
115
+ throw new ExitError(1,
116
+ `lint-fix-has-regression-test: ${fixCommits.length} fix/feat commit(s) but ZERO behavioral test files (*.test.cjs) in the diff.\n` +
117
+ `Auto-generated fixtures (tests/fixtures/, *-baseline.json) do NOT count.\n\n` +
118
+ `Fix commits:\n${commitList}\n\n` +
119
+ `CONTRIBUTING.md:47: "Write a test that would have caught the bug."\n` +
120
+ `Add a regression test to an existing tests/*.test.cjs file, or set ` +
121
+ `GSD_SKIP_REGRESSION_TEST_GATE=1 if no behavioral test is possible (auditable in CI).`
122
+ );
123
+ }
124
+
125
+ console.log(
126
+ `lint-fix-has-regression-test: PASS — ${fixCommits.length} fix/feat commit(s), ` +
127
+ `${testFiles.length} behavioral test file(s): ${testFiles.join(', ')}`
128
+ );
129
+ }
130
+
131
+ runMain(main);
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * lint-portable-timeout.cjs — ban hardcoded GNU-`timeout` in gsd
6
+ * workflow / agent / reference / command markdown (#2351).
7
+ *
8
+ * ## Why
9
+ *
10
+ * `timeout` and `gtimeout` are GNU coreutils. Stock macOS ships NEITHER
11
+ * (`brew install coreutils` only provides `gtimeout`, and only if installed).
12
+ * A hardcoded `timeout <n> <cmd>` inside an executable workflow snippet exits
13
+ * 127 ("command not found") on such a host, and the gate that runs it — which
14
+ * only distinguishes 0 (pass) / 124 (timeout) / other (fail) — misreports a
15
+ * perfectly good build or test command as a FAILURE (#2351).
16
+ *
17
+ * The portable, coreutils-independent replacement is the
18
+ * `gsd_run run-with-timeout <secs> [--] <cmd…>` verb (gsd-core/bin/gsd-tools.cjs):
19
+ * a Node-based wall-clock cap that keeps GNU `timeout`'s exit-code contract
20
+ * (124 on timeout) on every platform. The resolution lives there ONCE and is
21
+ * reused by every call site instead of a per-file `command -v timeout` probe.
22
+ *
23
+ * This ratchet fails the build if a NEW bare `timeout`/`gtimeout` execution
24
+ * slips into any of these surfaces.
25
+ *
26
+ * ## What PASSES
27
+ *
28
+ * - `gsd_run run-with-timeout 300 -- bash -c "$CMD"` — the approved verb.
29
+ * - `command -v timeout` / `command -v gtimeout` / `which timeout` capability
30
+ * PROBES — portable: they detect the binary, they do not unconditionally
31
+ * execute it (see gsd-core/workflows/review.md's `_AGY_KILLER` fallback).
32
+ * - Prose ("timed out after 5 minutes"), config keys
33
+ * (`workflow.test_gate_timeout`), CI `timeout-minutes:`, the agy
34
+ * `--print-timeout` flag, `$TIMEOUT`-style variable names — none of which is
35
+ * a bare timeout command invocation.
36
+ *
37
+ * ## What FAILS
38
+ *
39
+ * A bare `timeout <duration> …` / `gtimeout <duration> …` command invocation,
40
+ * where `<duration>` is a number, a `"$VAR"`, or a `${VAR}` (optionally with a
41
+ * leading `-k`/`-s` option).
42
+ */
43
+
44
+ const fs = require('fs');
45
+ const path = require('path');
46
+ const { ExitError, runMain } = require('./lib/cli-exit.cjs');
47
+
48
+ const ROOT = path.join(__dirname, '..');
49
+
50
+ // Surfaces whose markdown carries agent-executed bash. Kept broad so the guard
51
+ // catches a regression anywhere a workflow snippet could bound a command.
52
+ const DEFAULT_ROOTS = ['gsd-core/workflows', 'gsd-core/references', 'agents', 'commands'];
53
+
54
+ // Capability probes to strip BEFORE testing for an invocation, so a portable
55
+ // `command -v timeout` on the same line is never mistaken for a bare execution.
56
+ const PROBE_RE = /command\s+-v\s+g?timeout|which\s+g?timeout/g;
57
+
58
+ // A `timeout`/`gtimeout` token INVOKED AS A COMMAND with a duration argument.
59
+ // Anchored to a command position — line start or right after `| & ; ( ` {` — so
60
+ // prose ("increase the timeout 30 seconds") and the `--print-timeout` flag / the
61
+ // approved `run-with-timeout` verb (no separator before "timeout") never match.
62
+ // Between the token and the duration, allow any number of leading options in
63
+ // short OR long form (`-k5`, `-k 5`, `--kill-after=5`, `--foreground`,
64
+ // `--signal=KILL`). The duration is a digit, `$((arith))`, a `$VAR`, or `${VAR}`.
65
+ const EXEC_RE = /(?:^|[|&;(`{])[ \t]*g?timeout[ \t]+(?:-{1,2}[\w-]+(?:=\S+)?[ \t]+)*["']?(?:\$\{?[A-Za-z_]|\$\(\(|\d)/;
66
+
67
+ /**
68
+ * Locate bare `timeout`/`gtimeout` invocations in a block of text.
69
+ *
70
+ * Pure (no I/O): callers pass the file contents; the caller reads files. Returns
71
+ * structured findings so tests assert on typed values, never on grepped text.
72
+ *
73
+ * @param {string} text file contents
74
+ * @returns {{ line: number, snippet: string }[]} findings (empty array = clean)
75
+ */
76
+ function findRawTimeoutInvocations(text) {
77
+ const findings = [];
78
+ const lines = String(text).split(/\r?\n/);
79
+ for (let i = 0; i < lines.length; i += 1) {
80
+ const stripped = lines[i].replace(PROBE_RE, '');
81
+ if (EXEC_RE.test(stripped)) findings.push({ line: i + 1, snippet: lines[i].trim() });
82
+ }
83
+ return findings;
84
+ }
85
+
86
+ function walkMarkdown(dir) {
87
+ const out = [];
88
+ let entries;
89
+ try {
90
+ entries = fs.readdirSync(dir, { withFileTypes: true });
91
+ } catch {
92
+ return out; // a missing root is not an error — some surfaces are optional
93
+ }
94
+ for (const entry of entries) {
95
+ const full = path.join(dir, entry.name);
96
+ if (entry.isDirectory()) out.push(...walkMarkdown(full));
97
+ else if (entry.isFile() && entry.name.endsWith('.md')) out.push(full);
98
+ }
99
+ return out;
100
+ }
101
+
102
+ /**
103
+ * Scan the given roots (repo-relative) for bare timeout invocations.
104
+ * @param {string[]} roots
105
+ * @returns {{ file: string, line: number, snippet: string }[]}
106
+ */
107
+ function scan(roots = DEFAULT_ROOTS) {
108
+ const offenders = [];
109
+ for (const rel of roots) {
110
+ const abs = path.isAbsolute(rel) ? rel : path.join(ROOT, rel);
111
+ for (const file of walkMarkdown(abs)) {
112
+ const findings = findRawTimeoutInvocations(fs.readFileSync(file, 'utf8'));
113
+ for (const f of findings) {
114
+ offenders.push({ file: path.relative(ROOT, file), line: f.line, snippet: f.snippet });
115
+ }
116
+ }
117
+ }
118
+ return offenders;
119
+ }
120
+
121
+ function main() {
122
+ const rootsEnv = process.env.GSD_LINT_PORTABLE_TIMEOUT_ROOTS;
123
+ const roots = rootsEnv ? rootsEnv.split(path.delimiter).filter(Boolean) : DEFAULT_ROOTS;
124
+ const offenders = scan(roots);
125
+ if (offenders.length > 0) {
126
+ const detail = offenders.map((o) => ` ${o.file}:${o.line} ${o.snippet}`).join('\n');
127
+ throw new ExitError(
128
+ 1,
129
+ 'lint-portable-timeout: hardcoded `timeout`/`gtimeout` is not portable — stock\n' +
130
+ 'macOS ships no coreutils, so these exit 127 and misreport a passing command as a\n' +
131
+ 'failure. Use `gsd_run run-with-timeout <secs> [--] <cmd…>` instead (#2351):\n' +
132
+ detail,
133
+ );
134
+ }
135
+ console.log(`ok lint-portable-timeout: no hardcoded timeout invocations in ${roots.length} root(s)`);
136
+ }
137
+
138
+ module.exports = { findRawTimeoutInvocations, scan, DEFAULT_ROOTS };
139
+
140
+ if (require.main === module) runMain(main);
@@ -56,6 +56,15 @@ const REGISTRY = [
56
56
  sourceFile: 'src/init.cts',
57
57
  testFile: 'tests/agent-skills.test.cjs',
58
58
  },
59
+ {
60
+ // Registered by #1880 (ADR-1411 amendment "corrupt is not absent"). Until
61
+ // this entry existed the config-loader seam carried the provenance contract
62
+ // with nothing guarding it, so a regression that collapsed configured_empty
63
+ // back into not_configured would have shipped silently.
64
+ verb: 'config-loader',
65
+ sourceFile: 'src/config-loader.cts',
66
+ testFile: 'tests/config-loader.test.cjs',
67
+ },
59
68
  ];
60
69
 
61
70
  // Markers that MUST appear in every registered verb's test file.
@@ -60,6 +60,7 @@
60
60
  "state": {
61
61
  "files": [
62
62
  "state-acquirestatelock-non-eexist.test.cjs",
63
+ "state-command-cutover.test.cjs",
63
64
  "state-prune.test.cjs",
64
65
  "state-rebuild-cli.test.cjs",
65
66
  "state-rebuild.test.cjs",
@@ -145,6 +145,10 @@ const COVERED = {
145
145
  tests: [
146
146
  'tests/frontmatter.property.test.cjs',
147
147
  'tests/frontmatter.unit.test.cjs',
148
+ // #1882 added the unterminated-fence detection to frontmatter.cjs, and the tests that
149
+ // constrain it live here. Without this entry the mutants in that branch are covered by
150
+ // no test in the shard, so the module's score drops even though the behaviour is tested.
151
+ 'tests/unusable-input.test.cjs',
148
152
  ],
149
153
  minScore: 62,
150
154
  },
@@ -102,6 +102,12 @@ ALLOWLIST=(
102
102
  # exec command strings (execFileSync('npm', ['install'])) as test DATA the rule
103
103
  # must lint — not attack vectors. ADR-1703 Phase 4 (#1726).
104
104
  'tests/no-bare-npm-exec.rule.test.cjs'
105
+ # #2547 — the Kimi field-shadowing regression proves gsd-prompt-guard still
106
+ # SCANS the reconstructed edit[].new content when a model-supplied new_string
107
+ # tries to shadow it. The fixture must be a real injection phrase or the test
108
+ # asserts nothing: it is the payload the guard is required to catch, carried
109
+ # as test DATA. Same class as the read-injection-scanner suites above.
110
+ 'tests/kimi-payload-field-shadowing.security.test.cjs'
105
111
  )
106
112
 
107
113
  is_allowlisted() {