@opengsd/gsd-core 1.7.0-rc.3 → 1.7.0-rc.5

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 (115) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +20 -0
  4. package/README.ja-JP.md +4 -3
  5. package/README.ko-KR.md +4 -3
  6. package/README.md +4 -3
  7. package/README.pt-BR.md +4 -3
  8. package/README.zh-CN.md +4 -3
  9. package/agents/gsd-doc-classifier.md +105 -0
  10. package/agents/gsd-doc-synthesizer.md +61 -0
  11. package/agents/gsd-ui-checker.md +28 -0
  12. package/bin/install.js +652 -466
  13. package/commands/gsd/map-codebase.md +4 -4
  14. package/commands/gsd/ns-project.md +2 -1
  15. package/commands/gsd/onboard.md +46 -0
  16. package/gsd-core/bin/gsd-tools.cjs +87 -3
  17. package/gsd-core/bin/lib/api-coverage.cjs +466 -0
  18. package/gsd-core/bin/lib/audit.cjs +6 -3
  19. package/gsd-core/bin/lib/capability-loader.cjs +11 -9
  20. package/gsd-core/bin/lib/capability-registry.cjs +741 -62
  21. package/gsd-core/bin/lib/capability-state.cjs +117 -18
  22. package/gsd-core/bin/lib/capability-validator.cjs +1 -1
  23. package/gsd-core/bin/lib/capability-writer.cjs +21 -2
  24. package/gsd-core/bin/lib/check-command-router.cjs +242 -3
  25. package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +136 -0
  26. package/gsd-core/bin/lib/claude-orchestration.cjs +404 -0
  27. package/gsd-core/bin/lib/clusters.cjs +1 -0
  28. package/gsd-core/bin/lib/command-aliases.cjs +8 -0
  29. package/gsd-core/bin/lib/commands.cjs +7 -5
  30. package/gsd-core/bin/lib/config-loader.cjs +1 -0
  31. package/gsd-core/bin/lib/config.cjs +96 -0
  32. package/gsd-core/bin/lib/core-utils.cjs +4 -1
  33. package/gsd-core/bin/lib/host-integration-adapters/cline-sdk-binding.cjs +234 -0
  34. package/gsd-core/bin/lib/host-integration-adapters/imperative-hook-bus.cjs +145 -0
  35. package/gsd-core/bin/lib/host-integration.cjs +16 -0
  36. package/gsd-core/bin/lib/init-command-router.cjs +4 -0
  37. package/gsd-core/bin/lib/init.cjs +99 -99
  38. package/gsd-core/bin/lib/install-effort-resolver.cjs +213 -0
  39. package/gsd-core/bin/lib/install-engine.cjs +159 -16
  40. package/gsd-core/bin/lib/install-profiles.cjs +2 -0
  41. package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
  42. package/gsd-core/bin/lib/loop-resolver.cjs +75 -18
  43. package/gsd-core/bin/lib/markdown-sectionizer.cjs +50 -11
  44. package/gsd-core/bin/lib/milestone.cjs +3 -3
  45. package/gsd-core/bin/lib/model-resolver.cjs +69 -4
  46. package/gsd-core/bin/lib/normalize-test-command.cjs +187 -0
  47. package/gsd-core/bin/lib/onboard-projection.cjs +309 -0
  48. package/gsd-core/bin/lib/phase-id.cjs +132 -3
  49. package/gsd-core/bin/lib/phase.cjs +78 -16
  50. package/gsd-core/bin/lib/planning-workspace.cjs +17 -0
  51. package/gsd-core/bin/lib/roadmap-command-router.cjs +5 -4
  52. package/gsd-core/bin/lib/roadmap-parser.cjs +21 -30
  53. package/gsd-core/bin/lib/roadmap-upgrade.cjs +9 -9
  54. package/gsd-core/bin/lib/roadmap.cjs +42 -56
  55. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +13 -6
  56. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +2 -2
  57. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +17 -10
  58. package/gsd-core/bin/lib/runtime-homes.cjs +8 -0
  59. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +33 -25
  60. package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
  61. package/gsd-core/bin/lib/spec-section.cjs +111 -0
  62. package/gsd-core/bin/lib/state-transition.cjs +1 -1
  63. package/gsd-core/bin/lib/state.cjs +24 -24
  64. package/gsd-core/bin/lib/surface.cjs +81 -30
  65. package/gsd-core/bin/lib/uat.cjs +4 -1
  66. package/gsd-core/bin/lib/validate.cjs +15 -6
  67. package/gsd-core/bin/lib/verify.cjs +33 -37
  68. package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
  69. package/gsd-core/bin/shared/model-catalog.json +11 -6
  70. package/gsd-core/references/api-coverage.md +104 -0
  71. package/gsd-core/references/gsd-run-resolver.md +8 -0
  72. package/gsd-core/references/model-profiles.md +2 -2
  73. package/gsd-core/references/planning-config.md +2 -0
  74. package/gsd-core/references/specless-probe-fallback.md +172 -0
  75. package/gsd-core/templates/config.json +2 -1
  76. package/gsd-core/templates/project.md +1 -1
  77. package/gsd-core/workflows/audit-fix.md +9 -1
  78. package/gsd-core/workflows/code-review-fix.md +7 -3
  79. package/gsd-core/workflows/code-review.md +4 -1
  80. package/gsd-core/workflows/discuss-phase-assumptions.md +4 -1
  81. package/gsd-core/workflows/do.md +4 -3
  82. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +8 -4
  83. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +42 -0
  84. package/gsd-core/workflows/execute-phase.md +1 -25
  85. package/gsd-core/workflows/help/modes/brief.md +2 -1
  86. package/gsd-core/workflows/help/modes/default.md +2 -1
  87. package/gsd-core/workflows/help/modes/full.md +11 -1
  88. package/gsd-core/workflows/help/modes/topic.md +1 -1
  89. package/gsd-core/workflows/onboard.md +277 -0
  90. package/gsd-core/workflows/plan-phase.md +31 -2
  91. package/gsd-core/workflows/quick.md +22 -2
  92. package/gsd-core/workflows/review.md +59 -13
  93. package/gsd-core/workflows/settings-advanced.md +5 -5
  94. package/gsd-core/workflows/settings.md +2 -2
  95. package/gsd-core/workflows/verify-phase.md +3 -2
  96. package/gsd-core/workflows/verify-work.md +38 -0
  97. package/hooks/dist/gsd-cursor-pre-tool.js +76 -0
  98. package/hooks/dist/gsd-cursor-stop.js +48 -0
  99. package/hooks/dist/gsd-cursor-subagent-start.js +50 -0
  100. package/hooks/dist/gsd-cursor-subagent-stop.js +40 -0
  101. package/hooks/dist/managed-hooks-registry.cjs +4 -0
  102. package/hooks/gsd-cursor-pre-tool.js +76 -0
  103. package/hooks/gsd-cursor-stop.js +48 -0
  104. package/hooks/gsd-cursor-subagent-start.js +50 -0
  105. package/hooks/gsd-cursor-subagent-stop.js +40 -0
  106. package/hooks/managed-hooks-registry.cjs +4 -0
  107. package/package.json +2 -1
  108. package/scripts/build-hooks.js +5 -1
  109. package/scripts/gen-golden-install-parity-zcode.cjs +77 -0
  110. package/scripts/lint-phase-id-drift.cjs +150 -0
  111. package/scripts/run-tests.cjs +26 -4
  112. package/scripts/sync-runtime-launcher.cjs +20 -2
  113. package/skills/gsd-map-codebase/SKILL.md +3 -3
  114. package/skills/gsd-ns-project/SKILL.md +1 -0
  115. package/skills/gsd-onboard/SKILL.md +46 -0
