@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
@@ -30,7 +30,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
30
30
  return (mod && mod.__esModule) ? mod : { "default": mod };
31
31
  };
32
32
  Object.defineProperty(exports, "__esModule", { value: true });
33
- exports.PLANNING_ARTIFACTS = void 0;
33
+ exports.PLANNING_ARTIFACTS = exports.FIELD_LINE_BREAK_RE = void 0;
34
34
  exports.parsePlanningDoc = parsePlanningDoc;
35
35
  exports.findField = findField;
36
36
  exports.readNode = readNode;
@@ -48,6 +48,15 @@ const artifacts_cjs_1 = require("./artifacts.cjs");
48
48
  // is imported as a default import (esModuleInterop), not a named import.
49
49
  const frontmatter_cjs_1 = __importDefault(require("./frontmatter.cjs"));
50
50
  const frontmatter_fence_cjs_1 = require("./frontmatter-fence.cjs");
51
+ /**
52
+ * #4998: every character that ends a line for the planning-document readers.
53
+ * Their `^`/`$` run with the `m` flag, and a JavaScript multiline anchor
54
+ * matches at U+2028 LINE SEPARATOR and U+2029 PARAGRAPH SEPARATOR as well as
55
+ * at CR and LF — so a single-line value carrying either one forges a sibling
56
+ * line just as `\n` does. Every single-line writer, here (`setFieldValue`,
57
+ * `replaceProse`) and in `state-document.cts`, refuses a value matching this.
58
+ */
59
+ exports.FIELD_LINE_BREAK_RE = /[\r\n\u2028\u2029]/;
51
60
  const { extractFrontmatter, FRONTMATTER_UNPARSEABLE } = frontmatter_cjs_1.default;
52
61
  /**
53
62
  * Canonical `.planning/` root artifact basenames this seam recognises,
@@ -562,13 +571,25 @@ function setFieldValue(doc, id, value) {
562
571
  return { ok: false, reason: `node kind '${node.kind}' is not writable this phase` };
563
572
  }
564
573
  // #4917 / ADR-4910 Decision 2 & 4: a boldField's token boundary is a LINE
565
- // boundary, not just an offset range — a value containing \n or \r escapes
574
+ // boundary, not just an offset range — a value containing any
575
+ // FIELD_LINE_BREAK_RE character (#4998: U+2028/U+2029 too) escapes
566
576
  // the field's own span and reparses as sibling structure (a forged field)
567
577
  // once spliced back into the source. Decision 4 licenses refusal for any
568
578
  // value the grammar cannot represent; Phase 3 may widen this to escaping,
569
579
  // but Phase 1 refuses outright. Do not remove this as an over-restriction.
570
- if (/[\r\n]/.test(value)) {
571
- return { ok: false, reason: 'field value must not contain a line break (\\r or \\n)' };
580
+ // One carve-out (#4998, Codex review): a U+2028/U+2029 the field ALREADY
581
+ // carries may stay when everything from it on is byte-identical — a rewrite
582
+ // of the text before it (phase renumbering a `Depends on`) adds no line, and
583
+ // refusing it left the field silently unrenumbered.
584
+ const breakAt = value.search(exports.FIELD_LINE_BREAK_RE);
585
+ if (breakAt !== -1) {
586
+ const current = doc.source.slice(node.valueSpan.start, node.valueSpan.end);
587
+ const currentBreak = current.search(exports.FIELD_LINE_BREAK_RE);
588
+ const keepsExistingTail = !/[\r\n]/.test(value) && currentBreak !== -1
589
+ && value.slice(breakAt) === current.slice(currentBreak);
590
+ if (!keepsExistingTail) {
591
+ return { ok: false, reason: 'field value must not contain a line break (\\r, \\n, U+2028 or U+2029)' };
592
+ }
572
593
  }
573
594
  // #4917 / ADR-4910 Decision 4: "a value that cannot be represented in the
574
595
  // grammar is refused by the writer, with a report." This is a GENERAL
@@ -664,7 +685,7 @@ function replaceProse(doc, from, to) {
664
685
  if (typeof from !== 'string' || from.length === 0) {
665
686
  return { ok: false, reason: 'replaceProse: `from` must be a non-empty string' };
666
687
  }
667
- if (typeof to !== 'string' || /[\r\n]/.test(from) || /[\r\n]/.test(to)) {
688
+ if (typeof to !== 'string' || exports.FIELD_LINE_BREAK_RE.test(from) || exports.FIELD_LINE_BREAK_RE.test(to)) {
668
689
  return { ok: false, reason: 'replaceProse: `from` and `to` must be single-line strings' };
669
690
  }
670
691
  if (doc.staged.size > 0) {
@@ -51,7 +51,7 @@ const planningSnapshotMod = require("./planning-snapshot.cjs");
51
51
  const { buildPlanningSnapshot, worstScope } = planningSnapshotMod;
52
52
  // eslint-disable-next-line @typescript-eslint/no-require-imports
53
53
  const planningWorkspaceMod = require("./planning-workspace.cjs");
54
- const { planningPaths } = planningWorkspaceMod;
54
+ const { planningPaths, resolvePhaseIdConvention } = planningWorkspaceMod;
55
55
  // eslint-disable-next-line @typescript-eslint/no-require-imports
56
56
  const planScan = require("./plan-scan.cjs");
57
57
  // eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -74,7 +74,7 @@ const verificationMod = require("./verification.cjs");
74
74
  const { readVerificationStatus, VerificationStatusError, failOnVerificationStatusError } = verificationMod;
75
75
  // eslint-disable-next-line @typescript-eslint/no-require-imports
76
76
  const phaseIdMod = require("./phase-id.cjs");
77
- const { phaseKeyFromDir, phaseKeyFromToken, phaseMarkdownRegexSource } = phaseIdMod;
77
+ const { phaseKeyFromDir, phaseKeyFromToken, phaseMarkdownRegexSource, extractPhaseDependencyTokens } = phaseIdMod;
78
78
  // eslint-disable-next-line @typescript-eslint/no-require-imports
79
79
  const roadmapParserMod = require("./roadmap-parser.cjs");
80
80
  const { extractCurrentMilestone } = roadmapParserMod;
@@ -407,7 +407,7 @@ function findDuplicateIds(reqMd) {
407
407
  }
408
408
  return dupes;
409
409
  }
410
- function buildRequirements(requirementsPath, knownPhaseKeys, diagnostics, planningRoot) {
410
+ function buildRequirements(requirementsPath, knownPhaseKeys, diagnostics, planningRoot, directoryContext) {
411
411
  const doc = readDocument(requirementsPath, planningRoot);
412
412
  if (!doc.exists) {
413
413
  diagnostics.push({
@@ -464,7 +464,7 @@ function buildRequirements(requirementsPath, knownPhaseKeys, diagnostics, planni
464
464
  // here too — it is a FILE-MEMBERSHIP predicate (#3511) for aggregate
465
465
  // `*-UAT.md` / `*-VERIFICATION.md` scans, not a phase-identity equality
466
466
  // test, and is unreliable for decimal phases.
467
- if (!knownPhaseKeys.has(phaseKeyFromToken(token))) {
467
+ if (!knownPhaseKeys.has(phaseKeyFromToken(token, directoryContext))) {
468
468
  diagnostics.push({
469
469
  code: INSPECT_DIAGNOSTIC.REQUIREMENT_PHASE_UNKNOWN,
470
470
  subject: `${item.id}->${token}`,
@@ -915,28 +915,16 @@ function extractGoalProse(sectionBody) {
915
915
  * token scan above), never a document-wide scan. `[]` when the line is
916
916
  * absent.
917
917
  */
