@opengsd/gsd-core 1.3.1 → 1.4.0-rc.2

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 (136) hide show
  1. package/.claude-plugin/plugin.json +23 -0
  2. package/GEMINI.md +53 -0
  3. package/agents/gsd-advisor-researcher.md +1 -20
  4. package/agents/gsd-ai-researcher.md +2 -21
  5. package/agents/gsd-code-fixer.md +1 -1
  6. package/agents/gsd-code-reviewer.md +1 -1
  7. package/agents/gsd-domain-researcher.md +2 -21
  8. package/agents/gsd-eval-auditor.md +1 -1
  9. package/agents/gsd-eval-planner.md +1 -1
  10. package/agents/gsd-executor.md +1 -1
  11. package/agents/gsd-framework-selector.md +1 -1
  12. package/agents/gsd-nyquist-auditor.md +1 -1
  13. package/agents/gsd-pattern-mapper.md +1 -1
  14. package/agents/gsd-phase-researcher.md +92 -166
  15. package/agents/gsd-planner.md +9 -36
  16. package/agents/gsd-project-researcher.md +62 -141
  17. package/agents/gsd-security-auditor.md +1 -1
  18. package/agents/gsd-ui-auditor.md +1 -1
  19. package/agents/gsd-ui-checker.md +1 -1
  20. package/agents/gsd-ui-researcher.md +3 -22
  21. package/agents/gsd-user-profiler.md +1 -1
  22. package/agents/gsd-verifier.md +8 -2
  23. package/bin/install.js +1977 -339
  24. package/commands/gsd/autonomous.md +2 -0
  25. package/commands/gsd/execute-phase.md +2 -0
  26. package/commands/gsd/graphify.md +11 -6
  27. package/commands/gsd/import.md +6 -2
  28. package/commands/gsd/plan-phase.md +4 -2
  29. package/commands/gsd/progress.md +1 -0
  30. package/commands/gsd/stats.md +1 -0
  31. package/commands/gsd/update.md +3 -2
  32. package/gemini-extension.json +6 -0
  33. package/gsd-core/bin/check-latest-version.cjs +61 -6
  34. package/gsd-core/bin/gsd-tools.cjs +238 -32
  35. package/gsd-core/bin/lib/check-command-router.cjs +1 -0
  36. package/gsd-core/bin/lib/cli-exit.cjs +42 -0
  37. package/gsd-core/bin/lib/command-routing-hub.cjs +1 -1
  38. package/gsd-core/bin/lib/commands.cjs +5 -4
  39. package/gsd-core/bin/lib/config.cjs +28 -4
  40. package/gsd-core/bin/lib/core.cjs +72 -28
  41. package/gsd-core/bin/lib/graphify.cjs +2 -2
  42. package/gsd-core/bin/lib/init-command-router.cjs +2 -2
  43. package/gsd-core/bin/lib/init.cjs +19 -3
  44. package/gsd-core/bin/lib/install-profiles.cjs +58 -0
  45. package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
  46. package/gsd-core/bin/lib/installer-migrations/000-first-time-baseline.cjs +1 -1
  47. package/gsd-core/bin/lib/intel.cjs +3 -20
  48. package/gsd-core/bin/lib/package-legitimacy.cjs +368 -0
  49. package/gsd-core/bin/lib/phase.cjs +3 -3
  50. package/gsd-core/bin/lib/research-provider.cjs +137 -0
  51. package/gsd-core/bin/lib/research-store.cjs +167 -0
  52. package/gsd-core/bin/lib/roadmap-upgrade.cjs +4 -19
  53. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +67 -9
  54. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +56 -0
  55. package/gsd-core/bin/lib/runtime-homes.cjs +32 -11
  56. package/gsd-core/bin/lib/security.cjs +73 -0
  57. package/gsd-core/bin/lib/shell-command-projection.cjs +9 -0
  58. package/gsd-core/bin/lib/surface.cjs +54 -11
  59. package/gsd-core/bin/lib/validate.cjs +2 -2
  60. package/gsd-core/bin/lib/verification-command-router.cjs +31 -0
  61. package/gsd-core/bin/lib/verification.cjs +193 -0
  62. package/gsd-core/bin/lib/verify.cjs +2 -2
  63. package/gsd-core/bin/lib/workstream-inventory.cjs +1 -1
  64. package/gsd-core/bin/lib/worktree-base-ref.cjs +325 -0
  65. package/gsd-core/bin/lib/worktree-safety.cjs +31 -0
  66. package/gsd-core/bin/shared/config-schema.manifest.json +2 -1
  67. package/gsd-core/bin/verify-reapply-patches.cjs +8 -11
  68. package/gsd-core/references/planner-load-graph-context.md +36 -0
  69. package/gsd-core/references/planning-config.md +3 -1
  70. package/gsd-core/references/research-documentation-lookup.md +29 -0
  71. package/gsd-core/references/research-philosophy.md +29 -0
  72. package/gsd-core/references/research-verification-protocol.md +27 -0
  73. package/gsd-core/workflows/execute-phase.md +19 -8
  74. package/gsd-core/workflows/help/modes/full.md +4 -3
  75. package/gsd-core/workflows/ingest-docs.md +3 -2
  76. package/gsd-core/workflows/plan-phase.md +14 -10
  77. package/gsd-core/workflows/plan-review-convergence.md +3 -3
  78. package/gsd-core/workflows/review.md +24 -7
  79. package/gsd-core/workflows/ship.md +5 -8
  80. package/gsd-core/workflows/spec-phase.md +2 -1
  81. package/gsd-core/workflows/update.md +34 -6
  82. package/hooks/dist/gsd-config-reload.js +133 -0
  83. package/hooks/dist/gsd-context-monitor.js +1 -1
  84. package/hooks/dist/gsd-cursor-post-tool.js +75 -0
  85. package/hooks/dist/gsd-cursor-session-start.js +52 -0
  86. package/hooks/dist/gsd-workflow-guard.js +1 -0
  87. package/hooks/dist/gsd-worktree-path-guard.js +1 -1
  88. package/hooks/dist/managed-hooks-registry.cjs +3 -0
  89. package/hooks/gsd-config-reload.js +133 -0
  90. package/hooks/gsd-context-monitor.js +1 -1
  91. package/hooks/gsd-cursor-post-tool.js +75 -0
  92. package/hooks/gsd-cursor-session-start.js +52 -0
  93. package/hooks/gsd-workflow-guard.js +1 -0
  94. package/hooks/gsd-worktree-path-guard.js +1 -1
  95. package/hooks/hooks.json +69 -0
  96. package/hooks/managed-hooks-registry.cjs +3 -0
  97. package/package.json +8 -1
  98. package/scripts/affected-tests-lib.cjs +3 -2
  99. package/scripts/build-hooks.js +7 -0
  100. package/scripts/changeset/cli.cjs +226 -28
  101. package/scripts/changeset/lint.cjs +5 -4
  102. package/scripts/changeset/new.cjs +4 -4
  103. package/scripts/check-alias-drift.cjs +77 -71
  104. package/scripts/check-env.cjs +185 -179
  105. package/scripts/check-npm-integrity.cjs +115 -109
  106. package/scripts/ci-guard-runner.cjs +11 -5
  107. package/scripts/ci-prepare-test-scope.cjs +27 -22
  108. package/scripts/ci-rebase-check.cjs +46 -45
  109. package/scripts/ci-test-scope.cjs +126 -22
  110. package/scripts/diff-touches-shipped-paths.cjs +52 -44
  111. package/scripts/gen-inventory-manifest.cjs +38 -32
  112. package/scripts/gen-research-agents.cjs +276 -0
  113. package/scripts/issue-dedupe.cjs +278 -0
  114. package/scripts/lib/cli-exit.cjs +56 -0
  115. package/scripts/lint-command-contract.cjs +28 -22
  116. package/scripts/lint-descriptions.cjs +32 -28
  117. package/scripts/lint-docs-required.cjs +4 -4
  118. package/scripts/lint-legacy-dir-name.cjs +56 -52
  119. package/scripts/lint-pr-check-project-dir.cjs +3 -1
  120. package/scripts/lint-shell-command-projection-drift.cjs +27 -22
  121. package/scripts/lint-skill-deps.cjs +31 -26
  122. package/scripts/lint-test-file-count.allowlist.json +2 -0
  123. package/scripts/lint-test-file-count.cjs +5 -4
  124. package/scripts/mutation-matrix.cjs +6 -3
  125. package/scripts/prompt-injection-scan.sh +1 -1
  126. package/scripts/release-notes/discord-release-summary.cjs +373 -0
  127. package/scripts/release-notes/format-github-release-notes.cjs +8 -3
  128. package/scripts/release-tarball-smoke.cjs +6 -4
  129. package/scripts/research-profiles.cjs +149 -0
  130. package/scripts/run-affected-tests.cjs +2 -1
  131. package/scripts/run-cross-platform-tests.cjs +11 -7
  132. package/scripts/run-tests.cjs +8 -7
  133. package/scripts/strip-prose-atrefs.cjs +1 -1
  134. package/scripts/sync-manifest-versions.cjs +119 -0
  135. package/scripts/sync-runtime-launcher.cjs +0 -3
  136. package/scripts/verify-npm-publish.cjs +14 -26