@@ -54,15 +54,18 @@ const { checkAgentsInstalled } = agentInstallCheck;
54
54
  const gitBaseBranch = require("./git-base-branch.cjs");
55
55
  const { gitWorktreeInfoInternal } = gitBaseBranch;
56
56
  const resolution_cjs_1 = require("./resolution.cjs");
57
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- onboard-projection.cjs is an export= CommonJS module
58
+ const onboardProjection = require("./onboard-projection.cjs");
59
+ const { REQUIRED_CODEBASE_MAP_FILES, buildOnboardProjection, hasCodeFilesInternal, hasPackageFileInternal, listCodebaseMapFiles, } = onboardProjection;
57
60
  const { output, error } = io;
58
61
  const { loadConfig, loadConfigResolved } = configLoader;
59
62
  const { resolveModelInternal, resolveGranularityInternal, assertValidGranularityOverride } = modelResolver;
60
63
  const { findPhaseInternal } = phaseLocator;
61
64
  const { getRoadmapPhaseInternal, getMilestoneInfo, getMilestonePhaseFilter, stripShippedMilestones, extractCurrentMilestone, } = roadmapParser;
62
65
  const { pathExistsInternal, generateSlugInternal, toPosixPath } = coreUtils;
63
- const { normalizePhaseName, phaseTokenMatches, stripProjectCodePrefix } = phaseId;
66
+ const { escapeRegex, normalizePhaseName, phaseTokenMatches, stripProjectCodePrefix, PHASE_NUMBER_TOKEN_SOURCE } = phaseId;
64
67
  const { pruneOrphanedWorktrees } = worktreeSafety;
65
- const { planningPaths, planningDir, planningRoot, getActiveWorkstream, findContextMdIn, } = planningWorkspace;
68
+ const { planningPaths, planningDir, planningRoot, listAvailableWorkstreams, getActiveWorkstream, findContextMdIn, } = planningWorkspace;
66
69
  const { determinePhaseStatus } = commandsMod;
67
70
  const { extractFrontmatter } = frontmatterMod;
68
71
  const { readVerificationStatus } = verificationMod;
@@ -71,6 +74,35 @@ const { evaluateUatPassed } = uatPredicateMod;
71
74
  void stripShippedMilestones;
72
75
  // Accept all bold/colon variants of the Requirements header (#2769)
73
76
  const REQUIREMENTS_HEADER_RE = /^\*\*Requirements:?\*\*[^\S\n]*:?[^\S\n]*([^\n]*)$/m;
