@opengsd/gsd-core 1.19.0 → 1.20.0

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 (106) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-code-fixer.compact.md +5 -5
  4. package/agents/gsd-code-fixer.md +9 -4
  5. package/agents/gsd-code-reviewer.compact.md +49 -8
  6. package/agents/gsd-code-reviewer.md +53 -14
  7. package/agents/gsd-executor.md +8 -4
  8. package/commands/gsd/update.md +5 -2
  9. package/gsd-core/bin/gsd-tools.cjs +73 -23
  10. package/gsd-core/bin/lib/capability-lifecycle.cjs +1 -12
  11. package/gsd-core/bin/lib/capability-registry.cjs +284 -93
  12. package/gsd-core/bin/lib/capability-validator.cjs +15 -0
  13. package/gsd-core/bin/lib/commands.cjs +27 -17
  14. package/gsd-core/bin/lib/config-loader.cjs +3 -3
  15. package/gsd-core/bin/lib/core-utils.cjs +11 -0
  16. package/gsd-core/bin/lib/frontmatter.cjs +21 -3
  17. package/gsd-core/bin/lib/gate-api-coverage-verify-pre.cjs +2 -2
  18. package/gsd-core/bin/lib/gate-context-drift.cjs +1 -1
  19. package/gsd-core/bin/lib/gate-phase-context.cjs +7 -8
  20. package/gsd-core/bin/lib/gate-schema-drift.cjs +1 -1
  21. package/gsd-core/bin/lib/git-info-exclude.cjs +1 -6
  22. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +13 -9
  23. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +9 -7
  24. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +3 -3
  25. package/gsd-core/bin/lib/init.cjs +180 -67
  26. package/gsd-core/bin/lib/io.cjs +6 -0
  27. package/gsd-core/bin/lib/markdown-sectionizer.cjs +34 -1
  28. package/gsd-core/bin/lib/markdown-table.cjs +23 -0
  29. package/gsd-core/bin/lib/mcp-server.cjs +8 -5
  30. package/gsd-core/bin/lib/milestone.cjs +18 -50
  31. package/gsd-core/bin/lib/phase-entry-grammar.cjs +119 -0
  32. package/gsd-core/bin/lib/phase-id-display.cjs +2 -0
  33. package/gsd-core/bin/lib/phase-id.cjs +609 -35
  34. package/gsd-core/bin/lib/phase-lifecycle.cjs +9 -74
  35. package/gsd-core/bin/lib/phase-locator.cjs +328 -19
  36. package/gsd-core/bin/lib/phase.cjs +2990 -666
  37. package/gsd-core/bin/lib/planning-document.cjs +26 -5
  38. package/gsd-core/bin/lib/planning-inspect.cjs +27 -35
  39. package/gsd-core/bin/lib/planning-snapshot.cjs +12 -7
  40. package/gsd-core/bin/lib/planning-workspace.cjs +3 -3
  41. package/gsd-core/bin/lib/read-only-git-env.cjs +23 -2
  42. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +2 -2
  43. package/gsd-core/bin/lib/review-lane-descriptor.cjs +22 -0
  44. package/gsd-core/bin/lib/review-lane-invocation.cjs +125 -18
  45. package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +11 -9
  46. package/gsd-core/bin/lib/roadmap-parser.cjs +730 -144
  47. package/gsd-core/bin/lib/roadmap-progress.cjs +136 -0
  48. package/gsd-core/bin/lib/roadmap-upgrade.cjs +8 -5
  49. package/gsd-core/bin/lib/roadmap.cjs +358 -106
  50. package/gsd-core/bin/lib/shell-command-projection.cjs +36 -9
  51. package/gsd-core/bin/lib/smart-entry.cjs +22 -11
  52. package/gsd-core/bin/lib/state-contract.cjs +11 -7
  53. package/gsd-core/bin/lib/state-document.cjs +332 -23
  54. package/gsd-core/bin/lib/state-transition.cjs +39 -10
  55. package/gsd-core/bin/lib/state.cjs +498 -179
  56. package/gsd-core/bin/lib/template.cjs +2 -2
  57. package/gsd-core/bin/lib/validate.cjs +3 -2
  58. package/gsd-core/bin/lib/vendor/re2js.cjs +863 -577
  59. package/gsd-core/bin/lib/vendor/re2js.d.cts +284 -219
  60. package/gsd-core/bin/lib/verify.cjs +2 -2
  61. package/gsd-core/bin/lib/workstream-inventory.cjs +26 -20
  62. package/gsd-core/bin/verify-reapply-patches.cjs +411 -10
  63. package/gsd-core/references/cicd-review-patterns.md +45 -0
  64. package/gsd-core/references/necessity-review.md +48 -0
  65. package/gsd-core/workflows/code-review-fix.md +46 -5
  66. package/gsd-core/workflows/code-review.md +3 -0
  67. package/gsd-core/workflows/execute-phase.md +1 -1
  68. package/gsd-core/workflows/execute-plan.md +1 -1
  69. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +13 -9
  70. package/gsd-core/workflows/plan-phase.md +1 -1
  71. package/gsd-core/workflows/pr-branch.md +2 -0
  72. package/gsd-core/workflows/reapply-patches.md +104 -0
  73. package/gsd-core/workflows/review.md +54 -141
  74. package/gsd-core/workflows/secure-phase.md +1 -1
  75. package/gsd-core/workflows/settings-advanced.md +57 -9
  76. package/gsd-core/workflows/ship.md +12 -0
  77. package/hooks/dist/gsd-graphify-update.sh +6 -0
  78. package/hooks/dist/gsd-statusline.js +3 -2
  79. package/hooks/dist/gsd-windsurf-pre-write.js +4 -4
  80. package/hooks/dist/gsd-workflow-guard.js +2 -1
  81. package/hooks/dist/gsd-worktree-path-guard.js +4 -4
  82. package/hooks/dist/lib/git-probe.js +16 -0
  83. package/hooks/gsd-graphify-update.sh +6 -0
  84. package/hooks/gsd-statusline.js +3 -2
  85. package/hooks/gsd-windsurf-pre-write.js +4 -4
  86. package/hooks/gsd-workflow-guard.js +2 -1
  87. package/hooks/gsd-worktree-path-guard.js +4 -4
  88. package/hooks/lib/git-probe.js +16 -0
  89. package/package.json +3 -3
  90. package/pi/package-agents/gsd-code-fixer.md +9 -4
  91. package/pi/package-agents/gsd-code-reviewer.md +53 -14
  92. package/pi/package-agents/gsd-executor.md +8 -4
  93. package/pi/package-agents/manifest.json +3 -3
  94. package/pi/package-skills/gsd-update/SKILL.md +4 -1
  95. package/scripts/ci-next-health.cjs +246 -23
  96. package/scripts/docs-guard-registry.cjs +2 -0
  97. package/scripts/gen-features.cjs +14 -2
  98. package/scripts/lib/drift-scan.cjs +27 -0
  99. package/scripts/lib/macos-conformance-tier.generated.cjs +6 -0
  100. package/scripts/lib/platform-conformance-tier.generated.cjs +11 -0
  101. package/scripts/lint-phase-enumeration-drift.cjs +8 -4
  102. package/scripts/lint-roadmap-progress-drift.cjs +421 -0
  103. package/scripts/mutation-matrix.cjs +23 -1
  104. package/scripts/sync-next-version.cjs +38 -6
  105. package/skills/gsd-update/SKILL.md +5 -2
  106. package/vscode/package.json +1 -1
