@opengsd/gsd-core 1.8.0 → 1.9.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 (174) 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 +31 -1
  4. package/agents/gsd-code-fixer.md +1 -1
  5. package/agents/gsd-codebase-mapper.md +1 -1
  6. package/agents/gsd-debug-session-manager.md +36 -0
  7. package/agents/gsd-executor.md +20 -7
  8. package/agents/gsd-intel-updater.md +3 -3
  9. package/agents/gsd-phase-researcher.md +4 -2
  10. package/agents/gsd-plan-checker.md +20 -0
  11. package/agents/gsd-planner.md +15 -23
  12. package/agents/gsd-project-researcher.md +2 -2
  13. package/agents/gsd-ui-auditor.md +0 -40
  14. package/bin/install.js +186 -55
  15. package/commands/gsd/plan-review-convergence.md +5 -1
  16. package/gsd-core/bin/gsd-tools.cjs +849 -2
  17. package/gsd-core/bin/lib/api-coverage.cjs +22 -8
  18. package/gsd-core/bin/lib/audit.cjs +8 -8
  19. package/gsd-core/bin/lib/capability-consent.cjs +40 -1
  20. package/gsd-core/bin/lib/capability-lifecycle.cjs +58 -0
  21. package/gsd-core/bin/lib/capability-loader.cjs +23 -1
  22. package/gsd-core/bin/lib/capability-registry.cjs +1353 -132
  23. package/gsd-core/bin/lib/capability-trust.cjs +468 -33
  24. package/gsd-core/bin/lib/capability-validator.cjs +882 -6
  25. package/gsd-core/bin/lib/check-command-router.cjs +12 -2
  26. package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +15 -0
  27. package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +102 -12
  28. package/gsd-core/bin/lib/claude-orchestration.cjs +125 -22
  29. package/gsd-core/bin/lib/commands.cjs +246 -18
  30. package/gsd-core/bin/lib/config-loader.cjs +200 -28
  31. package/gsd-core/bin/lib/config.cjs +90 -5
  32. package/gsd-core/bin/lib/estimate-cli.cjs +336 -0
  33. package/gsd-core/bin/lib/frontmatter.cjs +125 -15
  34. package/gsd-core/bin/lib/host-integration.cjs +215 -8
  35. package/gsd-core/bin/lib/init.cjs +44 -19
  36. package/gsd-core/bin/lib/install-engine.cjs +1 -0
  37. package/gsd-core/bin/lib/milestone.cjs +5 -5
  38. package/gsd-core/bin/lib/model-catalog.cjs +51 -1
  39. package/gsd-core/bin/lib/observability/logger.cjs +7 -2
  40. package/gsd-core/bin/lib/phase-command-router.cjs +10 -1
  41. package/gsd-core/bin/lib/phase-estimation.cjs +398 -0
  42. package/gsd-core/bin/lib/phase-id.cjs +278 -5
  43. package/gsd-core/bin/lib/phase.cjs +57 -5
  44. package/gsd-core/bin/lib/plan-drift-guard.cjs +1 -1
  45. package/gsd-core/bin/lib/plan-scan.cjs +1 -1
  46. package/gsd-core/bin/lib/planning-workspace.cjs +9 -2
  47. package/gsd-core/bin/lib/profile-output.cjs +34 -8
  48. package/gsd-core/bin/lib/review-lane-descriptor.cjs +927 -0
  49. package/gsd-core/bin/lib/review-lane-invocation.cjs +348 -0
  50. package/gsd-core/bin/lib/review-lane-runner.cjs +594 -0
  51. package/gsd-core/bin/lib/review-reviewer-selection.cjs +114 -32
  52. package/gsd-core/bin/lib/roadmap-parser.cjs +54 -6
  53. package/gsd-core/bin/lib/roadmap.cjs +10 -4
  54. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +31 -4
  55. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +1 -1
  56. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +140 -0
  57. package/gsd-core/bin/lib/runtime-name-policy.cjs +15 -2
  58. package/gsd-core/bin/lib/smart-entry.cjs +1 -1
  59. package/gsd-core/bin/lib/state-document.cjs +164 -20
  60. package/gsd-core/bin/lib/state-transition.cjs +28 -10
  61. package/gsd-core/bin/lib/state.cjs +141 -21
  62. package/gsd-core/bin/lib/uat-predicate.cjs +6 -4
  63. package/gsd-core/bin/lib/uat.cjs +9 -7
  64. package/gsd-core/bin/lib/ui-consideration-probe.cjs +2 -2
  65. package/gsd-core/bin/lib/unusable-input.cjs +216 -0
  66. package/gsd-core/bin/lib/validate.cjs +32 -0
  67. package/gsd-core/bin/lib/verification.cjs +51 -14
  68. package/gsd-core/bin/lib/verify.cjs +128 -20
  69. package/gsd-core/bin/lib/worktree-safety.cjs +360 -15
  70. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  71. package/gsd-core/bin/shared/config-schema.manifest.json +1 -13
  72. package/gsd-core/bin/shared/model-catalog.json +5 -0
  73. package/gsd-core/bin/shared/runtime-aliases.manifest.json +5 -0
  74. package/gsd-core/references/context-budget.md +40 -0
  75. package/gsd-core/references/gate-prompts.md +6 -3
  76. package/gsd-core/references/model-profile-resolution.md +64 -13
  77. package/gsd-core/references/offer-next.md +88 -0
  78. package/gsd-core/references/planning-config.md +2 -1
  79. package/gsd-core/references/reviewer-instances.md +28 -21
  80. package/gsd-core/references/runtime-aware-dispatch.md +42 -0
  81. package/gsd-core/references/ui-consideration-probe.md +2 -2
  82. package/gsd-core/references/worktree-branch-check.md +4 -4
  83. package/gsd-core/templates/summary-minimal.md +4 -0
  84. package/gsd-core/templates/summary-standard.md +4 -0
  85. package/gsd-core/templates/summary.md +7 -0
  86. package/gsd-core/workflows/ai-integration-phase.md +4 -4
  87. package/gsd-core/workflows/audit-fix.md +4 -0
  88. package/gsd-core/workflows/audit-milestone.md +8 -0
  89. package/gsd-core/workflows/autonomous.md +19 -15
  90. package/gsd-core/workflows/check-todos.md +2 -2
  91. package/gsd-core/workflows/code-review-fix.md +14 -6
  92. package/gsd-core/workflows/code-review.md +76 -19
  93. package/gsd-core/workflows/debug.md +10 -2
  94. package/gsd-core/workflows/diagnose-issues.md +4 -0
  95. package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -4
  96. package/gsd-core/workflows/discuss-phase/modes/auto.md +0 -6
  97. package/gsd-core/workflows/discuss-phase-assumptions.md +15 -9
  98. package/gsd-core/workflows/discuss-phase.md +2 -2
  99. package/gsd-core/workflows/docs-update.md +8 -0
  100. package/gsd-core/workflows/eval-review.md +1 -1
  101. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +4 -0
  102. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +160 -0
  103. package/gsd-core/workflows/execute-phase.md +85 -115
  104. package/gsd-core/workflows/execute-plan.md +5 -4
  105. package/gsd-core/workflows/explore.md +4 -0
  106. package/gsd-core/workflows/extract-learnings.md +21 -0
  107. package/gsd-core/workflows/help/modes/full.md +3 -3
  108. package/gsd-core/workflows/import.md +4 -1
  109. package/gsd-core/workflows/ingest-docs.md +4 -0
  110. package/gsd-core/workflows/map-codebase.md +13 -6
  111. package/gsd-core/workflows/new-milestone.md +10 -2
  112. package/gsd-core/workflows/new-project.md +11 -4
  113. package/gsd-core/workflows/next.md +5 -2
  114. package/gsd-core/workflows/plan-phase.md +42 -46
  115. package/gsd-core/workflows/plan-review-convergence.md +18 -14
  116. package/gsd-core/workflows/progress.md +1 -1
  117. package/gsd-core/workflows/quick.md +14 -3
  118. package/gsd-core/workflows/review.md +146 -575
  119. package/gsd-core/workflows/scan.md +9 -1
  120. package/gsd-core/workflows/secure-phase.md +10 -2
  121. package/gsd-core/workflows/ship.md +41 -11
  122. package/gsd-core/workflows/smart-entry.md +1 -1
  123. package/gsd-core/workflows/ui-phase.md +8 -1
  124. package/gsd-core/workflows/ui-review.md +8 -1
  125. package/gsd-core/workflows/update.md +104 -5
  126. package/gsd-core/workflows/validate-phase.md +10 -2
  127. package/gsd-core/workflows/verify-work.md +8 -1
  128. package/hooks/dist/gsd-cursor-session-start.js +6 -2
  129. package/hooks/dist/gsd-cursor-stop.js +6 -2
  130. package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
  131. package/hooks/dist/gsd-graphify-update.sh +9 -0
  132. package/hooks/dist/gsd-phase-boundary.sh +14 -2
  133. package/hooks/dist/gsd-prompt-guard.js +101 -2
  134. package/hooks/dist/gsd-read-guard.js +100 -2
  135. package/hooks/dist/gsd-read-injection-scanner.js +109 -2
  136. package/hooks/dist/gsd-statusline.js +9 -6
  137. package/hooks/dist/gsd-workflow-guard.js +110 -6
  138. package/hooks/dist/gsd-worktree-path-guard.js +132 -8
  139. package/hooks/dist/lib/cursor-workspace.js +74 -0
  140. package/hooks/gsd-cursor-session-start.js +6 -2
  141. package/hooks/gsd-cursor-stop.js +6 -2
  142. package/hooks/gsd-cursor-subagent-start.js +6 -2
  143. package/hooks/gsd-graphify-update.sh +9 -0
  144. package/hooks/gsd-phase-boundary.sh +14 -2
  145. package/hooks/gsd-prompt-guard.js +101 -2
  146. package/hooks/gsd-read-guard.js +100 -2
  147. package/hooks/gsd-read-injection-scanner.js +109 -2
  148. package/hooks/gsd-statusline.js +9 -6
  149. package/hooks/gsd-workflow-guard.js +110 -6
  150. package/hooks/gsd-worktree-path-guard.js +132 -8
  151. package/hooks/lib/cursor-workspace.js +74 -0
  152. package/package.json +7 -7
  153. package/pi/gsd.cjs +26 -1
  154. package/scripts/check-coverage-gate.cjs +51 -0
  155. package/scripts/check-glossary-refs.cjs +24 -0
  156. package/scripts/ci-test-scope.cjs +67 -17
  157. package/scripts/gen-adr-index.cjs +6 -4
  158. package/scripts/gen-capability-matrix.cjs +26 -2
  159. package/scripts/gen-capability-registry.cjs +132 -34
  160. package/scripts/gen-emitted-baseline.cjs +145 -0
  161. package/scripts/lint-compiled-artifact-sync.cjs +146 -0
  162. package/scripts/lint-emitted-drift-ack.cjs +149 -0
  163. package/scripts/lint-fix-has-regression-test.cjs +131 -0
  164. package/scripts/lint-resolution-provenance.cjs +9 -0
  165. package/scripts/mutation-matrix.cjs +4 -0
  166. package/scripts/prompt-injection-scan.sh +6 -0
  167. package/scripts/registry-schema.cjs +57 -8
  168. package/scripts/release-notes/conventional-title.cjs +19 -1
  169. package/scripts/release-notes/format-github-release-notes.cjs +7 -3
  170. package/scripts/workflow-size.cjs +16 -8
  171. package/skills/gsd-plan-review-convergence/SKILL.md +5 -1
  172. package/vscode/package.json +1 -1
  173. package/scripts/gen-golden-install-parity-zcode.cjs +0 -77
  174. package/scripts/update-size-baseline.cjs +0 -68
