@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
@@ -0,0 +1,373 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const cp = require('node:child_process');
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const { ExitError, runMain } = require('../lib/cli-exit.cjs');
8
+ const { classifyTitle } = require('./format-github-release-notes.cjs');
9
+
10
+ const DEFAULT_MAX_CONTENT = 1850;
11
+ const SECTION_LIMITS = Object.freeze([
12
+ ['Breaking', 4],
13
+ ['Security', 4],
14
+ ['Feature', 4],
15
+ ['Fix', 4],
16
+ ['Enhancement', 3],
17
+ ['Internal', 2],
18
+ ['New Contributors', 3],
19
+ ]);
20
+
21
+ function normalizeNewlines(value) {
22
+ return String(value || '').replace(/\r\n/g, '\n');
23
+ }
24
+
25
+ function releaseTag(release) {
26
+ return release.tagName || release.tag_name || release.name || 'release';
27
+ }
28
+
29
+ function releaseName(release) {
30
+ return release.name || releaseTag(release);
31
+ }
32
+
33
+ function releaseUrl(release) {
34
+ return release.url || release.html_url || '';
35
+ }
36
+
37
+ function releaseIsPrerelease(release) {
38
+ return release.isPrerelease === true || release.prerelease === true || /-/.test(releaseTag(release));
39
+ }
40
+
41
+ function loadPackageName(repoRoot = path.resolve(__dirname, '..', '..')) {
42
+ const pkgJson = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
43
+ return pkgJson.name || 'package';
44
+ }
45
+
46
+ function normalizeHeading(value) {
47
+ const heading = value.trim().replace(/:$/, '');
48
+ const lower = heading.toLowerCase();
49
+
50
+ if (lower.includes('breaking')) return 'Breaking';
51
+ if (lower.includes('security')) return 'Security';
52
+ if (lower === 'feature' || lower === 'features') return 'Feature';
53
+ if (lower === 'fix' || lower === 'fixes' || lower === 'bug fixes') return 'Fix';
54
+ if (lower === 'enhancement' || lower === 'enhancements' || lower === 'improvements') return 'Enhancement';
55
+ if (lower === 'internal' || lower === 'maintenance') return 'Internal';
56
+ if (lower === 'new contributors') return 'New Contributors';
57
+ if (lower === "what's changed" || lower === 'whats changed') return "What's Changed";
58
+ return heading;
59
+ }
60
+
61
+ function collectSections(body) {
62
+ const sections = new Map();
63
+ let current = null;
64
+
65
+ for (const line of normalizeNewlines(body).split('\n')) {
66
+ const trimmed = line.trim();
67
+ const heading = trimmed.match(/^#{2,3}\s+(.+)$/);
68
+
69
+ if (heading) {
70
+ current = normalizeHeading(heading[1]);
71
+ continue;
72
+ }
73
+
74
+ const bullet = trimmed.match(/^([*-])\s+(.+)$/);
75
+ if (!bullet || !current) continue;
76
+
77
+ let section = current;
78
+ if (section === "What's Changed") {
79
+ section = classifyTitle(trimmed);
80
+ }
81
+
82
+ if (!sections.has(section)) sections.set(section, []);
83
+ sections.get(section).push(cleanBullet(trimmed));
84
+ }
85
+
86
+ return sections;
87
+ }
88
+
89
+ function cleanBullet(value) {
90
+ let text = String(value)
91
+ .replace(/^[*-]\s+/, '')
92
+ .replace(/\s+by\s+@[A-Za-z0-9-]+\s+in\s+https:\/\/github\.com\/[^/\s]+\/[^/\s]+\/pull\/(\d+)/g, ' (#$1)')
93
+ .replace(/\[([^\]]+)\]\((https:\/\/github\.com\/[^/\s]+\/[^/\s]+\/(?:pull|issues)\/(\d+))\)/g, '$1 (#$3)')
94
+ .replace(/\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g, '$1')
95
+ .replace(/https:\/\/github\.com\/[^/\s]+\/[^/\s]+\/pull\/(\d+)/g, '#$1')
96
+ .replace(/https:\/\/github\.com\/[^/\s]+\/[^/\s]+\/issues\/(\d+)/g, '#$1')
97
+ .replace(/https:\/\/github\.com\/[^/\s]+\/[^/\s]+\/commit\/([0-9a-f]{7})[0-9a-f]*/gi, '$1')
98
+ .replace(/\*\*/g, '')
99
+ .replace(/\s+/g, ' ')
100
+ .trim();
101
+
102
+ text = text
103
+ .replace(/^\[codex\]\s+/i, '')
104
+ .replace(/^(?:feat|fix|docs|chore|ci|refactor|test)(?:\([^)]+\))?!?:\s+/i, '');
105
+ text = text.replace(/#(\d+)\s+\(#\1\)/g, '#$1');
106
+ text = text.replace(/\s+\(#(\d+)\)\s+\(#\1\)$/g, ' (#$1)');
107
+
108
+ return truncateAtWord(text, 220);
109
+ }
110
+
111
+ function truncateAtWord(value, maxLength) {
112
+ if (value.length <= maxLength) return value;
113
+ const splitAt = value.lastIndexOf(' ', maxLength - 3);
114
+ if (splitAt < 80) return `${value.slice(0, maxLength - 3)}...`;
115
+ return `${value.slice(0, splitAt)}...`;
116
+ }
117
+
118
+ function extractInstallCommand(body, packageName, release) {
119
+ const lines = normalizeNewlines(body).split('\n');
120
+ let inInstall = false;
121
+ let inFence = false;
122
+
123
+ for (const line of lines) {
124
+ const trimmed = line.trim();
125
+
126
+ if (/^##\s+Install\b/i.test(trimmed)) {
127
+ inInstall = true;
128
+ continue;
129
+ }
130
+
131
+ if (inInstall && /^##\s+/.test(trimmed)) break;
132
+ if (!inInstall) continue;
133
+
134
+ if (trimmed.startsWith('```')) {
135
+ inFence = !inFence;
136
+ continue;
137
+ }
138
+
139
+ if (inFence && /^npm\s+(?:i|install)\s+/.test(trimmed)) {
140
+ return trimmed;
141
+ }
142
+ }
143
+
144
+ const channel = releaseIsPrerelease(release) ? 'next' : 'latest';
145
+ return `npm i ${packageName}@${channel}`;
146
+ }
147
+
148
+ function buildDiscordReleasePayload({ release, packageName = loadPackageName(), maxContent = DEFAULT_MAX_CONTENT }) {
149
+ const tag = releaseTag(release);
150
+ const name = releaseName(release);
151
+ const url = releaseUrl(release);
152
+ const prerelease = releaseIsPrerelease(release);
153
+ const channel = prerelease ? 'next' : 'latest';
154
+ const body = release.body || '';
155
+ const sections = collectSections(body);
156
+ const footer = url ? `Full changelog: ${url}` : `Full changelog: ${tag}`;
157
+ const baseLines = [
158
+ `**${packageName} ${name}${prerelease ? ' pre-release' : ''} is out**`,
159
+ '',
160
+ 'Install:',
161
+ `\`${extractInstallCommand(body, packageName, release)}\``,
162
+ ];
163
+
164
+ const lines = baseLines.slice();
165
+ for (const [section, limit] of SECTION_LIMITS) {
166
+ appendSection(lines, section, sections.get(section) || [], limit, footer, maxContent);
167
+ }
168
+
169
+ if (lines.length === baseLines.length) {
170
+ appendLineGroup(lines, ['', 'No release notes were provided.'], footer, maxContent);
171
+ }
172
+
173
+ if (joinedLength(lines, footer) > maxContent) {
174
+ while (lines.length > baseLines.length && joinedLength(lines, footer) > maxContent) {
175
+ lines.pop();
176
+ }
177
+ }
178
+
179
+ const content = [...lines, '', footer].join('\n');
180
+ return {
181
+ username: 'GSD Releases',
182
+ content,
183
+ embeds: [
184
+ {
185
+ title: 'Release details',
186
+ url: url || undefined,
187
+ color: prerelease ? 0xd9a441 : 0x2f9e44,
188
+ fields: [
189
+ { name: 'Package', value: `\`${packageName}\``, inline: true },
190
+ { name: 'Channel', value: `\`${channel}\``, inline: true },
191
+ { name: 'Version', value: `\`${tag}\``, inline: true },
192
+ ],
193
+ },
194
+ ],
195
+ allowed_mentions: { parse: [] },
196
+ };
197
+ }
198
+
199
+ function appendSection(lines, section, bullets, limit, footer, maxContent) {
200
+ if (bullets.length === 0) return;
201
+
202
+ const label = sectionLabel(section);
203
+ const selected = [];
204
+ for (const bullet of bullets.slice(0, limit)) {
205
+ const candidate = ['', `**${label}**`, ...selected.map((item) => `- ${item}`), `- ${bullet}`];
206
+ if (joinedLength([...lines, ...candidate], footer) > maxContent) break;
207
+ selected.push(bullet);
208
+ }
209
+
210
+ if (selected.length === 0) return;
211
+
212
+ const group = ['', `**${label}**`, ...selected.map((item) => `- ${item}`)];
213
+ const remaining = bullets.length - selected.length;
214
+ if (remaining > 0) {
215
+ const moreLine = `- ...and ${remaining} more ${label.toLowerCase()}`;
216
+ if (joinedLength([...lines, ...group, moreLine], footer) <= maxContent) {
217
+ group.push(moreLine);
218
+ }
219
+ }
220
+
221
+ appendLineGroup(lines, group, footer, maxContent);
222
+ }
223
+
224
+ function appendLineGroup(lines, group, footer, maxContent) {
225
+ if (joinedLength([...lines, ...group], footer) <= maxContent) {
226
+ lines.push(...group);
227
+ }
228
+ }
229
+
230
+ function joinedLength(lines, footer) {
231
+ return [...lines, '', footer].join('\n').length;
232
+ }
233
+
234
+ function sectionLabel(section) {
235
+ if (section === 'Feature') return 'Features';
236
+ if (section === 'Fix') return 'Fixes';
237
+ if (section === 'Enhancement') return 'Enhancements';
238
+ return section;
239
+ }
240
+
241
+ function fetchRelease({ tag, repo, latest = false }) {
242
+ const args = ['release', 'view'];
243
+ if (!latest && tag) args.push(tag);
244
+ args.push('--json', 'tagName,name,isPrerelease,body,url');
245
+ if (repo) args.push('--repo', repo);
246
+ const raw = cp.execFileSync('gh', args, { encoding: 'utf8' });
247
+ return JSON.parse(raw);
248
+ }
249
+
250
+ async function postWebhook(webhookUrl, payload) {
251
+ const response = await fetch(webhookUrl, {
252
+ method: 'POST',
253
+ headers: { 'content-type': 'application/json' },
254
+ body: JSON.stringify(payload),
255
+ });
256
+
257
+ if (!response.ok) {
258
+ throw new ExitError(1, `Discord webhook failed with ${response.status}: ${await response.text()}`);
259
+ }
260
+ }
261
+
262
+ function parseArgs(argv) {
263
+ const opts = {
264
+ tag: null,
265
+ repo: null,
266
+ latest: false,
267
+ stdin: false,
268
+ post: false,
269
+ json: false,
270
+ allowMissingWebhook: false,
271
+ packageName: null,
272
+ maxContent: DEFAULT_MAX_CONTENT,
273
+ };
274
+
275
+ const args = argv.slice();
276
+ while (args.length > 0) {
277
+ const arg = args.shift();
278
+ if (arg === '--tag') {
279
+ opts.tag = requireValue(args, arg);
280
+ } else if (arg === '--repo') {
281
+ opts.repo = requireValue(args, arg);
282
+ } else if (arg === '--package') {
283
+ opts.packageName = requireValue(args, arg);
284
+ } else if (arg === '--max-content') {
285
+ opts.maxContent = Number(requireValue(args, arg));
286
+ } else if (arg === '--latest') {
287
+ opts.latest = true;
288
+ } else if (arg === '--stdin') {
289
+ opts.stdin = true;
290
+ } else if (arg === '--post') {
291
+ opts.post = true;
292
+ } else if (arg === '--json') {
293
+ opts.json = true;
294
+ } else if (arg === '--allow-missing-webhook') {
295
+ opts.allowMissingWebhook = true;
296
+ } else if (arg === '--help' || arg === '-h') {
297
+ process.stdout.write(
298
+ 'Usage: node scripts/release-notes/discord-release-summary.cjs [--tag <tag>|--latest|--stdin] [options]\n' +
299
+ '\n' +
300
+ 'Options:\n' +
301
+ ' --tag <tag> GitHub release tag to announce\n' +
302
+ ' --latest Announce the latest GitHub release\n' +
303
+ ' --stdin Read release JSON from stdin\n' +
304
+ ' --repo <owner/repo> Repository for gh release view\n' +
305
+ ' --package <name> Package name override\n' +
306
+ ' --post POST to DISCORD_WEBHOOK_URL\n' +
307
+ ' --allow-missing-webhook Warn and skip instead of failing when posting without a webhook\n' +
308
+ ' --json Print webhook payload JSON instead of content preview\n' +
309
+ ' --max-content <n> Max Discord content characters before webhook footer\n'
310
+ );
311
+ throw new ExitError(0);
312
+ } else {
313
+ throw new ExitError(2, `error: unknown argument ${arg}`);
314
+ }
315
+ }
316
+
317
+ if (!Number.isFinite(opts.maxContent) || opts.maxContent < 500 || opts.maxContent > 2000) {
318
+ throw new ExitError(2, 'error: --max-content must be between 500 and 2000');
319
+ }
320
+
321
+ return opts;
322
+ }
323
+
324
+ function requireValue(args, flag) {
325
+ const value = args.shift();
326
+ if (!value || value.startsWith('-')) {
327
+ throw new ExitError(2, `error: ${flag} requires a value`);
328
+ }
329
+ return value;
330
+ }
331
+
332
+ async function main() {
333
+ const opts = parseArgs(process.argv.slice(2));
334
+ let release;
335
+
336
+ if (opts.stdin) {
337
+ release = JSON.parse(fs.readFileSync('/dev/stdin', 'utf8'));
338
+ } else {
339
+ release = fetchRelease(opts);
340
+ }
341
+
342
+ const payload = buildDiscordReleasePayload({
343
+ release,
344
+ packageName: opts.packageName || loadPackageName(),
345
+ maxContent: opts.maxContent,
346
+ });
347
+
348
+ if (opts.post) {
349
+ const webhookUrl = process.env.DISCORD_WEBHOOK_URL;
350
+ if (!webhookUrl) {
351
+ if (opts.allowMissingWebhook) {
352
+ process.stderr.write('warning: DISCORD_WEBHOOK_URL is not set; skipping Discord announcement\n');
353
+ } else {
354
+ throw new ExitError(1, 'error: DISCORD_WEBHOOK_URL is not set');
355
+ }
356
+ } else {
357
+ await postWebhook(webhookUrl, payload);
358
+ }
359
+ }
360
+
361
+ process.stdout.write(opts.json ? `${JSON.stringify(payload, null, 2)}\n` : `${payload.content}\n`);
362
+ }
363
+
364
+ if (require.main === module) {
365
+ runMain(main);
366
+ }
367
+
368
+ module.exports = {
369
+ buildDiscordReleasePayload,
370
+ cleanBullet,
371
+ collectSections,
372
+ extractInstallCommand,
373
+ };
@@ -4,6 +4,7 @@ const path = require('path');
4
4
  const os = require('os');
5
5
  const fs = require('fs');
6
6
  const { execFileSync } = require('child_process');
7
+ const { runMain, ExitError } = require('../lib/cli-exit.cjs');
7
8
 
8
9
  /**
9
10
  * Classify a What's-Changed bullet line into 'Feature', 'Fix', or 'Enhancement'.
@@ -155,7 +156,7 @@ function formatReleaseNotes({ generatedBody, version, prerelease, packageName })
155
156
  }
156
157
 
157
158
  // CLI entry point
158
- if (require.main === module) {
159
+ function main() {
159
160
  try {
160
161
  const argv = process.argv.slice(2);
161
162
 
@@ -248,9 +249,13 @@ if (require.main === module) {
248
249
  process.stdout.write(formatted + '\n');
249
250
  }
250
251
  } catch (err) {
251
- process.stderr.write((err.message || String(err)) + '\n');
252
- process.exit(1);
252
+ if (err instanceof ExitError) throw err;
253
+ throw new ExitError(1, err && err.message ? err.message : String(err));
253
254
  }
254
255
  }
255
256
 
257
+ if (require.main === module) {
258
+ runMain(main);
259
+ }
260
+
256
261
  module.exports = { formatReleaseNotes, classifyTitle };
@@ -46,6 +46,7 @@ const fs = require('fs');
46
46
  const os = require('os');
47
47
  const path = require('path');
48
48
  const { PACKAGE_NAME } = require('../gsd-core/bin/lib/package-identity.cjs');
49
+ const { ExitError, runMain } = require('./lib/cli-exit.cjs');
49
50
  // 120 s proved too tight on Windows GitHub-hosted runners: cold-cache
50
51
  // `npm install -g` with a 1499-file tarball took ~120 s exactly, causing
51
52
  // spawnSync to fire SIGTERM and return { status: null, stdout: '', stderr: '' }
@@ -587,23 +588,24 @@ function cliMain() {
587
588
  };
588
589
  if (isJson) process.stdout.write(JSON.stringify(result) + '\n');
589
590
  cleanup(packDir, installPrefix, fixtureDir);
590
- process.exit(1);
591
+ throw new ExitError(1);
591
592
  }
592
593
  }
593
594
  } catch (err) {
595
+ if (err instanceof ExitError) throw err;
594
596
  const result = {
595
597
  code: SMOKE.PACK_FAILED,
596
598
  details: { error: err.message, stderr: err.stderr },
597
599
  };
598
600
  if (isJson) process.stdout.write(JSON.stringify(result) + '\n');
599
601
  cleanup(packDir, installPrefix, fixtureDir);
600
- process.exit(1);
602
+ throw new ExitError(1);
601
603
  }
602
604
 
603
605
  const result = runSmoke({ tarballPath, installPrefix, expectedVersion, fixtureDir });
604
606
  if (isJson) process.stdout.write(JSON.stringify(result) + '\n');
605
607
  cleanup(packDir, installPrefix, fixtureDir);
606
- process.exit(result.code === SMOKE.OK ? 0 : 1);
608
+ return result.code === SMOKE.OK ? 0 : 1;
607
609
  }
608
610
 
609
611
  function cleanup(...dirs) {
@@ -623,5 +625,5 @@ function cleanup(...dirs) {
623
625
  module.exports = { SMOKE, runSmoke, binInvocation };
624
626
 
625
627
  if (require.main === module) {
626
- cliMain();
628
+ runMain(cliMain);
627
629
  }
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * research-profiles.cjs — hand-authored profile table for the 7 researcher agents.
6
+ *
7
+ * Each profile field is derived verbatim from the agent's current committed state so
8
+ * that the initial --check in gen-research-agents.cjs is green by construction.
9
+ *
10
+ * Fields:
11
+ * name — verbatim frontmatter `name:` value
12
+ * description — verbatim frontmatter `description:` value
13
+ * color — verbatim frontmatter `color:` value
14
+ * tools — verbatim frontmatter `tools:` value (single string, comma-separated)
15
+ * requiredIncludes — @~/.claude/gsd-core/references/<file>.md strings the body MUST contain
16
+ * requiredSeamCalls — `gsd-tools query <cmd>` strings the body MUST contain
17
+ * outputContract — strings the body MUST contain (output path, return marker, etc.)
18
+ */
19
+
20
+ const PROFILES = [
21
+ {
22
+ name: 'gsd-project-researcher',
23
+ description:
24
+ 'Researches domain ecosystem before roadmap creation. Produces files in .planning/research/ consumed during roadmap creation. Spawned by /gsd:new-project or /gsd:new-milestone orchestrators.',
25
+ color: 'cyan',
26
+ tools:
27
+ 'Read, Write, Bash, Grep, Glob, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*',
28
+ requiredIncludes: [
29
+ '@~/.claude/gsd-core/references/research-documentation-lookup.md',
30
+ '@~/.claude/gsd-core/references/research-philosophy.md',
31
+ '@~/.claude/gsd-core/references/research-verification-protocol.md',
32
+ ],
33
+ requiredSeamCalls: [
34
+ 'gsd-tools query research-plan',
35
+ 'gsd-tools query research-store put',
36
+ 'gsd-tools query classify-confidence',
37
+ ],
38
+ outputContract: [
39
+ '.planning/research/',
40
+ '## RESEARCH COMPLETE',
41
+ ],
42
+ },
43
+ {
44
+ name: 'gsd-phase-researcher',
45
+ description:
46
+ 'Researches how to implement a phase before planning. Produces RESEARCH.md consumed by gsd-planner. Spawned by /gsd:plan-phase orchestrator.',
47
+ color: 'cyan',
48
+ tools:
49
+ 'Read, Write, Edit, Bash, Grep, Glob, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*',
50
+ requiredIncludes: [
51
+ '@~/.claude/gsd-core/references/research-documentation-lookup.md',
52
+ '@~/.claude/gsd-core/references/research-philosophy.md',
53
+ '@~/.claude/gsd-core/references/research-verification-protocol.md',
54
+ ],
55
+ requiredSeamCalls: [
56
+ 'gsd-tools query research-plan',
57
+ 'gsd-tools query research-store put',
58
+ 'gsd-tools query classify-confidence',
59
+ 'gsd-tools query package-legitimacy check',
60
+ ],
61
+ outputContract: [
62
+ '.planning/phases/XX-name/{phase_num}-RESEARCH.md',
63
+ '## RESEARCH COMPLETE',
64
+ ],
65
+ },
66
+ {
67
+ name: 'gsd-advisor-researcher',
68
+ description:
69
+ 'Researches a single gray area decision and returns a structured comparison table with rationale. Spawned by discuss-phase advisor mode.',
70
+ color: 'cyan',
71
+ tools: 'Read, Bash, Grep, Glob, WebSearch, WebFetch, mcp__context7__*',
72
+ requiredIncludes: [
73
+ '@~/.claude/gsd-core/references/research-documentation-lookup.md',
74
+ ],
75
+ requiredSeamCalls: [],
76
+ outputContract: [
77
+ '| Option | Pros | Cons | Complexity | Recommendation |',
78
+ '**Rationale:**',
79
+ ],
80
+ },
81
+ {
82
+ name: 'gsd-ai-researcher',
83
+ description:
84
+ 'Researches a chosen AI framework\'s official docs to produce implementation-ready guidance — best practices, syntax, core patterns, and pitfalls distilled for the specific use case. Writes the Framework Quick Reference and Implementation Guidance sections of AI-SPEC.md. Spawned by /gsd:ai-integration-phase orchestrator.',
85
+ color: 'green',
86
+ tools:
87
+ 'Read, Write, Edit, Bash, Grep, Glob, WebFetch, WebSearch, mcp__context7__*',
88
+ requiredIncludes: [
89
+ '@~/.claude/gsd-core/references/research-documentation-lookup.md',
90
+ ],
91
+ requiredSeamCalls: [],
92
+ outputContract: [
93
+ 'AI-SPEC.md',
94
+ 'Section 3',
95
+ 'Section 4',
96
+ ],
97
+ },
98
+ {
99
+ name: 'gsd-domain-researcher',
100
+ description:
101
+ 'Researches the business domain and real-world application context of the AI system being built. Surfaces domain expert evaluation criteria, industry-specific failure modes, regulatory context, and what "good" looks like for practitioners in this field — before the eval-planner turns it into measurable rubrics. Spawned by /gsd:ai-integration-phase orchestrator.',
102
+ color: 'purple',
103
+ tools:
104
+ 'Read, Write, Edit, Bash, Grep, Glob, WebSearch, WebFetch, mcp__context7__*',
105
+ requiredIncludes: [
106
+ '@~/.claude/gsd-core/references/research-documentation-lookup.md',
107
+ ],
108
+ requiredSeamCalls: [],
109
+ outputContract: [
110
+ 'AI-SPEC.md',
111
+ 'Section 1b',
112
+ ],
113
+ },
114
+ {
115
+ name: 'gsd-ui-researcher',
116
+ description:
117
+ 'Produces UI-SPEC.md design contract for frontend phases. Reads upstream artifacts, detects design system state, asks only unanswered questions. Spawned by /gsd:ui-phase orchestrator.',
118
+ color: 'purple',
119
+ tools:
120
+ 'Read, Write, Edit, Bash, Grep, Glob, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*',
121
+ requiredIncludes: [
122
+ '@~/.claude/gsd-core/references/research-documentation-lookup.md',
123
+ ],
124
+ requiredSeamCalls: [
125
+ 'gsd-tools query commit',
126
+ ],
127
+ outputContract: [
128
+ 'UI-SPEC.md',
129
+ '## UI-SPEC COMPLETE',
130
+ ],
131
+ },
132
+ {
133
+ name: 'gsd-research-synthesizer',
134
+ description:
135
+ 'Synthesizes research outputs from parallel researcher agents into SUMMARY.md. Spawned by /gsd:new-project after 4 researcher agents complete.',
136
+ color: 'purple',
137
+ tools: 'Read, Write, Bash',
138
+ requiredIncludes: [],
139
+ requiredSeamCalls: [
140
+ 'gsd-tools query commit',
141
+ ],
142
+ outputContract: [
143
+ '.planning/research/SUMMARY.md',
144
+ '## SYNTHESIS COMPLETE',
145
+ ],
146
+ },
147
+ ];
148
+
149
+ module.exports = { PROFILES };
@@ -2,5 +2,6 @@
2
2
  'use strict';
3
3
 
4
4
  const { runAffectedTests } = require('./affected-tests-lib.cjs');
5
+ const { runMain } = require('./lib/cli-exit.cjs');
5
6
 
6
- runAffectedTests();
7
+ runMain(runAffectedTests);
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { spawnSync } = require('child_process');
4
+ const { ExitError, runMain } = require('./lib/cli-exit.cjs');
4
5
 
5
6
  const CROSS_PLATFORM_TEST_REASON = Object.freeze({
6
7
  PASS: 'pass',
@@ -50,14 +51,17 @@ function runCrossPlatformTests(options = {}, deps = {}) {
50
51
  }
51
52
 
52
53
  if (require.main === module) {
53
- const result = runCrossPlatformTests();
54
- const line = `[cross-platform-tests] reason=${result.reason} exit=${result.exitCode}`;
55
- if (result.ok) {
56
- process.stdout.write(`${line}\n`);
57
- process.exit(0);
54
+ function main() {
55
+ const result = runCrossPlatformTests();
56
+ const line = `[cross-platform-tests] reason=${result.reason} exit=${result.exitCode}`;
57
+ if (result.ok) {
58
+ process.stdout.write(`${line}\n`);
59
+ return 0;
60
+ }
61
+ process.stderr.write(`${line}\n`);
62
+ throw new ExitError(result.exitCode);
58
63
  }
59
- process.stderr.write(`${line}\n`);
60
- process.exit(result.exitCode);
64
+ runMain(main);
61
65
  }
62
66
 
63
67
  module.exports = { CROSS_PLATFORM_TEST_REASON, runCrossPlatformTests };