@@ -15,6 +15,7 @@ const node_os_1 = __importDefault(require("node:os"));
15
15
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
16
16
  const clock_cjs_1 = require("./clock.cjs");
17
17
  const pattern_cjs_1 = require("./pattern.cjs");
18
+ const phase_entry_grammar_cjs_1 = require("./phase-entry-grammar.cjs");
18
19
  const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
19
20
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- io.cjs is an export= CommonJS module
20
21
  const io = require("./io.cjs");
@@ -33,6 +34,8 @@ const roadmapParser = require("./roadmap-parser.cjs");
33
34
  const coreUtils = require("./core-utils.cjs");
34
35
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-id.cjs is an export= CommonJS module
35
36
  const phaseId = require("./phase-id.cjs");
37
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-id-display.cjs is an export= CommonJS module
38
+ const phaseIdDisplayMod = require("./phase-id-display.cjs");
36
39
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- worktree-safety.cjs is an export= CommonJS module
37
40
  const worktreeSafety = require("./worktree-safety.cjs");
38
41
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-workspace.cjs is an export= CommonJS module
@@ -93,10 +96,11 @@ const { harvestPriorVerifyCommands } = verifyCommandGrounding;
93
96
  const { output, error, ERROR_REASON, formatDiagnosticToken } = io;
94
97
  const { loadConfig, loadConfigResolved } = configLoader;
95
98
  const { resolveModelInternal, resolveGranularityInternal, assertValidGranularityOverride } = modelResolver;
96
- const { findPhaseInternal, listMilestonePhaseDirs, listAllPhaseDirs } = phaseLocator;
99
+ const { phaseDirectoryOptions, findPhaseInternal, listMilestonePhaseDirs, listAllPhaseDirs, } = phaseLocator;
97
100
  const { getRoadmapPhaseInternal, getMilestoneInfo, stripShippedMilestones, extractCurrentMilestone, } = roadmapParser;
98
101
  const { pathExistsInternal, generateSlugInternal, toPosixPath } = coreUtils;
99
- const { comparePhaseNum, normalizePhaseName, stripProjectCodePrefix, PHASE_NUMBER_TOKEN_SOURCE, PHASE_DEP_REF_SOURCE, isForeignPrefixedPhaseQuery, isSentinelPhaseId, extractPhaseToken, scopeToPhase, renderPhaseBranchName, parsePhaseId, renderPhaseId, phaseHeadingPrefixSrcFor, PHASE_HEADING_BASELINE, buildPhaseHeadingScanRegex, } = phaseId;
102
+ const { comparePhaseNum, phaseDirectoryName, customPhaseQuery, customDirectoryOwner, phaseScanTokenSource, phaseKeyFromToken, phaseDirectoryContext, normalizePhaseName, stripProjectCodePrefix, PHASE_NUMBER_TOKEN_SOURCE, PHASE_DEP_REF_SOURCE, extractPhaseDependencyTokens, isForeignPrefixedPhaseQuery, isSentinelPhaseId, extractPhaseToken, scopeToPhase, renderPhaseBranchName, parsePhaseId, renderPhaseId, bracketQualifiedKey, phaseHeadingPrefixSrcFor, parsePhaseChecklistLine, PHASE_HEADING_BASELINE, buildPhaseHeadingScanRegex, } = phaseId;
103
+ const { phaseToken } = phaseIdDisplayMod;
100
104
  const { pruneOrphanedWorktrees } = worktreeSafety;
101
105
  const { planningPaths, planningDir, planningRoot, todosDir, listAvailableWorkstreams, peekActiveWorkstream, resolveEnvWorkstream, diagnoseUnresolvedActiveWorkstream, describeUnresolvedWorkstreamReason, findContextMdIn, findPhaseSpecIn, resolvePhaseIdConvention, worktreesOptedOut, } = planningWorkspace;
102
106
  const { extractFrontmatter, frontmatterBlock } = frontmatterMod;
