@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
@@ -170,6 +170,7 @@
170
170
 
171
171
  const fs = require('fs');
172
172
  const path = require('path');
173
+ const { ExitError, runMain } = require('./lib/cli-exit.cjs');
173
174
  const core = require('./lib/core.cjs');
174
175
  const { error, ERROR_REASON } = core;
175
176
  // Resolve findProjectRoot lazily at call time rather than binding it at module
@@ -197,6 +198,8 @@ const learnings = require('./lib/learnings.cjs');
197
198
  const gapChecker = require('./lib/gap-checker.cjs');
198
199
  const { routeStateCommand } = require('./lib/state-command-router.cjs');
199
200
  const { routeVerifyCommand } = require('./lib/verify-command-router.cjs');
201
+ const { routeVerificationCommand } = require('./lib/verification-command-router.cjs');
202
+ const verification = require('./lib/verification.cjs');
200
203
  const { routeInitCommand } = require('./lib/init-command-router.cjs');
201
204
  const { routePhaseCommand } = require('./lib/phase-command-router.cjs');
202
205
  const { routePhasesCommand } = require('./lib/phases-command-router.cjs');
@@ -298,13 +301,11 @@ async function main() {
298
301
 
299
302
  // Optional workstream override for parallel milestone work.
300
303
  // Priority: --ws flag > GSD_WORKSTREAM env var > session/shared pointer > null.
301
- let ws = null;
302
304
  let workstreamContext = null;
303
305
  try {
304
306
  workstreamContext = resolveActiveWorkstream(cwd, args, process.env, {
305
307
  getStored: getActiveWorkstream,
306
308
  });
307
- ws = workstreamContext.ws;
308
309
  args = workstreamContext.args;
309
310
  // Set env var so all modules (planningDir, planningPaths) auto-resolve workstream paths.
310
311
  applyResolvedWorkstreamEnv(workstreamContext, process.env);
@@ -378,8 +379,8 @@ async function main() {
378
379
  'current-timestamp, detect-custom-files, docs-init, effort, extract-messages, find-phase, ' +
379
380
  'from-gsd2, frontmatter, gap-analysis, generate-claude-md, generate-claude-profile, ' +
380
381
  'generate-dev-preferences, generate-slug, graphify, history-digest, init, intel, ' +
381
- 'learnings, list-todos, milestone, phase, phase-plan-index, phases, profile-questionnaire, ' +
382
- 'profile-sample, progress, prompt-budget, requirements, resolve-granularity, resolve-model, roadmap, scaffold, state, ' +
382
+ 'classify-confidence, learnings, list-todos, milestone, package-legitimacy, phase, phase-plan-index, phases, profile-questionnaire, ' +
383
+ 'profile-sample, progress, prompt-budget, requirements, research-plan, research-store, resolve-granularity, resolve-model, roadmap, scaffold, state, ' +
383
384
  'task, template, validate, verify, verify-path-exists, verify-summary, workstream, worktree\n\n' +
384
385
  'Global flags:\n' +
385
386
  ' --raw Emit raw output without post-processing\n' +
@@ -423,6 +424,7 @@ async function main() {
423
424
  'generate-slug', 'current-timestamp', 'verify-path-exists',
424
425
  'verify-summary', 'template', 'frontmatter', 'detect-custom-files',
425
426
  'worktree', 'prompt-budget',
427
+ 'research-store', 'research-plan', 'package-legitimacy', 'classify-confidence',
426
428
  ]);
427
429
  if (!SKIP_ROOT_RESOLUTION.has(command)) {
428
430
  cwd = findProjectRoot(cwd);
@@ -547,7 +549,22 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
547
549
  }
548
550
 
549
551
  case 'resolve-granularity': {
550
- commands.cmdResolveGranularity(cwd, args[1], raw);
552
+ // Parse optional --granularity <val> flag (space form only); positional is phase-type.
553
+ // The =form (--granularity=<val>) is intentionally not supported: parseNamedArgs and
554
+ // the /gsd:plan-phase + init plan-phase paths accept only the space form, so supporting
555
+ // = here alone would create an inconsistency (#703).
556
+ const granArgs = args.slice(1);
557
+ let granOverride;
558
+ const granPositionals = [];
559
+ for (let i = 0; i < granArgs.length; i++) {
560
+ const a = granArgs[i];
561
+ if (a === '--granularity' && granArgs[i + 1] !== undefined && !granArgs[i + 1].startsWith('--')) {
562
+ if (granOverride === undefined) { granOverride = granArgs[++i]; } else { ++i; }
563
+ } else {
564
+ granPositionals.push(a);
565
+ }
566
+ }
567
+ commands.cmdResolveGranularity(cwd, granPositionals[0], raw, granOverride);
551
568
  break;
552
569
  }
553
570
 
@@ -761,6 +778,26 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
761
778
  break;
762
779
  }
763
780
 
781
+ // ─── Verification Status ───────────────────────────────────────────────
782
+ //
783
+ // verification status <phaseDir>
784
+ // Read the first *-VERIFICATION.md in phaseDir and return
785
+ // { status, next_action, next_command } routing result.
786
+ //
787
+ // Note: `verification` (reads verifier-emitted status) is distinct from
788
+ // `verify` (runs verification checks like plan-structure/artifacts).
789
+
790
+ case 'verification': {
791
+ routeVerificationCommand({
792
+ verification,
793
+ args,
794
+ cwd,
795
+ raw,
796
+ error,
797
+ });
798
+ break;
799
+ }
800
+
764
801
  case 'generate-slug': {
765
802
  // Phase 6 (#3575): dispatch via SDK executeForCjs when available.
766
803
  // SDK handler: generateSlug in sdk/src/query/utils.ts.
@@ -1232,8 +1269,12 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1232
1269
  worktreeSafety.cmdWorktreeCleanupWave(cwd, args.slice(2));
1233
1270
  } else if (subcommand === 'reap-orphans') {
1234
1271
  worktreeSafety.cmdWorktreeReapOrphans(cwd);
1272
+ } else if (subcommand === 'base-check') {
1273
+ require('./lib/worktree-base-ref.cjs').cmdWorktreeBaseCheck(cwd, args.slice(2));
1274
+ } else if (subcommand === 'set-baseref') {
1275
+ require('./lib/worktree-base-ref.cjs').cmdWorktreeSetBaseRef(cwd, args.slice(2));
1235
1276
  } else {
1236
- error('Unknown worktree subcommand. Available: cleanup-wave, reap-orphans', ERROR_REASON.SDK_UNKNOWN_COMMAND);
1277
+ error('Unknown worktree subcommand. Available: cleanup-wave, reap-orphans, base-check, set-baseref', ERROR_REASON.SDK_UNKNOWN_COMMAND);
1237
1278
  }
1238
1279
  break;
1239
1280
  }
