@opengsd/gsd-core 1.8.0 → 1.9.1
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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +31 -1
- package/agents/gsd-code-fixer.md +107 -34
- package/agents/gsd-codebase-mapper.md +1 -1
- package/agents/gsd-debug-session-manager.md +36 -0
- package/agents/gsd-executor.md +20 -7
- package/agents/gsd-intel-updater.md +3 -3
- package/agents/gsd-phase-researcher.md +4 -2
- package/agents/gsd-plan-checker.md +20 -0
- package/agents/gsd-planner.md +15 -23
- package/agents/gsd-project-researcher.md +2 -2
- package/agents/gsd-ui-auditor.md +0 -40
- package/bin/install.js +236 -107
- package/commands/gsd/plan-review-convergence.md +5 -1
- package/gsd-core/bin/gsd-tools.cjs +882 -4
- package/gsd-core/bin/lib/api-coverage.cjs +22 -8
- package/gsd-core/bin/lib/audit.cjs +8 -8
- package/gsd-core/bin/lib/capability-consent.cjs +40 -1
- package/gsd-core/bin/lib/capability-lifecycle.cjs +58 -0
- package/gsd-core/bin/lib/capability-loader.cjs +23 -1
- package/gsd-core/bin/lib/capability-registry.cjs +1353 -132
- package/gsd-core/bin/lib/capability-trust.cjs +468 -33
- package/gsd-core/bin/lib/capability-validator.cjs +882 -6
- package/gsd-core/bin/lib/check-command-router.cjs +12 -2
- package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +15 -0
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +102 -12
- package/gsd-core/bin/lib/claude-orchestration.cjs +125 -22
- package/gsd-core/bin/lib/commands.cjs +246 -18
- package/gsd-core/bin/lib/config-loader.cjs +200 -28
- package/gsd-core/bin/lib/config.cjs +90 -5
- package/gsd-core/bin/lib/estimate-cli.cjs +336 -0
- package/gsd-core/bin/lib/frontmatter.cjs +125 -15
- package/gsd-core/bin/lib/host-integration.cjs +215 -8
- package/gsd-core/bin/lib/init.cjs +44 -19
- package/gsd-core/bin/lib/install-engine.cjs +1 -0
- package/gsd-core/bin/lib/milestone.cjs +36 -9
- package/gsd-core/bin/lib/model-catalog.cjs +51 -1
- package/gsd-core/bin/lib/observability/logger.cjs +7 -2
- package/gsd-core/bin/lib/phase-command-router.cjs +10 -1
- package/gsd-core/bin/lib/phase-estimation.cjs +398 -0
- package/gsd-core/bin/lib/phase-id.cjs +278 -5
- package/gsd-core/bin/lib/phase.cjs +61 -6
- package/gsd-core/bin/lib/plan-drift-guard.cjs +1 -1
- package/gsd-core/bin/lib/plan-scan.cjs +1 -1
- package/gsd-core/bin/lib/planning-workspace.cjs +9 -2
- package/gsd-core/bin/lib/profile-output.cjs +34 -8
- package/gsd-core/bin/lib/project-root.cjs +48 -0
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +927 -0
- package/gsd-core/bin/lib/review-lane-invocation.cjs +348 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +594 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +114 -32
- package/gsd-core/bin/lib/roadmap-parser.cjs +54 -6
- package/gsd-core/bin/lib/roadmap.cjs +10 -4
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +31 -4
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +1 -1
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +140 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +15 -2
- package/gsd-core/bin/lib/smart-entry.cjs +1 -1
- package/gsd-core/bin/lib/state-document.cjs +164 -20
- package/gsd-core/bin/lib/state-transition.cjs +28 -10
- package/gsd-core/bin/lib/state.cjs +141 -21
- package/gsd-core/bin/lib/uat-predicate.cjs +6 -4
- package/gsd-core/bin/lib/uat.cjs +9 -7
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +2 -2
- package/gsd-core/bin/lib/unusable-input.cjs +216 -0
- package/gsd-core/bin/lib/validate.cjs +32 -0
- package/gsd-core/bin/lib/verification.cjs +51 -14
- package/gsd-core/bin/lib/verify.cjs +146 -22
- package/gsd-core/bin/lib/worktree-safety.cjs +360 -15
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -13
- package/gsd-core/bin/shared/model-catalog.json +5 -0
- package/gsd-core/bin/shared/runtime-aliases.manifest.json +5 -0
- package/gsd-core/references/context-budget.md +40 -0
- package/gsd-core/references/gate-prompts.md +6 -3
- package/gsd-core/references/model-profile-resolution.md +64 -13
- package/gsd-core/references/offer-next.md +88 -0
- package/gsd-core/references/planning-config.md +2 -1
- package/gsd-core/references/reviewer-instances.md +28 -21
- package/gsd-core/references/runtime-aware-dispatch.md +42 -0
- package/gsd-core/references/ui-consideration-probe.md +2 -2
- package/gsd-core/references/worktree-branch-check.md +4 -4
- package/gsd-core/templates/summary-minimal.md +4 -0
- package/gsd-core/templates/summary-standard.md +4 -0
- package/gsd-core/templates/summary.md +7 -0
- package/gsd-core/workflows/ai-integration-phase.md +4 -4
- package/gsd-core/workflows/audit-fix.md +4 -0
- package/gsd-core/workflows/audit-milestone.md +8 -0
- package/gsd-core/workflows/autonomous.md +19 -15
- package/gsd-core/workflows/check-todos.md +2 -2
- package/gsd-core/workflows/code-review-fix.md +14 -6
- package/gsd-core/workflows/code-review.md +93 -21
- package/gsd-core/workflows/debug.md +10 -2
- package/gsd-core/workflows/diagnose-issues.md +4 -0
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -4
- package/gsd-core/workflows/discuss-phase/modes/auto.md +0 -6
- package/gsd-core/workflows/discuss-phase-assumptions.md +15 -9
- package/gsd-core/workflows/discuss-phase.md +2 -2
- package/gsd-core/workflows/docs-update.md +8 -0
- package/gsd-core/workflows/eval-review.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +4 -0
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +160 -0
- package/gsd-core/workflows/execute-phase.md +85 -115
- package/gsd-core/workflows/execute-plan.md +5 -4
- package/gsd-core/workflows/explore.md +4 -0
- package/gsd-core/workflows/extract-learnings.md +21 -0
- package/gsd-core/workflows/help/modes/full.md +3 -3
- package/gsd-core/workflows/import.md +4 -1
- package/gsd-core/workflows/ingest-docs.md +4 -0
- package/gsd-core/workflows/map-codebase.md +13 -6
- package/gsd-core/workflows/new-milestone.md +10 -2
- package/gsd-core/workflows/new-project.md +11 -4
- package/gsd-core/workflows/next.md +5 -2
- package/gsd-core/workflows/plan-phase.md +42 -46
- package/gsd-core/workflows/plan-review-convergence.md +18 -14
- package/gsd-core/workflows/progress.md +1 -1
- package/gsd-core/workflows/quick.md +14 -3
- package/gsd-core/workflows/review.md +146 -575
- package/gsd-core/workflows/scan.md +9 -1
- package/gsd-core/workflows/secure-phase.md +10 -2
- package/gsd-core/workflows/ship.md +41 -11
- package/gsd-core/workflows/smart-entry.md +1 -1
- package/gsd-core/workflows/ui-phase.md +8 -1
- package/gsd-core/workflows/ui-review.md +8 -1
- package/gsd-core/workflows/update.md +104 -5
- package/gsd-core/workflows/validate-phase.md +10 -2
- package/gsd-core/workflows/verify-work.md +8 -1
- package/hooks/dist/gsd-cursor-session-start.js +6 -2
- package/hooks/dist/gsd-cursor-stop.js +6 -2
- package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
- package/hooks/dist/gsd-graphify-update.sh +9 -0
- package/hooks/dist/gsd-phase-boundary.sh +14 -2
- package/hooks/dist/gsd-prompt-guard.js +101 -2
- package/hooks/dist/gsd-read-guard.js +100 -2
- package/hooks/dist/gsd-read-injection-scanner.js +109 -2
- package/hooks/dist/gsd-statusline.js +9 -6
- package/hooks/dist/gsd-workflow-guard.js +110 -6
- package/hooks/dist/gsd-worktree-path-guard.js +132 -8
- package/hooks/dist/lib/cursor-workspace.js +74 -0
- package/hooks/gsd-cursor-session-start.js +6 -2
- package/hooks/gsd-cursor-stop.js +6 -2
- package/hooks/gsd-cursor-subagent-start.js +6 -2
- package/hooks/gsd-graphify-update.sh +9 -0
- package/hooks/gsd-phase-boundary.sh +14 -2
- package/hooks/gsd-prompt-guard.js +101 -2
- package/hooks/gsd-read-guard.js +100 -2
- package/hooks/gsd-read-injection-scanner.js +109 -2
- package/hooks/gsd-statusline.js +9 -6
- package/hooks/gsd-workflow-guard.js +110 -6
- package/hooks/gsd-worktree-path-guard.js +132 -8
- package/hooks/lib/cursor-workspace.js +74 -0
- package/package.json +7 -7
- package/pi/gsd.cjs +26 -1
- package/scripts/check-coverage-gate.cjs +51 -0
- package/scripts/check-glossary-refs.cjs +24 -0
- package/scripts/ci-test-scope.cjs +67 -17
- package/scripts/gen-adr-index.cjs +6 -4
- package/scripts/gen-capability-matrix.cjs +26 -2
- package/scripts/gen-capability-registry.cjs +132 -34
- package/scripts/gen-emitted-baseline.cjs +145 -0
- package/scripts/gen-registry.cjs +39 -15
- package/scripts/lint-compiled-artifact-sync.cjs +146 -0
- package/scripts/lint-emitted-drift-ack.cjs +149 -0
- package/scripts/lint-fix-has-regression-test.cjs +131 -0
- package/scripts/lint-resolution-provenance.cjs +9 -0
- package/scripts/mutation-matrix.cjs +4 -0
- package/scripts/prompt-injection-scan.sh +6 -0
- package/scripts/registry-schema.cjs +372 -94
- package/scripts/release-notes/conventional-title.cjs +19 -1
- package/scripts/release-notes/format-github-release-notes.cjs +7 -3
- package/scripts/validate-registry.cjs +10 -6
- package/scripts/workflow-size.cjs +16 -8
- package/skills/gsd-plan-review-convergence/SKILL.md +5 -1
- package/vscode/package.json +1 -1
- package/scripts/gen-golden-install-parity-zcode.cjs +0 -77
- 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
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
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
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
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
|
-
*
|
|
55
|
-
*
|
|
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
|
|
58
|
-
|
|
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
|
|
75
|
-
if (
|
|
76
|
-
return
|
|
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
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|