77
+ // #2056: normalizePhaseName() strips ANY [A-Z][A-Z0-9_]*- prefix as a project
78
+ // code (e.g. 'MEM-01' → '01'), so a foreign-prefixed workstream/task id would
79
+ // collapse to its numeric suffix and resolve to an unrelated numeric phase via
80
+ // the dir/roadmap fallback. init plan-phase must require EXACT prefixed
81
+ // evidence — a phase dir whose token literally IS the prefixed query, or a
82
+ // roadmap entry literally headed with it — before accepting such a match. The
83
+ // configured project_code's own prefix (e.g. LKML-01 under project_code: LKML)
84
+ // is never foreign and always passes through.
85
+ function parsePhasePrefix(phase) {
86
+ const match = String(phase).match(/^([A-Z][A-Z0-9_]*)-(?=\d)/i);
87
+ return match ? match[1] : null;
88
+ }
89
+ function isForeignPrefixedPhaseQuery(phase, projectCode) {
90
+ const prefix = parsePhasePrefix(phase);
91
+ if (!prefix)
92
+ return false;
93
+ const configured = typeof projectCode === 'string' ? projectCode.trim() : '';
94
+ return !configured || prefix.toUpperCase() !== configured.toUpperCase();
95
+ }
96
+ function phaseInfoMatchesExactPrefix(phaseInfo, phase) {
97
+ const num = phaseInfo?.['phase_number'];
98
+ const numStr = typeof num === 'string' ? num : (typeof num === 'number' ? String(num) : '');
99
+ return numStr.toUpperCase() === phase.toUpperCase();
100
+ }
101
+ function roadmapPhaseMatchesExactPrefix(roadmapPhase, phase) {
102
+ const sectionRaw = roadmapPhase?.['section'];
103
+ const section = typeof sectionRaw === 'string' ? sectionRaw : '';
104
+ return new RegExp(`^#{2,4}\\s*Phase\\s+${escapeRegex(phase)}(?:\\b|\\s|:)`, 'i').test(section);
105
+ }
74
106
  function listPhaseSummaryFiles(phaseDir) {
75
107
  return scanPhasePlans(phaseDir)['summaryFiles'];
76
108
  }
@@ -329,8 +361,18 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
329
361
  error('phase required for init plan-phase');
330
362
  }
331
363
  const config = loadConfig(cwd);
364
+ const foreignPrefixedPhase = isForeignPrefixedPhaseQuery(phase, config.project_code);
332
365
  let phaseInfo = findPhaseInternal(cwd, phase);
333
- const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
366
+ // #2056: a foreign-prefixed query must only match a phase whose token literally
367
+ // IS the prefixed query (e.g. a real 'MEM-01-*' dir); otherwise the numeric
368
+ // fallback ('MEM-01' → '01') would resolve to the unrelated numeric phase.
369
+ if (foreignPrefixedPhase && !phaseInfoMatchesExactPrefix(phaseInfo, phase)) {
370
+ phaseInfo = null;
371
+ }
372
+ let roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
373
+ if (foreignPrefixedPhase && !roadmapPhaseMatchesExactPrefix(roadmapPhase, phase)) {
374
+ roadmapPhase = null;
375
+ }
334
376
  if (phaseInfo?.['archived'] && roadmapPhase?.['found']) {
335
377
  phaseInfo = null;
336
378
  }
