@opengsd/gsd-core 1.7.0-rc.4 → 1.7.0-rc.6

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 (113) 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/agents/gsd-doc-classifier.md +105 -0
  5. package/agents/gsd-doc-synthesizer.md +61 -0
  6. package/agents/gsd-ui-checker.md +30 -0
  7. package/agents/gsd-ui-researcher.md +1 -0
  8. package/bin/install.js +1568 -622
  9. package/gsd-core/bin/gsd-tools.cjs +40 -1
  10. package/gsd-core/bin/lib/api-coverage.cjs +466 -0
  11. package/gsd-core/bin/lib/audit.cjs +6 -3
  12. package/gsd-core/bin/lib/capability-loader.cjs +11 -9
  13. package/gsd-core/bin/lib/capability-registry.cjs +761 -84
  14. package/gsd-core/bin/lib/capability-validator.cjs +56 -18
  15. package/gsd-core/bin/lib/capability-writer.cjs +10 -1
  16. package/gsd-core/bin/lib/check-command-router.cjs +242 -3
  17. package/gsd-core/bin/lib/commands.cjs +7 -5
  18. package/gsd-core/bin/lib/config-loader.cjs +1 -0
  19. package/gsd-core/bin/lib/config.cjs +96 -0
  20. package/gsd-core/bin/lib/core-utils.cjs +4 -1
  21. package/gsd-core/bin/lib/host-integration-adapters/cline-sdk-binding.cjs +234 -0
  22. package/gsd-core/bin/lib/host-integration-adapters/imperative-hook-bus.cjs +145 -0
  23. package/gsd-core/bin/lib/host-integration.cjs +45 -4
  24. package/gsd-core/bin/lib/init.cjs +76 -39
  25. package/gsd-core/bin/lib/install-effort-resolver.cjs +213 -0
  26. package/gsd-core/bin/lib/install-engine.cjs +228 -18
  27. package/gsd-core/bin/lib/installer-migration-report.cjs +7 -0
  28. package/gsd-core/bin/lib/loop-resolver.cjs +68 -17
  29. package/gsd-core/bin/lib/markdown-sectionizer.cjs +50 -11
  30. package/gsd-core/bin/lib/mcp-server.cjs +18 -7
  31. package/gsd-core/bin/lib/milestone.cjs +3 -3
  32. package/gsd-core/bin/lib/normalize-test-command.cjs +187 -0
  33. package/gsd-core/bin/lib/phase-id.cjs +132 -3
  34. package/gsd-core/bin/lib/phase.cjs +78 -16
  35. package/gsd-core/bin/lib/planning-workspace.cjs +17 -0
  36. package/gsd-core/bin/lib/review-reviewer-selection.cjs +24 -7
  37. package/gsd-core/bin/lib/roadmap-command-router.cjs +5 -4
  38. package/gsd-core/bin/lib/roadmap-parser.cjs +21 -30
  39. package/gsd-core/bin/lib/roadmap-upgrade.cjs +9 -9
  40. package/gsd-core/bin/lib/roadmap.cjs +42 -56
  41. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +248 -44
  42. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +2 -2
  43. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +39 -23
  44. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +19 -5
  45. package/gsd-core/bin/lib/runtime-homes.cjs +30 -0
  46. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +534 -37
  47. package/gsd-core/bin/lib/runtime-name-policy.cjs +63 -5
  48. package/gsd-core/bin/lib/security.cjs +6 -36
  49. package/gsd-core/bin/lib/shell-command-projection.cjs +115 -2
  50. package/gsd-core/bin/lib/spec-section.cjs +111 -0
  51. package/gsd-core/bin/lib/stale-bake-guard.cjs +30 -10
  52. package/gsd-core/bin/lib/state-transition.cjs +1 -1
  53. package/gsd-core/bin/lib/state.cjs +24 -24
  54. package/gsd-core/bin/lib/surface.cjs +40 -6
  55. package/gsd-core/bin/lib/uat.cjs +4 -1
  56. package/gsd-core/bin/lib/ui-consideration-probe.cjs +249 -0
  57. package/gsd-core/bin/lib/validate.cjs +15 -6
  58. package/gsd-core/bin/lib/verify.cjs +33 -37
  59. package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
  60. package/gsd-core/bin/shared/model-catalog.json +14 -9
  61. package/gsd-core/references/api-coverage.md +104 -0
  62. package/gsd-core/references/model-profiles.md +2 -2
  63. package/gsd-core/references/planning-config.md +2 -0
  64. package/gsd-core/references/specless-probe-fallback.md +172 -0
  65. package/gsd-core/references/ui-consideration-probe.md +73 -0
  66. package/gsd-core/templates/UI-SPEC.md +25 -0
  67. package/gsd-core/templates/VALIDATION.md +2 -0
  68. package/gsd-core/templates/config.json +2 -1
  69. package/gsd-core/workflows/audit-fix.md +9 -1
  70. package/gsd-core/workflows/audit-milestone.md +7 -4
  71. package/gsd-core/workflows/code-review-fix.md +7 -3
  72. package/gsd-core/workflows/code-review.md +4 -1
  73. package/gsd-core/workflows/discuss-phase-assumptions.md +4 -1
  74. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +8 -4
  75. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +42 -0
  76. package/gsd-core/workflows/execute-phase.md +1 -25
  77. package/gsd-core/workflows/plan-phase.md +37 -2
  78. package/gsd-core/workflows/quick.md +2 -2
  79. package/gsd-core/workflows/review.md +59 -13
  80. package/gsd-core/workflows/settings-advanced.md +12 -9
  81. package/gsd-core/workflows/settings.md +2 -2
  82. package/gsd-core/workflows/ui-phase.md +146 -1
  83. package/gsd-core/workflows/validate-phase.md +2 -2
  84. package/gsd-core/workflows/verify-phase.md +3 -2
  85. package/gsd-core/workflows/verify-work.md +38 -0
  86. package/hooks/dist/gsd-cursor-pre-tool.js +76 -0
  87. package/hooks/dist/gsd-cursor-stop.js +48 -0
  88. package/hooks/dist/gsd-cursor-subagent-start.js +50 -0
  89. package/hooks/dist/gsd-cursor-subagent-stop.js +40 -0
  90. package/hooks/dist/gsd-windsurf-pre-command.js +275 -0
  91. package/hooks/dist/gsd-windsurf-pre-write.js +132 -0
  92. package/hooks/dist/managed-hooks-registry.cjs +6 -0
  93. package/hooks/gsd-cursor-pre-tool.js +76 -0
  94. package/hooks/gsd-cursor-stop.js +48 -0
  95. package/hooks/gsd-cursor-subagent-start.js +50 -0
  96. package/hooks/gsd-cursor-subagent-stop.js +40 -0
  97. package/hooks/gsd-windsurf-pre-command.js +275 -0
  98. package/hooks/gsd-windsurf-pre-write.js +132 -0
  99. package/hooks/managed-hooks-registry.cjs +6 -0
  100. package/package.json +9 -4
  101. package/pi/gsd.cjs +354 -0
  102. package/scripts/build-hooks.js +8 -1
  103. package/scripts/gen-golden-install-parity-zcode.cjs +11 -1
  104. package/scripts/gen-registry.cjs +128 -0
  105. package/scripts/lint-phase-id-drift.cjs +150 -0
  106. package/scripts/lint-test-file-count.allowlist.json +2 -1
  107. package/scripts/registry-schema.cjs +565 -0
  108. package/scripts/run-tests.cjs +21 -1
  109. package/scripts/validate-registry.cjs +117 -0
  110. package/vscode/browser.js +197 -0
  111. package/vscode/extension.js +383 -0
  112. package/vscode/host-binding.js +113 -0
  113. package/vscode/package.json +96 -0