@@ -18,6 +18,7 @@ exports.shouldPreserveExistingProgress = shouldPreserveExistingProgress;
18
18
  exports.normalizeProgressNumbers = normalizeProgressNumbers;
19
19
  exports.isStateTemplateDefault = isStateTemplateDefault;
20
20
  exports.stateReplaceFieldIfTemplate = stateReplaceFieldIfTemplate;
21
+ const markdown_table_cjs_1 = require("./markdown-table.cjs");
21
22
  // Internal helpers
22
23
  function escapeRegex(str) {
23
24
  return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -40,22 +41,167 @@ function isTableSeparatorRow(firstCell) {
40
41
  // A separator cell contains only dashes, colons (alignment hints), and whitespace.
41
42
  return /^[\s\-:]+$/.test(firstCell.trim());
42
43
  }
44
+ function countLeading(str) {
45
+ const match = /^[ \t]*/.exec(str);
46
+ return match ? match[0].length : 0;
47
+ }
48
+ /**
49
+ * Canonicalize one UTF-16 code unit per the ECMAScript non-unicode
50
+ * `Canonicalize` abstract operation, which governs how a case-insensitive
51
+ * (`/i`, no `u` flag) RegExp compares characters: take `ch.toUpperCase()`.
52
+ * The uppercasing is REJECTED (the original character is kept as-is) in
53
+ * either of two cases: (1) `ch.toUpperCase()` does not produce exactly one
54
+ * character (e.g. "ß" -> "SS" — a multi-character case-fold can never be a
55
+ * per-character regex match, so Canonicalize leaves it alone), or (2) it
56
+ * produces exactly one character but the original character's code point is
57
+ * >= 128 while the uppercased character's code point is < 128 (this is what
58
+ * stops a non-ASCII character from folding onto an ASCII one under `/i` —
59
+ * e.g. KELVIN SIGN U+212A uppercases to ASCII "K" (U+004B), so this rule
60
+ * rejects the fold and keeps U+212A, meaning `/k/i`/`/K/i` do NOT match
61
+ * U+212A). Otherwise, the uppercased character is used. Plain
62
+ * `.toLowerCase()`/`.toUpperCase()` folds both of these cases, which is
63
+ * exactly why they diverge from real regex `/i` semantics.
64
+ */
65
+ function canonicalizeCharForCaselessCompare(ch) {
66
+ const upper = ch.toUpperCase();
67
+ if (upper.length !== 1) {
68
+ return ch;
69
+ }
70
+ if (ch.charCodeAt(0) >= 128 && upper.charCodeAt(0) < 128) {
71
+ return ch;
72
+ }
73
+ return upper;
74
+ }
75
+ /**
76
+ * Canonicalize a whole string, one UTF-16 code unit at a time, per the
77
+ * ECMAScript non-unicode `Canonicalize` rule (see
78
+ * canonicalizeCharForCaselessCompare) so that two strings compare equal
79
+ * under this function iff a non-`u`-flag `/i` RegExp would treat them as
80
+ * the same literal text. This is the correct replacement for
81
+ * `.toLowerCase()` when replicating a non-`u` `/i` regex: `.toLowerCase()`
82
+ * folds some non-ASCII characters (e.g. KELVIN SIGN U+212A) onto their
83
+ * ASCII counterparts, which real `/i` regex semantics do not. Iteration is
84
+ * by UTF-16 code unit (not code point) to match how a non-`u` regex engine
85
+ * itself operates on surrogate halves individually.
86
+ */
87
+ function canonicalizeForCaselessCompare(str) {
88
+ let result = '';
89
+ for (let i = 0; i < str.length; i++) {
90
+ result += canonicalizeCharForCaselessCompare(str[i]);
91
+ }
92
+ return result;
93
+ }
43
94
  /**
44
- * Build a regex that matches a pipe-table row `| FieldName | value |` for the
45
- * given (already-escaped) field name. The match is case-insensitive and
46
- * tolerates variable amounts of whitespace around the cell contents.
95
+ * Return true when the caller's raw (untrimmed) `fieldName` may be considered
96
+ * to match a row's raw (untrimmed) field cell text. Faithfully replicates the
97
+ * backtracking of the regex this function replaced: `^(\|[ \t]*)(FieldName)
98
+ * ([ \t]*\|...)`. Group 1 (`\|[ \t]*`, greedy but backtrackable) can hand any
99
+ * PREFIX of the cell's leading `[ \t]` run over to group 2 (the literal,
100
+ * case-insensitive `fieldName` text) — so `fieldName` is tried at every offset
101
+ * `j` from 0 up to the length of that leading run. For a given `j` to be a
102
+ * genuine match, two things must hold: `rawCell.slice(j, j + fieldName.length)`
103
+ * must equal `fieldName` case-insensitively (group 2), AND everything left
104
+ * over after it — `rawCell.slice(j + fieldName.length)` — must be entirely
105
+ * `[ \t]` characters, because group 3 (`[ \t]*\|`) must consume that leftover
106
+ * as whitespace before it can reach the delimiter pipe.
47
107
  *
48
- * Capture group 1: leading pipe + whitespace before the field cell
49
- * Capture group 2: the field name cell text (trimmed)
50
- * Capture group 3: whitespace between field cell and separator pipe
51
- * Capture group 4: the value cell text (trimmed)
52
- * Capture group 5: trailing whitespace + closing pipe(s)
108
+ * A simple count-of-leading/trailing-whitespace comparison is NOT equivalent:
109
+ * it ignores that group 2 is a literal-character match, not a whitespace-
110
+ * class match, so it can produce false positives whenever `fieldName`'s own
111
+ * padding is a different run of `[ \t]` characters than the cell's (e.g.
112
+ * `fieldName` padded with spaces against a cell padded with tabs) — caught by
113
+ * differential fuzzing against the regex this replaces.
53
114
  *
54
- * We use a single-line match (`m` flag so ^ anchors work on each line) to
55
- * avoid cross-row replacement.
115
+ * The case-insensitive comparison itself is done via
116
+ * canonicalizeForCaselessCompare, NOT `.toLowerCase()`: the replaced regex
117
+ * used `/i` WITHOUT the `u` flag, whose case-folding is the ECMAScript
118
+ * non-unicode `Canonicalize` operation. `.toLowerCase()` folds some non-ASCII
119
+ * characters onto ASCII ones (e.g. KELVIN SIGN U+212A -> "k") that `/i`
120
+ * (no `u`) does NOT fold, so `.toLowerCase()` alone would NOT faithfully
121
+ * replicate the old regex's semantics; canonicalizeForCaselessCompare does.
56
122
  */
57
- function tableRowPattern(escapedFieldName) {
58
- return new RegExp(`^(\\|[ \\t]*)(${escapedFieldName})([ \\t]*\\|[ \\t]*)([^|\\n]*?)([ \\t]*\\|[ \\t]*)$`, 'im');
123
+ function fieldNameMatchesRawCell(fieldName, rawCell) {
124
+ const n = fieldName.length;
125
+ const cellLength = rawCell.length;
126
+ if (n > cellLength)
127
+ return false;
128
+ const leadingRun = countLeading(rawCell);
129
+ const maxOffset = Math.min(leadingRun, cellLength - n);
130
+ const canonicalFieldName = canonicalizeForCaselessCompare(fieldName);
131
+ for (let j = 0; j <= maxOffset; j++) {
132
+ if (canonicalizeForCaselessCompare(rawCell.slice(j, j + n)) !== canonicalFieldName)
133
+ continue;
134
+ if (/^[ \t]*$/.test(rawCell.slice(j + n)))
135
+ return true;
136
+ }
137
+ return false;
138
+ }
139
+ /**
140
+ * Locate the value cell of a pipe-table row `| FieldName | value |` for the
141
+ * given field name, by scanning `content` line by line (no whole-document
142
+ * regex). Only a strict two-column row (exactly 3 `|` chars, starting the
143
+ * line, ending the line after trailing space/tab is stripped) is considered;
144
+ * this is what makes a 3-column row or an unescaped-pipe-bearing value cell
145
+ * fail to match, mirroring the previous regex's behaviour. Separator rows
146
+ * (`| --- | --- |`) are skipped, not matched. The match is case-insensitive.
147
+ * A line terminator is `\r\n`, a lone `\r`, or a lone `\n` — matching the `m`
148
+ * flag semantics of the regex this function replaced. Returns the byte range
149
+ * of the value cell (after trimming surrounding space/tab) so the caller can
150
+ * splice it directly.
151
+ */
152
+ function locateFieldRow(content, fieldName) {
153
+ let lineStart = 0;
154
+ while (lineStart <= content.length) {
155
+ // A line terminator is `\r\n`, a lone `\r`, or a lone `\n` (JS treats a
156
+ // bare `\r` as a line terminator too — the regex this replaced used the
157
+ // `m` flag, which honors all three). Scan for whichever of `\r`/`\n`
158
+ // occurs first; if it's `\r` immediately followed by `\n`, the terminator
159
+ // is 2 chars wide, otherwise 1.
160
+ let terminatorIndex = -1;
161
+ let terminatorLength = 0;
162
+ for (let i = lineStart; i < content.length; i++) {
163
+ const ch = content[i];
164
+ if (ch === '\n') {
165
+ terminatorIndex = i;
166
+ terminatorLength = 1;
167
+ break;
168
+ }
169
+ if (ch === '\r') {
170
+ terminatorIndex = i;
171
+ terminatorLength = content[i + 1] === '\n' ? 2 : 1;
172
+ break;
173
+ }
174
+ }
175
+ const lineEnd = terminatorIndex === -1 ? content.length : terminatorIndex;
176
+ const line = content.slice(lineStart, lineEnd);
177
+ if (line.startsWith('|')) {
178
+ const pipeCount = (line.match(/\|/g) || []).length;
179
+ const trimmedEnd = line.replace(/[ \t]+$/, '');
180
+ if (pipeCount === 3 && trimmedEnd.endsWith('|')) {
181
+ const cells = (0, markdown_table_cjs_1.splitTableRow)(line);
182
+ if (cells.length === 2 && !isTableSeparatorRow(cells[0])) {
183
+ // Line has exactly 3 pipes (enforced above): opening pipe, the
184
+ // field/value separator pipe, and the row-closing pipe.
185
+ const fieldValueSeparatorPipe = line.indexOf('|', line.indexOf('|') + 1);
186
+ const rawCell = line.slice(1, fieldValueSeparatorPipe);
187
+ if (fieldNameMatchesRawCell(fieldName, rawCell)) {
188
+ const rowClosingPipe = line.indexOf('|', fieldValueSeparatorPipe + 1);
189
+ let valueStart = lineStart + fieldValueSeparatorPipe + 1;
190
+ while (content[valueStart] === ' ' || content[valueStart] === '\t')
191
+ valueStart++;
192
+ let valueEnd = lineStart + rowClosingPipe;
193
+ while (valueEnd - 1 >= valueStart && (content[valueEnd - 1] === ' ' || content[valueEnd - 1] === '\t'))
194
+ valueEnd--;
195
+ return { valueStart, valueEnd, rawValue: content.slice(valueStart, valueEnd) };
196
+ }
197
+ }
198
+ }
199
+ }
200
+ if (terminatorIndex === -1)
201
+ break;
202
+ lineStart = terminatorIndex + terminatorLength;
203
+ }
204
+ return null;
59
205
  }
60
206
  function stateExtractField(content, fieldName) {
61
207
  const escaped = escapeRegex(fieldName);
@@ -71,9 +217,9 @@ function stateExtractField(content, fieldName) {
71
217
  return plainMatch[1].trim();
72
218
  // Pipe-table format: | FieldName | value |
73
219
  // (Separator rows such as `| --- | --- |` are excluded.)
74
- const tableMatch = content.match(tableRowPattern(escaped));
75
- if (tableMatch && !isTableSeparatorRow(tableMatch[2]))
76
- return tableMatch[4].trim();
220
+ const hit = locateFieldRow(content, fieldName);
221
+ if (hit)
222
+ return hit.rawValue.trim();
77
223
  return null;
78
224
  }
79
225
  function stateReplaceField(content, fieldName, newValue) {
@@ -90,11 +236,9 @@ function stateReplaceField(content, fieldName, newValue) {
90
236
  }
91
237
  // Pipe-table format: | FieldName | value |
92
238
  // Preserve the surrounding pipe/whitespace structure; only swap the value cell.
93
- const tblPat = tableRowPattern(escaped);
94
- const tblMatch = content.match(tblPat);
95
- if (tblMatch && !isTableSeparatorRow(tblMatch[2])) {
96
- // Reconstruct the row, preserving the original surrounding whitespace/pipes.
97
- return content.replace(tblPat, (_m, leadPipe, fieldCell, midPipe, _oldVal, trailPipe) => `${leadPipe}${fieldCell}${midPipe}${newValue}${trailPipe}`);
239
+ const hit = locateFieldRow(content, fieldName);
240
+ if (hit) {
241
+ return content.slice(0, hit.valueStart) + newValue + content.slice(hit.valueEnd);
98
242
  }
99
243
  return null;
100
244
  }
@@ -241,7 +241,7 @@ function beginPhaseCore(content, intent, deps) {
241
241
  // #1255: body-field replacements operate on body only (frontmatter stripped),
242
242
  // not on the full content. The YAML `status:` key matches `^Status:\s*`
243
243
  // before the body pipe-table row if full content is passed.
244
- const existingFm = extractFrontmatter(content);
244
+ const existingFm = extractFrontmatter(content, deps.sourcePath);
245
245
  const hasFrontmatter = Object.keys(existingFm).length > 0;
246
246
  let body = stripFrontmatter(content);
247
247
  const reassemble = (b) => hasFrontmatter
@@ -314,7 +314,11 @@ function beginPhaseCore(content, intent, deps) {
314
314
  // (do not touch Plan:, Phase:, Status:, stopped_at, progress.percent).
315
315
  body = mutateCurrentPositionResume(body, intent, today, updated);
316
316
  }
317
- return { content: reassemble(body), updated };
317
+ // #2736: surface the #3127 resume decision so the adapter can drop its
318
+ // intent-first current_phase_name override on a resume — the core just
319
+ // preserved the mid-flight name, and an override would drift frontmatter
320
+ // away from the preserved body value.
321
+ return { content: reassemble(body), updated, data: { resumed: isAlreadyExecuting } };
318
322
  }
319
323
  /**
320
324
  * Find the `## Current Position` section, return its `{start, end}` byte
@@ -523,7 +527,7 @@ function advancePlanCore(content, deps) {
523
527
  // not on the full content. The YAML `status:` key matches `^Status:\s*`
524
528
  // before the body field if full content is passed (codex Phase 2 review:
525
529
  // HIGH blocking finding — same pattern beginPhaseCore already handles).
526
- const existingFm = extractFrontmatter(content);
530
+ const existingFm = extractFrontmatter(content, deps.sourcePath);
527
531
  const hasFrontmatter = Object.keys(existingFm).length > 0;
528
532
  let body = stripFrontmatter(content);
529
533
  const reassemble = (b) => hasFrontmatter
@@ -645,7 +649,7 @@ function completePhaseCore(content, intent, deps) {
645
649
  }
646
650
  // #1255: body-field replacements operate on body only (frontmatter stripped),
647
651
  // so the YAML `status:` / `current_phase:` keys cannot shadow the body fields.
648
- const existingFm = extractFrontmatter(content);
652
+ const existingFm = extractFrontmatter(content, deps.sourcePath);
649
653
  const hasFrontmatter = Object.keys(existingFm).length > 0;
650
654
  let body = stripFrontmatter(content);
651
655
  const reassemble = (b) => hasFrontmatter
@@ -789,7 +793,7 @@ function plannedPhaseCore(content, intent, deps) {
789
793
  }
790
794
  }
791
795
  // #1255: body-field replacements operate on body only.
792
- const existingFm = extractFrontmatter(content);
796
+ const existingFm = extractFrontmatter(content, deps.sourcePath);
793
797
  const hasFrontmatter = Object.keys(existingFm).length > 0;
794
798
  let body = stripFrontmatter(content);
795
799
  const reassemble = (b) => hasFrontmatter
@@ -831,6 +835,18 @@ function plannedPhaseCore(content, intent, deps) {
831
835
  }, statusDefaults, lastActivityDefaults);
832
836
  if (body !== beforePos)
833
837
  updated.push('Current Position');
838
+ // #2400 Bug B: sync progress.total_plans to the frontmatter when a plan count
839
+ // is given. This writes the explicitly-provided count — it is NOT a re-derivation
840
+ // from disk (#500 RC1 is about deriving from a half-planned snapshot, not about
841
+ // refusing to write an explicitly-passed argument).
842
+ if (intent.planCount !== null && intent.planCount !== undefined && hasFrontmatter) {
843
+ const fmProgress = existingFm['progress'] || {};
844
+ if (fmProgress['total_plans'] !== intent.planCount) {
845
+ fmProgress['total_plans'] = intent.planCount;
846
+ existingFm['progress'] = fmProgress;
847
+ updated.push('progress.total_plans');
848
+ }
849
+ }
834
850
  return { content: reassemble(body), updated };
835
851
  }
836
852
  // ----------------------------------------------------------------------------
@@ -868,7 +884,7 @@ function milestoneSwitchCore(content, intent, deps) {
868
884
  'progress',
869
885
  'Current Position',
870
886
  ];
871
- const existingFm = extractFrontmatter(content);
887
+ const existingFm = extractFrontmatter(content, deps.sourcePath);
872
888
  const body = stripFrontmatter(content);
873
889
  const resolvedName = (intent.name && intent.name.trim()) || 'milestone';
874
890
  // ## Current Position reset body (mirrors state.cts:2371-2375).
@@ -1013,7 +1029,7 @@ function milestoneCompleteCore(content, intent, deps) {
1013
1029
  }
1014
1030
  }
1015
1031
  // #1255: body-field replacements operate on body only.
1016
- const existingFm = extractFrontmatter(content);
1032
+ const existingFm = extractFrontmatter(content, deps.sourcePath);
1017
1033
  const hasFrontmatter = Object.keys(existingFm).length > 0;
1018
1034
  let body = stripFrontmatter(content);
1019
1035
  const reassemble = (b) => hasFrontmatter
@@ -1339,7 +1355,9 @@ function rebuildCore(content, _intent, deps) {
1339
1355
  let modified = content;
1340
1356
  // §2 Decision: re-derive derived sections, preserve others. Order is
1341
1357
  // oldest-section-first so log entries appear in body order.
1342
- modified = reconcileCurrentPosition(modified, timestamp, log);
1358
+ // sourcePath threaded so `state rebuild --dry-run` names the file: that branch reads STATE.md
1359
+ // directly rather than through readModifyWriteStateMd, so nothing upstream has named it yet.
1360
+ modified = reconcileCurrentPosition(modified, timestamp, log, deps.sourcePath);
1343
1361
  modified = reconcileByPhaseTable(modified, deps, timestamp, log);
1344
1362
  modified = stripTemplatePlaceholders(modified, timestamp, log);
1345
1363
  modified = deduplicateSessionArchive(modified, timestamp, log);
@@ -1373,8 +1391,8 @@ function rebuildCore(content, _intent, deps) {
1373
1391
  * the key (Leaky-Abstractions guard — don't synthesize values the canonical
1374
1392
  * source doesn't have).
1375
1393
  */
1376
- function reconcileCurrentPosition(content, timestamp, log) {
1377
- const fm = extractFrontmatter(content);
1394
+ function reconcileCurrentPosition(content, timestamp, log, sourcePath) {
1395
+ const fm = extractFrontmatter(content, sourcePath);
1378
1396
  if (!fm || typeof fm !== 'object')
1379
1397
  return content;
1380
1398
  let modified = content;