@opengsd/gsd-core 1.7.0-rc.6 → 1.7.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/agents/gsd-executor.md +2 -1
- package/agents/gsd-security-auditor.md +13 -15
- package/bin/install.js +5 -4
- package/commands/gsd/mempalace-capture.md +27 -1
- package/commands/gsd/surface.md +6 -6
- package/gsd-core/bin/gsd-tools.cjs +63 -2
- package/gsd-core/bin/lib/api-coverage.cjs +3 -4
- package/gsd-core/bin/lib/audit.cjs +7 -6
- package/gsd-core/bin/lib/capability-registry.cjs +57 -57
- package/gsd-core/bin/lib/check-command-router.cjs +1 -1
- package/gsd-core/bin/lib/clock.cjs +19 -0
- package/gsd-core/bin/lib/commands.cjs +48 -9
- package/gsd-core/bin/lib/config-loader.cjs +6 -2
- package/gsd-core/bin/lib/config.cjs +12 -0
- package/gsd-core/bin/lib/core-utils.cjs +8 -2
- package/gsd-core/bin/lib/drift.cjs +4 -4
- package/gsd-core/bin/lib/frontmatter.cjs +22 -0
- package/gsd-core/bin/lib/gsd2-import.cjs +2 -1
- package/gsd-core/bin/lib/init.cjs +27 -13
- package/gsd-core/bin/lib/install-engine.cjs +3 -2
- package/gsd-core/bin/lib/installer-migration-authoring.cjs +2 -1
- package/gsd-core/bin/lib/installer-migrations.cjs +1 -1
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +342 -0
- package/gsd-core/bin/lib/markdown-table.cjs +698 -0
- package/gsd-core/bin/lib/milestone.cjs +217 -31
- package/gsd-core/bin/lib/phase-command-router.cjs +50 -2
- 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 +436 -61
- package/gsd-core/bin/lib/plan-scan.cjs +3 -0
- package/gsd-core/bin/lib/roadmap-parser.cjs +218 -13
- package/gsd-core/bin/lib/roadmap.cjs +100 -49
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +7 -6
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +3 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +2 -1
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +25 -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 +4 -3
- package/gsd-core/bin/lib/state-transition.cjs +100 -45
- package/gsd-core/bin/lib/state.cjs +391 -126
- package/gsd-core/bin/lib/surface.cjs +2 -2
- package/gsd-core/bin/lib/template.cjs +2 -1
- package/gsd-core/bin/lib/uat.cjs +54 -8
- package/gsd-core/bin/lib/ui-safety-gate.cjs +23 -1
- package/gsd-core/bin/lib/verify.cjs +4 -3
- 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 +2 -0
- package/gsd-core/references/checkpoints.md +12 -0
- package/gsd-core/workflows/add-tests.md +1 -1
- package/gsd-core/workflows/debug.md +2 -0
- package/gsd-core/workflows/execute-phase.md +5 -3
- package/gsd-core/workflows/fast.md +8 -22
- package/gsd-core/workflows/progress.md +2 -2
- package/gsd-core/workflows/quick.md +2 -0
- package/gsd-core/workflows/review.md +42 -3
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/ship.md +8 -2
- package/gsd-core/workflows/spec-phase.md +1 -1
- package/gsd-core/workflows/transition.md +1 -1
- package/hooks/dist/gsd-statusline.js +164 -14
- package/hooks/gsd-statusline.js +164 -14
- package/package.json +4 -2
- package/scripts/ci-test-scope.cjs +39 -1
- package/scripts/gen-golden-install-parity-zcode.cjs +35 -45
- package/scripts/gen-install-tree-fixtures.cjs +75 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -1
- package/scripts/lint-table-schema-drift.cjs +157 -0
- package/skills/gsd-mempalace-capture/SKILL.md +27 -1
- 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
|
|
@@ -381,29 +385,131 @@ function cmdStateRecordMetric(cwd, options, raw) {
|
|
|
381
385
|
let _recorded = false;
|
|
382
386
|
let created = false;
|
|
383
387
|
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
388
|
const newRow = `| Phase ${phase} P${plan} | ${duration} | ${tasks || '-'} tasks | ${files || '-'} files |`;
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
389
|
+
// Find the "## Performance Metrics" section via the markdown-sectionizer
|
|
390
|
+
// seam (ADR-2143 §7) — supersedes the prior hand-rolled section+table
|
|
391
|
+
// regex.
|
|
392
|
+
const metricsSection = (0, markdown_sectionizer_cjs_1.collectSection)(content, (h) => /^performance metrics$/i.test(h.text.trim()));
|
|
393
|
+
const eol = metricsSection && /\r\n/.test(metricsSection.body) ? '\r\n' : '\n';
|
|
394
|
+
const lines = metricsSection ? metricsSection.body.split(/\r?\n/) : [];
|
|
395
|
+
// Locate THIS command's OWN metrics table by its HEADER shape, using the
|
|
396
|
+
// exact same splitTableRow/isDelimiterRow header/delimiter-shape checks
|
|
397
|
+
// `parseMarkdownTable` uses. A live "## Performance Metrics" section
|
|
398
|
+
// (gsd-core/templates/state.md:39-56) also carries the "By Phase"
|
|
399
|
+
// velocity table (`| Phase | Plans | Total | Avg/Plan |`) — the prior
|
|
400
|
+
// "first table in the section" targeting spliced every per-plan row into
|
|
401
|
+
// THAT table instead, polluting it on EVERY plan completion
|
|
402
|
+
// (execute-plan.md:414 calls record-metric per-plan) (#2245/#2143).
|
|
403
|
+
// Matching the header cells to this command's own canonical
|
|
404
|
+
// `Plan | Duration | Tasks | Files` shape (case-insensitive/trimmed)
|
|
405
|
+
// finds the right table regardless of what else shares the section, and
|
|
406
|
+
// deliberately does NOT require `parseMarkdownTable(...).ok` (which
|
|
407
|
+
// additionally requires every DATA row's cell count to match the
|
|
408
|
+
// header) — a single ragged sibling row (a hand-edited stray/extra pipe)
|
|
409
|
+
// must not blind this scan (#2245 Blocker 2 parity with the other
|
|
410
|
+
// Phase-4 ragged-tolerance fixes: updateTableCell / findTableStartOffset).
|
|
411
|
+
const METRICS_HEADER = ['plan', 'duration', 'tasks', 'files'];
|
|
412
|
+
let headerIdx = -1;
|
|
413
|
+
for (let i = 0; i < lines.length - 1; i++) {
|
|
414
|
+
const trimmed = lines[i].trim();
|
|
415
|
+
if (!trimmed.startsWith('|') || trimmed.indexOf('|', 1) === -1)
|
|
416
|
+
continue;
|
|
417
|
+
const delimiterLine = lines[i + 1];
|
|
418
|
+
if (delimiterLine === undefined || !delimiterLine.trim().startsWith('|'))
|
|
419
|
+
continue;
|
|
420
|
+
const headerCells = (0, markdown_table_cjs_1.splitTableRow)(lines[i]);
|
|
421
|
+
const delimiterCells = (0, markdown_table_cjs_1.splitTableRow)(delimiterLine);
|
|
422
|
+
if (!(0, markdown_table_cjs_1.isDelimiterRow)(delimiterCells) || delimiterCells.length !== headerCells.length)
|
|
423
|
+
continue;
|
|
424
|
+
const normalized = headerCells.map((cell) => cell.trim().toLowerCase());
|
|
425
|
+
const isMetricsHeader = normalized.length === METRICS_HEADER.length
|
|
426
|
+
&& normalized.every((cell, idx) => cell === METRICS_HEADER[idx]);
|
|
427
|
+
if (isMetricsHeader) {
|
|
428
|
+
headerIdx = i;
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
const hasTable = headerIdx !== -1;
|
|
433
|
+
if (metricsSection && hasTable) {
|
|
434
|
+
const delimiterIdx = headerIdx + 1;
|
|
435
|
+
const prefixLines = lines.slice(0, delimiterIdx + 1);
|
|
436
|
+
// Ragged-tolerant row scan: every consecutive `|`-prefixed line
|
|
437
|
+
// following the delimiter counts as an existing row REGARDLESS of its
|
|
438
|
+
// cell count matching the header — a ragged sibling row must never
|
|
439
|
+
// blind this scan to the table's true last row (unlike
|
|
440
|
+
// `parsedTable.value.rows.length`, which this replaces). Anchored to
|
|
441
|
+
// the METRICS table's OWN header/delimiter (`headerIdx` above), never
|
|
442
|
+
// the section's first table (#2245/#2143).
|
|
443
|
+
let lastRowIdx = delimiterIdx;
|
|
444
|
+
for (let i = delimiterIdx + 1; i < lines.length; i++) {
|
|
445
|
+
if (!lines[i].trim().startsWith('|'))
|
|
446
|
+
break;
|
|
447
|
+
lastRowIdx = i;
|
|
448
|
+
}
|
|
449
|
+
const rowCount = lastRowIdx - delimiterIdx;
|
|
450
|
+
_recorded = true;
|
|
451
|
+
let newBody;
|
|
452
|
+
if (rowCount > 0) {
|
|
453
|
+
// Splice the new row immediately after the table's LAST existing data
|
|
454
|
+
// row — every other byte of the section, INCLUDING any trailing prose
|
|
455
|
+
// that follows the table (e.g. the default template's "**Recent
|
|
456
|
+
// Trend:**" subsection + "*Updated after each plan completion*"
|
|
457
|
+
// footer), is preserved verbatim. The prior implementation truncated
|
|
458
|
+
// the section body to header+delimiter+rows+newRow, silently dropping
|
|
459
|
+
// everything that followed the table on a live STATE.md (#2245
|
|
460
|
+
// Blocker 1 — a per-plan path, run after every plan execution).
|
|
461
|
+
// `lastRowIdx` (computed above by the ragged-tolerant scan) already
|
|
462
|
+
// equals `delimiterIdx + rowCount` by construction.
|
|
463
|
+
const before = lines.slice(0, lastRowIdx + 1);
|
|
464
|
+
const after = lines.slice(lastRowIdx + 1);
|
|
465
|
+
newBody = [...before, newRow, ...after].join(eol);
|
|
392
466
|
}
|
|
393
467
|
else {
|
|
394
|
-
|
|
468
|
+
// No existing data rows (e.g. a "None yet" placeholder line instead of
|
|
469
|
+
// a real row) — replace the placeholder/table-body remainder with the
|
|
470
|
+
// new row, matching the section's prior (verified) collapse-to-
|
|
471
|
+
// first-row behavior for an otherwise-empty table.
|
|
472
|
+
// No trailing eol here: replaceSection's `content.slice(bodyEnd)`
|
|
473
|
+
// already supplies the newline(s) that followed the (trimEnd()-ed)
|
|
474
|
+
// section body.
|
|
475
|
+
newBody = prefixLines.join(eol) + eol + newRow;
|
|
395
476
|
}
|
|
477
|
+
return (0, markdown_sectionizer_cjs_1.replaceSection)(content, metricsSection, newBody);
|
|
478
|
+
}
|
|
479
|
+
if (metricsSection) {
|
|
480
|
+
// Section EXISTS but carries no metrics table of its own — e.g. a live
|
|
481
|
+
// STATE.md whose "## Performance Metrics" section holds only the
|
|
482
|
+
// By-Phase velocity table (gsd-core/templates/state.md:48). Self-heal
|
|
483
|
+
// by appending a fresh Per-Plan Metrics table to the END of the
|
|
484
|
+
// section body — every existing byte (By-Phase table, Recent Trend,
|
|
485
|
+
// footer) is preserved verbatim, and no second "## Performance
|
|
486
|
+
// Metrics" heading is introduced. The section already existed, so
|
|
487
|
+
// `created` stays false (#2245/#2143).
|
|
396
488
|
_recorded = true;
|
|
397
|
-
|
|
489
|
+
const newBody = metricsSection.body
|
|
490
|
+
+ eol + '**Per-Plan Metrics:**'
|
|
491
|
+
+ eol + eol
|
|
492
|
+
+ '| Plan | Duration | Tasks | Files |'
|
|
493
|
+
+ eol
|
|
494
|
+
+ '|------|----------|-------|-------|'
|
|
495
|
+
+ eol
|
|
496
|
+
+ newRow
|
|
497
|
+
+ eol;
|
|
498
|
+
return (0, markdown_sectionizer_cjs_1.replaceSection)(content, metricsSection, newBody);
|
|
398
499
|
}
|
|
399
|
-
// Section absent — DWIM: auto-create canonical
|
|
400
|
-
// then append the row. Matches state
|
|
500
|
+
// Section absent (or malformed) — DWIM: auto-create canonical
|
|
501
|
+
// ## Performance Metrics scaffold, then append the row. Matches state
|
|
502
|
+
// begin-phase / advance-plan DWIM behavior. Header corrected to this
|
|
503
|
+
// command's own canonical shape (`Plan | Duration | Tasks | Files`) —
|
|
504
|
+
// the prior scaffold's `| Phase | Plan | Duration | Notes |` header
|
|
505
|
+
// matched neither the appended row's shape nor the canonical table
|
|
506
|
+
// above (#2245/#2143).
|
|
401
507
|
const scaffold = [
|
|
402
508
|
'',
|
|
403
509
|
'## Performance Metrics',
|
|
404
510
|
'',
|
|
405
|
-
'|
|
|
406
|
-
'
|
|
511
|
+
'| Plan | Duration | Tasks | Files |',
|
|
512
|
+
'|------|----------|-------|-------|',
|
|
407
513
|
newRow,
|
|
408
514
|
'',
|
|
409
515
|
].join('\n');
|
|
@@ -447,18 +553,31 @@ function cmdStateUpdateProgress(cwd, raw) {
|
|
|
447
553
|
const _totalPlans = totalPlans;
|
|
448
554
|
const _totalSummaries = totalSummaries;
|
|
449
555
|
readModifyWriteStateMd(statePath, (content) => {
|
|
450
|
-
//
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
556
|
+
// #2177: match against the BODY only. With /i the patterns below would
|
|
557
|
+
// otherwise hit the YAML frontmatter `progress:` key first (and `\s*` would
|
|
558
|
+
// eat its newline, mangling the nested block), while the body Progress: line
|
|
559
|
+
// — which frontmatter `percent` is re-derived from on every write — stays
|
|
560
|
+
// stale and silently reverts the update.
|
|
561
|
+
const body = stripFrontmatter(content);
|
|
562
|
+
const fmPrefix = content.slice(0, content.length - body.length);
|
|
563
|
+
// Swap only the machine segment ("[bar] NN%" or bare "NN%"), preserving any
|
|
564
|
+
// descriptive suffix an agent authored, e.g. "(2/4 plans done; blocked on…)".
|
|
565
|
+
const machineSegment = /(?:\[[^\]\r\n]*\][ \t]*)?\d{1,3}%/;
|
|
566
|
+
const replaceValue = (value) => machineSegment.test(value)
|
|
567
|
+
? value.replace(machineSegment, progressStr)
|
|
568
|
+
: progressStr;
|
|
569
|
+
// Try **Progress:** bold format first, then plain Progress: format.
|
|
570
|
+
const boldProgressPattern = /(\*\*Progress:\*\*[ \t]*)([^\r\n]*)/i;
|
|
571
|
+
const plainProgressPattern = /^(Progress:[ \t]*)([^\r\n]*)/im;
|
|
572
|
+
const pattern = boldProgressPattern.test(body)
|
|
573
|
+
? boldProgressPattern
|
|
574
|
+
: plainProgressPattern.test(body)
|
|
575
|
+
? plainProgressPattern
|
|
576
|
+
: null;
|
|
577
|
+
if (!pattern)
|
|
578
|
+
return content;
|
|
579
|
+
updated = true;
|
|
580
|
+
return fmPrefix + body.replace(pattern, (_match, prefix, value) => `${prefix}${replaceValue(value)}`);
|
|
462
581
|
}, cwd);
|
|
463
582
|
if (updated) {
|
|
464
583
|
output({ updated: true, percent, completed: _totalSummaries, total: _totalPlans, bar: progressStr }, raw, progressStr);
|
|
@@ -961,15 +1080,22 @@ function cmdStateRecordSession(cwd, options, raw) {
|
|
|
961
1080
|
* Match the session section body from a STATE.md body. #1101: recognise the
|
|
962
1081
|
* bootstrap `## Session Continuity` heading but PREFER the normalized `## Session`
|
|
963
1082
|
* block when both exist (legacy duplicate files), so the reader agrees with the
|
|
964
|
-
* writer (which updates `## Session` first).
|
|
965
|
-
*
|
|
966
|
-
*
|
|
967
|
-
*
|
|
968
|
-
*
|
|
1083
|
+
* writer (which updates `## Session` first). Level-2-exact heading match
|
|
1084
|
+
* (excludes an h3 `### Session Continuity`); the exact `'session continuity'`
|
|
1085
|
+
* text match still excludes `## Session Continuity Archive` (preserving the
|
|
1086
|
+
* #2444 scoping). Migrated onto the `collectSection` seam (#2143 audit,
|
|
1087
|
+
* epic #2143): CRLF-safe — the prior hand-rolled `[ \t]*\n` regex silently
|
|
1088
|
+
* failed to match a CRLF `## Session\r\n` heading line (the `\r` broke the
|
|
1089
|
+
* `[ \t]*\n` boundary); `tokenizeHeadings` strips the trailing `\r` before
|
|
1090
|
+
* heading-text extraction, so this now matches CRLF headings too.
|
|
1091
|
+
* Returns the section body, or null.
|
|
969
1092
|
*/
|
|
970
1093
|
function matchSessionSection(body) {
|
|
971
|
-
|
|
972
|
-
|
|
1094
|
+
const isSession = (h) => h.level === 2 && h.text.trim().toLowerCase() === 'session';
|
|
1095
|
+
const isSessionContinuity = (h) => h.level === 2 && h.text.trim().toLowerCase() === 'session continuity';
|
|
1096
|
+
const section = (0, markdown_sectionizer_cjs_1.collectSection)(body, isSession, { levelBounded: true })
|
|
1097
|
+
?? (0, markdown_sectionizer_cjs_1.collectSection)(body, isSessionContinuity, { levelBounded: true });
|
|
1098
|
+
return section ? section.body : null;
|
|
973
1099
|
}
|
|
974
1100
|
function parseProsePhaseField(value) {
|
|
975
1101
|
// #2121 Phase 2 (#2125): delegate to the canonical anchored parser so this
|
|
@@ -1036,14 +1162,15 @@ function cmdStateSnapshot(cwd, raw) {
|
|
|
1036
1162
|
const totalPhases = totalPhasesRaw ? parseInt(totalPhasesRaw, 10) : null;
|
|
1037
1163
|
const totalPlansInPhase = totalPlansRaw ? parseInt(totalPlansRaw, 10) : null;
|
|
1038
1164
|
const progressPercent = progressRaw ? parseInt(progressRaw.replace('%', ''), 10) : null;
|
|
1039
|
-
// Extract decisions table
|
|
1165
|
+
// Extract decisions table — via the markdown-sectionizer/markdown-table
|
|
1166
|
+
// seams (ADR-2143 §7), cells addressed by column NAME rather than a
|
|
1167
|
+
// hand-rolled section+table regex.
|
|
1040
1168
|
const decisions = [];
|
|
1041
|
-
const
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
const
|
|
1045
|
-
|
|
1046
|
-
const cells = row.split('|').map(c => c.trim()).filter(Boolean);
|
|
1169
|
+
const decisionsSection = (0, markdown_sectionizer_cjs_1.collectSection)(body, (h) => /^decisions made$/i.test(h.text.trim()));
|
|
1170
|
+
const decisionsTable = decisionsSection ? (0, markdown_table_cjs_1.parseMarkdownTable)(decisionsSection.body) : null;
|
|
1171
|
+
if (decisionsTable && decisionsTable.ok) {
|
|
1172
|
+
for (const row of decisionsTable.value.rows) {
|
|
1173
|
+
const cells = decisionsTable.value.columns.map((c) => (row[c] ?? '').trim()).filter(Boolean);
|
|
1047
1174
|
if (cells.length >= 3) {
|
|
1048
1175
|
decisions.push({
|
|
1049
1176
|
phase: cells[0],
|
|
@@ -1055,10 +1182,9 @@ function cmdStateSnapshot(cwd, raw) {
|
|
|
1055
1182
|
}
|
|
1056
1183
|
// Extract blockers list
|
|
1057
1184
|
const blockers = [];
|
|
1058
|
-
const
|
|
1059
|
-
if (
|
|
1060
|
-
const
|
|
1061
|
-
const items = blockersSection.match(/^-\s+(.+)$/gm) || [];
|
|
1185
|
+
const blockersSection = (0, markdown_sectionizer_cjs_1.collectSection)(body, (h) => h.level === 2 && h.text.trim().toLowerCase() === 'blockers', { levelBounded: true });
|
|
1186
|
+
if (blockersSection) {
|
|
1187
|
+
const items = blockersSection.body.match(/^-\s+(.+)$/gm) || [];
|
|
1062
1188
|
for (const item of items) {
|
|
1063
1189
|
blockers.push(item.replace(/^-\s+/, '').trim());
|
|
1064
1190
|
}
|
|
@@ -1072,8 +1198,8 @@ function cmdStateSnapshot(cwd, raw) {
|
|
|
1072
1198
|
// #1101: prefer the canonical `## Session` block, falling back to the bootstrap
|
|
1073
1199
|
// `## Session Continuity` heading. See matchSessionSection for the anchoring.
|
|
1074
1200
|
const sessionMatch = matchSessionSection(body);
|
|
1075
|
-
if (sessionMatch) {
|
|
1076
|
-
const sessionSection = sessionMatch
|
|
1201
|
+
if (sessionMatch !== null) {
|
|
1202
|
+
const sessionSection = sessionMatch;
|
|
1077
1203
|
// Accept both `**Last Date:**` (canonical template form) and `**Last session:**`
|
|
1078
1204
|
// (the form written by the DWIM auto-create / normalize path added for #944).
|
|
1079
1205
|
const lastDateMatch = sessionSection.match(/\*\*Last Date:\*\*\s*(.+)/i)
|
|
@@ -1191,18 +1317,19 @@ function buildStateFrontmatter(bodyContent, cwd) {
|
|
|
1191
1317
|
// #1101: prefer the canonical `## Session` block, falling back to the bootstrap
|
|
1192
1318
|
// `## Session Continuity` heading. See matchSessionSection for the anchoring.
|
|
1193
1319
|
const sessionSectionMatch = matchSessionSection(bodyContent);
|
|
1194
|
-
const sessionBodyScope = sessionSectionMatch
|
|
1320
|
+
const sessionBodyScope = sessionSectionMatch ?? bodyContent;
|
|
1195
1321
|
const stoppedAt = (0, state_document_cjs_1.stateExtractField)(sessionBodyScope, 'Stopped At') || (0, state_document_cjs_1.stateExtractField)(sessionBodyScope, 'Stopped at');
|
|
1196
1322
|
const pausedAt = (0, state_document_cjs_1.stateExtractField)(bodyContent, 'Paused At');
|
|
1197
1323
|
let milestone = null;
|
|
1198
1324
|
let milestoneName = null;
|
|
1199
1325
|
if (cwd) {
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1326
|
+
// DEAD catch removed (#2245 audit): getMilestoneInfo has its own outer
|
|
1327
|
+
// try/catch (roadmap-parser.cts) that already swallows every internal
|
|
1328
|
+
// failure and always returns a MilestoneInfo — it never throws, so this
|
|
1329
|
+
// wrapper could never be triggered.
|
|
1330
|
+
const info = getMilestoneInfo(cwd);
|
|
1331
|
+
milestone = info.version;
|
|
1332
|
+
milestoneName = info.name;
|
|
1206
1333
|
}
|
|
1207
1334
|
let totalPhases = totalPhasesRaw ? parseInt(totalPhasesRaw, 10) : null;
|
|
1208
1335
|
let completedPhases = null;
|
|
@@ -1336,6 +1463,13 @@ function buildStateFrontmatter(bodyContent, cwd) {
|
|
|
1336
1463
|
completedPlans = cached.completedPlans;
|
|
1337
1464
|
milestoneUnbounded = cached.milestoneBounded === false;
|
|
1338
1465
|
}
|
|
1466
|
+
/* best-effort (#2245 audit): this is a READ path building STATE.md's
|
|
1467
|
+
* display frontmatter. The real throw source is fs.readdirSync(phasesDir)
|
|
1468
|
+
* a few lines up — an inaccessible/racily-removed phases dir must not
|
|
1469
|
+
* crash `state show`; on failure this simply keeps whatever
|
|
1470
|
+
* frontmatter-derived totals/completedPhases/etc. were already set
|
|
1471
|
+
* above, a graceful degrade rather than a corrupted write (nothing is
|
|
1472
|
+
* persisted from this block). */
|
|
1339
1473
|
}
|
|
1340
1474
|
catch { /* intentionally empty */ }
|
|
1341
1475
|
}
|
|
@@ -1391,19 +1525,6 @@ function buildStateFrontmatter(bodyContent, cwd) {
|
|
|
1391
1525
|
fm['progress'] = progress;
|
|
1392
1526
|
return fm;
|
|
1393
1527
|
}
|
|
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
1528
|
function syncStateFrontmatter(content, cwd) {
|
|
1408
1529
|
// Read existing frontmatter BEFORE stripping — it may contain values
|
|
1409
1530
|
// that the body no longer has (e.g., Status field removed by an agent).
|
|
@@ -1425,7 +1546,17 @@ function syncStateFrontmatter(content, cwd) {
|
|
|
1425
1546
|
// existing frontmatter already holds; only an empty derived value falls through
|
|
1426
1547
|
// to this guard (the primary #905 preserve path below handles that).
|
|
1427
1548
|
const MILESTONE_NAME_PLACEHOLDER = 'milestone';
|
|
1428
|
-
|
|
1549
|
+
// #2135: widen the preserve guard. A bad derive is not always the literal
|
|
1550
|
+
// placeholder — getMilestoneInfo can return a delimiter-led fragment
|
|
1551
|
+
// ("— Active Milestone") when the roadmap regex mis-binds. Preserve the
|
|
1552
|
+
// existing curated name unless the derived value actually looks like a name:
|
|
1553
|
+
// non-empty, not the placeholder, and not punctuation-led.
|
|
1554
|
+
const derivedName = derivedFm['milestone_name'];
|
|
1555
|
+
const derivedLooksLikeName = typeof derivedName === 'string'
|
|
1556
|
+
&& derivedName.length > 0
|
|
1557
|
+
&& derivedName !== MILESTONE_NAME_PLACEHOLDER
|
|
1558
|
+
&& !/^[\s—–:-]/.test(derivedName);
|
|
1559
|
+
if (!derivedLooksLikeName &&
|
|
1429
1560
|
existingFm['milestone_name'] &&
|
|
1430
1561
|
existingFm['milestone_name'] !== MILESTONE_NAME_PLACEHOLDER) {
|
|
1431
1562
|
derivedFm['milestone_name'] = existingFm['milestone_name'];
|
|
@@ -1472,6 +1603,15 @@ function syncStateFrontmatter(content, cwd) {
|
|
|
1472
1603
|
if (!derivedFm['progress'] && existingFm['progress']) {
|
|
1473
1604
|
derivedFm['progress'] = (0, state_document_cjs_1.normalizeProgressNumbers)(existingFm['progress']);
|
|
1474
1605
|
}
|
|
1606
|
+
// #2202: carry forward any existing frontmatter key that the schema does not
|
|
1607
|
+
// own, so custom/unknown keys are not silently dropped on every mutating verb.
|
|
1608
|
+
// Schema-owned keys (already in derivedFm from buildStateFrontmatter + the
|
|
1609
|
+
// preserve guards above) still win.
|
|
1610
|
+
for (const key of Object.keys(existingFm)) {
|
|
1611
|
+
if (!(key in derivedFm) && existingFm[key] !== undefined) {
|
|
1612
|
+
derivedFm[key] = existingFm[key];
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1475
1615
|
const yamlStr = reconstructFrontmatter(derivedFm);
|
|
1476
1616
|
return `---\n${yamlStr}\n---\n\n${body}`;
|
|
1477
1617
|
}
|
|
@@ -1786,7 +1926,7 @@ function readModifyWriteStateMd(statePath, transformFn, cwd, options, clock) {
|
|
|
1786
1926
|
// A stale "Stopped at:" in a non-Session section (e.g. Session Continuity
|
|
1787
1927
|
// Archive prose) must not interfere with the delta comparison.
|
|
1788
1928
|
const preSessionMatch = matchSessionSection(preBody);
|
|
1789
|
-
const preSessionScope = preSessionMatch
|
|
1929
|
+
const preSessionScope = preSessionMatch ?? preBody;
|
|
1790
1930
|
const preBodyStoppedAt = (0, state_document_cjs_1.stateExtractField)(preSessionScope, 'Stopped At') || (0, state_document_cjs_1.stateExtractField)(preSessionScope, 'Stopped at');
|
|
1791
1931
|
// ADR-1769 Phase 6 / #1743 / #1695: snapshot the body source for the curated
|
|
1792
1932
|
// current_phase_name (the `Phase:` line parseProsePhaseField harvests). When
|
|
@@ -1815,7 +1955,7 @@ function readModifyWriteStateMd(statePath, transformFn, cwd, options, clock) {
|
|
|
1815
1955
|
// Bug #1230 / Change B: scope stopped_at delta to the ## Session section,
|
|
1816
1956
|
// consistent with the pre-transform snapshot above and buildStateFrontmatter.
|
|
1817
1957
|
const postSessionMatch = matchSessionSection(postBody);
|
|
1818
|
-
const postSessionScope = postSessionMatch
|
|
1958
|
+
const postSessionScope = postSessionMatch ?? postBody;
|
|
1819
1959
|
const postBodyStoppedAt = (0, state_document_cjs_1.stateExtractField)(postSessionScope, 'Stopped At') || (0, state_document_cjs_1.stateExtractField)(postSessionScope, 'Stopped at');
|
|
1820
1960
|
// ADR-1769 Phase 6 / #1695: post-transform body Phase source for the
|
|
1821
1961
|
// current_phase_name delta comparison.
|
|
@@ -1971,6 +2111,37 @@ function cmdSignalResume(cwd, raw) {
|
|
|
1971
2111
|
output({ resumed: true, removed }, raw, removed ? 'true' : 'false');
|
|
1972
2112
|
}
|
|
1973
2113
|
// ─── Gate Functions (STATE.md consistency enforcement) ────────────────────────
|
|
2114
|
+
/**
|
|
2115
|
+
* Find the character offset where the FIRST GFM table whose header is a
|
|
2116
|
+
* superset of `required` column names begins (order-independent; extra
|
|
2117
|
+
* columns tolerated) — the position-aware counterpart to markdown-table's
|
|
2118
|
+
* `findTableWithColumns`, used to scope `updateTableCell` (which always
|
|
2119
|
+
* operates on "the first table in its input") to the RIGHT table when an
|
|
2120
|
+
* unrelated earlier table (that doesn't itself name every required column)
|
|
2121
|
+
* may precede it in the same document. Returns `null` when no such table is
|
|
2122
|
+
* found. Never trips the table-regex fingerprint (no `[^|]` cell-capture
|
|
2123
|
+
* class) and never throws.
|
|
2124
|
+
*
|
|
2125
|
+
* Ragged-tolerant (#2245 Blocker 2): accepts the offset the moment a HEADER
|
|
2126
|
+
* line names every required column — it deliberately does NOT additionally
|
|
2127
|
+
* require `parseMarkdownTable(text.slice(m.index)).ok`, which validates every
|
|
2128
|
+
* DATA row's cell count. A ragged sibling row anywhere in the table used to
|
|
2129
|
+
* make that whole-table parse fail, so the offset came back `null` and the
|
|
2130
|
+
* caller's `updateTableCell` calls (which scope to this offset) never even
|
|
2131
|
+
* ran against an otherwise-perfectly-findable row.
|
|
2132
|
+
*/
|
|
2133
|
+
function findTableStartOffset(text, required) {
|
|
2134
|
+
const lineRe = /^[ \t]*\|.*\|[ \t]*$/gm;
|
|
2135
|
+
let m;
|
|
2136
|
+
while ((m = lineRe.exec(text)) !== null) {
|
|
2137
|
+
const trimmed = m[0].trim();
|
|
2138
|
+
const cols = trimmed.replace(/^\|/, '').replace(/\|$/, '').split(/(?<!\\)\|/).map((c) => c.trim());
|
|
2139
|
+
if (required.every((rq) => cols.includes(rq))) {
|
|
2140
|
+
return m.index;
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
return null;
|
|
2144
|
+
}
|
|
1974
2145
|
/**
|
|
1975
2146
|
* Update the ## Performance Metrics section in STATE.md content.
|
|
1976
2147
|
* Increments Velocity totals and upserts a By Phase table row.
|
|
@@ -1982,45 +2153,128 @@ function updatePerformanceMetricsSection(content, cwd, phaseNum, planCount, summ
|
|
|
1982
2153
|
// the same phase again upserts the same row, so the column sum is stable. The previous
|
|
1983
2154
|
// blind-add (prevTotal + summaryCount) re-read the cumulative total each call and
|
|
1984
2155
|
// double-counted on every re-run. (#1582)
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
2156
|
+
//
|
|
2157
|
+
// Located by column NAME via the markdown-table seam (ADR-2143 §7) —
|
|
2158
|
+
// supersedes the prior module-level byPhaseTablePattern regex for the
|
|
2159
|
+
// existence/lookup half of this logic.
|
|
2160
|
+
const byPhaseCols = ['Phase', 'Plans', 'Total', 'Avg/Plan'];
|
|
2161
|
+
// Ragged-tolerant (#2245 Blocker 2): scope to the table's start offset
|
|
2162
|
+
// (findTableStartOffset — itself now ragged-tolerant, see above) rather
|
|
2163
|
+
// than gating existence/lookup on findTableWithColumns, which requires the
|
|
2164
|
+
// WHOLE table to parse — a ragged row for a DIFFERENT phase used to
|
|
2165
|
+
// silently no-op every phase's upsert.
|
|
2166
|
+
const tableStart = findTableStartOffset(content, byPhaseCols);
|
|
2167
|
+
if (tableStart !== null) {
|
|
1988
2168
|
// Match the existing row for this phase, tolerating leading-zero padding in either
|
|
1989
2169
|
// direction (#1659): canonicalize a numeric phase to its integer form so a seeded
|
|
1990
2170
|
// "| 05 |" row is upserted (not duplicated) by `phase complete 5`, and vice-versa.
|
|
1991
2171
|
const phaseNumStr = String(phaseNum);
|
|
1992
2172
|
const canonCell = /^\d+$/.test(phaseNumStr) ? `0*${Number(phaseNumStr)}` : escapeRegex(phaseNumStr);
|
|
1993
|
-
const
|
|
1994
|
-
const
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
2173
|
+
const phaseCellRe = new RegExp(`^${canonCell}$`, 'i');
|
|
2174
|
+
const rowMatch = (row) => phaseCellRe.test((row['Phase'] ?? '').trim());
|
|
2175
|
+
const before = content.slice(0, tableStart);
|
|
2176
|
+
let tableText = content.slice(tableStart);
|
|
2177
|
+
// Ragged-tolerant existence probe: a no-op updateTableCell write on the
|
|
2178
|
+
// identifying "Phase" column (its own tolerant row scan) decides whether
|
|
2179
|
+
// this phase's row already exists, without requiring every OTHER row in
|
|
2180
|
+
// the table to also parse cleanly.
|
|
2181
|
+
let rowExists = false;
|
|
2182
|
+
const existsProbe = (0, markdown_table_cjs_1.updateTableCell)(tableText, rowMatch, 'Phase', (current) => {
|
|
2183
|
+
rowExists = true;
|
|
2184
|
+
return current;
|
|
2185
|
+
});
|
|
2186
|
+
void existsProbe;
|
|
2187
|
+
if (rowExists) {
|
|
2188
|
+
// Update existing row — one updateTableCell call per column (Phase
|
|
2189
|
+
// itself may also change shape, e.g. "05" -> "5" per #1659).
|
|
2190
|
+
const phaseResult = (0, markdown_table_cjs_1.updateTableCell)(tableText, rowMatch, 'Phase', ` ${phaseNum} `);
|
|
2191
|
+
if (phaseResult.ok)
|
|
2192
|
+
tableText = phaseResult.value;
|
|
2193
|
+
const plansResult = (0, markdown_table_cjs_1.updateTableCell)(tableText, rowMatch, 'Plans', ` ${summaryCount} `);
|
|
2194
|
+
if (plansResult.ok)
|
|
2195
|
+
tableText = plansResult.value;
|
|
2196
|
+
const totalResult = (0, markdown_table_cjs_1.updateTableCell)(tableText, rowMatch, 'Total', ' - ');
|
|
2197
|
+
if (totalResult.ok)
|
|
2198
|
+
tableText = totalResult.value;
|
|
2199
|
+
const avgResult = (0, markdown_table_cjs_1.updateTableCell)(tableText, rowMatch, 'Avg/Plan', ' - ');
|
|
2200
|
+
if (avgResult.ok)
|
|
2201
|
+
tableText = avgResult.value;
|
|
2202
|
+
content = before + tableText;
|
|
1998
2203
|
}
|
|
1999
2204
|
else {
|
|
2000
|
-
//
|
|
2001
|
-
|
|
2002
|
-
|
|
2205
|
+
// Row doesn't exist — INSERT a new row. Row insertion (unlike a cell
|
|
2206
|
+
// update) is outside updateTableCell's scope (ADR-2143 §7 Phase 4);
|
|
2207
|
+
// `insertTableRow` (markdown-table.cjs) is its name-addressed,
|
|
2208
|
+
// header-order-agnostic sibling (#2245 audit: this used to locate the
|
|
2209
|
+
// table via `byPhaseTablePattern`, a canonical-column-ORDER-only regex,
|
|
2210
|
+
// and build the row as a hardcoded positional literal — so a reordered/
|
|
2211
|
+
// superset By-Phase header, already tolerated above by
|
|
2212
|
+
// findTableStartOffset and read by-NAME in the update/sum halves,
|
|
2213
|
+
// silently inserted NOTHING).
|
|
2214
|
+
//
|
|
2215
|
+
// Drop a lone all-placeholder row first (e.g. the freshly-scaffolded
|
|
2216
|
+
// "| - | - | - | - |" seed row) — same convention the prior
|
|
2217
|
+
// canonical-order path used, generalized to any column order/count:
|
|
2218
|
+
// a row whose every PRESENT cell is "-" is the placeholder.
|
|
2219
|
+
const placeholderRow = (row) => Object.values(row).every((cell) => cell.trim() === '-');
|
|
2220
|
+
const withoutPlaceholder = (0, markdown_table_cjs_1.deleteTableRow)(tableText, placeholderRow);
|
|
2221
|
+
if (withoutPlaceholder.ok)
|
|
2222
|
+
tableText = withoutPlaceholder.value;
|
|
2223
|
+
// Map the By-Phase values onto the table's ACTUAL header columns by
|
|
2224
|
+
// NAME — an unrecognized column (a superset header) falls back to "-",
|
|
2225
|
+
// insertTableRow's default.
|
|
2226
|
+
const valueFor = (col) => {
|
|
2227
|
+
if (col === 'Phase')
|
|
2228
|
+
return String(phaseNum);
|
|
2229
|
+
if (col === 'Plans')
|
|
2230
|
+
return String(summaryCount);
|
|
2231
|
+
if (col === 'Total' || col === 'Avg/Plan')
|
|
2232
|
+
return '-';
|
|
2233
|
+
return undefined;
|
|
2234
|
+
};
|
|
2235
|
+
const insertResult = (0, markdown_table_cjs_1.insertTableRow)(tableText, valueFor);
|
|
2236
|
+
if (insertResult.ok)
|
|
2237
|
+
tableText = insertResult.value;
|
|
2238
|
+
content = before + tableText;
|
|
2003
2239
|
}
|
|
2004
|
-
content = content.replace(byPhaseTablePattern, (_match, tableHeader) => `${tableHeader}${tableBody}\n`);
|
|
2005
2240
|
}
|
|
2006
2241
|
// Velocity: Total plans completed — DERIVED as the sum of the By-Phase Plans column
|
|
2007
|
-
//
|
|
2008
|
-
//
|
|
2009
|
-
//
|
|
2010
|
-
//
|
|
2242
|
+
// across all data rows. Idempotent by construction (re-running phase complete upserts
|
|
2243
|
+
// the same row → same sum) and self-healing (a hand-edited inflated total is corrected
|
|
2244
|
+
// to the true sum on the next completion). When the By-Phase table is absent, leave the
|
|
2245
|
+
// velocity total unchanged rather than guess. (#1582)
|
|
2246
|
+
//
|
|
2247
|
+
// Ragged-tolerant AND name-addressed (#2245 audit): each data row is split via
|
|
2248
|
+
// `splitTableRow` and its "Plans" cell located by the HEADER's own column
|
|
2249
|
+
// order (not a fixed ordinal), so a reordered/superset By-Phase header is
|
|
2250
|
+
// summed correctly instead of silently reading the wrong cell. A row that's
|
|
2251
|
+
// too short to physically contain the "Plans" column is skipped, not
|
|
2252
|
+
// treated as an error — mirrors updateTableCell's ragged-row tolerance
|
|
2253
|
+
// (a hand-edited/ragged row for one phase must not blank out the derived
|
|
2254
|
+
// total for every phase). Still scoped via findTableStartOffset so the RIGHT
|
|
2255
|
+
// table is summed when an earlier unrelated table also has a "Phase"
|
|
2256
|
+
// column (#2012).
|
|
2011
2257
|
if (/Total plans completed:\s*(\d+|\[N\])/.test(content)) {
|
|
2012
|
-
const
|
|
2013
|
-
if (
|
|
2258
|
+
const sumTableStart = findTableStartOffset(content, byPhaseCols);
|
|
2259
|
+
if (sumTableStart !== null) {
|
|
2260
|
+
const tableLines = content.slice(sumTableStart).split(/\r?\n/);
|
|
2261
|
+
const headerCells = (0, markdown_table_cjs_1.splitTableRow)(tableLines[0] ?? '');
|
|
2262
|
+
const plansIdx = headerCells.indexOf('Plans');
|
|
2014
2263
|
let sum = 0;
|
|
2015
|
-
|
|
2016
|
-
//
|
|
2017
|
-
//
|
|
2018
|
-
//
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
const
|
|
2022
|
-
|
|
2023
|
-
|
|
2264
|
+
if (plansIdx !== -1) {
|
|
2265
|
+
// The delimiter row is skipped by NAME (isDelimiterRow), not by a
|
|
2266
|
+
// hardcoded "always line index 1" assumption, so this stays
|
|
2267
|
+
// self-consistent with the ragged-tolerant read below.
|
|
2268
|
+
const delimiterCells = (0, markdown_table_cjs_1.splitTableRow)(tableLines[1] ?? '');
|
|
2269
|
+
const dataStart = (0, markdown_table_cjs_1.isDelimiterRow)(delimiterCells) ? 2 : 1;
|
|
2270
|
+
for (const row of tableLines.slice(dataStart)) {
|
|
2271
|
+
if (!row.trim().startsWith('|'))
|
|
2272
|
+
break;
|
|
2273
|
+
const cells = (0, markdown_table_cjs_1.splitTableRow)(row);
|
|
2274
|
+
if (plansIdx < cells.length && /^\d+$/.test(cells[plansIdx])) {
|
|
2275
|
+
sum += parseInt(cells[plansIdx], 10);
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2024
2278
|
}
|
|
2025
2279
|
content = content.replace(/Total plans completed:\s*(\d+|\[N\])/, `Total plans completed: ${sum}`);
|
|
2026
2280
|
}
|
|
@@ -2137,7 +2391,11 @@ function cmdStateValidate(cwd, raw) {
|
|
|
2137
2391
|
drift['verification_status'] = { state_status: status, verification: 'passed' };
|
|
2138
2392
|
}
|
|
2139
2393
|
}
|
|
2140
|
-
catch { /*
|
|
2394
|
+
catch { /* best-effort (#2245 audit): cmdStateValidate is a diagnostic
|
|
2395
|
+
* warnings scan across N VERIFICATION.md files — one unreadable file
|
|
2396
|
+
* (permission/race) must not abort the scan of the rest; it's simply
|
|
2397
|
+
* excluded from drift detection. */
|
|
2398
|
+
}
|
|
2141
2399
|
}
|
|
2142
2400
|
// Check if all plans have summaries but status still says executing
|
|
2143
2401
|
if (diskPlans > 0 && diskSummaries >= diskPlans && /executing/i.test(status)) {
|
|
@@ -2148,7 +2406,14 @@ function cmdStateValidate(cwd, raw) {
|
|
|
2148
2406
|
}
|
|
2149
2407
|
}
|
|
2150
2408
|
}
|
|
2151
|
-
catch { /*
|
|
2409
|
+
catch { /* best-effort (#2245 audit): cmdStateValidate is a read-only
|
|
2410
|
+
* diagnostic scan of the current phase's directory (readdirSync +
|
|
2411
|
+
* scanPhasePlans). A disk-scan failure here means drift detection for
|
|
2412
|
+
* this phase is skipped for this run, degrading to "no warnings from
|
|
2413
|
+
* that scan" rather than crashing the validate command — the same
|
|
2414
|
+
* degrade-on-scan-failure pattern buildStateFrontmatter's own disk scan
|
|
2415
|
+
* already uses. */
|
|
2416
|
+
}
|
|
2152
2417
|
}
|
|
2153
2418
|
const valid = warnings.length === 0;
|
|
2154
2419
|
output({ valid, warnings, drift }, raw, undefined);
|
|
@@ -2237,33 +2502,33 @@ function cmdStateSync(cwd, options, raw) {
|
|
|
2237
2502
|
}
|
|
2238
2503
|
// Determine total phases from ROADMAP (may be larger than realized disk dirs).
|
|
2239
2504
|
// Mirrors the logic in buildStateFrontmatter so both report consistent percents (#3242 Bug B).
|
|
2505
|
+
// DEAD catch removed (#2245 audit): every operation in this block is a regex
|
|
2506
|
+
// exec/test over an already-read string plus pure Set/Math ops — none of
|
|
2507
|
+
// which can throw — so the try/catch could never be triggered.
|
|
2240
2508
|
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;
|
|
2509
|
+
let roadmapPhaseCount = 0;
|
|
2510
|
+
if (syncRoadmapScope !== null) {
|
|
2511
|
+
// #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
2512
|
+
const phaseHeadingPattern = /#{2,4}\s*Phase\s+([\w][\w.-]*)(?:\s*\([^)\n]{0,200}\))?\s*:/gi;
|
|
2513
|
+
let m;
|
|
2514
|
+
while ((m = phaseHeadingPattern.exec(syncRoadmapScope)) !== null) {
|
|
2515
|
+
// Only count tokens that contain at least one digit — excludes
|
|
2516
|
+
// pure-word section headings (Overview, Details) while keeping
|
|
2517
|
+
// numeric phases (01, 05.1) and project-code IDs (PROJ-42).
|
|
2518
|
+
if (!/\d/.test(m[1]))
|
|
2519
|
+
continue;
|
|
2520
|
+
// #1514: retired/folded phases are struck through; exclude from total.
|
|
2521
|
+
if (syncRetiredPhaseNums.has(phaseKeyFromToken(m[1])))
|
|
2522
|
+
continue;
|
|
2523
|
+
roadmapPhaseCount++;
|
|
2264
2524
|
}
|
|
2265
2525
|
}
|
|
2266
|
-
|
|
2526
|
+
if (roadmapPhaseCount > 0) {
|
|
2527
|
+
syncTotalPhases = Math.max(entries.length, roadmapPhaseCount);
|
|
2528
|
+
}
|
|
2529
|
+
else {
|
|
2530
|
+
syncTotalPhases = entries.length;
|
|
2531
|
+
}
|
|
2267
2532
|
// ADR-1769 Phase 7: the body writes (Total Plans in Phase, Progress bar, Last
|
|
2268
2533
|
// Activity) are the pure `syncCore` in src/state-transition.cts.
|
|
2269
2534
|
// #1761: when a milestone version is set in frontmatter but the ROADMAP has no
|
|
@@ -2383,7 +2648,7 @@ function cmdStatePrune(cwd, options, raw) {
|
|
|
2383
2648
|
}, cwd);
|
|
2384
2649
|
// Write archived entries to STATE-ARCHIVE.md
|
|
2385
2650
|
if (archived.length > 0) {
|
|
2386
|
-
const timestamp = clock_cjs_1.realClock.
|
|
2651
|
+
const timestamp = clock_cjs_1.realClock.localToday();
|
|
2387
2652
|
let archiveContent = (0, shell_command_projection_cjs_1.platformReadSync)(archivePath);
|
|
2388
2653
|
if (archiveContent === null) {
|
|
2389
2654
|
archiveContent = '# STATE Archive\n\nPruned entries from STATE.md. Recoverable but no longer loaded into agent context.\n\n';
|
|
@@ -2562,7 +2827,7 @@ function cmdStateCompletePhase(cwd, raw, overridePhase) {
|
|
|
2562
2827
|
output({ updated: [], phase: resolvedPhase, idempotent: true, note: 'phase already superseded; no-op' }, raw, 'false');
|
|
2563
2828
|
return;
|
|
2564
2829
|
}
|
|
2565
|
-
const today = clock_cjs_1.realClock.
|
|
2830
|
+
const today = clock_cjs_1.realClock.localToday();
|
|
2566
2831
|
const updated = [];
|
|
2567
2832
|
readModifyWriteStateMd(statePath, (content) => {
|
|
2568
2833
|
const currentPhase = resolvedPhase;
|