@@ -63,9 +63,9 @@ const { resolveModelInternal, resolveGranularityInternal, assertValidGranularity
63
63
  const { findPhaseInternal } = phaseLocator;
64
64
  const { getRoadmapPhaseInternal, getMilestoneInfo, getMilestonePhaseFilter, stripShippedMilestones, extractCurrentMilestone, } = roadmapParser;
65
65
  const { pathExistsInternal, generateSlugInternal, toPosixPath } = coreUtils;
66
- const { normalizePhaseName, phaseTokenMatches, stripProjectCodePrefix } = phaseId;
66
+ const { escapeRegex, normalizePhaseName, phaseTokenMatches, stripProjectCodePrefix, PHASE_NUMBER_TOKEN_SOURCE, isForeignPrefixedPhaseQuery } = phaseId;
67
67
  const { pruneOrphanedWorktrees } = worktreeSafety;
68
- const { planningPaths, planningDir, planningRoot, getActiveWorkstream, findContextMdIn, } = planningWorkspace;
68
+ const { planningPaths, planningDir, planningRoot, listAvailableWorkstreams, getActiveWorkstream, findContextMdIn, } = planningWorkspace;
69
69
  const { determinePhaseStatus } = commandsMod;
70
70
  const { extractFrontmatter } = frontmatterMod;
71
71
  const { readVerificationStatus } = verificationMod;
@@ -74,6 +74,37 @@ const { evaluateUatPassed } = uatPredicateMod;
74
74
  void stripShippedMilestones;
75
75
  // Accept all bold/colon variants of the Requirements header (#2769)
76
76
  const REQUIREMENTS_HEADER_RE = /^\*\*Requirements:?\*\*[^\S\n]*:?[^\S\n]*([^\n]*)$/m;
77
+ // #2056/#2104: isForeignPrefixedPhaseQuery is imported from phase-id.cts
78
+ // (the canonical predicate). parsePhasePrefix is no longer needed locally.
79
+ // phaseInfoMatchesExactPrefix and roadmapPhaseMatchesExactPrefix are local
80
+ // helpers that post-filter the lookup results for foreign-prefix queries.
81
+ function phaseInfoMatchesExactPrefix(phaseInfo, phase) {
82
+ const num = phaseInfo?.['phase_number'];
83
+ const numStr = typeof num === 'string' ? num : (typeof num === 'number' ? String(num) : '');
84
+ return numStr.toUpperCase() === phase.toUpperCase();
85
+ }
86
+ function roadmapPhaseMatchesExactPrefix(roadmapPhase, phase) {
87
+ const sectionRaw = roadmapPhase?.['section'];
88
+ const section = typeof sectionRaw === 'string' ? sectionRaw : '';
89
+ return new RegExp(`^#{2,4}\\s*Phase\\s+${escapeRegex(phase)}(?:\\b|\\s|:)`, 'i').test(section);
90
+ }
91
+ // #2104: shared helpers that wrap findPhaseInternal / getRoadmapPhaseInternal
92
+ // with the #2056 foreign-prefix guard, so every init command gets the same
93
+ // protection without duplicating the guard logic at each call site.
94
+ function guardedFindPhase(cwd, phase, projectCode) {
95
+ let phaseInfo = findPhaseInternal(cwd, phase);
96
+ if (isForeignPrefixedPhaseQuery(phase, projectCode) && !phaseInfoMatchesExactPrefix(phaseInfo, phase)) {
97
+ phaseInfo = null;
98
+ }
99
+ return phaseInfo;
100
+ }
101
+ function guardedGetRoadmapPhase(cwd, phase, projectCode) {
102
+ let roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
103
+ if (isForeignPrefixedPhaseQuery(phase, projectCode) && !roadmapPhaseMatchesExactPrefix(roadmapPhase, phase)) {
104
+ roadmapPhase = null;
105
+ }
106
+ return roadmapPhase;
107
+ }
77
108
  function listPhaseSummaryFiles(phaseDir) {
78
109
  return scanPhasePlans(phaseDir)['summaryFiles'];
79
110
  }
@@ -229,9 +260,9 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
229
260
  error('phase required for init execute-phase');
230
261
  }