@@ -479,84 +521,23 @@ function cmdInitNewProject(cwd, raw) {
479
521
  const hasFirecrawl = !!(process.env['FIRECRAWL_API_KEY'] || node_fs_1.default.existsSync(firecrawlKeyFile));
480
522
  const exaKeyFile = node_path_1.default.join(homedir, '.gsd', 'exa_api_key');
481
523
  const hasExaSearch = !!(process.env['EXA_API_KEY'] || node_fs_1.default.existsSync(exaKeyFile));
482
- let hasCode = false;
483
- let hasPackageFile = false;
484
- try {
485
- const codeExtensions = new Set([
486
- '.ts', '.js', '.py', '.go', '.rs', '.swift', '.java',
487
- '.kt', '.kts',
488
- '.c', '.cpp', '.h',
489
- '.cs',
490
- '.rb',
491
- '.php',
492
- '.dart',
493
- '.m', '.mm',
494
- '.scala',
495
- '.groovy',
496
- '.lua',
497
- '.r', '.R',
498
- '.zig',
499
- '.ex', '.exs',
500
- '.clj',
501
- ]);
502
- const skipDirs = new Set([
503
- 'node_modules', '.git', '.planning', '.claude', '.codex',
504
- '__pycache__', 'target', 'dist', 'build',
505
- ]);
506
- function findCodeFiles(dir, depth) {
507
- if (depth > 3)
508
- return false;
509
- let entries;
510
- try {
511
- entries = node_fs_1.default.readdirSync(dir, { withFileTypes: true });
512
- }
513
- catch {
514
- return false;
515
- }
516
- for (const entry of entries) {
517
- if (entry.isFile() && codeExtensions.has(node_path_1.default.extname(entry.name)))
518
- return true;
519
- if (entry.isDirectory() && !skipDirs.has(entry.name)) {
520
- if (findCodeFiles(node_path_1.default.join(dir, entry.name), depth + 1))
521
- return true;
522
- }
523
- }
524
- return false;
525
- }
526
- hasCode = findCodeFiles(cwd, 0);
527
- }
528
- catch {
529
- /* intentionally empty — best-effort detection */
530
- }
531
- hasPackageFile =
532
- pathExistsInternal(cwd, 'package.json') ||
533
- pathExistsInternal(cwd, 'requirements.txt') ||
534
- pathExistsInternal(cwd, 'Cargo.toml') ||
535
- pathExistsInternal(cwd, 'go.mod') ||
536
- pathExistsInternal(cwd, 'Package.swift') ||
537
- pathExistsInternal(cwd, 'build.gradle') ||
538
- pathExistsInternal(cwd, 'build.gradle.kts') ||
539
- pathExistsInternal(cwd, 'pom.xml') ||
540
- pathExistsInternal(cwd, 'Gemfile') ||
541
- pathExistsInternal(cwd, 'composer.json') ||
542
- pathExistsInternal(cwd, 'pubspec.yaml') ||
543
- pathExistsInternal(cwd, 'CMakeLists.txt') ||
544
- pathExistsInternal(cwd, 'Makefile') ||
545
- pathExistsInternal(cwd, 'build.zig') ||
546
- pathExistsInternal(cwd, 'mix.exs') ||
547
- pathExistsInternal(cwd, 'project.clj');
524
+ const hasCode = hasCodeFilesInternal(cwd);
525
+ const hasPackageFile = hasPackageFileInternal(cwd);
526
+ const isBrownfield = hasCode || hasPackageFile;
527
+ const codebaseMapFiles = listCodebaseMapFiles(cwd);
528
+ const hasCodebaseMap = codebaseMapFiles.length === REQUIRED_CODEBASE_MAP_FILES.length;
548
529
  const result = {
549
530
  researcher_model: resolveModelInternal(cwd, 'gsd-project-researcher'),
550
531
  synthesizer_model: resolveModelInternal(cwd, 'gsd-research-synthesizer'),
551
532
  roadmapper_model: resolveModelInternal(cwd, 'gsd-roadmapper'),
552
533
  commit_docs: config.commit_docs,
553
534
  project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
554
- has_codebase_map: pathExistsInternal(cwd, '.planning/codebase'),
535
+ has_codebase_map: hasCodebaseMap,
555
536
  planning_exists: pathExistsInternal(cwd, '.planning'),
556
537
  has_existing_code: hasCode,
557
538
  has_package_file: hasPackageFile,
558
- is_brownfield: hasCode || hasPackageFile,
559
- needs_codebase_map: (hasCode || hasPackageFile) && !pathExistsInternal(cwd, '.planning/codebase'),
539
+ is_brownfield: isBrownfield,
540
+ needs_codebase_map: isBrownfield && !hasCodebaseMap,
560
541
  ...getInitGitState(cwd),
561
542
  brave_search_available: hasBraveSearch,
562
543
  firecrawl_available: hasFirecrawl,
@@ -631,6 +612,9 @@ function cmdInitQuick(cwd, description, raw) {
631
612
  executor_model: resolveModelInternal(cwd, 'gsd-executor'),
632
613
  checker_model: resolveModelInternal(cwd, 'gsd-plan-checker'),
633
614
  verifier_model: resolveModelInternal(cwd, 'gsd-verifier'),
615
+ // #2072: the quick review step spawns gsd-code-reviewer; resolve its own model
616
+ // so model_overrides / models.verification apply (was reusing executor_model).
617
+ reviewer_model: resolveModelInternal(cwd, 'gsd-code-reviewer'),
634
618
  commit_docs: config.commit_docs,
635
619
  branch_name: quickBranchName,
636
620
  quick_id: quickId,
@@ -656,6 +640,19 @@ function cmdInitIngestDocs(cwd, raw) {
656
640
  };
657
641
  output(withProjectRoot(cwd, result), raw);
658
642
  }
643
+ function cmdInitOnboard(cwd, raw, options = {}) {
644
+ const config = loadConfig(cwd);
645
+ const workflowConfig = (config.workflow ?? {});
646
+ const result = {
647
+ ...buildOnboardProjection(cwd, {
648
+ commitDocs: !!config.commit_docs,
649
+ fast: options['fast'] === true,
650
+ textMode: options['text'] === true || !!config.text_mode || !!workflowConfig['text_mode'],
651
+ }),
652
+ ...getInitGitState(cwd),
653
+ };
654
+ output(withProjectRoot(cwd, result), raw);
655
+ }
659
656
  function cmdInitResume(cwd, raw) {
660
657
  const config = loadConfig(cwd);
661
658
  let interruptedAgentId = null;
@@ -919,8 +916,8 @@ function cmdInitMilestoneOp(cwd, raw) {
919
916
  const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
920
917
  const roadmapRaw = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
921
918
  const currentSection = extractCurrentMilestone(roadmapRaw, cwd);
922
- // #1729: `(?:\s*\([^)\n]*\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
923
- const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)(?:\s*\([^)\n]*\))?\s*:/gi;
919
+ // #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
920
+ const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:`, 'gi');
924
921
  let m;
925
922
  while ((m = phasePattern.exec(currentSection)) !== null) {
926
923
  if (/^999(?:\.|$)/.test(m[1]))
@@ -941,7 +938,7 @@ function cmdInitMilestoneOp(cwd, raw) {
941
938
  for (const e of entries) {
942
939
  if (!e.isDirectory())
943
940
  continue;
944
- const m = stripProjectCodePrefix(e.name).match(/^(\d+[A-Z]?(?:\.\d+)*)/);
941
+ const m = stripProjectCodePrefix(e.name).match(new RegExp(`^(${PHASE_NUMBER_TOKEN_SOURCE})`));
945
942
  if (!m)
946
943
  continue;
947
944
  diskPhaseDirs.set(canonicalizePhase(m[1]), e.name);
@@ -1069,13 +1066,13 @@ function cmdInitManager(cwd, raw) {
1069
1066
  }
1070
1067
  })();
1071
1068
  const _checkboxStates = new Map();
1072
- const _cbPattern = /-\s*\[(x| )\]\s*.*Phase\s+(\d+[A-Z]?(?:\.\d+)*)[:\s]/gi;
1069
+ const _cbPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})[:\\s]`, 'gi');
1073
1070
  let _cbMatch;
1074
1071
  while ((_cbMatch = _cbPattern.exec(content)) !== null) {
1075
1072
  _checkboxStates.set(_cbMatch[2], _cbMatch[1].toLowerCase() === 'x');
1076
1073
  }
1077
- // #1729: `(?:\s*\([^)\n]*\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
1078
- const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)(?:\s*\([^)\n]*\))?\s*:\s*([^\n]+)/gi;
1074
+ // #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
1075
+ const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:\\s*([^\\n]+)`, 'gi');
1079
1076
  const phases = [];
1080
1077
  let match;
1081
1078
  while ((match = phasePattern.exec(content)) !== null) {
@@ -1193,7 +1190,7 @@ function cmdInitManager(cwd, raw) {
1193
1190
  .filter((p) => p['phase_complete'] === true)
1194
1191
  .map((p) => normalizePhaseNumber(p['number'])));
1195
1192
  const phaseMap = new Map(phases.map((p) => [normalizePhaseNumber(p['number']), p]));
1196
- const _allCompletedPattern = /-\s*\[x\]\s*.*Phase\s+(\d+[A-Z]?(?:\.\d+)*)[:\s]/gi;
1193
+ const _allCompletedPattern = new RegExp(`-\\s*\\[x\\]\\s*.*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})[:\\s]`, 'gi');
1197
1194
  let _allMatch;
1198
1195
  while ((_allMatch = _allCompletedPattern.exec(rawContent)) !== null) {
1199
1196
  const phaseNum = normalizePhaseNumber(_allMatch[1]);
@@ -1225,7 +1222,7 @@ function cmdInitManager(cwd, raw) {
1225
1222
  phase['deps_satisfied'] = true;
1226
1223
  }
1227
1224
  else {
1228
- const depNums = phase['depends_on'].match(/\d+[A-Z]?(?:\.\d+)*/gi) || [];
1225
+ const depNums = phase['depends_on'].match(new RegExp(`${PHASE_NUMBER_TOKEN_SOURCE}`, 'gi')) || [];
1229
1226
  phase['deps_satisfied'] = depNums.every((n) => completedNums.has(normalizePhaseNumber(n)));
1230
1227
  phase['dep_phases'] = depNums;
1231
1228
  }
@@ -1364,18 +1361,7 @@ function cmdInitProgress(cwd, raw) {
1364
1361
  // reporting a stale root milestone. Require an explicit workstream instead.
1365
1362
  // Mirror planningDir's resolution (GSD_WORKSTREAM env > stored active pointer) so
1366
1363
  // an explicit --ws (which sets GSD_WORKSTREAM) satisfies the check.
1367
- const _wsRoot = node_path_1.default.join(planningRoot(cwd), 'workstreams');
1368
- let _availableWorkstreams = [];
1369
- try {
1370
- _availableWorkstreams = node_fs_1.default
1371
- .readdirSync(_wsRoot, { withFileTypes: true })
1372
- .filter((e) => e.isDirectory())
1373
- .map((e) => e.name)
1374
- .sort();
1375
- }
1376
- catch {
1377
- /* no workstreams dir → flat mode */
1378
- }
1364
+ const _availableWorkstreams = listAvailableWorkstreams(cwd);
1379
1365
  const _resolvedWorkstream = process.env['GSD_WORKSTREAM'] || getActiveWorkstream(cwd);
1380
1366
  if (_availableWorkstreams.length > 0 && !_resolvedWorkstream) {
1381
1367
  error(`init.progress requires a workstream in workstream mode — no active workstream is set, so root STATE.md (likely stale) would be reported. ` +
@@ -1391,14 +1377,14 @@ function cmdInitProgress(cwd, raw) {
1391
1377
  const roadmapCheckboxStates = new Map();
1392
1378
  try {
1393
1379
  const roadmapContent = extractCurrentMilestone(node_fs_1.default.readFileSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md'), 'utf-8'), cwd);
1394
- // #1729: `(?:\s*\([^)\n]*\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
1395
- const headingPattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)(?:\s*\([^)\n]*\))?\s*:\s*([^\n]+)/gi;
1380
+ // #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
1381
+ const headingPattern = new RegExp(`#{2,4}\\s*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:\\s*([^\\n]+)`, 'gi');
1396
1382
  let hm;