@@ -1528,33 +1569,26 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1528
1569
 
1529
1570
  // ── Validate required args ─────────────────────────────────────────
1530
1571
  if (!budgetStr) {
1531
- process.stderr.write('Error: --budget <N> is required\n');
1532
- process.exit(1);
1572
+ throw new ExitError(1, 'Error: --budget <N> is required');
1533
1573
  }
1534
1574
  const budget = parseInt(budgetStr, 10);
1535
1575
  if (!Number.isFinite(budget) || budget <= 0) {
1536
- process.stderr.write('Error: --budget must be a positive integer\n');
1537
- process.exit(1);
1576
+ throw new ExitError(1, 'Error: --budget must be a positive integer');
1538
1577
  }
1539
1578
  if (!instructionsFile) {
1540
- process.stderr.write('Error: --instructions-file <path> is required\n');
1541
- process.exit(1);
1579
+ throw new ExitError(1, 'Error: --instructions-file <path> is required');
1542
1580
  }
1543
1581
  if (!roadmapFile) {
1544
- process.stderr.write('Error: --roadmap-file <path> is required\n');
1545
- process.exit(1);
1582
+ throw new ExitError(1, 'Error: --roadmap-file <path> is required');
1546
1583
  }
1547
1584
  if (planFiles.length === 0) {
1548
- process.stderr.write('Error: at least one --plan-file <path> is required\n');
1549
- process.exit(1);
1585
+ throw new ExitError(1, 'Error: at least one --plan-file <path> is required');
1550
1586
  }
