@opengsd/gsd-core 1.7.0-rc.6 → 1.8.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +14 -0
- package/README.md +2 -0
- package/agents/gsd-debug-session-manager.md +42 -4
- package/agents/gsd-debugger.md +87 -29
- package/agents/gsd-executor.md +31 -3
- package/agents/gsd-planner.md +29 -36
- package/agents/gsd-security-auditor.md +13 -15
- package/agents/gsd-verifier.md +2 -2
- package/bin/install.js +1157 -84
- package/commands/gsd/ai-integration-phase.md +1 -1
- package/commands/gsd/mempalace-capture.md +31 -1
- package/commands/gsd/new-milestone.md +1 -1
- package/commands/gsd/plan-phase.md +5 -3
- package/commands/gsd/plan-review-convergence.md +3 -2
- package/commands/gsd/surface.md +6 -6
- package/gsd-core/bin/gsd-tools.cjs +1866 -2434
- package/gsd-core/bin/lib/adapter-imperative.cjs +8 -1
- package/gsd-core/bin/lib/agent-command-router.cjs +20 -5
- package/gsd-core/bin/lib/api-coverage.cjs +341 -49
- package/gsd-core/bin/lib/audit.cjs +7 -6
- package/gsd-core/bin/lib/broken-windows.cjs +716 -0
- package/gsd-core/bin/lib/capability-command-router.cjs +733 -0
- package/gsd-core/bin/lib/capability-registry.cjs +157 -88
- package/gsd-core/bin/lib/capability-writer.cjs +6 -1
- package/gsd-core/bin/lib/check-command-router.cjs +129 -26
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +115 -27
- package/gsd-core/bin/lib/claude-orchestration.cjs +84 -9
- package/gsd-core/bin/lib/clock.cjs +19 -0
- package/gsd-core/bin/lib/command-aliases.cjs +14 -0
- package/gsd-core/bin/lib/commands.cjs +129 -13
- package/gsd-core/bin/lib/config-loader.cjs +20 -4
- package/gsd-core/bin/lib/config.cjs +81 -18
- package/gsd-core/bin/lib/core-utils.cjs +14 -3
- package/gsd-core/bin/lib/decisions.cjs +32 -8
- package/gsd-core/bin/lib/docs.cjs +6 -0
- package/gsd-core/bin/lib/drift.cjs +4 -4
- package/gsd-core/bin/lib/external-descriptor-trust.cjs +14 -2
- package/gsd-core/bin/lib/frontmatter.cjs +22 -0
- package/gsd-core/bin/lib/gap-checker.cjs +17 -2
- package/gsd-core/bin/lib/gsd2-import.cjs +2 -1
- package/gsd-core/bin/lib/init.cjs +138 -60
- package/gsd-core/bin/lib/install-engine.cjs +301 -25
- package/gsd-core/bin/lib/install-profiles.cjs +239 -1
- package/gsd-core/bin/lib/installer-migration-authoring.cjs +2 -1
- package/gsd-core/bin/lib/installer-migrations/005-opencode-baseline-commands-dir.cjs +146 -0
- package/gsd-core/bin/lib/installer-migrations/006-pi-extension-cjs-to-js.cjs +91 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +45 -6
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +449 -0
- package/gsd-core/bin/lib/markdown-table.cjs +698 -0
- package/gsd-core/bin/lib/milestone.cjs +463 -43
- package/gsd-core/bin/lib/model-catalog.cjs +19 -4
- package/gsd-core/bin/lib/model-resolver.cjs +189 -7
- package/gsd-core/bin/lib/onboard-projection.cjs +11 -8
- package/gsd-core/bin/lib/phase-command-router.cjs +50 -2
- package/gsd-core/bin/lib/phase-id.cjs +26 -4
- package/gsd-core/bin/lib/phase-lifecycle.cjs +62 -36
- package/gsd-core/bin/lib/phase-locator.cjs +23 -2
- package/gsd-core/bin/lib/phase.cjs +636 -72
- package/gsd-core/bin/lib/plan-scan.cjs +73 -2
- package/gsd-core/bin/lib/roadmap-parser.cjs +225 -17
- package/gsd-core/bin/lib/roadmap.cjs +113 -52
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +14 -7
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +3 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +24 -9
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +41 -17
- package/gsd-core/bin/lib/schema-detect.cjs +2 -1
- package/gsd-core/bin/lib/security.cjs +1 -1
- package/gsd-core/bin/lib/shell-command-projection.cjs +61 -25
- package/gsd-core/bin/lib/smart-entry.cjs +73 -7
- package/gsd-core/bin/lib/state-document.cjs +7 -4
- package/gsd-core/bin/lib/state-transition.cjs +122 -46
- package/gsd-core/bin/lib/state.cjs +456 -137
- package/gsd-core/bin/lib/surface.cjs +53 -11
- package/gsd-core/bin/lib/template.cjs +2 -1
- package/gsd-core/bin/lib/uat.cjs +474 -13
- package/gsd-core/bin/lib/ui-safety-gate.cjs +23 -1
- package/gsd-core/bin/lib/validate.cjs +12 -8
- package/gsd-core/bin/lib/verification.cjs +112 -17
- package/gsd-core/bin/lib/verify.cjs +224 -25
- package/gsd-core/bin/lib/workstream.cjs +3 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +1 -1
- package/gsd-core/bin/lib/write-set.cjs +38 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +5 -2
- package/gsd-core/references/api-coverage.md +37 -7
- package/gsd-core/references/checkpoints.md +13 -1
- package/gsd-core/references/common-bug-patterns.md +13 -0
- package/gsd-core/references/debugger-bug-taxonomy.md +111 -0
- package/gsd-core/references/debugger-fix-acceptance.md +157 -0
- package/gsd-core/references/debugger-philosophy.md +1 -0
- package/gsd-core/references/debugger-prevention.md +98 -0
- package/gsd-core/references/debugger-rca-branching.md +98 -0
- package/gsd-core/references/debugger-repro-hardening.md +130 -0
- package/gsd-core/references/debugger-sbfl.md +110 -0
- package/gsd-core/references/debugger-semantic-recall.md +81 -0
- package/gsd-core/references/execute-phase-quota-recovery.md +55 -0
- package/gsd-core/references/execute-phase-requirement-revert.md +8 -0
- package/gsd-core/references/execute-phase-response-language.md +7 -0
- package/gsd-core/references/planner-antipatterns.md +6 -0
- package/gsd-core/references/planner-mvp-mode.md +12 -13
- package/gsd-core/references/planner-preconditions.md +156 -0
- package/gsd-core/references/planner-reversibility.md +132 -0
- package/gsd-core/references/reviewer-instances.md +9 -7
- package/gsd-core/references/skeleton-template.md +1 -1
- package/gsd-core/references/thinking-models-planning.md +3 -1
- package/gsd-core/templates/DEBUG.md +5 -3
- package/gsd-core/workflows/add-phase.md +2 -0
- package/gsd-core/workflows/add-tests.md +4 -2
- package/gsd-core/workflows/add-todo.md +32 -1
- package/gsd-core/workflows/ai-integration-phase.md +4 -2
- package/gsd-core/workflows/audit-fix.md +2 -2
- package/gsd-core/workflows/check-todos.md +3 -1
- package/gsd-core/workflows/cleanup.md +7 -1
- package/gsd-core/workflows/code-review.md +17 -5
- package/gsd-core/workflows/complete-milestone.md +3 -0
- package/gsd-core/workflows/debug.md +27 -5
- package/gsd-core/workflows/diagnose-issues.md +1 -1
- package/gsd-core/workflows/discovery-phase.md +7 -0
- package/gsd-core/workflows/discuss-phase/templates/context.md +16 -2
- package/gsd-core/workflows/discuss-phase-assumptions.md +3 -0
- package/gsd-core/workflows/do.md +7 -1
- package/gsd-core/workflows/docs-update.md +1 -0
- package/gsd-core/workflows/eval-review.md +3 -0
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +4 -4
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -2
- package/gsd-core/workflows/execute-phase.md +30 -37
- package/gsd-core/workflows/execute-plan.md +15 -4
- package/gsd-core/workflows/fast.md +8 -22
- package/gsd-core/workflows/graduation.md +3 -0
- package/gsd-core/workflows/health.md +7 -1
- package/gsd-core/workflows/help/modes/full.md +6 -2
- package/gsd-core/workflows/import.md +8 -2
- package/gsd-core/workflows/inbox.md +7 -0
- package/gsd-core/workflows/ingest-docs.md +15 -10
- package/gsd-core/workflows/manager.md +3 -1
- package/gsd-core/workflows/map-codebase.md +4 -4
- package/gsd-core/workflows/mvp-phase.md +3 -0
- package/gsd-core/workflows/new-milestone.md +69 -21
- package/gsd-core/workflows/new-project.md +17 -15
- package/gsd-core/workflows/new-workspace.md +3 -1
- package/gsd-core/workflows/onboard.md +3 -0
- package/gsd-core/workflows/plan-phase.md +14 -5
- package/gsd-core/workflows/plan-review-convergence.md +48 -3
- package/gsd-core/workflows/plant-seed.md +3 -0
- package/gsd-core/workflows/profile-user.md +7 -1
- package/gsd-core/workflows/progress.md +33 -5
- package/gsd-core/workflows/quick.md +21 -7
- package/gsd-core/workflows/remove-workspace.md +3 -0
- package/gsd-core/workflows/review.md +123 -68
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +4 -1
- package/gsd-core/workflows/settings-integrations.md +3 -0
- package/gsd-core/workflows/settings.md +3 -0
- package/gsd-core/workflows/ship.md +58 -5
- package/gsd-core/workflows/sketch.md +3 -0
- package/gsd-core/workflows/smart-entry.md +3 -0
- package/gsd-core/workflows/spec-phase.md +1 -1
- package/gsd-core/workflows/spike.md +7 -1
- package/gsd-core/workflows/transition.md +1 -1
- package/gsd-core/workflows/ui-phase.md +3 -1
- package/gsd-core/workflows/ui-review.md +3 -0
- package/gsd-core/workflows/undo.md +7 -0
- package/gsd-core/workflows/update.md +2 -0
- package/gsd-core/workflows/validate-phase.md +3 -0
- package/gsd-core/workflows/verify-phase.md +2 -2
- package/gsd-core/workflows/verify-work.md +7 -3
- package/hooks/dist/gsd-context-monitor.js +27 -9
- package/hooks/dist/gsd-statusline.js +252 -17
- package/hooks/gsd-context-monitor.js +27 -9
- package/hooks/gsd-statusline.js +252 -17
- package/package.json +8 -4
- package/pi/gsd.cjs +8 -2
- package/scripts/changeset/lint.cjs +1 -0
- package/scripts/changeset/parse.cjs +26 -0
- package/scripts/check-glossary-refs.cjs +220 -0
- package/scripts/ci-rebase-check.cjs +48 -4
- package/scripts/ci-test-scope.cjs +39 -1
- package/scripts/gen-adr-index.cjs +526 -0
- package/scripts/gen-golden-install-parity-zcode.cjs +35 -45
- package/scripts/gen-install-tree-fixtures.cjs +75 -0
- package/scripts/gen-test-timings.cjs +201 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -1
- package/scripts/lint-portable-timeout.cjs +140 -0
- package/scripts/lint-table-schema-drift.cjs +157 -0
- package/scripts/lint-test-file-count.allowlist.json +1 -0
- package/scripts/release-tarball-smoke.cjs +18 -11
- package/scripts/run-tests.cjs +420 -58
- package/skills/gsd-ai-integration-phase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +31 -1
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-plan-phase/SKILL.md +5 -3
- package/skills/gsd-plan-review-convergence/SKILL.md +3 -2
- package/skills/gsd-surface/SKILL.md +6 -6
- package/vscode/package.json +1 -1
|
@@ -30,7 +30,7 @@ const { planningDir, planningPaths } = planningWorkspace;
|
|
|
30
30
|
const clock_cjs_1 = require("./clock.cjs");
|
|
31
31
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
32
32
|
const frontmatter = require("./frontmatter.cjs");
|
|
33
|
-
const { extractFrontmatter, reconstructFrontmatter } = frontmatter;
|
|
33
|
+
const { extractFrontmatter, reconstructFrontmatter, stripFrontmatter } = frontmatter;
|
|
34
34
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
35
35
|
const scanPhasePlans = require("./plan-scan.cjs");
|
|
36
36
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
@@ -38,6 +38,7 @@ const stateTransitionMod = require("./state-transition.cjs");
|
|
|
38
38
|
const { transitionCore, applyStatePreservation, sliceCurrentPositionSection } = stateTransitionMod;
|
|
39
39
|
const state_document_cjs_1 = require("./state-document.cjs");
|
|
40
40
|
const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
41
|
+
const markdown_table_cjs_1 = require("./markdown-table.cjs");
|
|
41
42
|
const STATE_PROGRESS_RESYNC_FIELDS = new Set([
|
|
42
43
|
'Progress',
|
|
43
44
|
'Total Plans in Phase',
|
|
@@ -143,8 +144,11 @@ function _stateLockBodyPid(lockPath) {
|
|
|
143
144
|
}
|
|
144
145
|
// Monotonic sequence for unique stale-steal rename targets (no crypto dependency).
|
|
145
146
|
let _stateStealSeq = 0;
|
|
146
|
-
//
|
|
147
|
-
|
|
147
|
+
// The `byPhaseTablePattern` regex hoisted here for #320 (canonical-column-
|
|
148
|
+
// ORDER-only By-Phase table match) is retired (#2245 audit): its last caller
|
|
149
|
+
// — updatePerformanceMetricsSection's row-INSERT branch — now locates the
|
|
150
|
+
// table via findTableStartOffset/insertTableRow, name-addressed and
|
|
151
|
+
// header-order-agnostic like the update/sum halves of the same function.
|
|
148
152
|
// ─── ADR-1372 T6: seam-based section splice helper ───────────────────────────
|
|
149
153
|
// Shared stop predicates corresponding to the regex lookaheads used in state.cts:
|
|
150
154
|
// STOP_H2_PLUS : (?=\n##|$) — stops at any heading with level ≥ 2
|
|
@@ -166,6 +170,12 @@ function cmdStateLoad(cwd, raw) {
|
|
|
166
170
|
state_exists: stateExists,
|
|
167
171
|
roadmap_exists: roadmapExists,
|
|
168
172
|
config_exists: configExists,
|
|
173
|
+
// #2376: absolute (anchored on cwd), not orchestrator-cwd-relative — a
|
|
174
|
+
// spawned subagent's own cwd may differ from the orchestrator's.
|
|
175
|
+
// debug.md has no init.* call of its own; it reads this field from
|
|
176
|
+
// `state load` to build debug_file_path for its gsd-debug-session-manager
|
|
177
|
+
// spawns instead of hardcoding '.planning/debug/{slug}.md'.
|
|
178
|
+
debug_dir: (0, shell_command_projection_cjs_1.toPosixPath)(node_path_1.default.join(planDir, 'debug')),
|
|
169
179
|
};
|
|
170
180
|
// For --raw, output a condensed key=value format
|
|
171
181
|
if (raw) {
|
|
@@ -381,29 +391,131 @@ function cmdStateRecordMetric(cwd, options, raw) {
|
|
|
381
391
|
let _recorded = false;
|
|
382
392
|
let created = false;
|
|
383
393
|
readModifyWriteStateMd(statePath, (content) => {
|
|
384
|
-
// Find Performance Metrics section and its table
|
|
385
|
-
const metricsPattern = /(##\s*Performance Metrics[\s\S]*?\n\|[^\n]+\n\|[-|\s]+\n)([\s\S]*?)(?=\n##|\n$|$)/i; // allow-adhoc-markdown: metrics-table write-path section-collect in state.cts; pending collectSection migration #1372
|
|
386
|
-
const metricsMatch = content.match(metricsPattern);
|
|
387
394
|
const newRow = `| Phase ${phase} P${plan} | ${duration} | ${tasks || '-'} tasks | ${files || '-'} files |`;
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
395
|
+
// Find the "## Performance Metrics" section via the markdown-sectionizer
|
|
396
|
+
// seam (ADR-2143 §7) — supersedes the prior hand-rolled section+table
|
|
397
|
+
// regex.
|
|
398
|
+
const metricsSection = (0, markdown_sectionizer_cjs_1.collectSection)(content, (h) => /^performance metrics$/i.test(h.text.trim()));
|
|
399
|
+
const eol = metricsSection && /\r\n/.test(metricsSection.body) ? '\r\n' : '\n';
|
|
400
|
+
const lines = metricsSection ? metricsSection.body.split(/\r?\n/) : [];
|
|
401
|
+
// Locate THIS command's OWN metrics table by its HEADER shape, using the
|
|
402
|
+
// exact same splitTableRow/isDelimiterRow header/delimiter-shape checks
|
|
403
|
+
// `parseMarkdownTable` uses. A live "## Performance Metrics" section
|
|
404
|
+
// (gsd-core/templates/state.md:39-56) also carries the "By Phase"
|
|
405
|
+
// velocity table (`| Phase | Plans | Total | Avg/Plan |`) — the prior
|
|
406
|
+
// "first table in the section" targeting spliced every per-plan row into
|
|
407
|
+
// THAT table instead, polluting it on EVERY plan completion
|
|
408
|
+
// (execute-plan.md:414 calls record-metric per-plan) (#2245/#2143).
|
|
409
|
+
// Matching the header cells to this command's own canonical
|
|
410
|
+
// `Plan | Duration | Tasks | Files` shape (case-insensitive/trimmed)
|
|
411
|
+
// finds the right table regardless of what else shares the section, and
|
|
412
|
+
// deliberately does NOT require `parseMarkdownTable(...).ok` (which
|
|
413
|
+
// additionally requires every DATA row's cell count to match the
|
|
414
|
+
// header) — a single ragged sibling row (a hand-edited stray/extra pipe)
|
|
415
|
+
// must not blind this scan (#2245 Blocker 2 parity with the other
|
|
416
|
+
// Phase-4 ragged-tolerance fixes: updateTableCell / findTableStartOffset).
|
|
417
|
+
const METRICS_HEADER = ['plan', 'duration', 'tasks', 'files'];
|
|
418
|
+
let headerIdx = -1;
|
|
419
|
+
for (let i = 0; i < lines.length - 1; i++) {
|
|
420
|
+
const trimmed = lines[i].trim();
|
|
421
|
+
if (!trimmed.startsWith('|') || trimmed.indexOf('|', 1) === -1)
|
|
422
|
+
continue;
|
|
423
|
+
const delimiterLine = lines[i + 1];
|
|
424
|
+
if (delimiterLine === undefined || !delimiterLine.trim().startsWith('|'))
|
|
425
|
+
continue;
|
|
426
|
+
const headerCells = (0, markdown_table_cjs_1.splitTableRow)(lines[i]);
|
|
427
|
+
const delimiterCells = (0, markdown_table_cjs_1.splitTableRow)(delimiterLine);
|
|
428
|
+
if (!(0, markdown_table_cjs_1.isDelimiterRow)(delimiterCells) || delimiterCells.length !== headerCells.length)
|
|
429
|
+
continue;
|
|
430
|
+
const normalized = headerCells.map((cell) => cell.trim().toLowerCase());
|
|
431
|
+
const isMetricsHeader = normalized.length === METRICS_HEADER.length
|
|
432
|
+
&& normalized.every((cell, idx) => cell === METRICS_HEADER[idx]);
|
|
433
|
+
if (isMetricsHeader) {
|
|
434
|
+
headerIdx = i;
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
const hasTable = headerIdx !== -1;
|
|
439
|
+
if (metricsSection && hasTable) {
|
|
440
|
+
const delimiterIdx = headerIdx + 1;
|
|
441
|
+
const prefixLines = lines.slice(0, delimiterIdx + 1);
|
|
442
|
+
// Ragged-tolerant row scan: every consecutive `|`-prefixed line
|
|
443
|
+
// following the delimiter counts as an existing row REGARDLESS of its
|
|
444
|
+
// cell count matching the header — a ragged sibling row must never
|
|
445
|
+
// blind this scan to the table's true last row (unlike
|
|
446
|
+
// `parsedTable.value.rows.length`, which this replaces). Anchored to
|
|
447
|
+
// the METRICS table's OWN header/delimiter (`headerIdx` above), never
|
|
448
|
+
// the section's first table (#2245/#2143).
|
|
449
|
+
let lastRowIdx = delimiterIdx;
|
|
450
|
+
for (let i = delimiterIdx + 1; i < lines.length; i++) {
|
|
451
|
+
if (!lines[i].trim().startsWith('|'))
|
|
452
|
+
break;
|
|
453
|
+
lastRowIdx = i;
|
|
454
|
+
}
|
|
455
|
+
const rowCount = lastRowIdx - delimiterIdx;
|
|
456
|
+
_recorded = true;
|
|
457
|
+
let newBody;
|
|
458
|
+
if (rowCount > 0) {
|
|
459
|
+
// Splice the new row immediately after the table's LAST existing data
|
|
460
|
+
// row — every other byte of the section, INCLUDING any trailing prose
|
|
461
|
+
// that follows the table (e.g. the default template's "**Recent
|
|
462
|
+
// Trend:**" subsection + "*Updated after each plan completion*"
|
|
463
|
+
// footer), is preserved verbatim. The prior implementation truncated
|
|
464
|
+
// the section body to header+delimiter+rows+newRow, silently dropping
|
|
465
|
+
// everything that followed the table on a live STATE.md (#2245
|
|
466
|
+
// Blocker 1 — a per-plan path, run after every plan execution).
|
|
467
|
+
// `lastRowIdx` (computed above by the ragged-tolerant scan) already
|
|
468
|
+
// equals `delimiterIdx + rowCount` by construction.
|
|
469
|
+
const before = lines.slice(0, lastRowIdx + 1);
|
|
470
|
+
const after = lines.slice(lastRowIdx + 1);
|
|
471
|
+
newBody = [...before, newRow, ...after].join(eol);
|
|
392
472
|
}
|
|
393
473
|
else {
|
|
394
|
-
|
|
474
|
+
// No existing data rows (e.g. a "None yet" placeholder line instead of
|
|
475
|
+
// a real row) — replace the placeholder/table-body remainder with the
|
|
476
|
+
// new row, matching the section's prior (verified) collapse-to-
|
|
477
|
+
// first-row behavior for an otherwise-empty table.
|
|
478
|
+
// No trailing eol here: replaceSection's `content.slice(bodyEnd)`
|
|
479
|
+
// already supplies the newline(s) that followed the (trimEnd()-ed)
|
|
480
|
+
// section body.
|
|
481
|
+
newBody = prefixLines.join(eol) + eol + newRow;
|
|
395
482
|
}
|
|
483
|
+
return (0, markdown_sectionizer_cjs_1.replaceSection)(content, metricsSection, newBody);
|
|
484
|
+
}
|
|
485
|
+
if (metricsSection) {
|
|
486
|
+
// Section EXISTS but carries no metrics table of its own — e.g. a live
|
|
487
|
+
// STATE.md whose "## Performance Metrics" section holds only the
|
|
488
|
+
// By-Phase velocity table (gsd-core/templates/state.md:48). Self-heal
|
|
489
|
+
// by appending a fresh Per-Plan Metrics table to the END of the
|
|
490
|
+
// section body — every existing byte (By-Phase table, Recent Trend,
|
|
491
|
+
// footer) is preserved verbatim, and no second "## Performance
|
|
492
|
+
// Metrics" heading is introduced. The section already existed, so
|
|
493
|
+
// `created` stays false (#2245/#2143).
|
|
396
494
|
_recorded = true;
|
|
397
|
-
|
|
495
|
+
const newBody = metricsSection.body
|
|
496
|
+
+ eol + '**Per-Plan Metrics:**'
|
|
497
|
+
+ eol + eol
|
|
498
|
+
+ '| Plan | Duration | Tasks | Files |'
|
|
499
|
+
+ eol
|
|
500
|
+
+ '|------|----------|-------|-------|'
|
|
501
|
+
+ eol
|
|
502
|
+
+ newRow
|
|
503
|
+
+ eol;
|
|
504
|
+
return (0, markdown_sectionizer_cjs_1.replaceSection)(content, metricsSection, newBody);
|
|
398
505
|
}
|
|
399
|
-
// Section absent — DWIM: auto-create canonical
|
|
400
|
-
// then append the row. Matches state
|
|
506
|
+
// Section absent (or malformed) — DWIM: auto-create canonical
|
|
507
|
+
// ## Performance Metrics scaffold, then append the row. Matches state
|
|
508
|
+
// begin-phase / advance-plan DWIM behavior. Header corrected to this
|
|
509
|
+
// command's own canonical shape (`Plan | Duration | Tasks | Files`) —
|
|
510
|
+
// the prior scaffold's `| Phase | Plan | Duration | Notes |` header
|
|
511
|
+
// matched neither the appended row's shape nor the canonical table
|
|
512
|
+
// above (#2245/#2143).
|
|
401
513
|
const scaffold = [
|
|
402
514
|
'',
|
|
403
515
|
'## Performance Metrics',
|
|
404
516
|
'',
|
|
405
|
-
'|
|
|
406
|
-
'
|
|
517
|
+
'| Plan | Duration | Tasks | Files |',
|
|
518
|
+
'|------|----------|-------|-------|',
|
|
407
519
|
newRow,
|
|
408
520
|
'',
|
|
409
521
|
].join('\n');
|
|
@@ -447,18 +559,31 @@ function cmdStateUpdateProgress(cwd, raw) {
|
|
|
447
559
|
const _totalPlans = totalPlans;
|
|
448
560
|
const _totalSummaries = totalSummaries;
|
|
449
561
|
readModifyWriteStateMd(statePath, (content) => {
|
|
450
|
-
//
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
562
|
+
// #2177: match against the BODY only. With /i the patterns below would
|
|
563
|
+
// otherwise hit the YAML frontmatter `progress:` key first (and `\s*` would
|
|
564
|
+
// eat its newline, mangling the nested block), while the body Progress: line
|
|
565
|
+
// — which frontmatter `percent` is re-derived from on every write — stays
|
|
566
|
+
// stale and silently reverts the update.
|
|
567
|
+
const body = stripFrontmatter(content);
|
|
568
|
+
const fmPrefix = content.slice(0, content.length - body.length);
|
|
569
|
+
// Swap only the machine segment ("[bar] NN%" or bare "NN%"), preserving any
|
|
570
|
+
// descriptive suffix an agent authored, e.g. "(2/4 plans done; blocked on…)".
|
|
571
|
+
const machineSegment = /(?:\[[^\]\r\n]*\][ \t]*)?\d{1,3}%/;
|
|
572
|
+
const replaceValue = (value) => machineSegment.test(value)
|
|
573
|
+
? value.replace(machineSegment, progressStr)
|
|
574
|
+
: progressStr;
|
|
575
|
+
// Try **Progress:** bold format first, then plain Progress: format.
|
|
576
|
+
const boldProgressPattern = /(\*\*Progress:\*\*[ \t]*)([^\r\n]*)/i;
|
|
577
|
+
const plainProgressPattern = /^(Progress:[ \t]*)([^\r\n]*)/im;
|
|
578
|
+
const pattern = boldProgressPattern.test(body)
|
|
579
|
+
? boldProgressPattern
|
|
580
|
+
: plainProgressPattern.test(body)
|
|
581
|
+
? plainProgressPattern
|
|
582
|
+
: null;
|
|
583
|
+
if (!pattern)
|
|
584
|
+
return content;
|
|
585
|
+
updated = true;
|
|
586
|
+
return fmPrefix + body.replace(pattern, (_match, prefix, value) => `${prefix}${replaceValue(value)}`);
|
|
462
587
|
}, cwd);
|
|
463
588
|
if (updated) {
|
|
464
589
|
output({ updated: true, percent, completed: _totalSummaries, total: _totalPlans, bar: progressStr }, raw, progressStr);
|
|
@@ -884,6 +1009,15 @@ function cmdStateRecordSession(cwd, options, raw) {
|
|
|
884
1009
|
// duplicate section alongside an existing one.
|
|
885
1010
|
const existingCanonicalSession = /^## Session[ \t]*$/im.test(content);
|
|
886
1011
|
const existingSessionContinuity = /^## Session Continuity[ \t]*$/im.test(content);
|
|
1012
|
+
// Track whether the chosen branch's rewrite actually matched. The detector
|
|
1013
|
+
// regexes (existingCanonicalSession/existingSessionContinuity) are CRLF-
|
|
1014
|
+
// tolerant ($ under /m treats \r as a line terminator); the writer regexes
|
|
1015
|
+
// below must be too. If a writer regex silently fails to match (line-ending
|
|
1016
|
+
// mismatch, unexpected heading shape, ...), do NOT report success — the
|
|
1017
|
+
// caller would believe fields were persisted that were silently dropped
|
|
1018
|
+
// (#2450). The append branch always sets rewriteMatched=true (it always
|
|
1019
|
+
// mutates content).
|
|
1020
|
+
let rewriteMatched = false;
|
|
887
1021
|
if (existingCanonicalSession) {
|
|
888
1022
|
// Normalize in place: replace the ENTIRE BODY of the existing ## Session
|
|
889
1023
|
// section (heading + all content up to the next ## heading or EOF) with
|
|
@@ -891,7 +1025,13 @@ function cmdStateRecordSession(cwd, options, raw) {
|
|
|
891
1025
|
// `(?!^## )[\s\S]` consumes every line that doesn't start with "## ",
|
|
892
1026
|
// which correctly stops at the next section boundary without consuming it.
|
|
893
1027
|
// A trailing blank line is added so the next ## heading keeps its spacing.
|
|
894
|
-
|
|
1028
|
+
//
|
|
1029
|
+
// CRLF-tolerant (`\r?\n` after `[ \t]*`): the prior literal `\n` could not
|
|
1030
|
+
// match a CRLF STATE.md (`---\r\n`), silently no-op'ing the replace while
|
|
1031
|
+
// updated.push(...) reported success — #2450. The detector regex on the
|
|
1032
|
+
// line above (`/^## Session[ \t]*$/im`) was already CRLF-tolerant, so the
|
|
1033
|
+
// asymmetry armed the bug.
|
|
1034
|
+
const canonicalReplacement = [
|
|
895
1035
|
'## Session',
|
|
896
1036
|
'',
|
|
897
1037
|
`**Last session:** ${now}`,
|
|
@@ -899,7 +1039,11 @@ function cmdStateRecordSession(cwd, options, raw) {
|
|
|
899
1039
|
`**Resume file:** ${resumeValue}`,
|
|
900
1040
|
'',
|
|
901
1041
|
'',
|
|
902
|
-
].join('\n')
|
|
1042
|
+
].join('\n');
|
|
1043
|
+
content = content.replace(/^(## Session[ \t]*\r?\n(?:(?!^## )[\s\S])*)/m, () => {
|
|
1044
|
+
rewriteMatched = true;
|
|
1045
|
+
return canonicalReplacement;
|
|
1046
|
+
});
|
|
903
1047
|
}
|
|
904
1048
|
else if (existingSessionContinuity) {
|
|
905
1049
|
// #1101: a `## Session Continuity` section already exists (bootstrap
|
|
@@ -910,6 +1054,8 @@ function cmdStateRecordSession(cwd, options, raw) {
|
|
|
910
1054
|
// (e.g. prose like "Next recommended action"). Fields already updated in
|
|
911
1055
|
// place above (needs* false) are not re-inserted. A function replacement
|
|
912
1056
|
// is used so `$`-bearing caller values are inserted literally (#3454).
|
|
1057
|
+
//
|
|
1058
|
+
// CRLF-tolerant (`\r?\n`): same #2450 fix as the canonical branch above.
|
|
913
1059
|
const linesToInsert = [];
|
|
914
1060
|
if (needsLastSession)
|
|
915
1061
|
linesToInsert.push(`**Last session:** ${now}`);
|
|
@@ -921,8 +1067,18 @@ function cmdStateRecordSession(cwd, options, raw) {
|
|
|
921
1067
|
// Case-insensitive to match the `existingSessionContinuity` detection
|
|
922
1068
|
// above (#1101 review F3) — otherwise a lowercase heading would detect
|
|
923
1069
|
// but no-op the insert while still reporting the fields as updated.
|
|
924
|
-
content = content.replace(/^(## Session Continuity[ \t]*\n)/im, (_m, heading) =>
|
|
1070
|
+
content = content.replace(/^(## Session Continuity[ \t]*\r?\n)/im, (_m, heading) => {
|
|
1071
|
+
rewriteMatched = true;
|
|
1072
|
+
return heading + linesToInsert.join('\n') + '\n';
|
|
1073
|
+
});
|
|
925
1074
|
}
|
|
1075
|
+
// No `else` branch: if linesToInsert.length === 0 the outer guard at
|
|
1076
|
+
// :1144 (callerSuppliedValues && (needsStoppedAt || needsResumeFile
|
|
1077
|
+
// || needsLastSession)) could not have fired, so this whole block is
|
|
1078
|
+
// unreachable. Leaving `rewriteMatched = false` here is the fail-loud
|
|
1079
|
+
// posture — a future change to the outer guard or needs* computation
|
|
1080
|
+
// that makes this branch reachable will surface as a missing
|
|
1081
|
+
// updated[] entry (silent recorded:false) rather than re-arming #2450.
|
|
926
1082
|
}
|
|
927
1083
|
else {
|
|
928
1084
|
// No session heading exists at all — append a new canonical section.
|
|
@@ -936,14 +1092,30 @@ function cmdStateRecordSession(cwd, options, raw) {
|
|
|
936
1092
|
'',
|
|
937
1093
|
].join('\n');
|
|
938
1094
|
content = content.trimEnd() + '\n' + scaffold;
|
|
1095
|
+
rewriteMatched = true;
|
|
1096
|
+
}
|
|
1097
|
+
// #2450 defensive invariant: only report sessionCreated/updated when the
|
|
1098
|
+
// chosen branch's rewrite actually mutated content. Unreachable when the
|
|
1099
|
+
// writer regexes above stay in sync with the CRLF-tolerant detector —
|
|
1100
|
+
// but unreachable-defensive is the right posture for a silent-success
|
|
1101
|
+
// gate. A no-op replace here means a future line-ending or shape drift
|
|
1102
|
+
// between detector and writer; fail to record rather than claim success.
|
|
1103
|
+
//
|
|
1104
|
+
// Scope limitation (not a regression of this fix): the gate covers only
|
|
1105
|
+
// the section-rewrite block. The earlier in-place stateReplaceField
|
|
1106
|
+
// successes at :1081/:1083/:1089/:1101/:1108/:1114 push to `updated`
|
|
1107
|
+
// unconditionally — those represent fields that DID land on disk via
|
|
1108
|
+
// same-line replace (CRLF-agnostic seam), so unconditional push is
|
|
1109
|
+
// correct. The class-defect防御 here is for the INSERT path only.
|
|
1110
|
+
if (rewriteMatched) {
|
|
1111
|
+
sessionCreated = true;
|
|
1112
|
+
if (needsLastSession)
|
|
1113
|
+
updated.push('Last session');
|
|
1114
|
+
if (needsStoppedAt)
|
|
1115
|
+
updated.push('Stopped At');
|
|
1116
|
+
if (needsResumeFile)
|
|
1117
|
+
updated.push('Resume File');
|
|
939
1118
|
}
|
|
940
|
-
sessionCreated = true;
|
|
941
|
-
if (needsLastSession)
|
|
942
|
-
updated.push('Last session');
|
|
943
|
-
if (needsStoppedAt)
|
|
944
|
-
updated.push('Stopped At');
|
|
945
|
-
if (needsResumeFile)
|
|
946
|
-
updated.push('Resume File');
|
|
947
1119
|
}
|
|
948
1120
|
return content;
|
|
949
1121
|
}, cwd);
|
|
@@ -961,15 +1133,22 @@ function cmdStateRecordSession(cwd, options, raw) {
|
|
|
961
1133
|
* Match the session section body from a STATE.md body. #1101: recognise the
|
|
962
1134
|
* bootstrap `## Session Continuity` heading but PREFER the normalized `## Session`
|
|
963
1135
|
* block when both exist (legacy duplicate files), so the reader agrees with the
|
|
964
|
-
* writer (which updates `## Session` first).
|
|
965
|
-
*
|
|
966
|
-
*
|
|
967
|
-
*
|
|
968
|
-
*
|
|
1136
|
+
* writer (which updates `## Session` first). Level-2-exact heading match
|
|
1137
|
+
* (excludes an h3 `### Session Continuity`); the exact `'session continuity'`
|
|
1138
|
+
* text match still excludes `## Session Continuity Archive` (preserving the
|
|
1139
|
+
* #2444 scoping). Migrated onto the `collectSection` seam (#2143 audit,
|
|
1140
|
+
* epic #2143): CRLF-safe — the prior hand-rolled `[ \t]*\n` regex silently
|
|
1141
|
+
* failed to match a CRLF `## Session\r\n` heading line (the `\r` broke the
|
|
1142
|
+
* `[ \t]*\n` boundary); `tokenizeHeadings` strips the trailing `\r` before
|
|
1143
|
+
* heading-text extraction, so this now matches CRLF headings too.
|
|
1144
|
+
* Returns the section body, or null.
|
|
969
1145
|
*/
|
|
970
1146
|
function matchSessionSection(body) {
|
|
971
|
-
|
|
972
|
-
|
|
1147
|
+
const isSession = (h) => h.level === 2 && h.text.trim().toLowerCase() === 'session';
|
|
1148
|
+
const isSessionContinuity = (h) => h.level === 2 && h.text.trim().toLowerCase() === 'session continuity';
|
|
1149
|
+
const section = (0, markdown_sectionizer_cjs_1.collectSection)(body, isSession, { levelBounded: true })
|
|
1150
|
+
?? (0, markdown_sectionizer_cjs_1.collectSection)(body, isSessionContinuity, { levelBounded: true });
|
|
1151
|
+
return section ? section.body : null;
|
|
973
1152
|
}
|
|
974
1153
|
function parseProsePhaseField(value) {
|
|
975
1154
|
// #2121 Phase 2 (#2125): delegate to the canonical anchored parser so this
|
|
@@ -1036,14 +1215,15 @@ function cmdStateSnapshot(cwd, raw) {
|
|
|
1036
1215
|
const totalPhases = totalPhasesRaw ? parseInt(totalPhasesRaw, 10) : null;
|
|
1037
1216
|
const totalPlansInPhase = totalPlansRaw ? parseInt(totalPlansRaw, 10) : null;
|
|
1038
1217
|
const progressPercent = progressRaw ? parseInt(progressRaw.replace('%', ''), 10) : null;
|
|
1039
|
-
// Extract decisions table
|
|
1218
|
+
// Extract decisions table — via the markdown-sectionizer/markdown-table
|
|
1219
|
+
// seams (ADR-2143 §7), cells addressed by column NAME rather than a
|
|
1220
|
+
// hand-rolled section+table regex.
|
|
1040
1221
|
const decisions = [];
|
|
1041
|
-
const
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
const
|
|
1045
|
-
|
|
1046
|
-
const cells = row.split('|').map(c => c.trim()).filter(Boolean);
|
|
1222
|
+
const decisionsSection = (0, markdown_sectionizer_cjs_1.collectSection)(body, (h) => /^decisions made$/i.test(h.text.trim()));
|
|
1223
|
+
const decisionsTable = decisionsSection ? (0, markdown_table_cjs_1.parseMarkdownTable)(decisionsSection.body) : null;
|
|
1224
|
+
if (decisionsTable && decisionsTable.ok) {
|
|
1225
|
+
for (const row of decisionsTable.value.rows) {
|
|
1226
|
+
const cells = decisionsTable.value.columns.map((c) => (row[c] ?? '').trim()).filter(Boolean);
|
|
1047
1227
|
if (cells.length >= 3) {
|
|
1048
1228
|
decisions.push({
|
|
1049
1229
|
phase: cells[0],
|
|
@@ -1055,10 +1235,9 @@ function cmdStateSnapshot(cwd, raw) {
|
|
|
1055
1235
|
}
|
|
1056
1236
|
// Extract blockers list
|
|
1057
1237
|
const blockers = [];
|
|
1058
|
-
const
|
|
1059
|
-
if (
|
|
1060
|
-
const
|
|
1061
|
-
const items = blockersSection.match(/^-\s+(.+)$/gm) || [];
|
|
1238
|
+
const blockersSection = (0, markdown_sectionizer_cjs_1.collectSection)(body, (h) => h.level === 2 && h.text.trim().toLowerCase() === 'blockers', { levelBounded: true });
|
|
1239
|
+
if (blockersSection) {
|
|
1240
|
+
const items = blockersSection.body.match(/^-\s+(.+)$/gm) || [];
|
|
1062
1241
|
for (const item of items) {
|
|
1063
1242
|
blockers.push(item.replace(/^-\s+/, '').trim());
|
|
1064
1243
|
}
|
|
@@ -1072,8 +1251,8 @@ function cmdStateSnapshot(cwd, raw) {
|
|
|
1072
1251
|
// #1101: prefer the canonical `## Session` block, falling back to the bootstrap
|
|
1073
1252
|
// `## Session Continuity` heading. See matchSessionSection for the anchoring.
|
|
1074
1253
|
const sessionMatch = matchSessionSection(body);
|
|
1075
|
-
if (sessionMatch) {
|
|
1076
|
-
const sessionSection = sessionMatch
|
|
1254
|
+
if (sessionMatch !== null) {
|
|
1255
|
+
const sessionSection = sessionMatch;
|
|
1077
1256
|
// Accept both `**Last Date:**` (canonical template form) and `**Last session:**`
|
|
1078
1257
|
// (the form written by the DWIM auto-create / normalize path added for #944).
|
|
1079
1258
|
const lastDateMatch = sessionSection.match(/\*\*Last Date:\*\*\s*(.+)/i)
|
|
@@ -1191,18 +1370,19 @@ function buildStateFrontmatter(bodyContent, cwd) {
|
|
|
1191
1370
|
// #1101: prefer the canonical `## Session` block, falling back to the bootstrap
|
|
1192
1371
|
// `## Session Continuity` heading. See matchSessionSection for the anchoring.
|
|
1193
1372
|
const sessionSectionMatch = matchSessionSection(bodyContent);
|
|
1194
|
-
const sessionBodyScope = sessionSectionMatch
|
|
1373
|
+
const sessionBodyScope = sessionSectionMatch ?? bodyContent;
|
|
1195
1374
|
const stoppedAt = (0, state_document_cjs_1.stateExtractField)(sessionBodyScope, 'Stopped At') || (0, state_document_cjs_1.stateExtractField)(sessionBodyScope, 'Stopped at');
|
|
1196
1375
|
const pausedAt = (0, state_document_cjs_1.stateExtractField)(bodyContent, 'Paused At');
|
|
1197
1376
|
let milestone = null;
|
|
1198
1377
|
let milestoneName = null;
|
|
1199
1378
|
if (cwd) {
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1379
|
+
// DEAD catch removed (#2245 audit): getMilestoneInfo has its own outer
|
|
1380
|
+
// try/catch (roadmap-parser.cts) that already swallows every internal
|
|
1381
|
+
// failure and always returns a MilestoneInfo — it never throws, so this
|
|
1382
|
+
// wrapper could never be triggered.
|
|
1383
|
+
const info = getMilestoneInfo(cwd);
|
|
1384
|
+
milestone = info.version;
|
|
1385
|
+
milestoneName = info.name;
|
|
1206
1386
|
}
|
|
1207
1387
|
let totalPhases = totalPhasesRaw ? parseInt(totalPhasesRaw, 10) : null;
|
|
1208
1388
|
let completedPhases = null;
|
|
@@ -1336,6 +1516,13 @@ function buildStateFrontmatter(bodyContent, cwd) {
|
|
|
1336
1516
|
completedPlans = cached.completedPlans;
|
|
1337
1517
|
milestoneUnbounded = cached.milestoneBounded === false;
|
|
1338
1518
|
}
|
|
1519
|
+
/* best-effort (#2245 audit): this is a READ path building STATE.md's
|
|
1520
|
+
* display frontmatter. The real throw source is fs.readdirSync(phasesDir)
|
|
1521
|
+
* a few lines up — an inaccessible/racily-removed phases dir must not
|
|
1522
|
+
* crash `state show`; on failure this simply keeps whatever
|
|
1523
|
+
* frontmatter-derived totals/completedPhases/etc. were already set
|
|
1524
|
+
* above, a graceful degrade rather than a corrupted write (nothing is
|
|
1525
|
+
* persisted from this block). */
|
|
1339
1526
|
}
|
|
1340
1527
|
catch { /* intentionally empty */ }
|
|
1341
1528
|
}
|
|
@@ -1391,19 +1578,6 @@ function buildStateFrontmatter(bodyContent, cwd) {
|
|
|
1391
1578
|
fm['progress'] = progress;
|
|
1392
1579
|
return fm;
|
|
1393
1580
|
}
|
|
1394
|
-
function stripFrontmatter(content) {
|
|
1395
|
-
// Strip ALL frontmatter blocks at the start of the file.
|
|
1396
|
-
// Handles CRLF line endings and multiple stacked blocks (corruption recovery).
|
|
1397
|
-
// Greedy: keeps stripping ---...--- blocks separated by optional whitespace.
|
|
1398
|
-
let result = content;
|
|
1399
|
-
while (true) {
|
|
1400
|
-
const stripped = result.replace(/^\s*---\r?\n[\s\S]*?\r?\n---\s*/, '');
|
|
1401
|
-
if (stripped === result)
|
|
1402
|
-
break;
|
|
1403
|
-
result = stripped;
|
|
1404
|
-
}
|
|
1405
|
-
return result;
|
|
1406
|
-
}
|
|
1407
1581
|
function syncStateFrontmatter(content, cwd) {
|
|
1408
1582
|
// Read existing frontmatter BEFORE stripping — it may contain values
|
|
1409
1583
|
// that the body no longer has (e.g., Status field removed by an agent).
|
|
@@ -1425,7 +1599,17 @@ function syncStateFrontmatter(content, cwd) {
|
|
|
1425
1599
|
// existing frontmatter already holds; only an empty derived value falls through
|
|
1426
1600
|
// to this guard (the primary #905 preserve path below handles that).
|
|
1427
1601
|
const MILESTONE_NAME_PLACEHOLDER = 'milestone';
|
|
1428
|
-
|
|
1602
|
+
// #2135: widen the preserve guard. A bad derive is not always the literal
|
|
1603
|
+
// placeholder — getMilestoneInfo can return a delimiter-led fragment
|
|
1604
|
+
// ("— Active Milestone") when the roadmap regex mis-binds. Preserve the
|
|
1605
|
+
// existing curated name unless the derived value actually looks like a name:
|
|
1606
|
+
// non-empty, not the placeholder, and not punctuation-led.
|
|
1607
|
+
const derivedName = derivedFm['milestone_name'];
|
|
1608
|
+
const derivedLooksLikeName = typeof derivedName === 'string'
|
|
1609
|
+
&& derivedName.length > 0
|
|
1610
|
+
&& derivedName !== MILESTONE_NAME_PLACEHOLDER
|
|
1611
|
+
&& !/^[\s—–:-]/.test(derivedName);
|
|
1612
|
+
if (!derivedLooksLikeName &&
|
|
1429
1613
|
existingFm['milestone_name'] &&
|
|
1430
1614
|
existingFm['milestone_name'] !== MILESTONE_NAME_PLACEHOLDER) {
|
|
1431
1615
|
derivedFm['milestone_name'] = existingFm['milestone_name'];
|
|
@@ -1472,6 +1656,15 @@ function syncStateFrontmatter(content, cwd) {
|
|
|
1472
1656
|
if (!derivedFm['progress'] && existingFm['progress']) {
|
|
1473
1657
|
derivedFm['progress'] = (0, state_document_cjs_1.normalizeProgressNumbers)(existingFm['progress']);
|
|
1474
1658
|
}
|
|
1659
|
+
// #2202: carry forward any existing frontmatter key that the schema does not
|
|
1660
|
+
// own, so custom/unknown keys are not silently dropped on every mutating verb.
|
|
1661
|
+
// Schema-owned keys (already in derivedFm from buildStateFrontmatter + the
|
|
1662
|
+
// preserve guards above) still win.
|
|
1663
|
+
for (const key of Object.keys(existingFm)) {
|
|
1664
|
+
if (!(key in derivedFm) && existingFm[key] !== undefined) {
|
|
1665
|
+
derivedFm[key] = existingFm[key];
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1475
1668
|
const yamlStr = reconstructFrontmatter(derivedFm);
|
|
1476
1669
|
return `---\n${yamlStr}\n---\n\n${body}`;
|
|
1477
1670
|
}
|
|
@@ -1786,7 +1979,7 @@ function readModifyWriteStateMd(statePath, transformFn, cwd, options, clock) {
|
|
|
1786
1979
|
// A stale "Stopped at:" in a non-Session section (e.g. Session Continuity
|
|
1787
1980
|
// Archive prose) must not interfere with the delta comparison.
|
|
1788
1981
|
const preSessionMatch = matchSessionSection(preBody);
|
|
1789
|
-
const preSessionScope = preSessionMatch
|
|
1982
|
+
const preSessionScope = preSessionMatch ?? preBody;
|
|
1790
1983
|
const preBodyStoppedAt = (0, state_document_cjs_1.stateExtractField)(preSessionScope, 'Stopped At') || (0, state_document_cjs_1.stateExtractField)(preSessionScope, 'Stopped at');
|
|
1791
1984
|
// ADR-1769 Phase 6 / #1743 / #1695: snapshot the body source for the curated
|
|
1792
1985
|
// current_phase_name (the `Phase:` line parseProsePhaseField harvests). When
|
|
@@ -1815,7 +2008,7 @@ function readModifyWriteStateMd(statePath, transformFn, cwd, options, clock) {
|
|
|
1815
2008
|
// Bug #1230 / Change B: scope stopped_at delta to the ## Session section,
|
|
1816
2009
|
// consistent with the pre-transform snapshot above and buildStateFrontmatter.
|
|
1817
2010
|
const postSessionMatch = matchSessionSection(postBody);
|
|
1818
|
-
const postSessionScope = postSessionMatch
|
|
2011
|
+
const postSessionScope = postSessionMatch ?? postBody;
|
|
1819
2012
|
const postBodyStoppedAt = (0, state_document_cjs_1.stateExtractField)(postSessionScope, 'Stopped At') || (0, state_document_cjs_1.stateExtractField)(postSessionScope, 'Stopped at');
|
|
1820
2013
|
// ADR-1769 Phase 6 / #1695: post-transform body Phase source for the
|
|
1821
2014
|
// current_phase_name delta comparison.
|
|
@@ -1830,6 +2023,7 @@ function readModifyWriteStateMd(statePath, transformFn, cwd, options, clock) {
|
|
|
1830
2023
|
const postFm = extractFrontmatter(synced);
|
|
1831
2024
|
const preservation = applyStatePreservation({
|
|
1832
2025
|
preFm, postFm, preFmSnapshot, resync,
|
|
2026
|
+
deriveProgressKeys: options?.deriveProgressKeys === true,
|
|
1833
2027
|
preBodyStatus, postBodyStatus,
|
|
1834
2028
|
preBodyStoppedAt, postBodyStoppedAt,
|
|
1835
2029
|
preBodyPhaseSource, postBodyPhaseSource,
|
|
@@ -1971,6 +2165,37 @@ function cmdSignalResume(cwd, raw) {
|
|
|
1971
2165
|
output({ resumed: true, removed }, raw, removed ? 'true' : 'false');
|
|
1972
2166
|
}
|
|
1973
2167
|
// ─── Gate Functions (STATE.md consistency enforcement) ────────────────────────
|
|
2168
|
+
/**
|
|
2169
|
+
* Find the character offset where the FIRST GFM table whose header is a
|
|
2170
|
+
* superset of `required` column names begins (order-independent; extra
|
|
2171
|
+
* columns tolerated) — the position-aware counterpart to markdown-table's
|
|
2172
|
+
* `findTableWithColumns`, used to scope `updateTableCell` (which always
|
|
2173
|
+
* operates on "the first table in its input") to the RIGHT table when an
|
|
2174
|
+
* unrelated earlier table (that doesn't itself name every required column)
|
|
2175
|
+
* may precede it in the same document. Returns `null` when no such table is
|
|
2176
|
+
* found. Never trips the table-regex fingerprint (no `[^|]` cell-capture
|
|
2177
|
+
* class) and never throws.
|
|
2178
|
+
*
|
|
2179
|
+
* Ragged-tolerant (#2245 Blocker 2): accepts the offset the moment a HEADER
|
|
2180
|
+
* line names every required column — it deliberately does NOT additionally
|
|
2181
|
+
* require `parseMarkdownTable(text.slice(m.index)).ok`, which validates every
|
|
2182
|
+
* DATA row's cell count. A ragged sibling row anywhere in the table used to
|
|
2183
|
+
* make that whole-table parse fail, so the offset came back `null` and the
|
|
2184
|
+
* caller's `updateTableCell` calls (which scope to this offset) never even
|
|
2185
|
+
* ran against an otherwise-perfectly-findable row.
|
|
2186
|
+
*/
|
|
2187
|
+
function findTableStartOffset(text, required) {
|
|
2188
|
+
const lineRe = /^[ \t]*\|.*\|[ \t]*$/gm;
|
|
2189
|
+
let m;
|
|
2190
|
+
while ((m = lineRe.exec(text)) !== null) {
|
|
2191
|
+
const trimmed = m[0].trim();
|
|
2192
|
+
const cols = trimmed.replace(/^\|/, '').replace(/\|$/, '').split(/(?<!\\)\|/).map((c) => c.trim());
|
|
2193
|
+
if (required.every((rq) => cols.includes(rq))) {
|
|
2194
|
+
return m.index;
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
return null;
|
|
2198
|
+
}
|
|
1974
2199
|
/**
|
|
1975
2200
|
* Update the ## Performance Metrics section in STATE.md content.
|
|
1976
2201
|
* Increments Velocity totals and upserts a By Phase table row.
|
|
@@ -1982,45 +2207,128 @@ function updatePerformanceMetricsSection(content, cwd, phaseNum, planCount, summ
|
|
|
1982
2207
|
// the same phase again upserts the same row, so the column sum is stable. The previous
|
|
1983
2208
|
// blind-add (prevTotal + summaryCount) re-read the cumulative total each call and
|
|
1984
2209
|
// double-counted on every re-run. (#1582)
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
2210
|
+
//
|
|
2211
|
+
// Located by column NAME via the markdown-table seam (ADR-2143 §7) —
|
|
2212
|
+
// supersedes the prior module-level byPhaseTablePattern regex for the
|
|
2213
|
+
// existence/lookup half of this logic.
|
|
2214
|
+
const byPhaseCols = ['Phase', 'Plans', 'Total', 'Avg/Plan'];
|
|
2215
|
+
// Ragged-tolerant (#2245 Blocker 2): scope to the table's start offset
|
|
2216
|
+
// (findTableStartOffset — itself now ragged-tolerant, see above) rather
|
|
2217
|
+
// than gating existence/lookup on findTableWithColumns, which requires the
|
|
2218
|
+
// WHOLE table to parse — a ragged row for a DIFFERENT phase used to
|
|
2219
|
+
// silently no-op every phase's upsert.
|
|
2220
|
+
const tableStart = findTableStartOffset(content, byPhaseCols);
|
|
2221
|
+
if (tableStart !== null) {
|
|
1988
2222
|
// Match the existing row for this phase, tolerating leading-zero padding in either
|
|
1989
2223
|
// direction (#1659): canonicalize a numeric phase to its integer form so a seeded
|
|
1990
2224
|
// "| 05 |" row is upserted (not duplicated) by `phase complete 5`, and vice-versa.
|
|
1991
2225
|
const phaseNumStr = String(phaseNum);
|
|
1992
2226
|
const canonCell = /^\d+$/.test(phaseNumStr) ? `0*${Number(phaseNumStr)}` : escapeRegex(phaseNumStr);
|
|
1993
|
-
const
|
|
1994
|
-
const
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
2227
|
+
const phaseCellRe = new RegExp(`^${canonCell}$`, 'i');
|
|
2228
|
+
const rowMatch = (row) => phaseCellRe.test((row['Phase'] ?? '').trim());
|
|
2229
|
+
const before = content.slice(0, tableStart);
|
|
2230
|
+
let tableText = content.slice(tableStart);
|
|
2231
|
+
// Ragged-tolerant existence probe: a no-op updateTableCell write on the
|
|
2232
|
+
// identifying "Phase" column (its own tolerant row scan) decides whether
|
|
2233
|
+
// this phase's row already exists, without requiring every OTHER row in
|
|
2234
|
+
// the table to also parse cleanly.
|
|
2235
|
+
let rowExists = false;
|
|
2236
|
+
const existsProbe = (0, markdown_table_cjs_1.updateTableCell)(tableText, rowMatch, 'Phase', (current) => {
|
|
2237
|
+
rowExists = true;
|
|
2238
|
+
return current;
|
|
2239
|
+
});
|
|
2240
|
+
void existsProbe;
|
|
2241
|
+
if (rowExists) {
|
|
2242
|
+
// Update existing row — one updateTableCell call per column (Phase
|
|
2243
|
+
// itself may also change shape, e.g. "05" -> "5" per #1659).
|
|
2244
|
+
const phaseResult = (0, markdown_table_cjs_1.updateTableCell)(tableText, rowMatch, 'Phase', ` ${phaseNum} `);
|
|
2245
|
+
if (phaseResult.ok)
|
|
2246
|
+
tableText = phaseResult.value;
|
|
2247
|
+
const plansResult = (0, markdown_table_cjs_1.updateTableCell)(tableText, rowMatch, 'Plans', ` ${summaryCount} `);
|
|
2248
|
+
if (plansResult.ok)
|
|
2249
|
+
tableText = plansResult.value;
|
|
2250
|
+
const totalResult = (0, markdown_table_cjs_1.updateTableCell)(tableText, rowMatch, 'Total', ' - ');
|
|
2251
|
+
if (totalResult.ok)
|
|
2252
|
+
tableText = totalResult.value;
|
|
2253
|
+
const avgResult = (0, markdown_table_cjs_1.updateTableCell)(tableText, rowMatch, 'Avg/Plan', ' - ');
|
|
2254
|
+
if (avgResult.ok)
|
|
2255
|
+
tableText = avgResult.value;
|
|
2256
|
+
content = before + tableText;
|
|
1998
2257
|
}
|
|
1999
2258
|
else {
|
|
2000
|
-
//
|
|
2001
|
-
|
|
2002
|
-
|
|
2259
|
+
// Row doesn't exist — INSERT a new row. Row insertion (unlike a cell
|
|
2260
|
+
// update) is outside updateTableCell's scope (ADR-2143 §7 Phase 4);
|
|
2261
|
+
// `insertTableRow` (markdown-table.cjs) is its name-addressed,
|
|
2262
|
+
// header-order-agnostic sibling (#2245 audit: this used to locate the
|
|
2263
|
+
// table via `byPhaseTablePattern`, a canonical-column-ORDER-only regex,
|
|
2264
|
+
// and build the row as a hardcoded positional literal — so a reordered/
|
|
2265
|
+
// superset By-Phase header, already tolerated above by
|
|
2266
|
+
// findTableStartOffset and read by-NAME in the update/sum halves,
|
|
2267
|
+
// silently inserted NOTHING).
|
|
2268
|
+
//
|
|
2269
|
+
// Drop a lone all-placeholder row first (e.g. the freshly-scaffolded
|
|
2270
|
+
// "| - | - | - | - |" seed row) — same convention the prior
|
|
2271
|
+
// canonical-order path used, generalized to any column order/count:
|
|
2272
|
+
// a row whose every PRESENT cell is "-" is the placeholder.
|
|
2273
|
+
const placeholderRow = (row) => Object.values(row).every((cell) => cell.trim() === '-');
|
|
2274
|
+
const withoutPlaceholder = (0, markdown_table_cjs_1.deleteTableRow)(tableText, placeholderRow);
|
|
2275
|
+
if (withoutPlaceholder.ok)
|
|
2276
|
+
tableText = withoutPlaceholder.value;
|
|
2277
|
+
// Map the By-Phase values onto the table's ACTUAL header columns by
|
|
2278
|
+
// NAME — an unrecognized column (a superset header) falls back to "-",
|
|
2279
|
+
// insertTableRow's default.
|
|
2280
|
+
const valueFor = (col) => {
|
|
2281
|
+
if (col === 'Phase')
|
|
2282
|
+
return String(phaseNum);
|
|
2283
|
+
if (col === 'Plans')
|
|
2284
|
+
return String(summaryCount);
|
|
2285
|
+
if (col === 'Total' || col === 'Avg/Plan')
|
|
2286
|
+
return '-';
|
|
2287
|
+
return undefined;
|
|
2288
|
+
};
|
|
2289
|
+
const insertResult = (0, markdown_table_cjs_1.insertTableRow)(tableText, valueFor);
|
|
2290
|
+
if (insertResult.ok)
|
|
2291
|
+
tableText = insertResult.value;
|
|
2292
|
+
content = before + tableText;
|
|
2003
2293
|
}
|
|
2004
|
-
content = content.replace(byPhaseTablePattern, (_match, tableHeader) => `${tableHeader}${tableBody}\n`);
|
|
2005
2294
|
}
|
|
2006
2295
|
// Velocity: Total plans completed — DERIVED as the sum of the By-Phase Plans column
|
|
2007
|
-
//
|
|
2008
|
-
//
|
|
2009
|
-
//
|
|
2010
|
-
//
|
|
2296
|
+
// across all data rows. Idempotent by construction (re-running phase complete upserts
|
|
2297
|
+
// the same row → same sum) and self-healing (a hand-edited inflated total is corrected
|
|
2298
|
+
// to the true sum on the next completion). When the By-Phase table is absent, leave the
|
|
2299
|
+
// velocity total unchanged rather than guess. (#1582)
|
|
2300
|
+
//
|
|
2301
|
+
// Ragged-tolerant AND name-addressed (#2245 audit): each data row is split via
|
|
2302
|
+
// `splitTableRow` and its "Plans" cell located by the HEADER's own column
|
|
2303
|
+
// order (not a fixed ordinal), so a reordered/superset By-Phase header is
|
|
2304
|
+
// summed correctly instead of silently reading the wrong cell. A row that's
|
|
2305
|
+
// too short to physically contain the "Plans" column is skipped, not
|
|
2306
|
+
// treated as an error — mirrors updateTableCell's ragged-row tolerance
|
|
2307
|
+
// (a hand-edited/ragged row for one phase must not blank out the derived
|
|
2308
|
+
// total for every phase). Still scoped via findTableStartOffset so the RIGHT
|
|
2309
|
+
// table is summed when an earlier unrelated table also has a "Phase"
|
|
2310
|
+
// column (#2012).
|
|
2011
2311
|
if (/Total plans completed:\s*(\d+|\[N\])/.test(content)) {
|
|
2012
|
-
const
|
|
2013
|
-
if (
|
|
2312
|
+
const sumTableStart = findTableStartOffset(content, byPhaseCols);
|
|
2313
|
+
if (sumTableStart !== null) {
|
|
2314
|
+
const tableLines = content.slice(sumTableStart).split(/\r?\n/);
|
|
2315
|
+
const headerCells = (0, markdown_table_cjs_1.splitTableRow)(tableLines[0] ?? '');
|
|
2316
|
+
const plansIdx = headerCells.indexOf('Plans');
|
|
2014
2317
|
let sum = 0;
|
|
2015
|
-
|
|
2016
|
-
//
|
|
2017
|
-
//
|
|
2018
|
-
//
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
const
|
|
2022
|
-
|
|
2023
|
-
|
|
2318
|
+
if (plansIdx !== -1) {
|
|
2319
|
+
// The delimiter row is skipped by NAME (isDelimiterRow), not by a
|
|
2320
|
+
// hardcoded "always line index 1" assumption, so this stays
|
|
2321
|
+
// self-consistent with the ragged-tolerant read below.
|
|
2322
|
+
const delimiterCells = (0, markdown_table_cjs_1.splitTableRow)(tableLines[1] ?? '');
|
|
2323
|
+
const dataStart = (0, markdown_table_cjs_1.isDelimiterRow)(delimiterCells) ? 2 : 1;
|
|
2324
|
+
for (const row of tableLines.slice(dataStart)) {
|
|
2325
|
+
if (!row.trim().startsWith('|'))
|
|
2326
|
+
break;
|
|
2327
|
+
const cells = (0, markdown_table_cjs_1.splitTableRow)(row);
|
|
2328
|
+
if (plansIdx < cells.length && /^\d+$/.test(cells[plansIdx])) {
|
|
2329
|
+
sum += parseInt(cells[plansIdx], 10);
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2024
2332
|
}
|
|
2025
2333
|
content = content.replace(/Total plans completed:\s*(\d+|\[N\])/, `Total plans completed: ${sum}`);
|
|
2026
2334
|
}
|
|
@@ -2059,7 +2367,7 @@ function cmdStatePlannedPhase(cwd, phaseNumber, planCount, raw) {
|
|
|
2059
2367
|
const result = transitionCore(content, intent, deps);
|
|
2060
2368
|
updated = result.updated;
|
|
2061
2369
|
return result.content;
|
|
2062
|
-
}, cwd, { resync: false });
|
|
2370
|
+
}, cwd, { resync: false, deriveProgressKeys: true });
|
|
2063
2371
|
output({ updated, phase: phaseNumber, plan_count: planCount }, raw, updated.length > 0 ? 'true' : 'false');
|
|
2064
2372
|
}
|
|
2065
2373
|
/**
|
|
@@ -2137,7 +2445,11 @@ function cmdStateValidate(cwd, raw) {
|
|
|
2137
2445
|
drift['verification_status'] = { state_status: status, verification: 'passed' };
|
|
2138
2446
|
}
|
|
2139
2447
|
}
|
|
2140
|
-
catch { /*
|
|
2448
|
+
catch { /* best-effort (#2245 audit): cmdStateValidate is a diagnostic
|
|
2449
|
+
* warnings scan across N VERIFICATION.md files — one unreadable file
|
|
2450
|
+
* (permission/race) must not abort the scan of the rest; it's simply
|
|
2451
|
+
* excluded from drift detection. */
|
|
2452
|
+
}
|
|
2141
2453
|
}
|
|
2142
2454
|
// Check if all plans have summaries but status still says executing
|
|
2143
2455
|
if (diskPlans > 0 && diskSummaries >= diskPlans && /executing/i.test(status)) {
|
|
@@ -2148,7 +2460,14 @@ function cmdStateValidate(cwd, raw) {
|
|
|
2148
2460
|
}
|
|
2149
2461
|
}
|
|
2150
2462
|
}
|
|
2151
|
-
catch { /*
|
|
2463
|
+
catch { /* best-effort (#2245 audit): cmdStateValidate is a read-only
|
|
2464
|
+
* diagnostic scan of the current phase's directory (readdirSync +
|
|
2465
|
+
* scanPhasePlans). A disk-scan failure here means drift detection for
|
|
2466
|
+
* this phase is skipped for this run, degrading to "no warnings from
|
|
2467
|
+
* that scan" rather than crashing the validate command — the same
|
|
2468
|
+
* degrade-on-scan-failure pattern buildStateFrontmatter's own disk scan
|
|
2469
|
+
* already uses. */
|
|
2470
|
+
}
|
|
2152
2471
|
}
|
|
2153
2472
|
const valid = warnings.length === 0;
|
|
2154
2473
|
output({ valid, warnings, drift }, raw, undefined);
|
|
@@ -2237,33 +2556,33 @@ function cmdStateSync(cwd, options, raw) {
|
|
|
2237
2556
|
}
|
|
2238
2557
|
// Determine total phases from ROADMAP (may be larger than realized disk dirs).
|
|
2239
2558
|
// Mirrors the logic in buildStateFrontmatter so both report consistent percents (#3242 Bug B).
|
|
2559
|
+
// DEAD catch removed (#2245 audit): every operation in this block is a regex
|
|
2560
|
+
// exec/test over an already-read string plus pure Set/Math ops — none of
|
|
2561
|
+
// which can throw — so the try/catch could never be triggered.
|
|
2240
2562
|
let syncTotalPhases = null;
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
roadmapPhaseCount++;
|
|
2257
|
-
}
|
|
2258
|
-
}
|
|
2259
|
-
if (roadmapPhaseCount > 0) {
|
|
2260
|
-
syncTotalPhases = Math.max(entries.length, roadmapPhaseCount);
|
|
2261
|
-
}
|
|
2262
|
-
else {
|
|
2263
|
-
syncTotalPhases = entries.length;
|
|
2563
|
+
let roadmapPhaseCount = 0;
|
|
2564
|
+
if (syncRoadmapScope !== null) {
|
|
2565
|
+
// #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
2566
|
+
const phaseHeadingPattern = /#{2,4}\s*Phase\s+([\w][\w.-]*)(?:\s*\([^)\n]{0,200}\))?\s*:/gi;
|
|
2567
|
+
let m;
|
|
2568
|
+
while ((m = phaseHeadingPattern.exec(syncRoadmapScope)) !== null) {
|
|
2569
|
+
// Only count tokens that contain at least one digit — excludes
|
|
2570
|
+
// pure-word section headings (Overview, Details) while keeping
|
|
2571
|
+
// numeric phases (01, 05.1) and project-code IDs (PROJ-42).
|
|
2572
|
+
if (!/\d/.test(m[1]))
|
|
2573
|
+
continue;
|
|
2574
|
+
// #1514: retired/folded phases are struck through; exclude from total.
|
|
2575
|
+
if (syncRetiredPhaseNums.has(phaseKeyFromToken(m[1])))
|
|
2576
|
+
continue;
|
|
2577
|
+
roadmapPhaseCount++;
|
|
2264
2578
|
}
|
|
2265
2579
|
}
|
|
2266
|
-
|
|
2580
|
+
if (roadmapPhaseCount > 0) {
|
|
2581
|
+
syncTotalPhases = Math.max(entries.length, roadmapPhaseCount);
|
|
2582
|
+
}
|
|
2583
|
+
else {
|
|
2584
|
+
syncTotalPhases = entries.length;
|
|
2585
|
+
}
|
|
2267
2586
|
// ADR-1769 Phase 7: the body writes (Total Plans in Phase, Progress bar, Last
|
|
2268
2587
|
// Activity) are the pure `syncCore` in src/state-transition.cts.
|
|
2269
2588
|
// #1761: when a milestone version is set in frontmatter but the ROADMAP has no
|
|
@@ -2383,7 +2702,7 @@ function cmdStatePrune(cwd, options, raw) {
|
|
|
2383
2702
|
}, cwd);
|
|
2384
2703
|
// Write archived entries to STATE-ARCHIVE.md
|
|
2385
2704
|
if (archived.length > 0) {
|
|
2386
|
-
const timestamp = clock_cjs_1.realClock.
|
|
2705
|
+
const timestamp = clock_cjs_1.realClock.localToday();
|
|
2387
2706
|
let archiveContent = (0, shell_command_projection_cjs_1.platformReadSync)(archivePath);
|
|
2388
2707
|
if (archiveContent === null) {
|
|
2389
2708
|
archiveContent = '# STATE Archive\n\nPruned entries from STATE.md. Recoverable but no longer loaded into agent context.\n\n';
|
|
@@ -2562,7 +2881,7 @@ function cmdStateCompletePhase(cwd, raw, overridePhase) {
|
|
|
2562
2881
|
output({ updated: [], phase: resolvedPhase, idempotent: true, note: 'phase already superseded; no-op' }, raw, 'false');
|
|
2563
2882
|
return;
|
|
2564
2883
|
}
|
|
2565
|
-
const today = clock_cjs_1.realClock.
|
|
2884
|
+
const today = clock_cjs_1.realClock.localToday();
|
|
2566
2885
|
const updated = [];
|
|
2567
2886
|
readModifyWriteStateMd(statePath, (content) => {
|
|
2568
2887
|
const currentPhase = resolvedPhase;
|