@@ -134,8 +138,8 @@ function roadmapPhaseMatchesExactPrefix(roadmapPhase, phase) {
134
138
  // #2104: shared helpers that wrap findPhaseInternal / getRoadmapPhaseInternal
135
139
  // with the #2056 foreign-prefix guard, so every init command gets the same
136
140
  // protection without duplicating the guard logic at each call site.
137
- function guardedFindPhase(cwd, phase, projectCode) {
138
- let phaseInfo = findPhaseInternal(cwd, phase);
141
+ function guardedFindPhase(cwd, phase, projectCode, phaseNaming) {
142
+ let phaseInfo = findPhaseInternal(cwd, phase, undefined, phaseDirectoryOptions(cwd, { phase_naming: phaseNaming, project_code: projectCode }));
139
143
  if (isForeignPrefixedPhaseQuery(phase, projectCode) && !phaseInfoMatchesExactPrefix(phaseInfo, phase)) {
140
144
  phaseInfo = null;
141
145
  }
@@ -902,7 +906,7 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
902
906
  error('phase required for init execute-phase');
903
907
  }
904
908
  const config = loadConfig(cwd);
905
- let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
909
+ let phaseInfo = guardedFindPhase(cwd, phase, config.project_code, config.phase_naming);
906
910
  // #3216: getMilestoneInfo now returns a ScopedResult — `.value` carries the
907
911
  // MilestoneInfo (or null on any non-COMPLETE scope). NOT display-only: when
908
912
  // `branching_strategy === 'milestone'`, `milestone['version']`/`['name']`
@@ -1077,7 +1081,7 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
1077
1081
  }
1078
1082
  const config = loadConfig(cwd);
1079
1083
  // #2056/#2104: foreign-prefixed queries must not collapse to numeric phases.
1080
- let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
1084
+ let phaseInfo = guardedFindPhase(cwd, phase, config.project_code, config.phase_naming);
1081
1085
  const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
1082
1086
  phaseInfo = applyRoadmapFallback(phaseInfo, roadmapPhase, (rp) => {
1083
1087
  const phaseName = rp['phase_name'];
@@ -1111,12 +1115,14 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
1111
1115
  const phaseNamePlan = resolvePhaseDisplayName(phaseInfo, roadmapPhase);
1112
1116
  const rawProjectCodePlan = config.project_code || '';
1113
1117
  let expectedPhaseDirPlan = null;
1114
- if (!phaseDirPlan && phaseNumberPlan && phaseNamePlan) {
1118
+ const namingContextPlan = phaseDirectoryContext(config);
1119
+ const customNamingPlan = customPhaseQuery(phase, namingContextPlan) !== null;
1120
+ if ((!phaseDirPlan || customNamingPlan) && phaseNumberPlan && phaseNamePlan) {
1115
1121
  const paddedNum = normalizePhaseName(phaseNumberPlan);
1116
- const slug = (generateSlugInternal(phaseNamePlan) || '').substring(0, 60);
1122
+ const expectedName = roadmapPhase?.['phase_name'] || phaseNamePlan;
1123
+ const slug = (generateSlugInternal(expectedName) || '').substring(0, 60);
1117
1124
  if (slug) {
1118
- const prefix = rawProjectCodePlan ? `${rawProjectCodePlan}-` : '';
1119
- const dirName = `${prefix}${paddedNum}-${slug}`;
1125
+ const dirName = phaseDirectoryName(customPhaseQuery(phaseNumberPlan, namingContextPlan) ? phaseNumberPlan : paddedNum, slug, rawProjectCodePlan);
1120
1126
  // #2376: absolute — see comment on phase_dir below.
1121
1127
  expectedPhaseDirPlan = toPosixPath(node_path_1.default.join(planningPaths(cwd).phases, dirName));
1122
1128
  }
@@ -1707,7 +1713,7 @@ function cmdInitVerifyWork(cwd, phase, raw) {
1707
1713
  }
1708
1714
  const config = loadConfig(cwd);
1709
1715
  const _slashRuntime = (0, runtime_slash_cjs_1.resolveRuntime)(cwd);
1710
- let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
1716
+ let phaseInfo = guardedFindPhase(cwd, phase, config.project_code, config.phase_naming);
1711
1717
  const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
1712
1718
  phaseInfo = applyRoadmapFallback(phaseInfo, roadmapPhase, (rp) => {
1713
1719
  const phaseName = rp['phase_name'];
@@ -1814,7 +1820,7 @@ function cmdInitVerifyWork(cwd, phase, raw) {
1814
1820
  */
1815
1821
  function cmdInitCodeReview(cwd, phase, raw, options = {}) {
1816
1822
  const config = loadConfig(cwd);
1817
- let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
1823
+ let phaseInfo = guardedFindPhase(cwd, phase, config.project_code, config.phase_naming);
1818
1824
  const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
1819
1825
  phaseInfo = applyRoadmapFallback(phaseInfo, roadmapPhase, (rp) => {
1820
1826
  const rpName = rp['phase_name'];
@@ -1880,7 +1886,7 @@ function cmdInitCodeReview(cwd, phase, raw, options = {}) {
1880
1886
  */
1881
1887
  function cmdInitReview(cwd, phase, raw, options = {}) {
1882
1888
  const config = loadConfig(cwd);
1883
- let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
1889
+ let phaseInfo = guardedFindPhase(cwd, phase, config.project_code, config.phase_naming);
1884
1890
  const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
1885
1891
  phaseInfo = applyRoadmapFallback(phaseInfo, roadmapPhase, (rp) => ({
1886
1892
  found: true,
@@ -1939,7 +1945,7 @@ function cmdInitReview(cwd, phase, raw, options = {}) {
1939
1945
  */
1940
1946
  function cmdInitDiscussPhaseAssumptions(cwd, phase, raw, options = {}) {
1941
1947
  const config = loadConfig(cwd);
1942
- let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
1948
+ let phaseInfo = guardedFindPhase(cwd, phase, config.project_code, config.phase_naming);
1943
1949
  const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
1944
1950
  phaseInfo = applyRoadmapFallback(phaseInfo, roadmapPhase, (rp) => {
1945
1951
  const rpName = rp['phase_name'];
@@ -1995,7 +2001,7 @@ function cmdInitDiscussPhaseAssumptions(cwd, phase, raw, options = {}) {
1995
2001
  }
1996
2002
  function cmdInitPhaseOp(cwd, phase, raw) {
1997
2003
  const config = loadConfig(cwd);
1998
- let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
2004
+ let phaseInfo = guardedFindPhase(cwd, phase, config.project_code, config.phase_naming);
1999
2005
  // #2237: surface ambiguous phase-directory collisions instead of silently
2000
2006
  // taking the first match when unrelated projects share a .planning/phases/ tree.
2001
2007
  if (phaseInfo?.['ambiguous_matches']) {
@@ -2598,37 +2604,37 @@ function cmdInitManager(cwd, raw) {
2598
2604
  }
2599
2605
  const rawContent = node_fs_1.default.readFileSync(paths.roadmap, 'utf-8');
2600
2606
  const content = extractCurrentMilestone(rawContent, cwd);
2607
+ const directoryContext = phaseDirectoryOptions(cwd, config, rawContent);
2601
2608
  // #3185 (ADR-3180 Decision 1): "which phase directories belong to the
2602
2609
  // CURRENT milestone" is the scoped question listMilestonePhaseDirs owns —
2603
2610
  // routed through it instead of a hand-rolled readdirSync + a separate
2604
2611
  // getMilestonePhaseFilter window check (which also never excluded
2605
2612
  // sentinels, unlike the owner).
2606
2613
  const _checkboxStates = new Map();
2607
- // #4982: anchored (`^`, `m`) so the phase label must be the FIRST thing
2608
- // after the checkbox (tolerating only an optional `**` bold marker) —
2609
- // the prior unanchored `\s*.*` let the greedy `.*` bind to the LAST
2610
- // "Phase N" mentioned anywhere in the line's prose instead of the line's
2611
- // own phase. First-match-wins (`if (!_checkboxStates.has(...))` below)
2614
+ // #4982: parsePhaseChecklistLine is line-anchored, so the phase label must
2615
+ // be the FIRST thing after the checkbox (tolerating only an optional `**`
2616
+ // bold marker), and a later "Phase N" in the line's prose cannot bind the
2617
+ // checkbox. First-match-wins (`if (!_checkboxStates.has(...))` below)
2612
2618
  // additionally guards against a later anchored line (e.g. a per-plan
2613
2619
  // sub-entry self-titled "Phase N ...") overwriting an already-recorded
2614
- // phase's own (first) checkbox — anchoring alone does not fully close
2615
- // that "last-match-wins" composition defect.
2616
- const _cbPattern = new RegExp(`^[ \\t]*-[ \\t]*\\[(x| )\\][ \\t]*(?:\\*\\*)?${phaseHeadingPrefix}(${PHASE_NUMBER_TOKEN_SOURCE})[:\\s]`, 'gim');
2617
- let _cbMatch;
2618
- while ((_cbMatch = _cbPattern.exec(content)) !== null) {
2619
- const phaseGroup = capturesBracketId ? 3 : 2;
2620
- const _cbKey = _cbMatch[phaseGroup];
2621
- if (!_checkboxStates.has(_cbKey)) {
2622
- _checkboxStates.set(_cbKey, _cbMatch[1].toLowerCase() === 'x');
2620
+ // phase's own (first) checkbox; anchoring alone does not fully close that
2621
+ // "last-match-wins" composition defect.
2622
+ for (const line of content.split(/\r?\n/)) {
2623
+ const checkbox = parsePhaseChecklistLine(line, phaseIdConvention, directoryContext);
2624
+ if (checkbox && !_checkboxStates.has(checkbox.phaseToken)) {
2625
+ _checkboxStates.set(checkbox.phaseToken, checkbox.checked);
2623
2626
  }
2624
2627
  }
2625
2628
  // #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
2626
- const phasePattern = new RegExp(`#{2,4}\\s*${phaseHeadingPrefix}(${PHASE_NUMBER_TOKEN_SOURCE})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:\\s*([^\\n]+)`, 'gi');
2629
+ const phasePattern = new RegExp(`#{2,4}\\s*${phaseHeadingPrefix}(${phaseScanTokenSource(directoryContext)})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:\\s*([^\\n]+)`, 'gi');
2627
2630
  const phases = [];
2631
+ const bracketIdsByPhaseNumber = new Map();
2628
2632
  let match;
2629
2633
  while ((match = phasePattern.exec(content)) !== null) {
2630
2634
  const bracketId = capturesBracketId ? match[1] : undefined;
2631
2635
  const phaseNum = capturesBracketId ? match[2] : match[1];
2636
+ if (bracketId)
2637
+ bracketIdsByPhaseNumber.set(phaseNum, bracketId);
2632
2638
  const phaseName = (capturesBracketId ? match[3] : match[2])
2633
2639
  .replace(/\(INSERTED\)/i, '')
2634
2640
  .trim();
@@ -2644,7 +2650,7 @@ function cmdInitManager(cwd, raw) {
2644
2650
  }
2645
2651
  const sectionStart = match.index;
2646
2652
  const restOfContent = content.slice(sectionStart);
2647
- const nextHeader = restOfContent.match(new RegExp(`\\n#{2,4}\\s+${phaseHeadingPrefixNoCapture}\\d[\\d.]*`, 'i'));
2653
+ const nextHeader = restOfContent.match(new RegExp(`\\n#{2,4}\\s+${phaseHeadingPrefixNoCapture}${phaseScanTokenSource(directoryContext)}`, 'i'));
2648
2654
  const sectionEnd = nextHeader
2649
2655
  ? sectionStart + nextHeader.index
2650
2656
  : content.length;
@@ -2674,7 +2680,7 @@ function cmdInitManager(cwd, raw) {
2674
2680
  // of reporting no_directory/phase_complete:false. Five other init
2675
2681
  // commands already route through this same primitive; this was the
2676
2682
  // holdout private copy (#4793-family declared-owner drift).
2677
- const located = findPhaseInternal(cwd, phaseNum, phaseIdConvention);
2683
+ const located = findPhaseInternal(cwd, phaseNum, phaseIdConvention, directoryContext);
2678
2684
  const dirMatch = located && located['found']
2679
2685
  ? node_path_1.default.posix.basename(String(located['directory']))
2680
2686
  : null;
@@ -2785,15 +2791,83 @@ function cmdInitManager(cwd, raw) {
2785
2791
  .filter((p) => p['phase_complete'] === true)
2786
2792
  .map((p) => normalizePhaseNumber(p['number'])));
2787
2793
  const phaseMap = new Map(phases.map((p) => [normalizePhaseNumber(p['number']), p]));
2788
- const _allCompletedPattern = new RegExp(`-\\s*\\[x\\]\\s*.*${phaseHeadingPrefixNoCapture}(${PHASE_NUMBER_TOKEN_SOURCE})[:\\s]`, 'gi');
2789
- let _allMatch;
2790
- while ((_allMatch = _allCompletedPattern.exec(rawContent)) !== null) {
2791
- const phaseNum = normalizePhaseNumber(_allMatch[1]);
2792
- const phase = phaseMap.get(phaseNum);
2793
- if (!phase || phase['phase_complete'] === true) {
2794
- completedNums.add(phaseNum);
2794
+ const bracketIdentityKey = (value) => {
2795
+ try {
2796
+ const id = parsePhaseId(value);
2797
+ const subphase = id.subphase ? `.${id.subphase}` : '';
2798
+ return bracketQualifiedKey(`${id.project}.${id.milestone}-${id.phase}${subphase}`, 'bracket');
2799
+ }
2800
+ catch {
2801
+ return null;
2802
+ }
2803
+ };
2804
+ const phaseByBracketIdentity = new Map();
2805
+ const bracketIdentityByPhaseNumber = new Map();
2806
+ const completedBracketIdentities = new Set();
2807
+ if (phaseIdConvention === 'bracket') {
2808
+ for (const phase of phases) {
2809
+ const phaseNumber = phase['number'];
2810
+ const bracketId = bracketIdsByPhaseNumber.get(phaseNumber);
2811
+ const canonicalPhaseToken = phaseToken(phaseNumber);
2812
+ // #4304: key each heading on the reader's canonical
2813
+ // token, not on a strict parse of its rendered display. An accepted
2814
+ // unpadded heading (`### [CK.02] 1:`) gets no display_id, because
2815
+ // parsePhaseId rejects `CK.02-1`, so it had no identity at all and a
2816
+ // checked `[CK.02] 01` row completed it while phase_complete was false.
2817
+ // phaseToken pads the number to `01`, and `CK.02-01` is the form
2818
+ // parsePhaseId accepts; canonical headings key exactly as before.
2819
+ const key = bracketId && canonicalPhaseToken
2820
+ ? bracketIdentityKey(`${bracketId}-${canonicalPhaseToken}`)
2821
+ : null;
2822
+ if (!key)
2823
+ continue;
2824
+ bracketIdentityByPhaseNumber.set(normalizePhaseNumber(phaseNumber), key);
2825
+ phaseByBracketIdentity.set(key, phase);
2826
+ if (phase['phase_complete'] === true)
2827
+ completedBracketIdentities.add(key);
2828
+ }
2829
+ }
2830
+ if (phaseIdConvention === 'bracket') {
2831
+ // A checked historical row satisfies only its own qualified identity. In
2832
+ // particular, `[CK.01] 01` must not complete bare/current `[CK.02] 01`.
2833
+ for (const line of rawContent.split(/\r?\n/)) {
2834
+ const checkbox = parsePhaseChecklistLine(line, phaseIdConvention);
2835
+ if (!checkbox?.checked || !checkbox.bracketId)
2836
+ continue;
2837
+ const identity = `[${checkbox.bracketId}] ${checkbox.phaseToken}`;
2838
+ const key = bracketIdentityKey(identity);
2839
+ if (!key)
2840
+ continue;
2841
+ const current = phaseMap.get(normalizePhaseNumber(checkbox.phaseToken));
2842
+ const currentKey = current
2843
+ ? bracketIdentityByPhaseNumber.get(normalizePhaseNumber(current['number'])) ?? null
2844
+ : null;
2845
+ if (!current || currentKey !== key || current['phase_complete'] === true) {
2846
+ completedBracketIdentities.add(key);
2847
+ }
2795
2848
  }
2796
2849
  }
2850
+ else {
2851
+ // Preserve the historical greedy non-bracket fallback byte-for-byte.
2852
+ const _allCompletedPattern = (0, phase_entry_grammar_cjs_1.phaseEntryRegExp)(`.*${phaseHeadingPrefixNoCapture}(${PHASE_NUMBER_TOKEN_SOURCE})[:\\s]`, 'gi');
2853
+ let _allMatch;
2854
+ while ((_allMatch = _allCompletedPattern.exec(rawContent)) !== null) {
2855
+ if (!(0, phase_entry_grammar_cjs_1.isPhaseEntryChecked)(_allMatch[0]))
2856
+ continue;
2857
+ const phaseNum = normalizePhaseNumber(_allMatch[1]);
2858
+ const phase = phaseMap.get(phaseNum);
2859
+ if (!phase || phase['phase_complete'] === true) {
2860
+ completedNums.add(phaseNum);
2861
+ }
2862
+ }
2863
+ }
2864
+ const currentDependencyNumber = (dependency) => {
2865
+ const identity = bracketIdentityKey(dependency);
2866
+ if (!identity)
2867
+ return normalizePhaseNumber(dependency);
2868
+ const current = phaseByBracketIdentity.get(identity);
2869
+ return current ? normalizePhaseNumber(current['number']) : null;
2870
+ };
2797
2871
  function reaches(from, to, visited = new Set()) {
2798
2872
  const normalizedFrom = normalizePhaseNumber(from);
2799
2873
  const normalizedTo = normalizePhaseNumber(to);
@@ -2803,15 +2877,18 @@ function cmdInitManager(cwd, raw) {
2803
2877
  const p = phaseMap.get(normalizedFrom);
2804
2878
  if (!p || !p['dep_phases'] || p['dep_phases'].length === 0)
2805
2879
  return false;
2806
- if (p['dep_phases'].some((dep) => normalizePhaseNumber(dep) === normalizedTo)) {
2880
+ if (p['dep_phases'].some((dep) => currentDependencyNumber(dep) === normalizedTo)) {
2807
2881
  return true;
2808
2882
  }
2809
- return p['dep_phases'].some((dep) => reaches(dep, to, visited));
2883
+ return p['dep_phases'].some((dep) => {
2884
+ const dependencyNumber = currentDependencyNumber(dep);
2885
+ return dependencyNumber === null ? false : reaches(dependencyNumber, to, visited);
2886
+ });
2810
2887
  }
2811
2888
  function hasDepRelationship(numA, numB) {
2812
2889
  return reaches(numA, numB) || reaches(numB, numA);
2813
2890
  }
2814
- // #4764: a phase reference in depends_on prose is a PHASE-SHAPED token in
2891
+ // #4764/#4304: a phase reference in depends_on prose is a PHASE-SHAPED token in
2815
2892
  // context — directly following "Phase"/"Phases" — never a bare digit run.
2816
2893
  // The previous whole-field scrape matched the token grammar against every
2817
2894
  // digit run, so calendar dates ("2026-09-14" → 2026, 09, 14), git shas
@@ -2824,9 +2901,27 @@ function cmdInitManager(cwd, raw) {
2824
2901
  // "Phase 1-3") — silently dropping a REAL dependency would clear
2825
2902
  // deps_satisfied prematurely, the dangerous direction. Negation prose
2826
2903
  // ("dropped the dependency on Phase 654") is NOT detected: the issue's own
2827
- // minimum keeps such tokens.
2828
- const depPhaseRefRe = new RegExp(`${PHASE_DEP_REF_SOURCE}`, 'gi');
2829
- const depTokenRe = new RegExp(`${PHASE_NUMBER_TOKEN_SOURCE}`, 'gi');
2904
+ // minimum keeps such tokens. Bracket repositories route through the same
2905
+ // phase-id owner so `[CK.02] 01` retains its complete qualified identity;
2906
+ // bare `01` still resolves against the active milestone. This prevents a
2907
+ // checked historical `[CK.01] 01` from satisfying incomplete `[CK.02] 01`.
2908
+ // Non-bracket extraction remains this manager surface's exact #4764 code
2909
+ // path below, including its case-insensitive token regex. Planning-inspect's
2910
+ // old token regex was case-sensitive, so forcing both through one widened
2911
+ // helper changed its `Phase 1a` bytes from `1` to `1a`.
2912
+ const legacyManagerDependencyTokens = (prose) => {
2913
+ const depPhaseRefRe = new RegExp(`${PHASE_DEP_REF_SOURCE}`, 'gi');
2914
+ const depTokenRe = new RegExp(`${PHASE_NUMBER_TOKEN_SOURCE}`, 'gi');
2915
+ const tokens = [];
2916
+ let refMatch;
2917
+ while ((refMatch = depPhaseRefRe.exec(prose)) !== null) {
2918
+ let tokenMatch;
2919
+ depTokenRe.lastIndex = 0;
2920
+ while ((tokenMatch = depTokenRe.exec(refMatch[1])) !== null)
2921
+ tokens.push(tokenMatch[0]);
2922
+ }
2923
+ return tokens;
2924
+ };
2830
2925
  for (const phase of phases) {
2831
2926
  if (!phase['depends_on'] ||
2832
2927
  /^none$/i.test(phase['depends_on'].trim())) {
@@ -2835,24 +2930,39 @@ function cmdInitManager(cwd, raw) {
2835
2930
  else {
2836
2931
  const prose = phase['depends_on'];
2837
2932
  const ownNumber = normalizePhaseNumber(phase['number']);
2933
+ const ownIdentity = typeof phase['display_id'] === 'string'
2934
+ ? bracketIdentityKey(phase['display_id'])
2935
+ : null;
2838
2936
  const depNums = [];
2839
2937
  const seen = new Set();
2840
- let refMatch;
2841
- depPhaseRefRe.lastIndex = 0;
2842
- while ((refMatch = depPhaseRefRe.exec(prose)) !== null) {
2843
- let tok;
2844
- depTokenRe.lastIndex = 0;
2845
- while ((tok = depTokenRe.exec(refMatch[1])) !== null) {
2846
- const normalized = normalizePhaseNumber(tok[0]);
2847
- if (normalized === ownNumber)
2848
- continue; // #4764: never the row's own phase
2849
- if (seen.has(normalized))
2938
+ const dependencyTokens = phaseIdConvention === 'bracket'
2939
+ ? extractPhaseDependencyTokens(prose, phaseIdConvention)
2940
+ : legacyManagerDependencyTokens(prose);
2941
+ for (const token of dependencyTokens) {
2942
+ const identity = bracketIdentityKey(token);
2943
+ if (identity) {
2944
+ if (identity === ownIdentity)
2945
+ continue;
2946
+ if (seen.has(identity))
2850
2947
  continue;
2851
- seen.add(normalized);
2852
- depNums.push(tok[0]);
2948
+ seen.add(identity);
2949
+ depNums.push(token);
2950
+ continue;
2853
2951
  }
2952
+ const normalized = normalizePhaseNumber(token);
2953
+ if (normalized === ownNumber)
2954
+ continue; // #4764: never the row's own phase
2955
+ if (seen.has(normalized))
2956
+ continue;
2957
+ seen.add(normalized);
2958
+ depNums.push(token);
2854
2959
  }
2855
- phase['deps_satisfied'] = depNums.every((n) => completedNums.has(normalizePhaseNumber(n)));
2960
+ phase['deps_satisfied'] = depNums.every((dependency) => {
2961
+ const identity = bracketIdentityKey(dependency);
2962
+ return identity
2963
+ ? completedBracketIdentities.has(identity)
2964
+ : completedNums.has(normalizePhaseNumber(dependency));
2965
+ });
2856
2966
  phase['dep_phases'] = depNums;
2857
2967
  }
2858
2968
  }
@@ -3316,6 +3426,7 @@ function cmdInitProgress(cwd, raw, options = {}) {
3316
3426
  `Available workstreams: ${_availableWorkstreams.join(', ')}`, ERROR_REASON.WORKSTREAM_MODE_NONE_ACTIVE);
3317
3427
  }
3318
3428
  const phasesDir = node_path_1.default.join(planningDir(cwd), 'phases');
3429
+ const directoryContext = phaseDirectoryOptions(cwd, config);
3319
3430
  const phases = [];
3320
3431
  let currentPhase = null;
3321
3432
  let nextPhase = null;
@@ -3328,7 +3439,7 @@ function cmdInitProgress(cwd, raw, options = {}) {
3328
3439
  // #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag, owned by
3329
3440
  // buildPhaseHeadingScanRegex (phase-id.cts) so this scan also recognizes
3330
3441
  // bracket-convention headings instead of hand-rolling a literal `Phase\s+`.
3331
- const { regex: headingPattern, phaseNumGroup: hNumGroup, phaseNameGroup: hNameGroup } = buildPhaseHeadingScanRegex(PHASE_HEADING_BASELINE.ANY_BRACKET, progressConvention);
3442
+ const { regex: headingPattern, phaseNumGroup: hNumGroup, phaseNameGroup: hNameGroup } = buildPhaseHeadingScanRegex(PHASE_HEADING_BASELINE.ANY_BRACKET, progressConvention, directoryContext);
3332
3443
  let hm;
3333
3444
  while ((hm = headingPattern.exec(roadmapContent)) !== null) {
3334
3445
  roadmapPhaseNums.add(hm[hNumGroup]);
@@ -3361,12 +3472,12 @@ function cmdInitProgress(cwd, raw, options = {}) {
3361
3472
  // matched (by absorbing the bracket text). This does NOT migrate the site
3362
3473
  // onto phaseHeadingPrefixSrcFor / the bracket convention — that stays
3363
3474
  // out of scope per the comment above (#4984 revert, pinned census).
3364
- const cbPattern = new RegExp(`^[ \\t]*-[ \\t]*\\[(x| )\\][ \\t]*(?:\\*\\*)?(?:\\[[^\\]\\n]{0,200}\\]\\s*)?Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})[:\\s]`, 'gim');
3475
+ const cbPattern = (0, phase_entry_grammar_cjs_1.phaseEntryRegExp)(`(?:\\[[^\\]\\n]{0,200}\\]\\s*)?Phase\\s+(${phaseScanTokenSource(directoryContext)})[:\\s]`, 'gim', { state: 'captured' });
3365
3476
  let cbm;
3366
3477
  while ((cbm = cbPattern.exec(roadmapContent)) !== null) {
3367
3478
  const cbKey = cbm[2];
3368
3479
  if (!roadmapCheckboxStates.has(cbKey)) {
3369
- roadmapCheckboxStates.set(cbKey, cbm[1].toLowerCase() === 'x');
3480
+ roadmapCheckboxStates.set(cbKey, (0, phase_entry_grammar_cjs_1.isBoxCharChecked)(cbm[1]));
3370
3481
  }
3371
3482
  }
3372
3483
  }
@@ -3383,10 +3494,12 @@ function cmdInitProgress(cwd, raw, options = {}) {
3383
3494
  // behavior-preserving).
3384
3495
  const dirs = listMilestonePhaseDirs(phasesDir, { cwd }).value;
3385
3496
  for (const dir of dirs) {
3386
- const dirMatch = dir.match(new RegExp(`^(${PHASE_NUMBER_TOKEN_SOURCE})-?(.*)`, 'i'));
3387
- const phaseNumber = dirMatch ? dirMatch[1] : dir;
3388
- const phaseName = dirMatch && dirMatch[2] ? dirMatch[2] : null;
3389
- seenPhaseNums.add(phaseNumber.replace(/^0+/, '') || '0');
3497
+ const dirMatch = dir.match(new RegExp(`^(${phaseScanTokenSource(directoryContext)})-?(.*)`, 'i'));
3498
+ const query = customPhaseQuery(dir, directoryContext);
3499
+ const owner = query && customDirectoryOwner(dir, query);
3500
+ const phaseNumber = owner ? owner.id : dirMatch ? dirMatch[1] : dir;
3501
+ const phaseName = owner ? owner.slug : dirMatch && dirMatch[2] ? dirMatch[2] : null;
3502
+ seenPhaseNums.add((phaseKeyFromToken(phaseNumber, directoryContext) ?? phaseNumber).toLowerCase());
3390
3503
  const phasePath = node_path_1.default.join(phasesDir, dir);
3391
3504
  const phaseFiles = node_fs_1.default.readdirSync(phasePath);
3392
3505
  // #5060: Phase Status Module — the single owner of "what state is
@@ -3431,7 +3544,7 @@ function cmdInitProgress(cwd, raw, options = {}) {
3431
3544
  /* otherwise intentionally empty */
3432
3545
  }
3433
3546
  for (const [num, name] of roadmapPhaseNames) {
3434
- const stripped = num.replace(/^0+/, '') || '0';
3547
+ const stripped = (phaseKeyFromToken(num, directoryContext) ?? num).toLowerCase();
3435
3548
  if (!seenPhaseNums.has(stripped)) {
3436
3549
  const checkboxComplete = roadmapCheckboxStates.get(num) === true ||
3437
3550
  roadmapCheckboxStates.get(stripped) === true;
@@ -3461,7 +3574,7 @@ function cmdInitProgress(cwd, raw, options = {}) {
3461
3574
  }
3462
3575
  }
3463
3576
  }
3464
- phases.sort((a, b) => comparePhaseNum(a['number'], b['number']));
3577
+ phases.sort((a, b) => comparePhaseNum(a['number'], b['number'], directoryContext));
3465
3578
  // #3581: the frontier is ROADMAP ORDER, not artifact presence. The disk loop
3466
3579
  // above could claim nextPhase from a stray out-of-order artifact directory
3467
3580
  // (a phase-9 UAT evidence dir while roadmap phase 8 was pending and
@@ -339,6 +339,9 @@ const ERROR_REASON = Object.freeze({
339
339
  SDK_MISSING_ARG: 'sdk_missing_arg',
340
340
  // workflow / phase
341
341
  PHASE_NOT_FOUND: 'phase_not_found',
342
+ PHASE_ID_COLLISION: 'phase_id_collision',
343
+ AMBIGUOUS_PHASE_OWNER: 'ambiguous_phase_owner',
344
+ PHASE_NAMING_NOT_CUSTOM: 'phase_naming_not_custom',
342
345
  PHASE_VERIFICATION_INCOMPLETE: 'phase_verification_incomplete',
343
346
  PHASE_PLAN_COVERAGE_INCOMPLETE: 'phase_plan_coverage_incomplete',
344
347
  SUMMARY_NO_PLANNING: 'summary_no_planning',
@@ -471,6 +474,9 @@ const REASON_TO_OUTCOME = Object.freeze({
471
474
  [ERROR_REASON.SDK_MISSING_ARG]: 'USAGE',
472
475
  [ERROR_REASON.GRAPHIFY_INVALID_QUERY]: 'USAGE',
473
476
  [ERROR_REASON.USAGE]: 'USAGE',
477
+ [ERROR_REASON.PHASE_ID_COLLISION]: 'USAGE',
478
+ [ERROR_REASON.AMBIGUOUS_PHASE_OWNER]: 'USAGE',
479
+ [ERROR_REASON.PHASE_NAMING_NOT_CUSTOM]: 'USAGE',
474
480
  // A specific, named thing does not exist / nothing was there to find —
475
481
  // genuine, known emptiness rather than a broken prerequisite.
476
482
  [ERROR_REASON.CONFIG_KEY_NOT_FOUND]: 'NO_INPUT',
@@ -30,6 +30,8 @@ exports.maskFences = maskFences;
30
30
  exports.editOutsideFences = editOutsideFences;
31
31
  exports.replaceSection = replaceSection;
32
32
  exports.withSection = withSection;
33
+ exports.headingSliceRange = headingSliceRange;
34
+ exports.editHeadingSlice = editHeadingSlice;
33
35
  exports.deleteSection = deleteSection;
34
36
  exports.extractXmlTagBodies = extractXmlTagBodies;
35
37
  const pattern_cjs_1 = require("./pattern.cjs");
@@ -1035,6 +1037,29 @@ function withSection(content, target, edit, opts = {}) {
1035
1037
  return content;
1036
1038
  return replaceSection(content, section, newBody);
1037
1039
  }
1040
+ /**
1041
+ * Range of the slice that starts at `headings[index]` and runs to the next
1042
+ * heading of level <= `maxLevel` (fence-aware: `headings` comes from
1043
+ * `tokenizeHeadings`). Unlike `collectSection` it keeps the heading line and
1044
+ * every trailing byte, so a caller that edits the slice can splice it back
1045
+ * byte-exact (#5762).
1046
+ */
1047
+ function headingSliceRange(content, headings, index, maxLevel = 2) {
1048
+ const next = headings.slice(index + 1).find((h) => h.level <= maxLevel);
1049
+ return { start: headings[index].offset, end: next ? next.offset : content.length };
1050
+ }
1051
+ /**
1052
+ * Run `edit` on `content.slice(range.start, end)` and splice the result back,
1053
+ * where `end` excludes the single newline that precedes the next heading — the
1054
+ * slice the pre-owner writers cut with `search(/\n#{1,2}[ \t]/)` — so a row
1055
+ * inserted at the end of a table keeps the blank line before that heading.
1056
+ */
1057
+ function editHeadingSlice(content, range, edit) {
1058
+ const end = range.end < content.length && range.end > range.start && content[range.end - 1] === '\n'
1059
+ ? range.end - 1
1060
+ : range.end;
1061
+ return content.slice(0, range.start) + edit(content.slice(range.start, end)) + content.slice(end);
1062
+ }
1038
1063
  // ─── deleteSection ────────────────────────────────────────────────────────────
1039
1064
  /**
1040
1065
  * Delete an entire section — the matching heading line ITSELF plus its body —
@@ -1065,6 +1090,8 @@ function withSection(content, target, edit, opts = {}) {
1065
1090
  * double-blank gap accumulates where the section used to sit. Content
1066
1091
  * elsewhere in the document is never touched.
1067
1092
  *
1093
+ * `endOffset` adds an optional caller-owned maximum boundary. It never widens
1094
+ * the heading-level range, and callers that omit it retain the legacy bytes.
1068
1095
  * Returns `content` unchanged when no heading matches `headingPredicate`
1069
1096
  * (bounded no-op, mirroring `withSection`'s miss behaviour).
1070
1097
  */
@@ -1103,7 +1130,13 @@ function deleteSection(content, headingPredicate, opts = {}) {
1103
1130
  }
1104
1131
  const eofOffset = acc;
1105
1132
  const sectionStart = lineOffsets[target.line - 1]; // start of the target heading LINE itself
1106
- const sectionEnd = stopLine <= lines.length ? lineOffsets[stopLine - 1] : eofOffset;
1133
+ const headingBoundedEnd = stopLine <= lines.length ? lineOffsets[stopLine - 1] : eofOffset;
1134
+ const explicitEnd = typeof opts.endOffset === 'number' && Number.isFinite(opts.endOffset)
1135
+ ? Math.max(0, Math.min(content.length, opts.endOffset))
1136
+ : headingBoundedEnd;
1137
+ const sectionEnd = Math.min(headingBoundedEnd, explicitEnd);
1138
+ if (sectionEnd <= sectionStart)
1139
+ return content;
1107
1140
  const before = content.slice(0, sectionStart);
1108
1141
  const after = content.slice(sectionEnd);
1109
1142
  // Collapse a resulting blank-line seam to at most one blank line (2 newlines).
@@ -25,6 +25,7 @@ exports.escapeCell = escapeCell;
25
25
  exports.appendQuickTaskRow = appendQuickTaskRow;
26
26
  exports.migrateQuickTasksTable = migrateQuickTasksTable;
27
27
  exports.resetQuickTaskRows = resetQuickTaskRows;
28
+ exports.updateTraceabilityCell = updateTraceabilityCell;
28
29
  const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
29
30
  // ─── Schema registry ──────────────────────────────────────────────────────────
30
31
  /**
@@ -978,5 +979,27 @@ function resetQuickTaskRows(stateContent) {
978
979
  const content = (0, markdown_sectionizer_cjs_1.replaceSection)(stateContent, section, newBody);
979
980
  return { ok: true, value: { content, cleared, variant: match.label } };
980
981
  }
982
+ /**
983
+ * `updateTableCell` scoped to REQUIREMENTS.md's `## Traceability` (or
984
+ * `## Traceability Status`) section (#2245 F1, #5762): `updateTableCell` binds
985
+ * to the first table it finds, and the shipped template puts an `## Out of
986
+ * Scope` table (`Feature | Reason`, no `Status` column) before Traceability, so
987
+ * an unscoped call never reaches the real rows. The ONE copy of this slice
988
+ * (it used to live in both milestone.cts and phase.cts); the heading rule is
989
+ * the Progress owner's — first level-2 heading outside fenced code, running to
990
+ * the next level-1/2 heading. No such heading: the whole text, as before.
991
+ */
992
+ function updateTraceabilityCell(text, match, column, newValue) {
993
+ const headings = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(text);
994
+ const index = headings.findIndex((h) => h.level === 2 && /^Traceability(?:[ \t]+Status)?\b/i.test(h.text));
995
+ if (index === -1)
996
+ return updateTableCell(text, match, column, newValue);
997
+ let result = { ok: false, reason: 'no matching row' };
998
+ const edited = (0, markdown_sectionizer_cjs_1.editHeadingSlice)(text, (0, markdown_sectionizer_cjs_1.headingSliceRange)(text, headings, index, 2), (scoped) => {
999
+ result = updateTableCell(scoped, match, column, newValue);
1000
+ return result.ok ? result.value : scoped;
1001
+ });
1002
+ return result.ok ? { ok: true, value: edited } : result;
1003
+ }
981
1004
  // Consumers: require('../gsd-core/bin/lib/markdown-table.cjs')
982
1005
  // Named CJS exports are the canonical surface (ADR-457 .cts → .cjs build-at-publish).
@@ -109,10 +109,10 @@ const TOOLS = [
109
109
  type: 'object',
110
110
  properties: {
111
111
  family: { type: 'string', description: 'Command family (e.g. "query", "state", "phase").' },
112
- subcommand: { type: 'string', description: 'Subcommand name.' },
112
+ subcommand: { type: 'string', description: 'Subcommand name, if the command takes one. Omit or leave empty for family-only commands.' },
113
113
  args: { type: 'array', items: {}, description: 'Positional args.' },
114
114
  },
115
- required: ['family', 'subcommand'],
115
+ required: ['family'],
116
116
  },
117
117
  },
118
118
  {
@@ -182,10 +182,13 @@ function callTool(name, args, ctx) {
182
182
  if (name === 'gsd_invoke_command') {
183
183
  const family = asString(a.family);
184
184
  const subcommand = asString(a.subcommand);
185
- if (!family || !subcommand) {
186
- return { isError: true, content: [{ type: 'text', text: 'gsd_invoke_command requires string "family" and "subcommand".' }] };
185
+ if (!family) {
186
+ return { isError: true, content: [{ type: 'text', text: 'gsd_invoke_command requires a non-empty string "family".' }] };
187
187
  }
188
- const res = dispatchGsdCommand({ family, subcommand, args: Array.isArray(a.args) ? a.args : [], cwd });
188
+ if (a.subcommand !== undefined && subcommand === null) {
189
+ return { isError: true, content: [{ type: 'text', text: 'gsd_invoke_command requires string "subcommand" when provided.' }] };
190
+ }
191
+ const res = dispatchGsdCommand({ family, subcommand: subcommand ?? undefined, args: Array.isArray(a.args) ? a.args : [], cwd });
189
192
  if (!res.ok) {
190
193
  return { isError: true, content: [{ type: 'text', text: res.stderr || res.stdout || `dispatch failed (exit ${res.code})` }] };
191
194
  }