1551
1587
  if (!outputPromptFile) {
1552
- process.stderr.write('Error: --output-prompt <path> is required\n');
1553
- process.exit(1);
1588
+ throw new ExitError(1, 'Error: --output-prompt <path> is required');
1554
1589
  }
1555
1590
  if (!outputMetadataFile) {
1556
- process.stderr.write('Error: --output-metadata <path> is required\n');
1557
- process.exit(1);
1591
+ throw new ExitError(1, 'Error: --output-metadata <path> is required');
1558
1592
  }
1559
1593
 
1560
1594
  // ── Validate and read required files ──────────────────────────────
@@ -1564,11 +1598,9 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1564
1598
  return await fs.promises.readFile(resolved, 'utf8');
1565
1599
  } catch (err) {
1566
1600
  if (err && err.code === 'ENOENT') {
1567
- process.stderr.write(`Error: file not found for ${flagName}: ${resolved}\n`);
1568
- process.exit(1);
1601
+ throw new ExitError(1, `Error: file not found for ${flagName}: ${resolved}`);
1569
1602
  }
1570
- process.stderr.write(`Error: cannot read file for ${flagName}: ${resolved}\n`);
1571
- process.exit(1);
1603
+ throw new ExitError(1, `Error: cannot read file for ${flagName}: ${resolved}`);
1572
1604
  }
1573
1605
  }
1574
1606
 
@@ -1579,8 +1611,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1579
1611
  return await fs.promises.readFile(resolved, 'utf8');
1580
1612
  } catch (err) {
1581
1613
  if (err && err.code === 'ENOENT') return null;
1582
- process.stderr.write(`Error: cannot read optional file: ${resolved}\n`);
1583
- process.exit(1);
1614
+ throw new ExitError(1, `Error: cannot read optional file: ${resolved}`);
1584
1615
  }
1585
1616
  }
1586
1617
 
@@ -1593,11 +1624,9 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1593
1624
  return { file: path.basename(p), content };
1594
1625
  } catch (err) {
1595
1626
  if (err && err.code === 'ENOENT') {
1596
- process.stderr.write(`Error: plan file not found: ${resolved}\n`);
1597
- process.exit(1);
1627
+ throw new ExitError(1, `Error: plan file not found: ${resolved}`);
1598
1628
  }
1599
- process.stderr.write(`Error: cannot read plan file: ${resolved}\n`);
1600
- process.exit(1);
1629
+ throw new ExitError(1, `Error: cannot read plan file: ${resolved}`);
1601
1630
  }
1602
1631
  }));
1603
1632
 
@@ -1626,7 +1655,7 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1626
1655
  await fs.promises.writeFile(path.resolve(outputPromptFile), prompt);
1627
1656
 
1628
1657
  if (metadata.hardFailed) {
1629
- process.exit(2);
1658
+ throw new ExitError(2);
1630
1659
  }
1631
1660
  break;
1632
1661
  }
@@ -1663,6 +1692,183 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1663
1692
  break;
1664
1693
  }
1665
1694
 