231
262
  const config = loadConfig(cwd);
232
- let phaseInfo = findPhaseInternal(cwd, phase);
263
+ let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
233
264
  const milestone = getMilestoneInfo(cwd);
234
- const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
265
+ const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
235
266
  if (phaseInfo?.['archived'] && roadmapPhase?.['found']) {
236
267
  phaseInfo = null;
237
268
  }
@@ -332,8 +363,9 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
332
363
  error('phase required for init plan-phase');
333
364
  }
334
365
  const config = loadConfig(cwd);
335
- let phaseInfo = findPhaseInternal(cwd, phase);
336
- const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
366
+ // #2056/#2104: foreign-prefixed queries must not collapse to numeric phases.
367
+ let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
368
+ const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
337
369
  if (phaseInfo?.['archived'] && roadmapPhase?.['found']) {
338
370
  phaseInfo = null;
339
371
  }
@@ -573,6 +605,9 @@ function cmdInitQuick(cwd, description, raw) {
573
605
  executor_model: resolveModelInternal(cwd, 'gsd-executor'),
574
606
  checker_model: resolveModelInternal(cwd, 'gsd-plan-checker'),
575
607
  verifier_model: resolveModelInternal(cwd, 'gsd-verifier'),
608
+ // #2072: the quick review step spawns gsd-code-reviewer; resolve its own model
609
+ // so model_overrides / models.verification apply (was reusing executor_model).
610
+ reviewer_model: resolveModelInternal(cwd, 'gsd-code-reviewer'),
576
611
  commit_docs: config.commit_docs,
577
612
  branch_name: quickBranchName,
578
613
  quick_id: quickId,
@@ -637,15 +672,15 @@ function cmdInitVerifyWork(cwd, phase, raw) {
637
672
  }
638
673
  const config = loadConfig(cwd);
639
674
  const _slashRuntime = (0, runtime_slash_cjs_1.resolveRuntime)(cwd);
640
- let phaseInfo = findPhaseInternal(cwd, phase);
675
+ let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
641
676
  if (phaseInfo?.['archived']) {
642
- const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
677
+ const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
643
678
  if (roadmapPhase?.['found']) {
644
679
  phaseInfo = null;
645
680
  }
646
681
  }
647
682
  if (!phaseInfo) {
648
- const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
683
+ const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
649
684
  if (roadmapPhase?.['found']) {
650
685
  const phaseName = roadmapPhase['phase_name'];
651
686
  phaseInfo = {
@@ -694,9 +729,9 @@ function cmdInitVerifyWork(cwd, phase, raw) {
694
729
  }
695
730
  function cmdInitPhaseOp(cwd, phase, raw) {
696
731
  const config = loadConfig(cwd);
697
- let phaseInfo = findPhaseInternal(cwd, phase);
732
+ let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
698
733
  if (phaseInfo?.['archived']) {
699
- const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
734
+ const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
700
735
  if (roadmapPhase?.['found']) {
701
736
  const phaseName = roadmapPhase['phase_name'];
702
737
  phaseInfo = {
@@ -717,7 +752,7 @@ function cmdInitPhaseOp(cwd, phase, raw) {
717
752
  }
718
753
  }
719
754
  if (!phaseInfo) {
720
- const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
755
+ const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
721
756
  if (roadmapPhase?.['found']) {
722
757
  const phaseName = roadmapPhase['phase_name'];
723
758
  phaseInfo = {
@@ -874,8 +909,8 @@ function cmdInitMilestoneOp(cwd, raw) {
874
909
  const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
875
910
  const roadmapRaw = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
876
911
  const currentSection = extractCurrentMilestone(roadmapRaw, cwd);
877
- // #1729: `(?:\s*\([^)\n]*\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
878
- const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)(?:\s*\([^)\n]*\))?\s*:/gi;
912
+ // #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
913
+ const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:`, 'gi');
879
914
  let m;
880
915
  while ((m = phasePattern.exec(currentSection)) !== null) {
881
916
  if (/^999(?:\.|$)/.test(m[1]))
@@ -896,7 +931,7 @@ function cmdInitMilestoneOp(cwd, raw) {
896
931
  for (const e of entries) {
897
932
  if (!e.isDirectory())
898
933
  continue;
899
- const m = stripProjectCodePrefix(e.name).match(/^(\d+[A-Z]?(?:\.\d+)*)/);
934
+ const m = stripProjectCodePrefix(e.name).match(new RegExp(`^(${PHASE_NUMBER_TOKEN_SOURCE})`));
900
935
  if (!m)
901
936
  continue;
902
937
  diskPhaseDirs.set(canonicalizePhase(m[1]), e.name);
@@ -1024,13 +1059,13 @@ function cmdInitManager(cwd, raw) {
1024
1059
  }
1025
1060
  })();
1026
1061
  const _checkboxStates = new Map();
1027
- const _cbPattern = /-\s*\[(x| )\]\s*.*Phase\s+(\d+[A-Z]?(?:\.\d+)*)[:\s]/gi;
1062
+ const _cbPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})[:\\s]`, 'gi');
1028
1063
  let _cbMatch;
1029
1064
  while ((_cbMatch = _cbPattern.exec(content)) !== null) {
1030
1065
  _checkboxStates.set(_cbMatch[2], _cbMatch[1].toLowerCase() === 'x');
1031
1066
  }
1032
- // #1729: `(?:\s*\([^)\n]*\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
1033
- const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)(?:\s*\([^)\n]*\))?\s*:\s*([^\n]+)/gi;
1067
+ // #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
1068
+ const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:\\s*([^\\n]+)`, 'gi');
1034
1069
  const phases = [];
1035
1070
  let match;
1036
1071
  while ((match = phasePattern.exec(content)) !== null) {
@@ -1148,7 +1183,7 @@ function cmdInitManager(cwd, raw) {
1148
1183
  .filter((p) => p['phase_complete'] === true)
1149
1184
  .map((p) => normalizePhaseNumber(p['number'])));
1150
1185
  const phaseMap = new Map(phases.map((p) => [normalizePhaseNumber(p['number']), p]));
1151
- const _allCompletedPattern = /-\s*\[x\]\s*.*Phase\s+(\d+[A-Z]?(?:\.\d+)*)[:\s]/gi;
1186
+ const _allCompletedPattern = new RegExp(`-\\s*\\[x\\]\\s*.*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})[:\\s]`, 'gi');
1152
1187
  let _allMatch;
1153
1188
  while ((_allMatch = _allCompletedPattern.exec(rawContent)) !== null) {
1154
1189
  const phaseNum = normalizePhaseNumber(_allMatch[1]);
@@ -1180,7 +1215,7 @@ function cmdInitManager(cwd, raw) {
1180
1215
  phase['deps_satisfied'] = true;
1181
1216
  }
1182
1217
  else {
1183
- const depNums = phase['depends_on'].match(/\d+[A-Z]?(?:\.\d+)*/gi) || [];
1218
+ const depNums = phase['depends_on'].match(new RegExp(`${PHASE_NUMBER_TOKEN_SOURCE}`, 'gi')) || [];
1184
1219
  phase['deps_satisfied'] = depNums.every((n) => completedNums.has(normalizePhaseNumber(n)));
1185
1220
  phase['dep_phases'] = depNums;
1186
1221
  }
@@ -1319,18 +1354,7 @@ function cmdInitProgress(cwd, raw) {
1319
1354
  // reporting a stale root milestone. Require an explicit workstream instead.
1320
1355
  // Mirror planningDir's resolution (GSD_WORKSTREAM env > stored active pointer) so
1321
1356
  // an explicit --ws (which sets GSD_WORKSTREAM) satisfies the check.
1322
- const _wsRoot = node_path_1.default.join(planningRoot(cwd), 'workstreams');
1323
- let _availableWorkstreams = [];
1324
- try {
1325
- _availableWorkstreams = node_fs_1.default
1326
- .readdirSync(_wsRoot, { withFileTypes: true })
1327
- .filter((e) => e.isDirectory())
1328
- .map((e) => e.name)
1329
- .sort();
1330
- }
1331
- catch {
1332
- /* no workstreams dir → flat mode */
1333
- }
1357
+ const _availableWorkstreams = listAvailableWorkstreams(cwd);
1334
1358
  const _resolvedWorkstream = process.env['GSD_WORKSTREAM'] || getActiveWorkstream(cwd);
1335
1359
  if (_availableWorkstreams.length > 0 && !_resolvedWorkstream) {
1336
1360
  error(`init.progress requires a workstream in workstream mode — no active workstream is set, so root STATE.md (likely stale) would be reported. ` +
@@ -1346,14 +1370,14 @@ function cmdInitProgress(cwd, raw) {
1346
1370
  const roadmapCheckboxStates = new Map();
1347
1371
  try {
1348
1372
  const roadmapContent = extractCurrentMilestone(node_fs_1.default.readFileSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md'), 'utf-8'), cwd);
1349
- // #1729: `(?:\s*\([^)\n]*\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
1350
- const headingPattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)(?:\s*\([^)\n]*\))?\s*:\s*([^\n]+)/gi;
1373
+ // #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
1374
+ const headingPattern = new RegExp(`#{2,4}\\s*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:\\s*([^\\n]+)`, 'gi');
1351
1375
  let hm;
1352
1376
  while ((hm = headingPattern.exec(roadmapContent)) !== null) {
1353
1377
  roadmapPhaseNums.add(hm[1]);
1354
1378
  roadmapPhaseNames.set(hm[1], hm[2].replace(/\(INSERTED\)/i, '').trim());
1355
1379
  }
1356
- const cbPattern = /-\s*\[(x| )\]\s*.*Phase\s+(\d+[A-Z]?(?:\.\d+)*)[:\s]/gi;
1380
+ const cbPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})[:\\s]`, 'gi');
1357
1381
  let cbm;
1358
1382
  while ((cbm = cbPattern.exec(roadmapContent)) !== null) {
1359
1383
  roadmapCheckboxStates.set(cbm[2], cbm[1].toLowerCase() === 'x');
@@ -1371,14 +1395,14 @@ function cmdInitProgress(cwd, raw) {
1371
1395
  .map((e) => e.name)
1372
1396
  .filter(isDirInMilestone)
1373
1397
  .sort((a, b) => {
1374
- const pa = a.match(/^(\d+[A-Z]?(?:\.\d+)*)/i);
1375
- const pb = b.match(/^(\d+[A-Z]?(?:\.\d+)*)/i);
1398
+ const pa = a.match(new RegExp(`^(${PHASE_NUMBER_TOKEN_SOURCE})`, 'i'));
1399
+ const pb = b.match(new RegExp(`^(${PHASE_NUMBER_TOKEN_SOURCE})`, 'i'));
1376
1400
  if (!pa || !pb)
1377
1401
  return a.localeCompare(b);
1378
1402
  return parseInt(pa[1], 10) - parseInt(pb[1], 10);
1379
1403
  });
1380
1404
  for (const dir of dirs) {
1381
- const dirMatch = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
1405
+ const dirMatch = dir.match(new RegExp(`^(${PHASE_NUMBER_TOKEN_SOURCE})-?(.*)`, 'i'));
1382
1406
  const phaseNumber = dirMatch ? dirMatch[1] : dir;
1383
1407
  const phaseName = dirMatch && dirMatch[2] ? dirMatch[2] : null;
1384
1408
  seenPhaseNums.add(phaseNumber.replace(/^0+/, '') || '0');
@@ -1870,11 +1894,24 @@ function buildSkillManifest(cwd, skillsDir = null) {
1870
1894
  kind: 'skills',
1871
1895
  },
1872
1896
  {
1873
- root: '~/.codex/skills',
1897
+ // ADR-1239 upgrade 3 (#2088): Codex's canonical skill root is
1898
+ // $HOME/.agents/skills (per codex core-skills loader.rs), resolved via
1899
+ // the skills-kind `home` override in getGlobalSkillsBase.
1900
+ root: '~/.agents/skills',
1874
1901
  path: (0, runtime_homes_cjs_1.getGlobalSkillsBase)('codex'),
1875
1902
  scope: 'global',
1876
1903
  kind: 'skills',
1877
1904
  },
1905
+ {
1906
+ // Codex's deprecated fallback skill root ($CODEX_HOME/skills). Kept as a
1907
+ // discovery-only legacy root so pre-move installs remain inventoried;
1908
+ // GSD no longer installs here (#2088).
1909
+ root: '~/.codex/skills',
1910
+ path: node_path_1.default.join((0, runtime_homes_cjs_1.getGlobalConfigDir)('codex'), 'skills'),
1911
+ scope: 'global',
1912
+ kind: 'skills',
1913
+ deprecated: true,
1914
+ },
1878
1915
  {
1879
1916
  root: '.claude/gsd-core/skills',
1880
1917
  path: node_path_1.default.join(node_os_1.default.homedir(), '.claude', 'gsd-core', 'skills'),
@@ -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
+ };