@@ -15,6 +15,7 @@
15
15
 
16
16
  const fs = require('fs');
17
17
  const path = require('path');
18
+ const { ExitError, runMain } = require('./lib/cli-exit.cjs');
18
19
 
19
20
  const MAX_LENGTH = 100;
20
21
  const COMMANDS_DIR = path.join(__dirname, '..', 'commands', 'gsd');
@@ -46,38 +47,41 @@ function getFiles() {
46
47
  .map(f => path.join(COMMANDS_DIR, f));
47
48
  }
48
49
 
49
- const files = getFiles();
50
- const violations = [];
50
+ function main() {
51
+ const files = getFiles();
52
+ const violations = [];
51
53
 
52
- for (const filePath of files) {
53
- let content;
54
- try {
55
- content = fs.readFileSync(filePath, 'utf-8');
56
- } catch (err) {
57
- process.stderr.write(`ERROR: Cannot read file: ${filePath}\n ${err.message}\n`);
58
- process.exit(1);
59
- }
54
+ for (const filePath of files) {
55
+ let content;
56
+ try {
57
+ content = fs.readFileSync(filePath, 'utf-8');
58
+ } catch (err) {
59
+ throw new ExitError(1, `ERROR: Cannot read file: ${filePath}\n ${err.message}`);
60
+ }
60
61
 
61
- const description = parseDescription(content);
62
- if (description === null) continue;
62
+ const description = parseDescription(content);
63
+ if (description === null) continue;
63
64
 
64
- if (description.length > MAX_LENGTH) {
65
- violations.push({ filePath, length: description.length, description });
65
+ if (description.length > MAX_LENGTH) {
66
+ violations.push({ filePath, length: description.length, description });
67
+ }
66
68
  }
67
- }
68
69
 
69
- if (violations.length === 0) {
70
- const checked = files.length;
71
- process.stdout.write(`ok lint-descriptions: ${checked} file(s) checked, 0 violations\n`);
72
- process.exit(0);
73
- }
70
+ if (violations.length === 0) {
71
+ const checked = files.length;
72
+ process.stdout.write(`ok lint-descriptions: ${checked} file(s) checked, 0 violations\n`);
73
+ return 0;
74
+ }
74
75
 
75
- process.stderr.write(`\nERROR lint-descriptions: ${violations.length} violation(s) found\n\n`);
76
- for (const v of violations) {
77
- const preview = v.description.length > 120 ? v.description.slice(0, 117) + '...' : v.description;
78
- process.stderr.write(` ${v.filePath}\n`);
79
- process.stderr.write(` Length : ${v.length} (max ${MAX_LENGTH})\n`);
80
- process.stderr.write(` Desc : ${preview}\n\n`);
76
+ process.stderr.write(`\nERROR lint-descriptions: ${violations.length} violation(s) found\n\n`);
77
+ for (const v of violations) {
78
+ const preview = v.description.length > 120 ? v.description.slice(0, 117) + '...' : v.description;
79
+ process.stderr.write(` ${v.filePath}\n`);
80
+ process.stderr.write(` Length : ${v.length} (max ${MAX_LENGTH})\n`);
81
+ process.stderr.write(` Desc : ${preview}\n\n`);
82
+ }
83
+ process.stderr.write(`Trim descriptions to <= ${MAX_LENGTH} chars. Flag docs belong in argument-hint:.\n\n`);
84
+ return 1;
81
85
  }
82
- process.stderr.write(`Trim descriptions to <= ${MAX_LENGTH} chars. Flag docs belong in argument-hint:.\n\n`);
83
- process.exit(1);
86
+
87
+ runMain(main);
@@ -14,6 +14,7 @@
14
14
  */
15
15
 
16
16
  const { parseFragment, FRAGMENT_ERROR } = require('./changeset/parse.cjs');
17
+ const { ExitError, runMain } = require('./lib/cli-exit.cjs');
17
18
 
18
19
  const LINT_REASON = Object.freeze({
19
20
  OK_NO_TRIGGERING_FRAGMENTS: 'ok_no_triggering_fragments',
@@ -161,8 +162,7 @@ function main() {
161
162
  );
162
163
  changedFiles = out.split('\n').filter(Boolean);
163
164
  } catch (e) {
164
- process.stderr.write(`could not compute diff: ${e.message}\n`);
165
- process.exit(2);
165
+ throw new ExitError(2, `could not compute diff: ${e.message}`);
166
166
  }
167
167
 
168
168
  const { fragments, malformed } = readFragmentsFromDisk(changedFiles, rootDir);
@@ -204,10 +204,10 @@ function main() {
204
204
  `exemption via \`<!-- docs-exempt: <reason> -->\` inside the fragment body also works.\n`,
205
205
  );
206
206
  }
207
- process.exit(verdict.ok ? 0 : 1);
207
+ return verdict.ok ? 0 : 1;
208
208
  }
209
209
 
210
- if (require.main === module) main();
210
+ if (require.main === module) runMain(main);
211
211
 
212
212
  module.exports = {
213
213
  evaluateLint,
@@ -33,6 +33,7 @@
33
33
  const { execFileSync } = require('child_process');
34
34
  const fs = require('fs');
35
35
  const path = require('path');
36
+ const { ExitError, runMain } = require('./lib/cli-exit.cjs');
36
37
 
37
38
  // Constructed with split to avoid self-match when this script is scanned.
38
39
  const FORBIDDEN = 'get-shit' + '-done';
@@ -90,67 +91,70 @@ function isBinary(fullPath) {
90
91
  }
91
92
  }
92
93
 
93
- // Enumerate tracked files via git ls-files so only committed/staged source is checked.
94
- let trackedFiles;
95
- try {
96
- trackedFiles = execFileSync('git', ['ls-files'], { cwd: REPO_ROOT, encoding: 'utf8' })
97
- .split('\n')
98
- .map((f) => f.trim())
99
- .filter(Boolean);
100
- } catch (err) {
101
- process.stderr.write('ERROR lint-legacy-dir-name: git ls-files failed: ' + err.message + '\n');
102
- process.exit(1);
103
- }
94
+ function main() {
95
+ // Enumerate tracked files via git ls-files so only committed/staged source is checked.
96
+ let trackedFiles;
97
+ try {
98
+ trackedFiles = execFileSync('git', ['ls-files'], { cwd: REPO_ROOT, encoding: 'utf8' })
99
+ .split('\n')
100
+ .map((f) => f.trim())
101
+ .filter(Boolean);
102
+ } catch (err) {
103
+ throw new ExitError(1, 'ERROR lint-legacy-dir-name: git ls-files failed: ' + err.message);
104
+ }
104
105
 
105
- const violations = [];
106
+ const violations = [];
106
107
 
107
- for (const relPath of trackedFiles) {
108
- if (isAllowlisted(relPath)) continue;
108
+ for (const relPath of trackedFiles) {
109
+ if (isAllowlisted(relPath)) continue;
109
110
 
110
- const fullPath = path.join(REPO_ROOT, relPath);
111
+ const fullPath = path.join(REPO_ROOT, relPath);
111
112
 
112
- // Skip this guard script itself.
113
- if (path.resolve(fullPath) === SELF_PATH) continue;
113
+ // Skip this guard script itself.
114
+ if (path.resolve(fullPath) === SELF_PATH) continue;
114
115
 
115
- if (isBinary(fullPath)) continue;
116
+ if (isBinary(fullPath)) continue;
116
117
 
117
- let content;
118
- try {
119
- content = fs.readFileSync(fullPath, 'utf8');
120
- } catch {
121
- // Unreadable files (permissions, etc.) — skip silently.
122
- continue;
123
- }
118
+ let content;
119
+ try {
120
+ content = fs.readFileSync(fullPath, 'utf8');
121
+ } catch {
122
+ // Unreadable files (permissions, etc.) — skip silently.
123
+ continue;
124
+ }
124
125
 
125
- const lines = content.split('\n');
126
- for (let i = 0; i < lines.length; i++) {
127
- const line = lines[i];
128
- // Skip lines that carry the explicit allow marker.
129
- if (line.includes(ALLOW_MARKER)) continue;
130
-
131
- FORBIDDEN_RE.lastIndex = 0;
132
- let match;
133
- while ((match = FORBIDDEN_RE.exec(line)) !== null) {
134
- violations.push({
135
- file: relPath,
136
- line: i + 1,
137
- col: match.index + 1,
138
- text: match[0],
139
- });
126
+ const lines = content.split('\n');
127
+ for (let i = 0; i < lines.length; i++) {
128
+ const line = lines[i];
129
+ // Skip lines that carry the explicit allow marker.
130
+ if (line.includes(ALLOW_MARKER)) continue;
131
+
132
+ FORBIDDEN_RE.lastIndex = 0;
133
+ let match;
134
+ while ((match = FORBIDDEN_RE.exec(line)) !== null) {
135
+ violations.push({
136
+ file: relPath,
137
+ line: i + 1,
138
+ col: match.index + 1,
139
+ text: match[0],
140
+ });
141
+ }
140
142
  }
141
143
  }
142
- }
143
144
 
144
- if (violations.length === 0) {
145
- process.stdout.write('ok lint-legacy-dir-name: ' + trackedFiles.length + ' tracked file(s) checked, 0 violations\n');
146
- process.exit(0);
147
- }
145
+ if (violations.length === 0) {
146
+ process.stdout.write('ok lint-legacy-dir-name: ' + trackedFiles.length + ' tracked file(s) checked, 0 violations\n');
147
+ return 0;
148
+ }
148
149
 
149
- process.stderr.write('\nERROR lint-legacy-dir-name: ' + violations.length + ' violation(s) found\n\n');
150
- for (const v of violations) {
151
- process.stderr.write(' ' + v.file + ':' + v.line + ':' + v.col + ' — ' + JSON.stringify(v.text) + '\n');
150
+ process.stderr.write('\nERROR lint-legacy-dir-name: ' + violations.length + ' violation(s) found\n\n');
151
+ for (const v of violations) {
152
+ process.stderr.write(' ' + v.file + ':' + v.line + ':' + v.col + ' — ' + JSON.stringify(v.text) + '\n');
153
+ }
154
+ process.stderr.write('\n');
155
+ process.stderr.write('Fix: rename to gsd-core, or add `gsd-allow-legacy-name` marker on the line if the\n');
156
+ process.stderr.write(' use is intentional (migration modules, tests, guard, changeset).\n\n');
157
+ return 1;
152
158
  }
153
- process.stderr.write('\n');
154
- process.stderr.write('Fix: rename to gsd-core, or add `gsd-allow-legacy-name` marker on the line if the\n');
155
- process.stderr.write(' use is intentional (migration modules, tests, guard, changeset).\n\n');
156
- process.exit(1);
159
+
160
+ runMain(main);
@@ -4,6 +4,8 @@
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
 
7
+ const { runMain } = require('./lib/cli-exit.cjs');
8
+
7
9
  const ROOT = path.join(__dirname, '..');
8
10
 
9
11
  const DEFAULT_RELATIVE_FILES = [
@@ -84,7 +86,7 @@ function main(argv = process.argv.slice(2)) {
84
86
  }
85
87
 
86
88
  if (require.main === module) {
87
- process.exit(main());
89
+ runMain(main);
88
90
  }
89
91
 
90
92
  module.exports = {
@@ -13,19 +13,9 @@
13
13
 
14
14
  const fs = require('fs');
15
15
  const path = require('path');
16
+ const { ExitError, runMain } = require('./lib/cli-exit.cjs');
16
17
 
17
18
  const ROOT = path.resolve(__dirname, '..');
18
- const targetArg = process.argv[2] || path.join(ROOT, 'bin', 'install.js');
19
- const target = path.resolve(targetArg);
20
- const rel = path.relative(ROOT, target);
21
-
22
- let content;
23
- try {
24
- content = fs.readFileSync(target, 'utf8');
25
- } catch (error) {
26
- process.stderr.write(`lint-shell-command-projection-drift: failed to read ${target}: ${error.message}\n`);
27
- process.exit(1);
28
- }
29
19
 
30
20
  const forbidden = [
31
21
  {
@@ -42,16 +32,31 @@ const forbidden = [
42
32
  },
43
33
  ];
44
34
 
45
- const matches = forbidden.filter((rule) => rule.pattern.test(content));
46
- if (matches.length === 0) {
47
- process.stdout.write(`ok shell-projection-drift: ${rel}\n`);
48
- process.exit(0);
49
- }
35
+ function main() {
36
+ const targetArg = process.argv[2] || path.join(ROOT, 'bin', 'install.js');
37
+ const target = path.resolve(targetArg);
38
+ const rel = path.relative(ROOT, target);
39
+
40
+ let content;
41
+ try {
42
+ content = fs.readFileSync(target, 'utf8');
43
+ } catch (error) {
44
+ throw new ExitError(1, `lint-shell-command-projection-drift: failed to read ${target}: ${error.message}`);
45
+ }
50
46
 
51
- process.stderr.write(`ERROR shell-projection-drift: inline serialized shim builders found in ${rel}\n`);
52
- for (const match of matches) {
53
- process.stderr.write(` - ${match.label}\n`);
47
+ const matches = forbidden.filter((rule) => rule.pattern.test(content));
48
+ if (matches.length === 0) {
49
+ process.stdout.write(`ok shell-projection-drift: ${rel}\n`);
50
+ return 0;
51
+ }
52
+
53
+ process.stderr.write(`ERROR shell-projection-drift: inline serialized shim builders found in ${rel}\n`);
54
+ for (const match of matches) {
55
+ process.stderr.write(` - ${match.label}\n`);
56
+ }
57
+ process.stderr.write('Route shim/wrapper rendering through gsd-core/bin/lib/shell-command-projection.cjs\n');
58
+ process.stderr.write('Safe subprocess execution via spawnSync/execFileSync is intentionally allowed.\n');
59
+ return 1;
54
60
  }
55
- process.stderr.write('Route shim/wrapper rendering through gsd-core/bin/lib/shell-command-projection.cjs\n');
56
- process.stderr.write('Safe subprocess execution via spawnSync/execFileSync is intentionally allowed.\n');
57
- process.exit(1);
61
+
62
+ runMain(main);
@@ -25,6 +25,7 @@
25
25
 
26
26
  const fs = require('fs');
27
27
  const path = require('path');
28
+ const { runMain } = require('./lib/cli-exit.cjs');
28
29
 
29
30
  const PROFILES_MODULE = path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'install-profiles.cjs');
30
31
  const { PROFILES, loadSkillsManifest, resolveProfile } = require(PROFILES_MODULE);
@@ -142,39 +143,43 @@ function checkProfileClosure(manifest) {
142
143
  // Main
143
144
  // ---------------------------------------------------------------------------
144
145
 
145
- const manifest = loadSkillsManifest(commandsDir);
146
- const allStems = new Set(
147
- [...manifest.keys()].filter((k) => !k.startsWith('_calls_agents_'))
148
- );
146
+ function main() {
147
+ const manifest = loadSkillsManifest(commandsDir);
148
+ const allStems = new Set(
149
+ [...manifest.keys()].filter((k) => !k.startsWith('_calls_agents_'))
150
+ );
149
151
 
150
- const consistencyViolations = checkFrontmatterBodyConsistency(manifest, allStems);
151
- const closureViolations = checkProfileClosure(manifest);
152
+ const consistencyViolations = checkFrontmatterBodyConsistency(manifest, allStems);
153
+ const closureViolations = checkProfileClosure(manifest);
152
154
 
153
- const totalViolations = consistencyViolations.length + closureViolations.length;
155
+ const totalViolations = consistencyViolations.length + closureViolations.length;
154
156
 
155
- if (totalViolations === 0) {
156
- const checked = manifest.size;
157
- process.stdout.write('ok lint-skill-deps: ' + checked + ' skill(s) checked, 0 violations\n');
158
- process.exit(0);
159
- }
157
+ if (totalViolations === 0) {
158
+ const checked = manifest.size;
159
+ process.stdout.write('ok lint-skill-deps: ' + checked + ' skill(s) checked, 0 violations\n');
160
+ return 0;
161
+ }
160
162
 
161
- process.stderr.write('\nERROR lint-skill-deps: ' + totalViolations + ' violation(s) found\n\n');
163
+ process.stderr.write('\nERROR lint-skill-deps: ' + totalViolations + ' violation(s) found\n\n');
162
164
 
163
- if (consistencyViolations.length > 0) {
164
- process.stderr.write('Frontmatter to body consistency violations (' + consistencyViolations.length + '):\n\n');
165
- for (const v of consistencyViolations) {
166
- process.stderr.write(' ' + v.filePath + '\n');
167
- process.stderr.write(' ' + v.message + '\n\n');
165
+ if (consistencyViolations.length > 0) {
166
+ process.stderr.write('Frontmatter to body consistency violations (' + consistencyViolations.length + '):\n\n');
167
+ for (const v of consistencyViolations) {
168
+ process.stderr.write(' ' + v.filePath + '\n');
169
+ process.stderr.write(' ' + v.message + '\n\n');
170
+ }
171
+ process.stderr.write('Fix: add missing deps to requires: in the skill frontmatter.\n\n');
168
172
  }
169
- process.stderr.write('Fix: add missing deps to requires: in the skill frontmatter.\n\n');
170
- }
171
173
 
172
- if (closureViolations.length > 0) {
173
- process.stderr.write('Profile closure violations (' + closureViolations.length + '):\n\n');
174
- for (const v of closureViolations) {
175
- process.stderr.write(' ' + v.message + '\n');
174
+ if (closureViolations.length > 0) {
175
+ process.stderr.write('Profile closure violations (' + closureViolations.length + '):\n\n');
176
+ for (const v of closureViolations) {
177
+ process.stderr.write(' ' + v.message + '\n');
178
+ }
179
+ process.stderr.write('\nFix: add the missing skills to the profile base set in install-profiles.cjs.\n\n');
176
180
  }
177
- process.stderr.write('\nFix: add the missing skills to the profile base set in install-profiles.cjs.\n\n');
181
+
182
+ return 1;
178
183
  }
179
184
 
180
- process.exit(1);
185
+ runMain(main);
@@ -43,6 +43,7 @@
43
43
  },
44
44
  "milestone": {
45
45
  "files": [
46
+ "bug-730-milestone-phase-details-scope.test.cjs",
46
47
  "milestone-archive.test.cjs",
47
48
  "milestone-helper.test.cjs",
48
49
  "milestone-prefixed-convention.test.cjs",
@@ -120,6 +121,7 @@
120
121
  "install": {
121
122
  "files": [
122
123
  "bug-410-install-defaults-test-mode-guard.test.cjs",
124
+ "enh-776-install-gemini-hook-events.test.cjs",
123
125
  "install-minimal-hooks.test.cjs",
124
126
  "install-path-detection.test.cjs",
125
127
  "install-regressions.test.cjs",
@@ -17,6 +17,7 @@
17
17
  const fs = require('fs');
18
18
  const path = require('path');
19
19
  const { assertWithinAllowlist } = require('./lib/allowlist-ratchet.cjs');
20
+ const { runMain } = require('./lib/cli-exit.cjs');
20
21
 
21
22
  const ROOT = path.join(__dirname, '..');
22
23
  const PROD_DIRS = [
@@ -201,14 +202,14 @@ function run() {
201
202
 
202
203
  if (jsonMode) {
203
204
  console.log(JSON.stringify({ ok: failures.length === 0, results, failures, hints: [] }, null, 2));
204
- process.exit(failures.length > 0 ? 1 : 0);
205
+ return failures.length > 0 ? 1 : 0;
205
206
  }
206
207
 
207
208
  if (failures.length === 0) {
208
209
  const inAllowlist = results.filter(r => r.verdict === Verdict.OK_IN_ALLOWLIST).length;
209
210
  console.log(`ok lint-test-file-count: ${results.length} module(s) checked, 0 failures` +
210
211
  (inAllowlist > 0 ? `, ${inAllowlist} allowlisted` : ''));
211
- process.exit(0);
212
+ return 0;
212
213
  }
213
214
 
214
215
  process.stderr.write(`\nERROR lint-test-file-count: ${failures.length} module(s) exceed the test-file limit\n\n`);
@@ -231,7 +232,7 @@ function run() {
231
232
  }
232
233
  process.stderr.write('\nFix: consolidate test files per module (one primary + one integration).\n');
233
234
  process.stderr.write('Or update scripts/lint-test-file-count.allowlist.json with PR justification.\n\n');
234
- process.exit(1);
235
+ return 1;
235
236
  }
236
237
 
237
238
  module.exports = {
@@ -242,4 +243,4 @@ module.exports = {
242
243
  _loadAllowlist: loadAllowlist,
243
244
  };
244
245
 
245
- if (require.main === module) run();
246
+ if (require.main === module) runMain(run);
@@ -32,6 +32,8 @@
32
32
  const { execFileSync } = require('child_process');
33
33
  const { readFileSync } = require('fs');
34
34
 
35
+ const { ExitError, runMain } = require('./lib/cli-exit.cjs');
36
+
35
37
  // ── Single source of truth: covered modules ───────────────────────────────────
36
38
  // Each entry: { cjs: '<built artifact>', tests: ['tests/...', ...] }
37
39
  // A module is "covered" iff its tests are wired into the Stryker command runner
@@ -121,7 +123,7 @@ function parseArgs(argv) {
121
123
  ' --base <ref> Git ref to diff against (default: origin/${GITHUB_BASE_REF:-next})',
122
124
  ' --print Human-readable output instead of JSON',
123
125
  ].join('\n'));
124
- process.exit(0);
126
+ throw new ExitError(0);
125
127
  } else {
126
128
  throw new Error(`unknown argument: ${arg}`);
127
129
  }
@@ -211,9 +213,10 @@ function main() {
211
213
  console.log(JSON.stringify(result, null, 2));
212
214
  }
213
215
  } catch (err) {
216
+ if (err instanceof ExitError) throw err;
214
217
  console.error(`mutation-matrix: ${err.message}`);
215
- process.exit(2);
218
+ throw new ExitError(2);
216
219
  }
217
220
  }
218
221
 
219
- main();
222
+ runMain(main);
@@ -83,7 +83,7 @@ ALLOWLIST=(
83
83
  # These files contain intentional injection examples / security-model prose
84
84
  # and are not attack vectors — they explain/demonstrate injection patterns.
85
85
  'TEST-EXAMPLES.md'
86
- 'docs/explanation/security-model.md'
86
+ 'explanation/security-model.md'
87
87
  )
88
88
 
89
89
  is_allowlisted() {