1695
+ // ─── Research Store ────────────────────────────────────────────────────
1696
+ //
1697
+ // research-store get <key> [--kind <k>]
1698
+ // -> getResearch(cwd, key, { homeDir }); searches both tiers; core.output(result, raw)
1699
+ // (--kind is accepted for backward compatibility but no longer drives tier selection)
1700
+ // research-store put <key> --content <str> --source <s> --provider <p>
1701
+ // --confidence <c> --kind <k>
1702
+ // -> putResearch(cwd, key, { content, source, provider, confidence, kind })
1703
+ //
1704
+ // Tier is derived from source: 'curated' source writes to process.env.HOME/.gsd/research-cache;
1705
+ // all other sources write to cwd/.planning/research/.cache.
1706
+ // Tests may override the home directory by setting the HOME env var.
1707
+
1708
+ case 'research-store': {
1709
+ const researchStore = require('./lib/research-store.cjs');
1710
+ const subcommand = args[1];
1711
+ const homeDir = process.env.HOME || require('os').homedir();
1712
+ if (subcommand === 'get') {
1713
+ const key = args[2];
1714
+ if (!key || key.startsWith('--')) {
1715
+ error('Usage: gsd-tools research-store get <key> [--kind <k>]', ERROR_REASON.USAGE);
1716
+ }
1717
+ if (!researchStore.isValidResearchKey(key)) {
1718
+ error('research-store: <key> must be a 64-char sha256 hex (use research-plan to obtain keys)', ERROR_REASON.USAGE);
1719
+ }
1720
+ // --kind is accepted but no longer drives tier selection; getResearch searches both tiers
1721
+ const result = researchStore.getResearch(cwd, key, { homeDir });
1722
+ core.output(result, raw);
1723
+ } else if (subcommand === 'put') {
1724
+ const key = args[2];
1725
+ if (!key || key.startsWith('--')) {
1726
+ error('Usage: gsd-tools research-store put <key> --content <str> --source <s> --provider <p> --confidence <c> --kind <k>', ERROR_REASON.USAGE);
1727
+ }
1728
+ if (!researchStore.isValidResearchKey(key)) {
1729
+ error('research-store: <key> must be a 64-char sha256 hex (use research-plan to obtain keys)', ERROR_REASON.USAGE);
1730
+ }
1731
+ const contentIdx = args.indexOf('--content');
1732
+ const sourceIdx = args.indexOf('--source');
1733
+ const providerIdx = args.indexOf('--provider');
1734
+ const confidenceIdx = args.indexOf('--confidence');
1735
+ const kindIdx = args.indexOf('--kind');
1736
+ // For each flag, if the following value is missing or itself starts with '--', reject.
1737
+ function getFlagValue(idx, flagName) {
1738
+ if (idx === -1) return null;
1739
+ const val = args[idx + 1];
1740
+ if (val === undefined || val.startsWith('--')) {
1741
+ error(`research-store put: missing value for ${flagName}`, ERROR_REASON.USAGE);
1742
+ }
1743
+ return val;
1744
+ }
1745
+ const content = getFlagValue(contentIdx, '--content');
1746
+ const source = getFlagValue(sourceIdx, '--source');
1747
+ const provider = getFlagValue(providerIdx, '--provider');
1748
+ const confidence = getFlagValue(confidenceIdx, '--confidence');
1749
+ const kind = getFlagValue(kindIdx, '--kind');
1750
+ if (!content || !source || !provider || !confidence || !kind) {
1751
+ error('Usage: gsd-tools research-store put <key> --content <str> --source <s> --provider <p> --confidence <c> --kind <k>', ERROR_REASON.USAGE);
1752
+ }
1753
+ const entry = researchStore.putResearch(cwd, key, { content, source, provider, confidence, kind }, { homeDir });
1754
+ core.output(entry, raw);
1755
+ } else {
1756
+ error('Unknown research-store subcommand. Available: get, put', ERROR_REASON.SDK_UNKNOWN_COMMAND);
1757
+ }
1758
+ break;
1759
+ }
1760
+
1761
+ // ─── Research Plan ─────────────────────────────────────────────────────
1762
+ //
1763
+ // research-plan --input <path>
1764
+ // Read+JSON.parse file; call planResearch({ questions, ecosystem, config, cwd })
1765
+ // { ecosystem, config, questions: [{ text, kind, library?, version? }] }
1766
+
1767
+ case 'research-plan': {
1768
+ const researchProvider = require('./lib/research-provider.cjs');
1769
+ const inputIdx = args.indexOf('--input');
1770
+ const inputPath = inputIdx !== -1 ? args[inputIdx + 1] : null;
1771
+ if (!inputPath || inputPath.startsWith('--')) {
1772
+ error('Usage: gsd-tools research-plan --input <path>', ERROR_REASON.USAGE);
1773
+ }
1774
+ let planInput;
1775
+ try {
1776
+ const raw_ = fs.readFileSync(path.resolve(inputPath), 'utf8');
1777
+ planInput = JSON.parse(raw_);
1778
+ } catch (readErr) {
1779
+ error(`research-plan: cannot read/parse --input file: ${inputPath}`, ERROR_REASON.USAGE);
1780
+ }
1781
+ if (planInput === null || typeof planInput !== 'object' || Array.isArray(planInput)) {
1782
+ error('research-plan: --input must be an object with a questions array', ERROR_REASON.USAGE);
1783
+ }
1784
+ if (!Array.isArray(planInput.questions)) {
1785
+ error('research-plan: --input must be an object with a questions array', ERROR_REASON.USAGE);
1786
+ }
1787
+ const { ecosystem = '', config: planConfig = {}, questions } = planInput;
1788
+ const homeDir = process.env.HOME || require('os').homedir();
1789
+ const plan = researchProvider.planResearch({ questions, ecosystem, config: planConfig, cwd, homeDir });
1790
+ core.output(plan, raw);
1791
+ break;
1792
+ }
1793
+
1794
+ // ─── Classify Confidence ──────────────────────────────────────────────
1795
+ //
1796
+ // classify-confidence --provider <id> [--package <name> --ecosystem <npm|pypi|crates>] [--verified]
1797
+ // -> classifyConfidence({ provider, verifiedAgainstOfficial, legitimacyVerdict }); core.output(result, raw)
1798
+ //
1799
+ // legitimacyVerdict is CODE-COMPUTED via checkPackages — never caller-supplied — so an agent cannot self-assert OK→HIGH.
1800
+
1801
+ case 'classify-confidence': {
1802
+ const researchProvider = require('./lib/research-provider.cjs');
1803
+ const providerIdx = args.indexOf('--provider');
1804
+ const provider = providerIdx !== -1 ? args[providerIdx + 1] : null;
1805
+ if (!provider || provider.startsWith('--')) {
1806
+ error('Usage: gsd-tools query classify-confidence --provider <id> [--package <name> --ecosystem <npm|pypi|crates>] [--verified]', ERROR_REASON.USAGE);
1807
+ }
1808
+ const verified = args.includes('--verified');
1809
+ const pkgIdx = args.indexOf('--package');
1810
+ const pkg = pkgIdx !== -1 ? args[pkgIdx + 1] : null;
1811
+ const ecoIdx = args.indexOf('--ecosystem');
1812
+ const ecosystem = ecoIdx !== -1 ? args[ecoIdx + 1] : null;
1813
+ let legitimacyVerdict = null;
1814
+ if (pkg && (!pkg.startsWith('--'))) {
1815
+ const VALID_ECOSYSTEMS = new Set(['npm', 'pypi', 'crates']);
1816
+ if (!ecosystem || ecosystem.startsWith('--') || !VALID_ECOSYSTEMS.has(ecosystem)) {
1817
+ error('Usage: gsd-tools query classify-confidence --provider <id> [--package <name> --ecosystem <npm|pypi|crates>] [--verified]', ERROR_REASON.USAGE);
1818
+ }
1819
+ const pkgLegitimacy = require('./lib/package-legitimacy.cjs');
1820
+ const results = await pkgLegitimacy.checkPackages({ ecosystem, packages: [pkg] }, {});
1821
+ legitimacyVerdict = results[0] ? results[0].verdict : null;
1822
+ }
1823
+ const confidence = researchProvider.classifyConfidence({ provider, verifiedAgainstOfficial: verified, legitimacyVerdict });
1824
+ core.output({ provider, package: pkg || null, ecosystem: ecosystem || null, legitimacyVerdict, verified, confidence }, raw);
1825
+ break;
1826
+ }
1827
+
1828
+ // ─── Package Legitimacy ────────────────────────────────────────────────
1829
+ //
1830
+ // package-legitimacy check --ecosystem <eco> <pkg1> <pkg2> ...
1831
+ //
1832
+ // checkPackages is ASYNC. This entire runCommand function is async, so
1833
+ // we can await directly. On rejection we call error() which exits.
1834
+
1835
+ case 'package-legitimacy': {
1836
+ const pkgLegitimacy = require('./lib/package-legitimacy.cjs');
1837
+ const subcommand = args[1];
1838
+ if (subcommand !== 'check') {
1839
+ error('Unknown package-legitimacy subcommand. Available: check', ERROR_REASON.SDK_UNKNOWN_COMMAND);
1840
+ }
1841
+ const ecoIdx = args.indexOf('--ecosystem');
1842
+ const ecosystem = ecoIdx !== -1 ? args[ecoIdx + 1] : null;
1843
+ const VALID_ECOSYSTEMS = new Set(['npm', 'pypi', 'crates']);
1844
+ if (!ecosystem || !VALID_ECOSYSTEMS.has(ecosystem)) {
1845
+ error('Usage: gsd-tools package-legitimacy check --ecosystem <npm|pypi|crates> <pkg1> ...', ERROR_REASON.USAGE);
1846
+ }
1847
+ // Collect positional package names.
1848
+ // Only --ecosystem takes a value. Every non-flag arg is a package name.
1849
+ // Any unknown --flag is a usage error (do not silently skip+consume the next arg).
1850
+ const packages = [];
1851
+ for (let i = 2; i < args.length; i++) {
1852
+ const a = args[i];
1853
+ if (a === '--ecosystem') { i++; continue; }
1854
+ if (a.startsWith('--')) {
1855
+ error(`package-legitimacy: unknown flag ${a}`, ERROR_REASON.USAGE);
1856
+ }
1857
+ packages.push(a);
1858
+ }
1859
+ if (packages.length === 0) {
1860
+ error('Usage: gsd-tools package-legitimacy check --ecosystem <eco> <pkg1> <pkg2> ...', ERROR_REASON.USAGE);
1861
+ }
1862
+ let pkgResults;
1863
+ try {
1864
+ pkgResults = await pkgLegitimacy.checkPackages({ ecosystem, packages }, {});
1865
+ } catch (pkgErr) {
1866
+ error(`package-legitimacy: ${pkgErr && pkgErr.message ? pkgErr.message : String(pkgErr)}`, ERROR_REASON.UNKNOWN);
1867
+ }
1868
+ core.output(pkgResults, raw);
1869
+ break;
1870
+ }
1871
+
1666
1872
  case 'effort': {
1667
1873
  const subcommand = args[1];
1668
1874
  if (subcommand === 'sync') {
@@ -1719,4 +1925,4 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1719
1925
  }
1720
1926
  }