1397
1383
  while ((hm = headingPattern.exec(roadmapContent)) !== null) {
1398
1384
  roadmapPhaseNums.add(hm[1]);
1399
1385
  roadmapPhaseNames.set(hm[1], hm[2].replace(/\(INSERTED\)/i, '').trim());
1400
1386
  }
1401
- const cbPattern = /-\s*\[(x| )\]\s*.*Phase\s+(\d+[A-Z]?(?:\.\d+)*)[:\s]/gi;
1387
+ const cbPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})[:\\s]`, 'gi');
1402
1388
  let cbm;
1403
1389
  while ((cbm = cbPattern.exec(roadmapContent)) !== null) {
1404
1390
  roadmapCheckboxStates.set(cbm[2], cbm[1].toLowerCase() === 'x');
@@ -1416,14 +1402,14 @@ function cmdInitProgress(cwd, raw) {
1416
1402
  .map((e) => e.name)
1417
1403
  .filter(isDirInMilestone)
1418
1404
  .sort((a, b) => {
1419
- const pa = a.match(/^(\d+[A-Z]?(?:\.\d+)*)/i);
1420
- const pb = b.match(/^(\d+[A-Z]?(?:\.\d+)*)/i);
1405
+ const pa = a.match(new RegExp(`^(${PHASE_NUMBER_TOKEN_SOURCE})`, 'i'));
1406
+ const pb = b.match(new RegExp(`^(${PHASE_NUMBER_TOKEN_SOURCE})`, 'i'));
1421
1407
  if (!pa || !pb)
1422
1408
  return a.localeCompare(b);
1423
1409
  return parseInt(pa[1], 10) - parseInt(pb[1], 10);
1424
1410
  });
1425
1411
  for (const dir of dirs) {
1426
- const dirMatch = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
1412
+ const dirMatch = dir.match(new RegExp(`^(${PHASE_NUMBER_TOKEN_SOURCE})-?(.*)`, 'i'));
1427
1413
  const phaseNumber = dirMatch ? dirMatch[1] : dir;
1428
1414
  const phaseName = dirMatch && dirMatch[2] ? dirMatch[2] : null;
1429
1415
  seenPhaseNums.add(phaseNumber.replace(/^0+/, '') || '0');
@@ -1915,11 +1901,24 @@ function buildSkillManifest(cwd, skillsDir = null) {
1915
1901
  kind: 'skills',
1916
1902
  },
1917
1903
  {
1918
- root: '~/.codex/skills',
1904
+ // ADR-1239 upgrade 3 (#2088): Codex's canonical skill root is
1905
+ // $HOME/.agents/skills (per codex core-skills loader.rs), resolved via
1906
+ // the skills-kind `home` override in getGlobalSkillsBase.
1907
+ root: '~/.agents/skills',
1919
1908
  path: (0, runtime_homes_cjs_1.getGlobalSkillsBase)('codex'),
1920
1909
  scope: 'global',
1921
1910
  kind: 'skills',
1922
1911
  },
1912
+ {
1913
+ // Codex's deprecated fallback skill root ($CODEX_HOME/skills). Kept as a
1914
+ // discovery-only legacy root so pre-move installs remain inventoried;
1915
+ // GSD no longer installs here (#2088).
1916
+ root: '~/.codex/skills',
1917
+ path: node_path_1.default.join((0, runtime_homes_cjs_1.getGlobalConfigDir)('codex'), 'skills'),
1918
+ scope: 'global',
1919
+ kind: 'skills',
1920
+ deprecated: true,
1921
+ },
1923
1922
  {
1924
1923
  root: '.claude/gsd-core/skills',
1925
1924
  path: node_path_1.default.join(node_os_1.default.homedir(), '.claude', 'gsd-core', 'skills'),
@@ -2099,6 +2098,7 @@ module.exports = {
2099
2098
  cmdInitNewMilestone,
2100
2099
  cmdInitQuick,
2101
2100
  cmdInitIngestDocs,
2101
+ cmdInitOnboard,
2102
2102
  cmdInitResume,
2103
2103
  cmdInitVerifyWork,
2104
2104
  cmdInitPhaseOp,
@@ -0,0 +1,213 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ /**
6
+ * install-effort-resolver — install-time effort resolution (#443), extracted from
7
+ * the package-root `bin/install.js` (#2071).
8
+ *
9
+ * `gsd-tools effort sync` (src/commands.cts `cmdEffortSync`) must mirror what the
10
+ * installer writes — home `~/.gsd/defaults.json` merged with the project's
11
+ * `.planning/config.json` — which the runtime resolver (`resolveEffortInternal`
12
+ * via `loadConfig`) does NOT do. It previously reached those two functions via
13
+ * `require('../../../bin/install.js')`, but the installer never copies the
14
+ * package-root `bin/install.js` into a runtime home, so `effort sync` crashed with
15
+ * MODULE_NOT_FOUND in every installed runtime (#2071). Moving the logic here — a
16
+ * `src/*.cts` module compiled into the shipped `gsd-core/bin/lib/` tree — lets both
17
+ * the installer AND `effort sync` require it from a location that is always present,
18
+ * keeping a single source of truth (no duplication / drift).
19
+ *
20
+ * Pure with respect to config: `readGsdEffectiveEffortConfig` performs the config
21
+ * reads; `resolveInstallTimeEffort` is pure given a pre-merged effort object.
22
+ */
23
+ const node_fs_1 = __importDefault(require("node:fs"));
24
+ const node_path_1 = __importDefault(require("node:path"));
25
+ const node_os_1 = __importDefault(require("node:os"));
26
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- model-resolver.cjs is an export= CommonJS module
27
+ const modelResolver = require("./model-resolver.cjs");
28
+ const { EFFORT_SET: GSD_EFFORT_SET } = modelResolver;
29
+ /**
30
+ * #2517 — Read a single GSD config file (defaults.json or per-project
31
+ * config.json) into a plain object, returning null on missing/empty files
32
+ * and warning to stderr on JSON parse failures so silent corruption can't
33
+ * mask broken configs (review finding #5).
34
+ */
35
+ function _readGsdConfigFile(absPath, label) {
36
+ if (!node_fs_1.default.existsSync(absPath))
37
+ return null;
38
+ let raw;
39
+ try {
40
+ raw = node_fs_1.default.readFileSync(absPath, 'utf-8');
41
+ }
42
+ catch (err) {
43
+ process.stderr.write(`gsd: warning — could not read ${label} (${absPath}): ${err.message}\n`);
44
+ return null;
45
+ }
46
+ try {
47
+ return JSON.parse(raw);
48
+ }
49
+ catch (err) {
50
+ process.stderr.write(`gsd: warning — invalid JSON in ${label} (${absPath}): ${err.message}\n`);
51
+ return null;
52
+ }
53
+ }
54
+ // #443 — model-catalog and config-defaults.manifest.json exports needed only
55
+ // by effort-resolution code paths (resolveInstallTimeEffort /
56
+ // generateCodexAgentToml / Claude .md effort injection). Loaded lazily the
57
+ // first time they are needed so that requiring this module in test contexts that
58
+ // never trigger an install does NOT produce module-load-time side effects (the
59
+ // manifest read + hard throw) that could alter subprocess exit codes or stderr.
60
+ let _gsdEffortCatalogCache = null;
61
+ function _getGsdEffortCatalog() {
62
+ if (_gsdEffortCatalogCache)
63
+ return _gsdEffortCatalogCache;
64
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- model-catalog.cjs is an export= CommonJS module
65
+ const { AGENT_DEFAULT_TIERS, renderEffortForRuntime } = require('./model-catalog.cjs');
66
+ // This module lives in gsd-core/bin/lib/, so the shared manifest is one level
67
+ // up in gsd-core/bin/shared/ (bin/lib → bin → shared). (In bin/install.js this
68
+ // path was `.., gsd-core, bin, shared` relative to the package-root bin/.)
69
+ const manifestPath = node_path_1.default.join(__dirname, '..', 'shared', 'config-defaults.manifest.json');
70
+ let manifestData;
71
+ try {
72
+ manifestData = JSON.parse(node_fs_1.default.readFileSync(manifestPath, 'utf-8'));
73
+ }
74
+ catch (_err) {
75
+ // Fail loudly — a missing manifest is a broken install, not a soft degradation.
76
+ throw new Error(`gsd install: cannot load config-defaults.manifest.json at ${manifestPath}: ${_err.message}`);
77
+ }
78
+ const tierDefaults = (manifestData.effort &&
79
+ manifestData.effort.routing_tier_defaults &&
80
+ typeof manifestData.effort.routing_tier_defaults === 'object' &&
81
+ !Array.isArray(manifestData.effort.routing_tier_defaults))
82
+ ? manifestData.effort.routing_tier_defaults
83
+ : { light: 'low', standard: 'high', heavy: 'xhigh' }; // guard: unreachable if manifest is valid
84
+ const effortDefault = (manifestData.effort && typeof manifestData.effort.default === 'string')
85
+ ? manifestData.effort.default
86
+ : 'high'; // guard: unreachable if manifest is valid
87
+ _gsdEffortCatalogCache = {
88
+ AGENT_DEFAULT_TIERS,
89
+ renderEffortForRuntime,
90
+ EFFORT_MANIFEST_TIER_DEFAULTS: tierDefaults,
91
+ EFFORT_MANIFEST_DEFAULT: effortDefault,
92
+ };
93
+ return _gsdEffortCatalogCache;
94
+ }
95
+ /**
96
+ * #443 — Read the merged `effort` config block for install-time effort resolution.
97
+ *
98
+ * Probes the same config sources as readGsdRuntimeProfileResolver (per-project
99
+ * `.planning/config.json` wins over `~/.gsd/defaults.json`) but extracts the
100
+ * `effort` object instead of the model-profile fields.
101
+ *
102
+ * Returns the merged `effort` object or null when neither source defines one.
103
+ * The caller can pass this to resolveInstallTimeEffort() which is pure and
104
+ * requires no filesystem access beyond what this helper already performs.
105
+ *
106
+ * @param targetDir Runtime install root (walks up to find .planning/).
107
+ */
108
+ function readGsdEffectiveEffortConfig(targetDir = null) {
109
+ const homeDefaults = _readGsdConfigFile(node_path_1.default.join(node_os_1.default.homedir(), '.gsd', 'defaults.json'), '~/.gsd/defaults.json');
110
+ let projectConfig = null;
111
+ if (targetDir) {
112
+ let probeDir = node_path_1.default.resolve(targetDir);
113
+ for (let depth = 0; depth < 8; depth += 1) {
114
+ const candidate = node_path_1.default.join(probeDir, '.planning', 'config.json');
115
+ if (node_fs_1.default.existsSync(candidate)) {
116
+ projectConfig = _readGsdConfigFile(candidate, '.planning/config.json');
117
+ break;
118
+ }
119
+ const parent = node_path_1.default.dirname(probeDir);
120
+ if (parent === probeDir)
121
+ break;
122
+ probeDir = parent;
123
+ }
124
+ }
125
+ const homeEffort = (homeDefaults && homeDefaults.effort && typeof homeDefaults.effort === 'object' && !Array.isArray(homeDefaults.effort))
126
+ ? homeDefaults.effort
127
+ : null;
128
+ const projectEffort = (projectConfig && projectConfig.effort && typeof projectConfig.effort === 'object' && !Array.isArray(projectConfig.effort))
129
+ ? projectConfig.effort
130
+ : null;
131
+ if (!homeEffort && !projectEffort)
132
+ return null;
133
+ // Per-project wins on conflict within each sub-field. Merge field-by-field so
134
+ // a project config that only sets agent_overrides still inherits global
135
+ // routing_tier_defaults and default.
136
+ return {
137
+ ...(homeEffort || {}),
138
+ ...(projectEffort || {}),
139
+ // Deep-merge agent_overrides (project wins per-key)
140
+ agent_overrides: {
141
+ ...((homeEffort && homeEffort.agent_overrides) || {}),
142
+ ...((projectEffort && projectEffort.agent_overrides) || {}),
143
+ },
144
+ };
145
+ }
146
+ /**
147
+ * #443 — Resolve install-time effort for a given agent, using the same
148
+ * precedence chain as resolveEffortInternal() in core.cjs, but operating
149
+ * on a pre-loaded effortCfg object (no loadConfig side-effects at install).
150
+ *
151
+ * Precedence (mirrors resolveEffortInternal):
152
+ * 1. effortCfg.agent_overrides[agentName]
153
+ * 2. effortCfg.routing_tier_defaults[agentTier] (if effortCfg present)
154
+ * — OR manifest tier defaults when effortCfg is null
155
+ * 3. effortCfg.default
156
+ * 4. 'high' (hardcoded fallback)
157
+ *
158
+ * @param effortCfg Result of readGsdEffectiveEffortConfig().
159
+ * @param agentName e.g. 'gsd-planner'
160
+ * @returns Universal effort string (low/medium/high/xhigh/max/minimal)
161
+ */
162
+ function resolveInstallTimeEffort(effortCfg, agentName) {
163
+ // Validates each candidate against the canonical EFFORT_SET (sourced once
164
+ // from model-resolver.cjs) before accepting it, mirroring resolveEffortInternal
165
+ // exactly. Invalid values fall through to the next precedence layer; final
166
+ // fallback 'high'.
167
+ // Step 1: agent_overrides
168
+ if (effortCfg) {
169
+ const ao = effortCfg.agent_overrides;
170
+ if (ao && typeof ao === 'object' && !Array.isArray(ao)) {
171
+ const v = ao[agentName];
172
+ if (typeof v === 'string' && GSD_EFFORT_SET.has(v))
173
+ return v;
174
+ }
175
+ }
176
+ // Step 2: routing_tier_defaults keyed by the agent's catalog tier
177
+ const { AGENT_DEFAULT_TIERS, EFFORT_MANIFEST_TIER_DEFAULTS, EFFORT_MANIFEST_DEFAULT } = _getGsdEffortCatalog();
178
+ const agentTier = AGENT_DEFAULT_TIERS[agentName];
179
+ if (agentTier) {
180
+ if (effortCfg && effortCfg.routing_tier_defaults &&
181
+ typeof effortCfg.routing_tier_defaults === 'object' &&
182
+ !Array.isArray(effortCfg.routing_tier_defaults)) {
183
+ const v = effortCfg.routing_tier_defaults[agentTier];
184
+ if (typeof v === 'string' && GSD_EFFORT_SET.has(v))
185
+ return v;
186
+ }
187
+ else if (!effortCfg) {
188
+ // No effort config — use manifest tier defaults
189
+ const v = EFFORT_MANIFEST_TIER_DEFAULTS[agentTier];
190
+ if (typeof v === 'string' && GSD_EFFORT_SET.has(v))
191
+ return v;
192
+ }
193
+ // effortCfg exists but has no routing_tier_defaults — fall through
194
+ }
195
+ // Step 3: effort.default
196
+ if (effortCfg) {
197
+ const d = effortCfg.default;
198
+ if (typeof d === 'string' && GSD_EFFORT_SET.has(d))
199
+ return d;
200
+ }
201
+ // Step 4: manifest default (sourced from config-defaults.manifest.json effort.default)
202
+ // If even the manifest default is invalid, fall back to 'high'.
203
+ if (typeof EFFORT_MANIFEST_DEFAULT === 'string' && GSD_EFFORT_SET.has(EFFORT_MANIFEST_DEFAULT)) {
204
+ return EFFORT_MANIFEST_DEFAULT;
205
+ }
206
+ return 'high';
207
+ }
208
+ module.exports = {
209
+ readGsdEffectiveEffortConfig,
210
+ resolveInstallTimeEffort,
211
+ _getGsdEffortCatalog,
212
+ _readGsdConfigFile,
213
+ };