918
- function extractDependencyTokens(sectionBody) {
918
+ function extractDependencyTokens(sectionBody, phaseIdConvention) {
919
919
  const m = DEPENDS_ON_LINE_RE.exec(sectionBody);
920
920
  if (!m)
921
921
  return [];
922
- // #4764: phase REFERENCES, not digit runs — the same prose-anchored grammar
923
- // init.manager's dep_phases extraction uses (owner: phase-id.cts's
924
- // PHASE_DEP_REF_SOURCE). The whole-field token scrape this replaces pulled
925
- // calendar dates, git shas and ledger ids in as dependencies. The grammar's
926
- // capture group 1 already excludes the "Phase(s)" anchor word, so no
927
- // prefix-strip literal is needed here. Self-exclusion (init.manager drops
928
- // the row's own number) is deliberately NOT applied: this reader has no row
929
- // context at the extraction site and reports informationally, it does not
930
- // gate.
931
- const refRe = new RegExp(phaseIdMod.PHASE_DEP_REF_SOURCE, 'gi');
932
- const tokenRe = new RegExp(phaseIdMod.PHASE_NUMBER_TOKEN_SOURCE, 'g');
933
- const tokens = [];
934
- let refMatch;
935
- while ((refMatch = refRe.exec(m[1])) !== null) {
936
- for (const t of refMatch[1].matchAll(tokenRe))
937
- tokens.push(t[0]);
938
- }
939
- return sortedUnique(tokens);
922
+ // #4764/#4304: phase REFERENCES, not digit runs. Both this informational
923
+ // reader and init.manager consume phase-id.cts's convention-aware owner, so
924
+ // bracket milestone digits never surface as dependencies. Self-exclusion is
925
+ // deliberately NOT applied here: this site has no row context and does not
926
+ // gate readiness.
927
+ return sortedUnique(extractPhaseDependencyTokens(m[1], phaseIdConvention));
940
928
  }
941
929
  /**
942
930
  * This phase's own ROADMAP.md section body — milestone-scoped via the SAME
@@ -949,9 +937,9 @@ function extractDependencyTokens(sectionBody) {
949
937
  * sibling phase whose TITLE merely mentions this phase's number is never
950
938
  * hijacked.
951
939
  */
952
- function findPhaseRoadmapSection(cwd, roadmapText, phaseId) {
940
+ function findPhaseRoadmapSection(cwd, roadmapText, phaseId, directoryContext) {
953
941
  const scoped = extractCurrentMilestone(roadmapText, cwd);
954
- const headingRe = new RegExp(`^(?:\\[[^\\]]{1,200}\\]\\s*)?Phase\\s+${phaseMarkdownRegexSource(phaseId)}(?=[\\s:(]|$)`, 'i');
942
+ const headingRe = new RegExp(`^(?:\\[[^\\]]{1,200}\\]\\s*)?Phase\\s+${phaseMarkdownRegexSource(phaseId, directoryContext)}(?=[\\s:(]|$)`, 'i');
955
943
  const section = collectSection(scoped, (h) => headingRe.test(h.text));
956
944
  return section ? section.body : null;
957
945
  }
@@ -976,7 +964,7 @@ function findPhaseRoadmapSection(cwd, roadmapText, phaseId) {
976
964
  * `readDocument(paths.roadmap)` read, keeps both the read path and the
977
965
  * "found vs missing vs unreadable" distinction singular.
978
966
  */
979
- function buildPhaseGoalAndDependencies(cwd, roadmapDoc, phaseId, phaseDirLabel, diagnostics) {
967
+ function buildPhaseGoalAndDependencies(cwd, roadmapDoc, phaseId, phaseDirLabel, phaseIdConvention, diagnostics, directoryContext) {
980
968
  if (!roadmapDoc.readable || roadmapDoc.text === null) {
981
969
  diagnostics.push({
982
970
  code: INSPECT_DIAGNOSTIC.ROADMAP_UNSCOPED,
@@ -999,7 +987,7 @@ function buildPhaseGoalAndDependencies(cwd, roadmapDoc, phaseId, phaseDirLabel,
999
987
  dependencies: { value: [], scope: SCOPE.UNSCOPED },
1000
988
  };
1001
989
  }
1002
- const sectionBody = findPhaseRoadmapSection(cwd, roadmapDoc.text, phaseId);
990
+ const sectionBody = findPhaseRoadmapSection(cwd, roadmapDoc.text, phaseId, directoryContext);
1003
991
  if (sectionBody === null) {
1004
992
  diagnostics.push({
1005
993
  code: INSPECT_DIAGNOSTIC.ROADMAP_UNSCOPED,
@@ -1013,7 +1001,7 @@ function buildPhaseGoalAndDependencies(cwd, roadmapDoc, phaseId, phaseDirLabel,
1013
1001
  }
1014
1002
  return {
1015
1003
  goal: { value: extractGoalProse(sectionBody), scope: SCOPE.COMPLETE },
1016
- dependencies: { value: extractDependencyTokens(sectionBody), scope: SCOPE.COMPLETE },
1004
+ dependencies: { value: extractDependencyTokens(sectionBody, phaseIdConvention), scope: SCOPE.COMPLETE },
1017
1005
  };
1018
1006
  }
1019
1007
  // ─── Entry points ─────────────────────────────────────────────────────────────
@@ -1031,6 +1019,7 @@ function buildPlanningInspectResult(cwd) {
1031
1019
  let statusError = null;
1032
1020
  const diagnostics = [];
1033
1021
  const paths = planningPaths(cwd);
1022
+ const phaseIdConvention = resolvePhaseIdConvention(cwd);
1034
1023
  const planningExists = node_fs_1.default.existsSync(paths.planning);
1035
1024
  if (!planningExists) {
1036
1025
  diagnostics.push({
@@ -1040,6 +1029,7 @@ function buildPlanningInspectResult(cwd) {
1040
1029
  });
1041
1030
  }
1042
1031
  const snapshot = buildPlanningSnapshot(cwd);
1032
+ const directoryContext = snapshot.directoryContext;
1043
1033
  if (snapshot.milestone.scope !== SCOPE.COMPLETE) {
1044
1034
  diagnostics.push({
1045
1035
  code: INSPECT_DIAGNOSTIC.ROADMAP_UNSCOPED,
@@ -1072,14 +1062,14 @@ function buildPlanningInspectResult(cwd) {
1072
1062
  // one phase.
1073
1063
  const checkboxByPhaseKey = new Map();
1074
1064
  for (const [token, ticked] of Object.entries(checkboxes)) {
1075
- checkboxByPhaseKey.set(phaseKeyFromToken(token), ticked);
1065
+ checkboxByPhaseKey.set(phaseKeyFromToken(token, directoryContext), ticked);
1076
1066
  }
1077
1067
  const phaseSnapshots = snapshot.phases.value;
1078
1068
  // Canonical per-directory identity keys (`phase-id.cts`'s
1079
1069
  // `phaseKeyFromDir`), not a raw regex scrape — this is the set
1080
1070
  // `buildRequirements` below matches Traceability tokens against via
1081
1071
  // `phaseKeyFromToken`.
1082
- const knownPhaseKeys = new Set(windowed.map((dir) => phaseKeyFromDir(dir)));
1072
+ const knownPhaseKeys = new Set(windowed.map((dir) => phaseKeyFromDir(dir, undefined, directoryContext)));
1083
1073
  // Read once, shared across every phase row's goal/dependencies lookup —
1084
1074
  // the same `readDocument` seam every other document read in this module
1085
1075
  // uses, never a second file-reading path.
@@ -1124,8 +1114,10 @@ function buildPlanningInspectResult(cwd) {
1124
1114
  verification = { status: null, next_action: err.message, route: '' };
1125
1115
  }
1126
1116
  const token = /^(\d+(?:\.\d+)*)/.exec(phase.dir);
1127
- const phaseId = token ? token[1] : null;
1128
- const { goal, dependencies } = buildPhaseGoalAndDependencies(cwd, roadmapDoc, phaseId, phase.dir, diagnostics);
1117
+ const custom = phaseIdMod.customPhaseQuery(phase.dir, directoryContext);
1118
+ const owner = custom && phaseIdMod.customPhaseDirectoryParts(phase.dir, custom);
1119
+ const phaseId = owner ? owner.id : token ? token[1] : null;
1120
+ const { goal, dependencies } = buildPhaseGoalAndDependencies(cwd, roadmapDoc, phaseId, phase.dir, phaseIdConvention, diagnostics, directoryContext);
1129
1121
  // `uat.foldScope`, NOT `uat.scope` (#3078 round-8). The two currently
1130
1122
  // agree at every call site — see `buildUatRows` for why the field is
1131
1123
  // still kept separate — so folding either one here produces the same
@@ -1159,8 +1151,8 @@ function buildPlanningInspectResult(cwd) {
1159
1151
  route: verification.route,
1160
1152
  },
1161
1153
  roadmap_acceptance: {
1162
- checkbox: checkboxByPhaseKey.has(phaseKeyFromDir(phase.dir))
1163
- ? checkboxByPhaseKey.get(phaseKeyFromDir(phase.dir))
1154
+ checkbox: checkboxByPhaseKey.has(phaseKeyFromDir(phase.dir, undefined, directoryContext))
1155
+ ? checkboxByPhaseKey.get(phaseKeyFromDir(phase.dir, undefined, directoryContext))
1164
1156
  : null,
1165
1157
  // ADR-3180 §7.4: a ticked checkbox is a human annotation with no
1166
1158
  // machine authority. Stated in the payload so a consumer cannot
@@ -1174,7 +1166,7 @@ function buildPlanningInspectResult(cwd) {
1174
1166
  scope: folded,
1175
1167
  };
1176
1168
  });
1177
- const requirements = buildRequirements(paths.requirements, knownPhaseKeys, diagnostics, paths.planning);
1169
+ const requirements = buildRequirements(paths.requirements, knownPhaseKeys, diagnostics, paths.planning, directoryContext);
1178
1170
  const phaseScope = worstScope(snapshot.phaseDirs.scope, snapshot.phases.scope, ...phaseRows.map((p) => p.scope));
1179
1171
  const acceptedPhases = makeFraction(phaseRows.filter((p) => p.complete).length, phaseRows.length, phaseScope, 'progress.accepted_phases', diagnostics);
1180
1172
  const completedPlans = makeFraction(phaseRows.reduce((sum, p) => sum + p.summary_count, 0), phaseRows.reduce((sum, p) => sum + p.plan_count, 0), phaseScope, 'progress.completed_plans', diagnostics);
@@ -29,7 +29,7 @@ const roadmapParserMod = require("./roadmap-parser.cjs");
29
29
  const { getMilestoneInfo, extractCurrentMilestone } = roadmapParserMod;
30
30
  // eslint-disable-next-line @typescript-eslint/no-require-imports
31
31
  const phaseLocatorMod = require("./phase-locator.cjs");
32
- const { listMilestonePhaseDirs, listAllPhaseDirs } = phaseLocatorMod;
32
+ const { phaseDirectoryOptions, listMilestonePhaseDirs, listAllPhaseDirs } = phaseLocatorMod;
33
33
  // eslint-disable-next-line @typescript-eslint/no-require-imports
34
34
  const verificationMod = require("./verification.cjs");
35
35
  const { isPhaseComplete } = verificationMod;
@@ -51,6 +51,7 @@ const { planningPaths, planningRoot, resolvePhaseIdConvention, resolveEnvWorkstr
51
51
  // PROSE, not quoted literals.
52
52
  const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
53
53
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
54
+ const phase_entry_grammar_cjs_1 = require("./phase-entry-grammar.cjs");
54
55
  // eslint-disable-next-line @typescript-eslint/no-require-imports
55
56
  const frontmatterMod = require("./frontmatter.cjs");
56
57
  const { extractFrontmatter, stripFrontmatter } = frontmatterMod;
@@ -586,7 +587,7 @@ function buildRoadmapDeclaredPhasesField(roadmapPath, convention) {
586
587
  * diagnostic (W011) whose entire purpose is flagging when the two DISAGREE —
587
588
  * reading the data is not re-litigating who is authoritative.
588
589
  */
589
- function buildRoadmapPhaseCheckboxesField(roadmapPath, convention) {
590
+ function buildRoadmapPhaseCheckboxesField(roadmapPath, convention, directoryContext) {
590
591
  if (!node_fs_1.default.existsSync(roadmapPath)) {
591
592
  return { value: {}, scope: SCOPE.UNREADABLE };
592
593
  }
@@ -605,7 +606,9 @@ function buildRoadmapPhaseCheckboxesField(roadmapPath, convention) {
605
606
  // vanish from the exclusion set and every unstarted bracket phase gains a
606
607
  // W006. NON-capturing (`capturing` defaults false), so the phase token stays
607
608
  // group 2 and the legacy repo compiles a byte-identical source.
608
- const checkboxRe = new RegExp(`-\\s*\\[([xX ])\\].*?${phaseHeadingPrefixSrcFor(PHASE_HEADING_BASELINE.LABEL_ONLY, convention)}0*(${PHASE_NUMBER_TOKEN_SOURCE})${OPTIONAL_PHASE_TAG_SOURCE}[:\\s]`, 'gi');
609
+ // The entry prefix (marker, box, bold token) is the phase-entry grammar's (#5764),
610
+ // the same one `buildNotStartedPhaseVariants` composes; group 1 is the box.
611
+ const checkboxRe = (0, phase_entry_grammar_cjs_1.phaseEntryRegExp)(`${phaseHeadingPrefixSrcFor(PHASE_HEADING_BASELINE.LABEL_ONLY, convention)}0*(${convention === 'bracket' ? PHASE_NUMBER_TOKEN_SOURCE : phaseIdMod.phaseScanTokenSource(directoryContext)})${OPTIONAL_PHASE_TAG_SOURCE}[:\\s]`, 'gi', { state: 'captured' });
609
612
  const value = {};
610
613
  let m;
611
614
  while ((m = checkboxRe.exec(content)) !== null) {
@@ -864,7 +867,7 @@ function buildArchivedPhaseTokensField(planBase, convention) {
864
867
  * ROADMAP.md degrades to an empty list, mirroring every other
865
868
  * ROADMAP-sourced field's absent-file handling.
866
869
  */
867
- function buildCurrentMilestoneRoadmapPhaseIdsField(cwd, roadmapPath, convention) {
870
+ function buildCurrentMilestoneRoadmapPhaseIdsField(cwd, roadmapPath, convention, directoryContext) {
868
871
  if (!node_fs_1.default.existsSync(roadmapPath))
869
872
  return { value: [], scope: SCOPE.UNREADABLE };
870
873
  let content;
@@ -902,7 +905,7 @@ function buildCurrentMilestoneRoadmapPhaseIdsField(cwd, roadmapPath, convention)
902
905
  // pinned to this site by name) would catch that narrowing, since the census
903
906
  // only pins `phaseHeadingPrefixSrcFor`'s own output, not this regex's tail.
904
907
  // Kept as its own composition to preserve this exact byte-for-behavior.
905
- const phasePattern = new RegExp(`#{2,4}\\s*${phaseHeadingPrefixSrcFor(PHASE_HEADING_BASELINE.LABEL_ONLY, convention, Boolean(bracketGroup))}(${PHASE_NUMBER_TOKEN_SOURCE})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:`, 'gi');
908
+ const phasePattern = new RegExp(`#{2,4}\\s*${phaseHeadingPrefixSrcFor(PHASE_HEADING_BASELINE.LABEL_ONLY, convention, Boolean(bracketGroup))}(${convention !== 'bracket' ? phaseIdMod.phaseScanTokenSource(directoryContext) : PHASE_NUMBER_TOKEN_SOURCE})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:`, 'gi');
906
909
  const value = [];
907
910
  for (const m of scoped.matchAll(phasePattern)) {
908
911
  const bracketId = bracketGroup ? m[1] : undefined;
@@ -1021,6 +1024,7 @@ function buildPlanningSnapshot(cwd) {
1021
1024
  // See the `phaseIdConvention` field's comment for why one resolution point is
1022
1025
  // load-bearing rather than a micro-optimisation.
1023
1026
  const phaseIdConvention = resolvePhaseIdConvention(cwd) ?? null;
1027
+ const directoryContext = phaseDirectoryOptions(cwd);
1024
1028
  // #4257: the workstream `planningPaths(cwd)` just scoped every read to
1025
1029
  // (its `planningDir` call applies this exact discriminator when handed no
1026
1030
  // `ws`), resolved through the same owner so W002's scope clause names the
@@ -1068,17 +1072,18 @@ function buildPlanningSnapshot(cwd) {
1068
1072
  statePhaseTokens: stateFields.statePhaseTokens,
1069
1073
  stateStatus: stateFields.stateStatus,
1070
1074
  roadmapDeclaredPhases: roadmapDeclared.declared,
1071
- roadmapPhaseCheckboxes: buildRoadmapPhaseCheckboxesField(paths.roadmap, phaseIdConvention),
1075
+ roadmapPhaseCheckboxes: buildRoadmapPhaseCheckboxesField(paths.roadmap, phaseIdConvention, directoryContext),
1072
1076
  researchValidationStatus: buildResearchValidationStatusField(paths.phases, phaseDirs.value, phaseDirs.scope, phaseIdConvention),
1073
1077
  milestoneArchiveStatus: buildMilestoneArchiveStatusField(cwd),
1074
1078
  planningRootFiles: buildPlanningRootFilesField(cwd),
1075
1079
  allPhaseDirNames,
1076
1080
  archivedPhaseTokens: buildArchivedPhaseTokensField(paths.planning, phaseIdConvention),
1077
- currentMilestoneRoadmapPhaseIds: buildCurrentMilestoneRoadmapPhaseIdsField(cwd, paths.roadmap, phaseIdConvention),
1081
+ currentMilestoneRoadmapPhaseIds: buildCurrentMilestoneRoadmapPhaseIdsField(cwd, paths.roadmap, phaseIdConvention, directoryContext),
1078
1082
  perPhasePlanNumbering: perPhasePlanScanFields.perPhasePlanNumbering,
1079
1083
  perPhaseOrphanSummaries: perPhasePlanScanFields.perPhaseOrphanSummaries,
1080
1084
  perPhaseWaveMissingPlans: perPhasePlanScanFields.perPhaseWaveMissingPlans,
1081
1085
  phaseIdConvention,
1086
+ directoryContext,
1082
1087
  roadmapSentinelPhaseTokens: roadmapDeclared.sentinelTokens,
1083
1088
  roadmapBracketIncoherences: buildRoadmapBracketIncoherencesField(paths.roadmap, phaseIdConvention),
1084
1089
  workstream,
@@ -186,12 +186,12 @@ function ownConfigValue(configPath, keyPath) {
186
186
  * GSD_PROJECT/GSD_WORKSTREAM value containing path separators or `..`, and
187
187
  * that shape resolves to "not present".
188
188
  */
189
- function readScopedConfigValue(cwd, keyPath) {
189
+ function readScopedConfigValue(cwd, keyPath, workstream) {
190
190
  try {
191
- const scoped = ownConfigValue(node_path_1.default.join(planningDir(cwd), 'config.json'), keyPath);
191
+ const scoped = ownConfigValue(node_path_1.default.join(planningDir(cwd, workstream), 'config.json'), keyPath);
192
192
  if (scoped.present)
193
193
  return scoped;
194
- if (resolveEnvWorkstream() !== null) {
194
+ if ((workstream === undefined ? resolveEnvWorkstream() : workstream) !== null) {
195
195
  return ownConfigValue(node_path_1.default.join(planningRoot(cwd), 'config.json'), keyPath);
196
196
  }
197
197
  }
@@ -35,17 +35,38 @@
35
35
  * two behave identically.
36
36
  */
37
37
  Object.defineProperty(exports, "__esModule", { value: true });
38
+ exports.GIT_LOCATION_ENV_VARS = void 0;
39
+ exports.scrubGitLocationEnv = scrubGitLocationEnv;
38
40
  exports.readOnlyGitEnv = readOnlyGitEnv;
41
+ /**
42
+ * #5548: env vars that make git act on a repository other than the one `cwd`
43
+ * names (git sets them inside hooks; wrappers export them). This is
44
+ * `git rev-parse --local-env-vars` minus GIT_CONFIG, GIT_CONFIG_PARAMETERS and
45
+ * GIT_CONFIG_COUNT, which inject config (e.g. CI `safe.directory`) rather than
46
+ * select a repository. (They are not repository-neutral: `GIT_CONFIG_COUNT` with a
47
+ * `core.worktree` pair can still redirect git, but a caller who controls the
48
+ * environment can already inject `core.sshCommand` or `core.fsmonitor`.)
49
+ * hooks/lib/git-probe.js carries a parity-tested copy.
50
+ */
51
+ exports.GIT_LOCATION_ENV_VARS = [
52
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES', 'GIT_OBJECT_DIRECTORY', 'GIT_DIR', 'GIT_WORK_TREE',
53
+ 'GIT_IMPLICIT_WORK_TREE', 'GIT_GRAFT_FILE', 'GIT_INDEX_FILE', 'GIT_NO_REPLACE_OBJECTS',
54
+ 'GIT_REPLACE_REF_BASE', 'GIT_PREFIX', 'GIT_SHALLOW_FILE', 'GIT_COMMON_DIR',
55
+ ];
56
+ /** Copy of `env` without the repo-location vars; case-insensitive (Windows keeps `Git_Dir` casing). */
57
+ function scrubGitLocationEnv(env) {
58
+ return Object.fromEntries(Object.entries(env).filter(([k]) => !exports.GIT_LOCATION_ENV_VARS.includes(k.toUpperCase())));
59
+ }
39
60
  /**
40
61
  * Build the env for a read-only git spawn.
41
62
  *
42
- * @param base Environment to spread, `process.env` by default. Callers that
63
+ * @param base Environment to spread, `process.env` minus the repo-location vars by default (#5548). Callers that
43
64
  * layer extra git variables on top (GIT_TERMINAL_PROMPT, a caller's own
44
65
  * overrides) pass them in `base`; callers that must let a caller-supplied
45
66
  * override win spread the result and apply their own keys afterwards.
46
67
  * @returns A fresh object each call — never the cached `process.env`.
47
68
  */
48
- function readOnlyGitEnv(base = process.env) {
69
+ function readOnlyGitEnv(base = scrubGitLocationEnv(process.env)) {
49
70
  const env = {};
50
71
  for (const [key, value] of Object.entries(base)) {
51
72
  // process.env carries undefined-valued keys on some platforms; spawn()
@@ -58,7 +58,7 @@ const { routeHubCommandFamily } = cjsCommandRouterAdapter;
58
58
  const { isCapabilityActive } = capabilityStateMod;
59
59
  const { resolveConfigKey } = capabilityActivationMod;
60
60
  const { loadConfig } = configLoaderMod;
61
- const { findPhaseInternal, listMilestonePhaseDirs } = phaseLocatorMod;
61
+ const { phaseDirectoryOptions, findPhaseInternal, listMilestonePhaseDirs } = phaseLocatorMod;
62
62
  const { planningDir } = planningWorkspaceMod;
63
63
  const CAPABILITY_ID = 'refactor-trigger';
64
64
  // Default CoreModule implementation. `_core` seam overrides this entirely
@@ -148,7 +148,7 @@ function requirePhaseArg(args, complexity, usage) {
148
148
  * REFACTOR_GIT_UNAVAILABLE, for this case.
149
149
  */
150
150
  function resolvePhaseDirForArg(cwd, phaseArg) {
151
- const search = findPhaseInternal(cwd, phaseArg);
151
+ const search = findPhaseInternal(cwd, phaseArg, undefined, phaseDirectoryOptions(cwd));
152
152
  if (!search || search.found !== true)
153
153
  return null;
154
154
  return { phaseDir: search.directory, padded: search.phase_number };
@@ -135,6 +135,8 @@ exports.REVIEWER_LANES = Object.freeze([
135
135
  evidenceClass: 'source-grounded',
136
136
  requiresBinaries: [],
137
137
  promptBudgetKey: 'review.max_prompt_tokens_per_reviewer.claude',
138
+ contextWindowTokens: 200000,
139
+ contextWindowConfigKey: 'review.context_window_tokens.claude',
138
140
  modelConfigKey: 'review.models.claude',
139
141
  effortConfigKey: 'review.effort.claude',
140
142
  defaultEffort: 'high',
@@ -166,6 +168,8 @@ exports.REVIEWER_LANES = Object.freeze([
166
168
  evidenceClass: 'source-grounded',
167
169
  requiresBinaries: [],
168
170
  promptBudgetKey: 'review.max_prompt_tokens_per_reviewer.codex',
171
+ contextWindowTokens: 272000,
172
+ contextWindowConfigKey: 'review.context_window_tokens.codex',
169
173
  modelConfigKey: 'review.models.codex',
170
174
  effortConfigKey: 'review.effort.codex',
171
175
  defaultEffort: 'high',
@@ -194,6 +198,8 @@ exports.REVIEWER_LANES = Object.freeze([
194
198
  evidenceClass: 'diff-only',
195
199
  requiresBinaries: [],
196
200
  promptBudgetKey: 'review.max_prompt_tokens_per_reviewer.coderabbit',
201
+ contextWindowTokens: null,
202
+ contextWindowConfigKey: null,
197
203
  // Accepts no model flag at all (review.md:367) — not merely "none configured".
198
204
  modelConfigKey: null,
199
205
  effortConfigKey: null,
@@ -224,6 +230,8 @@ exports.REVIEWER_LANES = Object.freeze([
224
230
  // stock Windows/Git-Bash (#2589) — is no longer a prerequisite for this lane.
225
231
  requiresBinaries: [],
226
232
  promptBudgetKey: 'review.max_prompt_tokens_per_reviewer.opencode',
233
+ contextWindowTokens: null,
234
+ contextWindowConfigKey: 'review.context_window_tokens.opencode',
227
235
  modelConfigKey: 'review.models.opencode',
228
236
  effortConfigKey: 'review.effort.opencode',
229
237
  defaultEffort: 'high',
@@ -250,6 +258,8 @@ exports.REVIEWER_LANES = Object.freeze([
250
258
  evidenceClass: 'source-grounded',
251
259
  requiresBinaries: [],
252
260
  promptBudgetKey: 'review.max_prompt_tokens_per_reviewer.qwen',
261
+ contextWindowTokens: 131072,
262
+ contextWindowConfigKey: 'review.context_window_tokens.qwen',
253
263
  modelConfigKey: null,
254
264
  effortConfigKey: null,
255
265
  defaultEffort: null,
@@ -278,6 +288,8 @@ exports.REVIEWER_LANES = Object.freeze([
278
288
  evidenceClass: 'source-grounded',
279
289
  requiresBinaries: [],
280
290
  promptBudgetKey: 'review.max_prompt_tokens_per_reviewer.cursor',
291
+ contextWindowTokens: null,
292
+ contextWindowConfigKey: 'review.context_window_tokens.cursor',
281
293
  // #3653: cursor-agent exposes --model (204 selectable models); wired the same as codex.
282
294
  modelConfigKey: 'review.models.cursor',
283
295
  effortConfigKey: null,
@@ -314,6 +326,8 @@ exports.REVIEWER_LANES = Object.freeze([
314
326
  // Phase 5b: the handler reads the transcript with JSON.parse per line, not `jq`.
315
327
  requiresBinaries: [],
316
328
  promptBudgetKey: 'review.max_prompt_tokens_per_reviewer.antigravity',
329
+ contextWindowTokens: null,
330
+ contextWindowConfigKey: 'review.context_window_tokens.antigravity',
317
331
  // NOT `review.models.antigravity` — the shipped key is `review.models.agy` (review.md:291) and
318
332
  // Phase 4 federated it under that name. This lane is why the key is declared, not derived.
319
333
  modelConfigKey: 'review.models.agy',
@@ -348,6 +362,8 @@ exports.REVIEWER_LANES = Object.freeze([
348
362
  // neither `jq` nor `curl` is a prerequisite any more (#2589's stated Windows hazard).
349
363
  requiresBinaries: [],
350
364
  promptBudgetKey: 'review.max_prompt_tokens_per_reviewer.ollama',
365
+ contextWindowTokens: null,
366
+ contextWindowConfigKey: 'review.context_window_tokens.ollama',
351
367
  modelConfigKey: 'review.models.ollama',
352
368
  effortConfigKey: null,
353
369
  defaultEffort: null,
@@ -378,6 +394,8 @@ exports.REVIEWER_LANES = Object.freeze([
378
394
  evidenceClass: 'source-grounded',
379
395
  requiresBinaries: [],
380
396
  promptBudgetKey: 'review.max_prompt_tokens_per_reviewer.lm_studio',
397
+ contextWindowTokens: null,
398
+ contextWindowConfigKey: 'review.context_window_tokens.lm_studio',
381
399
  modelConfigKey: 'review.models.lm_studio',
382
400
  effortConfigKey: null,
383
401
  defaultEffort: null,
@@ -408,6 +426,8 @@ exports.REVIEWER_LANES = Object.freeze([
408
426
  evidenceClass: 'source-grounded',
409
427
  requiresBinaries: [],
410
428
  promptBudgetKey: 'review.max_prompt_tokens_per_reviewer.llama_cpp',
429
+ contextWindowTokens: null,
430
+ contextWindowConfigKey: 'review.context_window_tokens.llama_cpp',
411
431
  modelConfigKey: 'review.models.llama_cpp',
412
432
  effortConfigKey: null,
413
433
  defaultEffort: null,
@@ -456,6 +476,8 @@ exports.REVIEWER_LANES = Object.freeze([
456
476
  evidenceClass: 'source-grounded',
457
477
  requiresBinaries: [],
458
478
  promptBudgetKey: 'review.max_prompt_tokens_per_reviewer.kimi-code',
479
+ contextWindowTokens: 262144,
480
+ contextWindowConfigKey: 'review.context_window_tokens.kimi-code',
459
481
  modelConfigKey: 'review.models.kimi-code',
460
482
  effortConfigKey: null,
461
483
  defaultEffort: null,