1721
1927
 
1722
- main();
1928
+ runMain(main);
@@ -239,6 +239,7 @@ function recentCommitMessages(projectDir) {
239
239
  cwd: projectDir,
240
240
  encoding: 'utf-8',
241
241
  maxBuffer: 4 * 1024 * 1024,
242
+ windowsHide: true,
242
243
  });
243
244
  }
244
245
  catch {
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ /**
3
+ * Error carrying a process exit code. CLI logic throws this instead of calling
4
+ * process.exit() (banned by n/no-process-exit); runMain() translates it into
5
+ * process.exitCode at the entrypoint.
6
+ */
7
+ class ExitError extends Error {
8
+ code;
9
+ hasUserMessage;
10
+ constructor(code = 1, message) {
11
+ super(message === undefined ? `process exit ${code}` : message);
12
+ this.name = 'ExitError';
13
+ this.code = code;
14
+ this.hasUserMessage = message !== undefined;
15
+ }
16
+ }
17
+ /**
18
+ * Run a CLI main and translate its outcome into process.exitCode (never
19
+ * process.exit, so n/no-process-exit stays satisfied; output flushes and
20
+ * process.on('exit') cleanup still fires). main may be sync or async:
21
+ * number return -> process.exitCode = it
22
+ * thrown ExitError -> process.exitCode = err.code (+ stderr err.message if hasUserMessage && code!=0)
23
+ * other throw -> stderr stack + process.exitCode = 1
24
+ */
25
+ function runMain(main) {
26
+ Promise.resolve()
27
+ .then(() => main())
28
+ .then((code) => { if (typeof code === 'number')
29
+ process.exitCode = code; })
30
+ .catch((err) => {
31
+ if (err instanceof ExitError) {
32
+ if (err.hasUserMessage && err.code !== 0)
33
+ process.stderr.write(`${err.message}\n`);
34
+ process.exitCode = err.code;
35
+ return;
36
+ }
37
+ const e = err;
38
+ process.stderr.write(`${e && e.stack ? e.stack : String(err)}\n`);
39
+ process.exitCode = 1;
40
+ });
41
+ }
42
+ module.exports = { ExitError, runMain };
@@ -189,7 +189,7 @@ function createHub({ cjsRegistry, manifest, logger } = {}) {
189
189
  }
190
190
  // err variant: already has kind + typed payload
191
191
  // Double-cast through unknown to satisfy strict index-signature check.
192
- return hubResult; // eslint-disable-line @typescript-eslint/no-unsafe-return
192
+ return hubResult;
193
193
  }
194
194
  /**
195
195
  * Emit a DispatchEvent to the injected logger.
@@ -14,7 +14,7 @@ const node_path_1 = __importDefault(require("node:path"));
14
14
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
15
15
  // eslint-disable-next-line @typescript-eslint/no-require-imports
16
16
  const core = require("./core.cjs");
17
- const { loadConfig, isGitIgnored, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, resolveModelInternal, resolveEffortInternal, resolveFastModeInternal, resolveEffortForTier, stripShippedMilestones: _stripShippedMilestones, extractCurrentMilestone, toPosixPath, output, error, findPhaseInternal, extractOneLinerFromBody, getRoadmapPhaseInternal, extractPhaseToken, resolveGranularityInternal, } = core;
17
+ const { loadConfig, isGitIgnored, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, resolveModelInternal, resolveEffortInternal, resolveFastModeInternal, resolveEffortForTier, stripShippedMilestones: _stripShippedMilestones, extractCurrentMilestone, toPosixPath, output, error, findPhaseInternal, extractOneLinerFromBody, getRoadmapPhaseInternal, extractPhaseToken, resolveGranularityInternal, assertValidGranularityOverride, } = core;
18
18
  const model_catalog_cjs_1 = require("./model-catalog.cjs");
19
19
  // eslint-disable-next-line @typescript-eslint/no-require-imports
20
20
  const planningWorkspace = require("./planning-workspace.cjs");
@@ -244,11 +244,12 @@ function cmdResolveModel(cwd, agentType, raw) {
244
244
  : { model, profile, effort, unknown_agent: true };
245
245
  output(result, raw, model);
246
246
  }
247
- function cmdResolveGranularity(cwd, phaseType, raw) {
247
+ function cmdResolveGranularity(cwd, phaseType, raw, override) {
248
248
  if (!phaseType) {
249
249
  error('phase-type required');
250
250
  }
251
- const granularity = resolveGranularityInternal(cwd, phaseType);
251
+ assertValidGranularityOverride(override, error);
252
+ const granularity = resolveGranularityInternal(cwd, phaseType, override);
252
253
  const result = (VALID_PHASE_TYPES).has(phaseType)
253
254
  ? { granularity, phase_type: phaseType }
254
255
  : { granularity, phase_type: phaseType, unknown_phase_type: true };
@@ -1016,7 +1017,7 @@ function cmdStats(cwd, format, raw) {
1016
1017
  const roadmapContent = extractCurrentMilestone(roadmapRaw, cwd);
1017
1018
  // Matches both plain numeric (Phase 1:) and milestone-prefixed (Phase 2-01:) headings.
1018
1019
  // Also tolerates optional [bracket-token] scope prefix on phase headings.
1019
- const headingPattern = /#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+([\w][\w.-]*(?:-[\w.-]+)*)\s*:\s*([^\n]+)/gi;
1020
+ const headingPattern = /#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+([\w][\w.-]*)\s*:\s*([^\n]+)/gi;
1020
1021
  let match;
1021
1022
  while ((match = headingPattern.exec(roadmapContent)) !== null) {
1022
1023
  const key = normalizePhaseName(match[1]);
@@ -147,6 +147,14 @@ function buildNewProjectConfig(userChoices) {
147
147
  const hasFirecrawl = !!(process.env['FIRECRAWL_API_KEY'] || node_fs_1.default.existsSync(firecrawlKeyFile));
148
148
  const exaKeyFile = node_path_1.default.join(homedir, '.gsd', 'exa_api_key');
149
149
  const hasExaSearch = !!(process.env['EXA_API_KEY'] || node_fs_1.default.existsSync(exaKeyFile));
150
+ const tavilyKeyFile = node_path_1.default.join(homedir, '.gsd', 'tavily_api_key');
151
+ const hasTavilySearch = !!(process.env['TAVILY_API_KEY'] || node_fs_1.default.existsSync(tavilyKeyFile));
152
+ const refKeyFile = node_path_1.default.join(homedir, '.gsd', 'ref_api_key');
153
+ const hasRefSearch = !!(process.env['REF_API_KEY'] || node_fs_1.default.existsSync(refKeyFile));
154
+ const perplexityKeyFile = node_path_1.default.join(homedir, '.gsd', 'perplexity_api_key');
155
+ const hasPerplexity = !!(process.env['PERPLEXITY_API_KEY'] || node_fs_1.default.existsSync(perplexityKeyFile));
156
+ const jinaKeyFile = node_path_1.default.join(homedir, '.gsd', 'jina_api_key');
157
+ const hasJina = !!(process.env['JINA_API_KEY'] || node_fs_1.default.existsSync(jinaKeyFile));
150
158
  // Load user-level defaults from ~/.gsd/defaults.json if available
151
159
  const globalDefaultsPath = node_path_1.default.join(homedir, '.gsd', 'defaults.json');
152
160
  let userDefaults = {};
@@ -176,6 +184,10 @@ function buildNewProjectConfig(userChoices) {
176
184
  brave_search: hasBraveSearch,
177
185
  firecrawl: hasFirecrawl,
178
186
  exa_search: hasExaSearch,
187
+ tavily_search: hasTavilySearch,
188
+ ref_search: hasRefSearch,
189
+ perplexity: hasPerplexity,
190
+ jina: hasJina,
179
191
  git: {
180
192
  branching_strategy: CONFIG_DEFAULTS.branching_strategy,
181
193
  create_tag: true,
@@ -377,18 +389,31 @@ function setConfigValue(cwd, keyPath, parsedValue) {
377
389
  catch (err) {
378
390
  error('Failed to read config.json: ' + err.message, ERROR_REASON.CONFIG_PARSE_FAILED);
379
391
  }
380
- // Set nested value using dot notation (e.g., "workflow.research")
392
+ // Set nested value using dot notation (e.g., "workflow.research").
393
+ // Prototype-pollution guard: reject dangerous segments via inline literal
394
+ // comparisons on the exact key used to index `current`, immediately before
395
+ // each write. The inline comparison is the barrier CodeQL's
396
+ // js/prototype-pollution-utility query recognises — the previous Set-based
397
+ // pre-loop check was functionally correct but not traced through, so
398
+ // code-scanning alert #26 kept firing. Behaviour is unchanged from #663.
381
399
  const keys = keyPath.split('.');
382
400
  let current = config;
383
401
  for (let i = 0; i < keys.length - 1; i++) {
384
402
  const key = keys[i];
403
+ if (key === '__proto__' || key === 'prototype' || key === 'constructor') {
404
+ error('Invalid config key (prototype pollution guard): ' + keyPath, ERROR_REASON.CONFIG_PARSE_FAILED);
405
+ }
385
406
  if (current[key] === undefined || typeof current[key] !== 'object') {
386
407
  current[key] = {};
387
408
  }
388
409
  current = current[key];
389
410
  }
390
- const previousValue = current[keys[keys.length - 1]]; // Capture previous value before overwriting
391
- current[keys[keys.length - 1]] = parsedValue;
411
+ const lastKey = keys[keys.length - 1];
412
+ if (lastKey === '__proto__' || lastKey === 'prototype' || lastKey === 'constructor') {
413
+ error('Invalid config key (prototype pollution guard): ' + keyPath, ERROR_REASON.CONFIG_PARSE_FAILED);
414
+ }
415
+ const previousValue = current[lastKey]; // Capture previous value before overwriting
416
+ current[lastKey] = parsedValue;
392
417
  // Write back
393
418
  try {
394
419
  (0, shell_command_projection_cjs_1.platformWriteSync)(configPath, JSON.stringify(config, null, 2));
@@ -611,7 +636,6 @@ function cmdConfigSetModelProfile(cwd, profile, raw) {
611
636
  if (!profile) {
612
637
  error(`Usage: config-set-model-profile <${VALID_PROFILES.join('|')}>`);
613
638
  }
614
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
615
639
  const normalizedProfile = profile.toLowerCase().trim();
616
640
  if (!VALID_PROFILES.includes(normalizedProfile)) {
617
641
  error(`Invalid profile '${String(profile)}'. Valid profiles: ${VALID_PROFILES.join(', ')}`);