@opengsd/gsd-core 1.5.0 → 1.6.0-rc.2
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/plugin.json +1 -1
- package/agents/gsd-plan-checker.md +34 -0
- package/agents/gsd-planner.md +2 -0
- package/agents/gsd-roadmapper.md +6 -0
- package/bin/install.js +199 -365
- package/commands/gsd/capture.md +5 -1
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +695 -5
- package/gsd-core/bin/lib/adr-parser.cjs +45 -23
- package/gsd-core/bin/lib/audit.cjs +2 -2
- package/gsd-core/bin/lib/capability-consent.cjs +763 -0
- package/gsd-core/bin/lib/capability-ledger.cjs +831 -0
- package/gsd-core/bin/lib/capability-lifecycle.cjs +1551 -0
- package/gsd-core/bin/lib/capability-loader.cjs +764 -0
- package/gsd-core/bin/lib/capability-lock.cjs +553 -0
- package/gsd-core/bin/lib/capability-registry.cjs +198 -4
- package/gsd-core/bin/lib/capability-source.cjs +1242 -0
- package/gsd-core/bin/lib/capability-state.cjs +9 -6
- package/gsd-core/bin/lib/capability-trust.cjs +550 -0
- package/gsd-core/bin/lib/capability-validator.cjs +2066 -0
- package/gsd-core/bin/lib/capability-writer.cjs +14 -5
- package/gsd-core/bin/lib/check-command-router.cjs +69 -18
- package/gsd-core/bin/lib/command-aliases.cjs +8 -0
- package/gsd-core/bin/lib/commands.cjs +247 -0
- package/gsd-core/bin/lib/config-loader.cjs +98 -84
- package/gsd-core/bin/lib/config-schema.cjs +26 -7
- package/gsd-core/bin/lib/config.cjs +7 -1
- package/gsd-core/bin/lib/decisions.cjs +149 -60
- package/gsd-core/bin/lib/frontmatter.cjs +7 -3
- package/gsd-core/bin/lib/gap-checker.cjs +126 -11
- package/gsd-core/bin/lib/init.cjs +91 -22
- package/gsd-core/bin/lib/legacy-cleanup.cjs +96 -0
- package/gsd-core/bin/lib/loop-resolver.cjs +26 -2
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +471 -0
- package/gsd-core/bin/lib/milestone.cjs +41 -2
- package/gsd-core/bin/lib/phase-command-router.cjs +5 -0
- package/gsd-core/bin/lib/phase-id.cjs +25 -11
- package/gsd-core/bin/lib/phase-lifecycle.cjs +14 -5
- package/gsd-core/bin/lib/phase.cjs +33 -4
- package/gsd-core/bin/lib/probe-core.cjs +7 -0
- package/gsd-core/bin/lib/prohibition-enforcement.cjs +59 -26
- package/gsd-core/bin/lib/project-root.cjs +89 -2
- package/gsd-core/bin/lib/resolution.cjs +26 -0
- package/gsd-core/bin/lib/roadmap-command-router.cjs +16 -3
- package/gsd-core/bin/lib/roadmap-parser.cjs +73 -106
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +47 -17
- package/gsd-core/bin/lib/roadmap.cjs +5 -2
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +423 -3
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +77 -0
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +1 -28
- package/gsd-core/bin/lib/runtime-homes.cjs +53 -1
- package/gsd-core/bin/lib/runtime-name-policy.cjs +44 -0
- package/gsd-core/bin/lib/semver-compare.cjs +127 -0
- package/gsd-core/bin/lib/shell-command-projection.cjs +55 -1
- package/gsd-core/bin/lib/state-document.cjs +4 -2
- package/gsd-core/bin/lib/state.cjs +317 -161
- package/gsd-core/bin/lib/surface.cjs +12 -19
- package/gsd-core/bin/lib/uat-predicate.cjs +7 -47
- package/gsd-core/bin/lib/uat.cjs +39 -26
- package/gsd-core/bin/lib/validate.cjs +5 -2
- package/gsd-core/bin/lib/verify.cjs +40 -15
- package/gsd-core/bin/lib/worktree-safety.cjs +202 -0
- package/gsd-core/bin/shared/config-defaults.manifest.json +6 -1
- package/gsd-core/bin/shared/config-schema.manifest.json +5 -1
- package/gsd-core/references/context-budget.md +8 -8
- package/gsd-core/references/execute-phase-between-wave-reset.md +43 -0
- package/gsd-core/references/execute-phase-context-guard.md +16 -0
- package/gsd-core/references/execute-phase-wave-guard.md +33 -0
- package/gsd-core/references/planner-antipatterns.md +48 -0
- package/gsd-core/references/planning-config.md +4 -0
- package/gsd-core/references/prohibition-probe.md +15 -9
- package/gsd-core/references/scout-codebase.md +2 -2
- package/gsd-core/workflows/autonomous.md +33 -33
- package/gsd-core/workflows/diagnose-issues.md +6 -1
- package/gsd-core/workflows/discuss-phase/templates/context.md +1 -1
- package/gsd-core/workflows/discuss-phase.md +1 -2
- package/gsd-core/workflows/execute-phase.md +12 -12
- package/gsd-core/workflows/help/modes/full.md +10 -0
- package/gsd-core/workflows/list-seeds.md +63 -0
- package/gsd-core/workflows/manager.md +37 -37
- package/gsd-core/workflows/pr-branch.md +156 -0
- package/gsd-core/workflows/quick.md +6 -1
- package/gsd-core/workflows/review.md +10 -2
- package/gsd-core/workflows/spec-phase.md +8 -3
- package/gsd-core/workflows/verify-phase.md +2 -2
- package/package.json +6 -3
- package/scripts/gen-capability-matrix.cjs +284 -0
- package/scripts/gen-capability-registry.cjs +96 -1853
- package/scripts/lint-regression-test-names.allowlist.json +1 -0
- package/scripts/lint-resolution-provenance.allowlist.json +1 -0
- package/scripts/lint-resolution-provenance.cjs +192 -0
- package/scripts/lint-test-file-count.allowlist.json +9 -0
- package/scripts/prompt-injection-scan.sh +1 -0
- package/scripts/run-tests.cjs +14 -0
- package/scripts/sync-manifest-versions.cjs +77 -5
|
@@ -34,6 +34,7 @@ const { extractFrontmatter, reconstructFrontmatter } = frontmatter;
|
|
|
34
34
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
35
35
|
const scanPhasePlans = require("./plan-scan.cjs");
|
|
36
36
|
const state_document_cjs_1 = require("./state-document.cjs");
|
|
37
|
+
const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
37
38
|
const STATE_PROGRESS_RESYNC_FIELDS = new Set([
|
|
38
39
|
'Progress',
|
|
39
40
|
'Total Plans in Phase',
|
|
@@ -66,6 +67,14 @@ process.on('exit', () => {
|
|
|
66
67
|
});
|
|
67
68
|
// Hoisted to module scope — compiled once, not per call (#320). Stateless (/i, used with .match).
|
|
68
69
|
const byPhaseTablePattern = /(\|\s*Phase\s*\|\s*Plans\s*\|\s*Total\s*\|\s*Avg\/Plan\s*\|[ \t]*\n\|(?:[- :\t]+\|)+[ \t]*\n)((?:[ \t]*\|[^\n]*\n)*)(?=\n|$)/i;
|
|
70
|
+
// ─── ADR-1372 T6: seam-based section splice helper ───────────────────────────
|
|
71
|
+
// Shared stop predicates corresponding to the regex lookaheads used in state.cts:
|
|
72
|
+
// STOP_H2_PLUS : (?=\n##|$) — stops at any heading with level ≥ 2
|
|
73
|
+
// STOP_H2_H3 : (?=\n###?|\n##[^#]|$) — stops at level 2 or 3
|
|
74
|
+
// STOP_H2_ONLY : (?=\n##[^#]|$) — stops at level 2 only
|
|
75
|
+
const STOP_H2_PLUS = (lv) => lv >= 2;
|
|
76
|
+
const STOP_H2_H3 = (lv) => lv === 2 || lv === 3;
|
|
77
|
+
const STOP_H2_ONLY = (lv) => lv === 2;
|
|
69
78
|
function cmdStateLoad(cwd, raw) {
|
|
70
79
|
const config = loadConfig(cwd);
|
|
71
80
|
const planDir = planningPaths(cwd).planning;
|
|
@@ -261,11 +270,25 @@ function stateReplaceFieldWithFallback(content, primary, fallback, value) {
|
|
|
261
270
|
* Fixes #1365: advance-plan could not update Status/Last activity after begin-phase.
|
|
262
271
|
*/
|
|
263
272
|
function updateCurrentPositionFields(content, fields) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
273
|
+
// ADR-1372 T6: locate ## Current Position using tokenizeHeadings, extract the
|
|
274
|
+
// untrimmed body span, apply field edits, then splice the modified body back in.
|
|
275
|
+
// Stop predicate mirrors (?=\n##|$): any heading with level ≥ 2.
|
|
276
|
+
const headings = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(content);
|
|
277
|
+
const posIdx = headings.findIndex(h => h.level === 2 && /^current\s+position$/i.test(h.text));
|
|
278
|
+
if (posIdx === -1)
|
|
267
279
|
return content;
|
|
268
|
-
|
|
280
|
+
const posHeading = headings[posIdx];
|
|
281
|
+
const lines = content.split('\n');
|
|
282
|
+
const posHeadingLine = lines[posHeading.line - 1];
|
|
283
|
+
const posBodyStart = posHeading.offset + posHeadingLine.length + 1;
|
|
284
|
+
let posBodyEnd = content.length;
|
|
285
|
+
for (let j = posIdx + 1; j < headings.length; j++) {
|
|
286
|
+
if (STOP_H2_PLUS(headings[j].level)) {
|
|
287
|
+
posBodyEnd = headings[j].offset - 1;
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
let posBody = content.slice(posBodyStart, posBodyEnd);
|
|
269
292
|
const statusDefaults = state_document_cjs_1.KNOWN_TEMPLATE_DEFAULTS['Status'];
|
|
270
293
|
const lastActivityDefaults = state_document_cjs_1.KNOWN_TEMPLATE_DEFAULTS['Last Activity'];
|
|
271
294
|
if (fields.status) {
|
|
@@ -339,7 +362,8 @@ function updateCurrentPositionFields(content, fields) {
|
|
|
339
362
|
posBody = replaced;
|
|
340
363
|
}
|
|
341
364
|
}
|
|
342
|
-
|
|
365
|
+
// Splice the modified body back in place of the original untrimmed span.
|
|
366
|
+
return content.slice(0, posBodyStart) + posBody + content.slice(posBodyEnd);
|
|
343
367
|
}
|
|
344
368
|
function cmdStateAdvancePlan(cwd, raw) {
|
|
345
369
|
const statePath = planningPaths(cwd).state;
|
|
@@ -435,7 +459,7 @@ function cmdStateRecordMetric(cwd, options, raw) {
|
|
|
435
459
|
let created = false;
|
|
436
460
|
readModifyWriteStateMd(statePath, (content) => {
|
|
437
461
|
// Find Performance Metrics section and its table
|
|
438
|
-
const metricsPattern = /(##\s*Performance Metrics[\s\S]*?\n\|[^\n]+\n\|[-|\s]+\n)([\s\S]*?)(?=\n##|\n$|$)/i;
|
|
462
|
+
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
|
|
439
463
|
const metricsMatch = content.match(metricsPattern);
|
|
440
464
|
const newRow = `| Phase ${phase} P${plan} | ${duration} | ${tasks || '-'} tasks | ${files || '-'} files |`;
|
|
441
465
|
if (metricsMatch) {
|
|
@@ -545,16 +569,34 @@ function cmdStateAddDecision(cwd, options, raw) {
|
|
|
545
569
|
let _added = false;
|
|
546
570
|
let created = false;
|
|
547
571
|
readModifyWriteStateMd(statePath, (content) => {
|
|
548
|
-
//
|
|
549
|
-
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
572
|
+
// ADR-1372 T6: find Decisions section via tokenizeHeadings; stop at level 2 or 3.
|
|
573
|
+
// Mirrors /(###?\s*(?:Decisions|Decisions Made|Accumulated.*Decisions)\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|$)/i
|
|
574
|
+
const decisionsPred = (lv, text) => (lv === 2 || lv === 3) && /^(?:Decisions|Decisions Made|Accumulated.*Decisions)$/i.test(text);
|
|
575
|
+
const sectionBody = (() => {
|
|
576
|
+
const hs = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(content);
|
|
577
|
+
const i = hs.findIndex(h => decisionsPred(h.level, h.text));
|
|
578
|
+
if (i === -1)
|
|
579
|
+
return null;
|
|
580
|
+
const h = hs[i];
|
|
581
|
+
const ls = content.split('\n');
|
|
582
|
+
const hl = ls[h.line - 1];
|
|
583
|
+
const bs = h.offset + hl.length + 1;
|
|
584
|
+
let se = content.length;
|
|
585
|
+
for (let j = i + 1; j < hs.length; j++) {
|
|
586
|
+
if (STOP_H2_H3(hs[j].level)) {
|
|
587
|
+
se = hs[j].offset - 1;
|
|
588
|
+
break;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return { bodyStart: bs, bodyEnd: se, body: content.slice(bs, se) };
|
|
592
|
+
})();
|
|
593
|
+
if (sectionBody !== null) {
|
|
594
|
+
let newBody = sectionBody.body;
|
|
553
595
|
// Remove placeholders
|
|
554
|
-
|
|
555
|
-
|
|
596
|
+
newBody = newBody.replace(/None yet\.?\s*\n?/gi, '').replace(/No decisions yet\.?\s*\n?/gi, '');
|
|
597
|
+
newBody = newBody.trimEnd() + '\n' + entry + '\n';
|
|
556
598
|
_added = true;
|
|
557
|
-
return content.
|
|
599
|
+
return content.slice(0, sectionBody.bodyStart) + newBody + content.slice(sectionBody.bodyEnd);
|
|
558
600
|
}
|
|
559
601
|
// Section absent — DWIM: auto-create canonical ## Decisions scaffold,
|
|
560
602
|
// then append the entry. Matches state begin-phase / advance-plan DWIM behavior.
|
|
@@ -598,14 +640,33 @@ function cmdStateAddBlocker(cwd, text, raw) {
|
|
|
598
640
|
let _added = false;
|
|
599
641
|
let created = false;
|
|
600
642
|
readModifyWriteStateMd(statePath, (content) => {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
643
|
+
// ADR-1372 T6: find Blockers/Concerns section via tokenizeHeadings; stop at level 2 or 3.
|
|
644
|
+
// Mirrors /(###?\s*(?:Blockers|Blockers\/Concerns|Concerns)\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|$)/i
|
|
645
|
+
const blockersPred = (lv, text) => (lv === 2 || lv === 3) && /^(?:Blockers|Blockers\/Concerns|Concerns)$/i.test(text);
|
|
646
|
+
const sectionSpan = (() => {
|
|
647
|
+
const hs = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(content);
|
|
648
|
+
const i = hs.findIndex(h => blockersPred(h.level, h.text));
|
|
649
|
+
if (i === -1)
|
|
650
|
+
return null;
|
|
651
|
+
const h = hs[i];
|
|
652
|
+
const ls = content.split('\n');
|
|
653
|
+
const hl = ls[h.line - 1];
|
|
654
|
+
const bs = h.offset + hl.length + 1;
|
|
655
|
+
let se = content.length;
|
|
656
|
+
for (let j = i + 1; j < hs.length; j++) {
|
|
657
|
+
if (STOP_H2_H3(hs[j].level)) {
|
|
658
|
+
se = hs[j].offset - 1;
|
|
659
|
+
break;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return { bodyStart: bs, bodyEnd: se, body: content.slice(bs, se) };
|
|
663
|
+
})();
|
|
664
|
+
if (sectionSpan !== null) {
|
|
665
|
+
let sectionBody = sectionSpan.body;
|
|
605
666
|
sectionBody = sectionBody.replace(/None\.?\s*\n?/gi, '').replace(/None yet\.?\s*\n?/gi, '');
|
|
606
667
|
sectionBody = sectionBody.trimEnd() + '\n' + entry + '\n';
|
|
607
668
|
_added = true;
|
|
608
|
-
return content.
|
|
669
|
+
return content.slice(0, sectionSpan.bodyStart) + sectionBody + content.slice(sectionSpan.bodyEnd);
|
|
609
670
|
}
|
|
610
671
|
// Section absent — DWIM: auto-create canonical ### Blockers scaffold.
|
|
611
672
|
const scaffold = [
|
|
@@ -666,18 +727,47 @@ function cmdStateAddRoadmapEvolution(cwd, options, raw) {
|
|
|
666
727
|
// Section boundaries mirror the sibling handlers (add-decision/add-blocker):
|
|
667
728
|
// a trailing CR on a CRLF STATE.md is absorbed by the lazy body and trimmed,
|
|
668
729
|
// so following sections are preserved without data loss (see the CRLF test).
|
|
730
|
+
//
|
|
731
|
+
// ADR-1372 T6: accPattern and subPattern migrated to tokenizeHeadings.
|
|
732
|
+
// accPattern = /(##\s*Accumulated Context\s*\n)([\s\S]*?)(?=\n##[^#]|$)/i
|
|
733
|
+
// → stop at level 2 only (STOP_H2_ONLY)
|
|
734
|
+
// subPattern = /(###\s*Roadmap Evolution\s*\n)([\s\S]*?)(?=\n###?|$)/i
|
|
735
|
+
// → applied to accBody; stop at level 2 or 3 (STOP_H2_H3)
|
|
669
736
|
readModifyWriteStateMd(statePath, (content) => {
|
|
670
|
-
|
|
671
|
-
const
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
const
|
|
737
|
+
// Locate ## Accumulated Context and extract its untrimmed body span.
|
|
738
|
+
const accHs = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(content);
|
|
739
|
+
const accIdx = accHs.findIndex(h => h.level === 2 && /^accumulated\s+context$/i.test(h.text));
|
|
740
|
+
if (accIdx !== -1) {
|
|
741
|
+
const accH = accHs[accIdx];
|
|
742
|
+
const contentLines = content.split('\n');
|
|
743
|
+
const accHL = contentLines[accH.line - 1];
|
|
744
|
+
const accBodyStart = accH.offset + accHL.length + 1;
|
|
745
|
+
let accBodyEnd = content.length;
|
|
746
|
+
for (let j = accIdx + 1; j < accHs.length; j++) {
|
|
747
|
+
if (STOP_H2_ONLY(accHs[j].level)) {
|
|
748
|
+
accBodyEnd = accHs[j].offset - 1;
|
|
749
|
+
break;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
const accBody = content.slice(accBodyStart, accBodyEnd);
|
|
675
753
|
// Find `### Roadmap Evolution` WITHIN the Accumulated Context body only.
|
|
676
|
-
//
|
|
677
|
-
|
|
678
|
-
const
|
|
679
|
-
|
|
680
|
-
|
|
754
|
+
// tokenizeHeadings is applied to accBody to scope the search.
|
|
755
|
+
// Stop predicate mirrors (?=\n###?|$): level 2 or 3.
|
|
756
|
+
const subHs = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(accBody);
|
|
757
|
+
const subIdx = subHs.findIndex(h => h.level === 3 && /^roadmap\s+evolution$/i.test(h.text));
|
|
758
|
+
if (subIdx !== -1) {
|
|
759
|
+
const subH = subHs[subIdx];
|
|
760
|
+
const accLines = accBody.split('\n');
|
|
761
|
+
const subHL = accLines[subH.line - 1];
|
|
762
|
+
const subBodyStart = subH.offset + subHL.length + 1;
|
|
763
|
+
let subBodyEnd = accBody.length;
|
|
764
|
+
for (let j = subIdx + 1; j < subHs.length; j++) {
|
|
765
|
+
if (STOP_H2_H3(subHs[j].level)) {
|
|
766
|
+
subBodyEnd = subHs[j].offset - 1;
|
|
767
|
+
break;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
let subBody = accBody.slice(subBodyStart, subBodyEnd);
|
|
681
771
|
// Dedupe: exact (trimmed) line already present is a no-op replay.
|
|
682
772
|
if (subBody.split('\n').some((line) => line.trim() === entry.trim())) {
|
|
683
773
|
duplicate = true;
|
|
@@ -685,14 +775,15 @@ function cmdStateAddRoadmapEvolution(cwd, options, raw) {
|
|
|
685
775
|
}
|
|
686
776
|
subBody = subBody.replace(/None yet\.?\s*\n?/gi, '');
|
|
687
777
|
subBody = subBody.trimEnd() + '\n' + entry + '\n';
|
|
688
|
-
|
|
689
|
-
|
|
778
|
+
// Splice subBody into accBody, then splice newAccBody into content.
|
|
779
|
+
const newAccBody = accBody.slice(0, subBodyStart) + subBody + accBody.slice(subBodyEnd);
|
|
780
|
+
return content.slice(0, accBodyStart) + newAccBody + content.slice(accBodyEnd);
|
|
690
781
|
}
|
|
691
782
|
// Subsection missing — append it at the end of the Accumulated Context body.
|
|
692
783
|
subsectionCreated = true;
|
|
693
784
|
const trimmedAcc = accBody.trimEnd();
|
|
694
785
|
const block = `${trimmedAcc ? `${trimmedAcc}\n\n` : ''}### Roadmap Evolution\n\n${entry}\n`;
|
|
695
|
-
return content.
|
|
786
|
+
return content.slice(0, accBodyStart) + block + content.slice(accBodyEnd);
|
|
696
787
|
}
|
|
697
788
|
// No `## Accumulated Context` — DWIM: create both at end of file.
|
|
698
789
|
// Mirrors the add-decision / add-blocker auto-create behavior.
|
|
@@ -732,25 +823,37 @@ function cmdStateResolveBlocker(cwd, text, raw) {
|
|
|
732
823
|
}
|
|
733
824
|
let resolved = false;
|
|
734
825
|
readModifyWriteStateMd(statePath, (content) => {
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
if (
|
|
748
|
-
|
|
826
|
+
// ADR-1372 T6: find Blockers/Concerns section via tokenizeHeadings; stop at level 2 or 3.
|
|
827
|
+
// Mirrors /(###?\s*(?:Blockers|Blockers\/Concerns|Concerns)\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|$)/i
|
|
828
|
+
const hs = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(content);
|
|
829
|
+
const i = hs.findIndex(h => (h.level === 2 || h.level === 3) && /^(?:Blockers|Blockers\/Concerns|Concerns)$/i.test(h.text));
|
|
830
|
+
if (i === -1)
|
|
831
|
+
return content;
|
|
832
|
+
const h = hs[i];
|
|
833
|
+
const ls = content.split('\n');
|
|
834
|
+
const hl = ls[h.line - 1];
|
|
835
|
+
const bs = h.offset + hl.length + 1;
|
|
836
|
+
let se = content.length;
|
|
837
|
+
for (let j = i + 1; j < hs.length; j++) {
|
|
838
|
+
if (STOP_H2_H3(hs[j].level)) {
|
|
839
|
+
se = hs[j].offset - 1;
|
|
840
|
+
break;
|
|
749
841
|
}
|
|
750
|
-
resolved = true;
|
|
751
|
-
return content.replace(sectionPattern, (_match, header) => `${header}${newBody}`);
|
|
752
842
|
}
|
|
753
|
-
|
|
843
|
+
const sectionBody = content.slice(bs, se);
|
|
844
|
+
const lines = sectionBody.split('\n');
|
|
845
|
+
const filtered = lines.filter(line => {
|
|
846
|
+
if (!line.startsWith('- '))
|
|
847
|
+
return true;
|
|
848
|
+
return !line.toLowerCase().includes(text.toLowerCase());
|
|
849
|
+
});
|
|
850
|
+
let newBody = filtered.join('\n');
|
|
851
|
+
// If section is now empty, add placeholder
|
|
852
|
+
if (!newBody.trim() || !newBody.includes('- ')) {
|
|
853
|
+
newBody = 'None\n';
|
|
854
|
+
}
|
|
855
|
+
resolved = true;
|
|
856
|
+
return content.slice(0, bs) + newBody + content.slice(se);
|
|
754
857
|
}, cwd);
|
|
755
858
|
if (resolved) {
|
|
756
859
|
output({ resolved: true, blocker: text }, raw, 'true');
|
|
@@ -942,8 +1045,8 @@ function cmdStateRecordSession(cwd, options, raw) {
|
|
|
942
1045
|
* Returns the match whose group 1 is the section body, or null.
|
|
943
1046
|
*/
|
|
944
1047
|
function matchSessionSection(body) {
|
|
945
|
-
return body.match(/(?:^|\n)##[ \t]*Session[ \t]*\n([\s\S]*?)(?=\n##|$)/i)
|
|
946
|
-
|| body.match(/(?:^|\n)##[ \t]*Session Continuity[ \t]*\n([\s\S]*?)(?=\n##|$)/i);
|
|
1048
|
+
return body.match(/(?:^|\n)##[ \t]*Session[ \t]*\n([\s\S]*?)(?=\n##|$)/i) // allow-adhoc-markdown: read-only session-section extract in state.cts; pending collectSection migration #1372
|
|
1049
|
+
|| body.match(/(?:^|\n)##[ \t]*Session Continuity[ \t]*\n([\s\S]*?)(?=\n##|$)/i); // allow-adhoc-markdown: read-only session-continuity section extract in state.cts; pending collectSection migration #1372
|
|
947
1050
|
}
|
|
948
1051
|
function parseProsePhaseField(value) {
|
|
949
1052
|
if (!value)
|
|
@@ -1018,7 +1121,7 @@ function cmdStateSnapshot(cwd, raw) {
|
|
|
1018
1121
|
const progressPercent = progressRaw ? parseInt(progressRaw.replace('%', ''), 10) : null;
|
|
1019
1122
|
// Extract decisions table
|
|
1020
1123
|
const decisions = [];
|
|
1021
|
-
const decisionsMatch = body.match(/##\s*Decisions Made[\s\S]*?\n\|[^\n]+\n\|[-|\s]+\n([\s\S]*?)(?=\n##|\n$|$)/i);
|
|
1124
|
+
const decisionsMatch = body.match(/##\s*Decisions Made[\s\S]*?\n\|[^\n]+\n\|[-|\s]+\n([\s\S]*?)(?=\n##|\n$|$)/i); // allow-adhoc-markdown: read-only decisions-table section-collect in state.cts; pending collectSection migration #1372
|
|
1022
1125
|
if (decisionsMatch) {
|
|
1023
1126
|
const tableBody = decisionsMatch[1];
|
|
1024
1127
|
const rows = tableBody.trim().split('\n').filter(r => r.includes('|'));
|
|
@@ -1035,7 +1138,7 @@ function cmdStateSnapshot(cwd, raw) {
|
|
|
1035
1138
|
}
|
|
1036
1139
|
// Extract blockers list
|
|
1037
1140
|
const blockers = [];
|
|
1038
|
-
const blockersMatch = body.match(/##\s*Blockers\s*\n([\s\S]*?)(?=\n##|$)/i);
|
|
1141
|
+
const blockersMatch = body.match(/##\s*Blockers\s*\n([\s\S]*?)(?=\n##|$)/i); // allow-adhoc-markdown: read-only blockers section-collect in state.cts; pending collectSection migration #1372
|
|
1039
1142
|
if (blockersMatch) {
|
|
1040
1143
|
const blockersSection = blockersMatch[1];
|
|
1041
1144
|
const items = blockersSection.match(/^-\s+(.+)$/gm) || [];
|
|
@@ -1195,7 +1298,8 @@ function buildStateFrontmatter(bodyContent, cwd) {
|
|
|
1195
1298
|
// Only count tokens that contain at least one digit — excludes
|
|
1196
1299
|
// pure-word section headings (Overview, Details) while keeping
|
|
1197
1300
|
// numeric phases (01, 05.1) and project-code IDs (PROJ-42).
|
|
1198
|
-
|
|
1301
|
+
// Also exclude 999.x backlog phases. Mirrors init.cts filter.
|
|
1302
|
+
if (/\d/.test(m[1]) && !/^999\b/.test(m[1]))
|
|
1199
1303
|
roadmapPhaseCount++;
|
|
1200
1304
|
}
|
|
1201
1305
|
}
|
|
@@ -1759,11 +1863,23 @@ function cmdStateBeginPhase(cwd, phaseNumber, phaseName, planCount, raw) {
|
|
|
1759
1863
|
updated.push('Current focus');
|
|
1760
1864
|
}
|
|
1761
1865
|
// Update ## Current Position section (#1104, #1365)
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1866
|
+
// ADR-1372 T6: positionPattern → tokenizeHeadings + spliceStateSection.
|
|
1867
|
+
// Mirrors /(##\s*Current Position\s*\n)([\s\S]*?)(?=\n##|$)/i; stop at level ≥ 2.
|
|
1868
|
+
const posHs = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(body);
|
|
1869
|
+
const posIdx = posHs.findIndex(h => h.level === 2 && /^current\s+position$/i.test(h.text));
|
|
1870
|
+
if (posIdx !== -1) {
|
|
1871
|
+
const posH = posHs[posIdx];
|
|
1872
|
+
const bodyLines = body.split('\n');
|
|
1873
|
+
const posHL = bodyLines[posH.line - 1];
|
|
1874
|
+
const posBodyStart = posH.offset + posHL.length + 1;
|
|
1875
|
+
let posBodyEnd = body.length;
|
|
1876
|
+
for (let j = posIdx + 1; j < posHs.length; j++) {
|
|
1877
|
+
if (STOP_H2_PLUS(posHs[j].level)) {
|
|
1878
|
+
posBodyEnd = posHs[j].offset - 1;
|
|
1879
|
+
break;
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
let posBody = body.slice(posBodyStart, posBodyEnd);
|
|
1767
1883
|
// Update or insert Phase line
|
|
1768
1884
|
const newPhase = `Phase: ${phaseNumber}${phaseName ? ` (${phaseName})` : ''} — EXECUTING`;
|
|
1769
1885
|
if (/^Phase:/m.test(posBody)) {
|
|
@@ -1816,22 +1932,33 @@ function cmdStateBeginPhase(cwd, phaseNumber, phaseName, planCount, raw) {
|
|
|
1816
1932
|
if (replaced !== null)
|
|
1817
1933
|
posBody = replaced;
|
|
1818
1934
|
}
|
|
1819
|
-
body = body.
|
|
1935
|
+
body = body.slice(0, posBodyStart) + posBody + body.slice(posBodyEnd);
|
|
1820
1936
|
updated.push('Current Position');
|
|
1821
1937
|
}
|
|
1822
1938
|
}
|
|
1823
1939
|
else {
|
|
1824
1940
|
// Resume path: only update Last activity timestamp in Current Position
|
|
1825
1941
|
// (do not touch Plan:, stopped_at, progress.percent, or plan counter)
|
|
1826
|
-
|
|
1827
|
-
const
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1942
|
+
// ADR-1372 T6: positionPattern → tokenizeHeadings; stop at level ≥ 2.
|
|
1943
|
+
const posHsR = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(body);
|
|
1944
|
+
const posIdxR = posHsR.findIndex(h => h.level === 2 && /^current\s+position$/i.test(h.text));
|
|
1945
|
+
if (posIdxR !== -1) {
|
|
1946
|
+
const posHR = posHsR[posIdxR];
|
|
1947
|
+
const bodyLinesR = body.split('\n');
|
|
1948
|
+
const posHLR = bodyLinesR[posHR.line - 1];
|
|
1949
|
+
const posBodyStartR = posHR.offset + posHLR.length + 1;
|
|
1950
|
+
let posBodyEndR = body.length;
|
|
1951
|
+
for (let j = posIdxR + 1; j < posHsR.length; j++) {
|
|
1952
|
+
if (STOP_H2_PLUS(posHsR[j].level)) {
|
|
1953
|
+
posBodyEndR = posHsR[j].offset - 1;
|
|
1954
|
+
break;
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
let posBody = body.slice(posBodyStartR, posBodyEndR);
|
|
1831
1958
|
const resumeActivity = `Last activity: ${today} — Phase ${phaseNumber} execution resumed (wave continue)`;
|
|
1832
1959
|
if (/^Last activity:/im.test(posBody)) {
|
|
1833
1960
|
posBody = posBody.replace(/^Last activity:.*$/im, resumeActivity);
|
|
1834
|
-
body = body.
|
|
1961
|
+
body = body.slice(0, posBodyStartR) + posBody + body.slice(posBodyEndR);
|
|
1835
1962
|
updated.push('Last activity (resume)');
|
|
1836
1963
|
}
|
|
1837
1964
|
else {
|
|
@@ -1840,7 +1967,7 @@ function cmdStateBeginPhase(cwd, phaseNumber, phaseName, planCount, raw) {
|
|
|
1840
1967
|
?? (0, state_document_cjs_1.stateReplaceField)(posBody, 'Last activity', resumeActivity);
|
|
1841
1968
|
if (replaced !== null) {
|
|
1842
1969
|
posBody = replaced;
|
|
1843
|
-
body = body.
|
|
1970
|
+
body = body.slice(0, posBodyStartR) + posBody + body.slice(posBodyEndR);
|
|
1844
1971
|
updated.push('Last activity (resume)');
|
|
1845
1972
|
}
|
|
1846
1973
|
}
|
|
@@ -2016,14 +2143,28 @@ function cmdStateMilestoneSwitch(cwd, version, name, raw) {
|
|
|
2016
2143
|
const content = (0, shell_command_projection_cjs_1.platformReadSync)(statePath) || '';
|
|
2017
2144
|
const existingFm = extractFrontmatter(content);
|
|
2018
2145
|
const body = stripFrontmatter(content);
|
|
2019
|
-
|
|
2146
|
+
// ADR-1372 T6: positionPattern → tokenizeHeadings + spliceStateSection.
|
|
2147
|
+
// Mirrors /(##\s*Current Position\s*\n)([\s\S]*?)(?=\n##|$)/i; stop at level ≥ 2.
|
|
2020
2148
|
const resetPositionBody = `\nPhase: Not started (defining requirements)\n` +
|
|
2021
2149
|
`Plan: —\n` +
|
|
2022
2150
|
`Status: Defining requirements\n` +
|
|
2023
2151
|
`Last activity: ${today} — Milestone ${version} started\n\n`;
|
|
2024
2152
|
let newBody;
|
|
2025
|
-
|
|
2026
|
-
|
|
2153
|
+
const msPosHs = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(body);
|
|
2154
|
+
const msPosIdx = msPosHs.findIndex(h => h.level === 2 && /^current\s+position$/i.test(h.text));
|
|
2155
|
+
if (msPosIdx !== -1) {
|
|
2156
|
+
const msPosH = msPosHs[msPosIdx];
|
|
2157
|
+
const msBodyLines = body.split('\n');
|
|
2158
|
+
const msPosHL = msBodyLines[msPosH.line - 1];
|
|
2159
|
+
const msPosBodyStart = msPosH.offset + msPosHL.length + 1;
|
|
2160
|
+
let msPosBodyEnd = body.length;
|
|
2161
|
+
for (let j = msPosIdx + 1; j < msPosHs.length; j++) {
|
|
2162
|
+
if (STOP_H2_PLUS(msPosHs[j].level)) {
|
|
2163
|
+
msPosBodyEnd = msPosHs[j].offset - 1;
|
|
2164
|
+
break;
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
newBody = body.slice(0, msPosBodyStart) + resetPositionBody + body.slice(msPosBodyEnd);
|
|
2027
2168
|
}
|
|
2028
2169
|
else {
|
|
2029
2170
|
const preface = body.trim().length > 0 ? body : '# Project State\n';
|
|
@@ -2284,16 +2425,41 @@ function cmdStatePrune(cwd, options, raw) {
|
|
|
2284
2425
|
const archived = [];
|
|
2285
2426
|
// Shared pruning logic applied to both dry-run and real passes.
|
|
2286
2427
|
// Returns { newContent, archivedSections }.
|
|
2428
|
+
// ADR-1372 T6: all four inline section-collect regexes replaced with
|
|
2429
|
+
// tokenizeHeadings + untrimmed-span splicing for byte-identical writes.
|
|
2287
2430
|
function prunePass(content) {
|
|
2288
2431
|
const sections = [];
|
|
2432
|
+
// Helper: locate a heading matching pred, extract untrimmed body [bs, se),
|
|
2433
|
+
// apply transform, and splice back. Returns updated content.
|
|
2434
|
+
// All prune-section patterns stop at level 2 or 3 (STOP_H2_H3).
|
|
2435
|
+
function pruneSectionSpan(c, pred, transform, sectionName) {
|
|
2436
|
+
const hs = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(c);
|
|
2437
|
+
const i = hs.findIndex(h => pred(h.level, h.text));
|
|
2438
|
+
if (i === -1)
|
|
2439
|
+
return c;
|
|
2440
|
+
const h = hs[i];
|
|
2441
|
+
const ls = c.split('\n');
|
|
2442
|
+
const hl = ls[h.line - 1];
|
|
2443
|
+
const bs = h.offset + hl.length + 1;
|
|
2444
|
+
let se = c.length;
|
|
2445
|
+
for (let j = i + 1; j < hs.length; j++) {
|
|
2446
|
+
if (STOP_H2_H3(hs[j].level)) {
|
|
2447
|
+
se = hs[j].offset - 1;
|
|
2448
|
+
break;
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
const body = c.slice(bs, se);
|
|
2452
|
+
const { keep, archive } = transform(body);
|
|
2453
|
+
if (archive.length > 0) {
|
|
2454
|
+
sections.push({ section: sectionName, count: archive.length, lines: archive });
|
|
2455
|
+
return c.slice(0, bs) + keep.join('\n') + c.slice(se);
|
|
2456
|
+
}
|
|
2457
|
+
return c;
|
|
2458
|
+
}
|
|
2289
2459
|
// Prune Decisions section: entries like "- [Phase N]: ..."
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
const lines = decMatch[2].split('\n');
|
|
2294
|
-
const keep = [];
|
|
2295
|
-
const archive = [];
|
|
2296
|
-
for (const line of lines) {
|
|
2460
|
+
content = pruneSectionSpan(content, (lv, text) => (lv === 2 || lv === 3) && /^(?:Decisions|Decisions Made|Accumulated.*Decisions)$/i.test(text), (body) => {
|
|
2461
|
+
const keep = [], archive = [];
|
|
2462
|
+
for (const line of body.split('\n')) {
|
|
2297
2463
|
const phaseMatch = line.match(/^\s*-\s*\[Phase\s+(\d+)/i);
|
|
2298
2464
|
if (phaseMatch && parseInt(phaseMatch[1], 10) <= cutoff) {
|
|
2299
2465
|
archive.push(line);
|
|
@@ -2302,19 +2468,12 @@ function cmdStatePrune(cwd, options, raw) {
|
|
|
2302
2468
|
keep.push(line);
|
|
2303
2469
|
}
|
|
2304
2470
|
}
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
content = content.replace(decisionPattern, (_m, header) => `${header}${keep.join('\n')}`);
|
|
2308
|
-
}
|
|
2309
|
-
}
|
|
2471
|
+
return { keep, archive };
|
|
2472
|
+
}, 'Decisions');
|
|
2310
2473
|
// Prune Recently Completed section: entries mentioning phase numbers
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
const lines = recMatch[2].split('\n');
|
|
2315
|
-
const keep = [];
|
|
2316
|
-
const archive = [];
|
|
2317
|
-
for (const line of lines) {
|
|
2474
|
+
content = pruneSectionSpan(content, (lv, text) => (lv === 2 || lv === 3) && /^recently\s+completed$/i.test(text), (body) => {
|
|
2475
|
+
const keep = [], archive = [];
|
|
2476
|
+
for (const line of body.split('\n')) {
|
|
2318
2477
|
const phaseMatch = line.match(/Phase\s+(\d+)/i);
|
|
2319
2478
|
if (phaseMatch && parseInt(phaseMatch[1], 10) <= cutoff) {
|
|
2320
2479
|
archive.push(line);
|
|
@@ -2323,20 +2482,13 @@ function cmdStatePrune(cwd, options, raw) {
|
|
|
2323
2482
|
keep.push(line);
|
|
2324
2483
|
}
|
|
2325
2484
|
}
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
content = content.replace(recentPattern, (_m, header) => `${header}${keep.join('\n')}`);
|
|
2329
|
-
}
|
|
2330
|
-
}
|
|
2485
|
+
return { keep, archive };
|
|
2486
|
+
}, 'Recently Completed');
|
|
2331
2487
|
// Prune resolved blockers: lines marked as resolved (strikethrough ~~text~~
|
|
2332
2488
|
// or "[RESOLVED]" prefix) with a phase reference older than cutoff
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
const lines = blockersMatch[2].split('\n');
|
|
2337
|
-
const keep = [];
|
|
2338
|
-
const archive = [];
|
|
2339
|
-
for (const line of lines) {
|
|
2489
|
+
content = pruneSectionSpan(content, (lv, text) => (lv === 2 || lv === 3) && /^(?:Blockers|Blockers\/Concerns|Blockers\s*&\s*Concerns)$/i.test(text), (body) => {
|
|
2490
|
+
const keep = [], archive = [];
|
|
2491
|
+
for (const line of body.split('\n')) {
|
|
2340
2492
|
const isResolved = /~~.*~~|\[RESOLVED\]/i.test(line);
|
|
2341
2493
|
const phaseMatch = line.match(/Phase\s+(\d+)/i);
|
|
2342
2494
|
if (isResolved && phaseMatch && parseInt(phaseMatch[1], 10) <= cutoff) {
|
|
@@ -2346,20 +2498,13 @@ function cmdStatePrune(cwd, options, raw) {
|
|
|
2346
2498
|
keep.push(line);
|
|
2347
2499
|
}
|
|
2348
2500
|
}
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
content = content.replace(blockersPattern, (_m, header) => `${header}${keep.join('\n')}`);
|
|
2352
|
-
}
|
|
2353
|
-
}
|
|
2501
|
+
return { keep, archive };
|
|
2502
|
+
}, 'Blockers (resolved)');
|
|
2354
2503
|
// Prune Performance Metrics table rows: keep only rows for phases > cutoff.
|
|
2355
2504
|
// Preserves header rows (| Phase | ... and |---|...) and any prose around the table.
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
const sectionLines = metricsMatch[2].split('\n');
|
|
2360
|
-
const keep = [];
|
|
2361
|
-
const archive = [];
|
|
2362
|
-
for (const line of sectionLines) {
|
|
2505
|
+
content = pruneSectionSpan(content, (lv, text) => (lv === 2 || lv === 3) && /^performance\s+metrics$/i.test(text), (body) => {
|
|
2506
|
+
const keep = [], archive = [];
|
|
2507
|
+
for (const line of body.split('\n')) {
|
|
2363
2508
|
// Table data row: starts with | followed by a number (phase)
|
|
2364
2509
|
const tableRowMatch = line.match(/^\|\s*(\d+)\s*\|/);
|
|
2365
2510
|
if (tableRowMatch) {
|
|
@@ -2376,11 +2521,8 @@ function cmdStatePrune(cwd, options, raw) {
|
|
|
2376
2521
|
keep.push(line);
|
|
2377
2522
|
}
|
|
2378
2523
|
}
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
content = content.replace(metricsPattern, (_m, header) => `${header}${keep.join('\n')}`);
|
|
2382
|
-
}
|
|
2383
|
-
}
|
|
2524
|
+
return { keep, archive };
|
|
2525
|
+
}, 'Performance Metrics');
|
|
2384
2526
|
return { newContent: content, archivedSections: sections };
|
|
2385
2527
|
}
|
|
2386
2528
|
if (dryRun) {
|
|
@@ -2501,50 +2643,64 @@ function cmdStateCompletePhase(cwd, raw, overridePhase) {
|
|
|
2501
2643
|
updated.push('Last Activity Description');
|
|
2502
2644
|
}
|
|
2503
2645
|
// Update ## Current Position section
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
const
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2646
|
+
// ADR-1372 T6: positionPattern → tokenizeHeadings; stop at level ≥ 2.
|
|
2647
|
+
// Mirrors /(##\s*Current Position\s*\n)([\s\S]*?)(?=\n##|$)/i
|
|
2648
|
+
{
|
|
2649
|
+
const cpHs = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(body);
|
|
2650
|
+
const cpIdx = cpHs.findIndex(h => h.level === 2 && /^current\s+position$/i.test(h.text));
|
|
2651
|
+
if (cpIdx !== -1) {
|
|
2652
|
+
const cpH = cpHs[cpIdx];
|
|
2653
|
+
const cpBodyLines = body.split('\n');
|
|
2654
|
+
const cpHL = cpBodyLines[cpH.line - 1];
|
|
2655
|
+
const cpBodyStart = cpH.offset + cpHL.length + 1;
|
|
2656
|
+
let cpBodyEnd = body.length;
|
|
2657
|
+
for (let j = cpIdx + 1; j < cpHs.length; j++) {
|
|
2658
|
+
if (STOP_H2_PLUS(cpHs[j].level)) {
|
|
2659
|
+
cpBodyEnd = cpHs[j].offset - 1;
|
|
2660
|
+
break;
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
let posBody = body.slice(cpBodyStart, cpBodyEnd);
|
|
2664
|
+
// Update Phase line to show COMPLETE
|
|
2665
|
+
const newPhase = `Phase: ${currentPhase} — COMPLETE`;
|
|
2666
|
+
if (/^Phase:/m.test(posBody)) {
|
|
2667
|
+
posBody = posBody.replace(/^Phase:.*$/m, newPhase);
|
|
2668
|
+
}
|
|
2669
|
+
else {
|
|
2670
|
+
// Pipe-table format in Current Position (#1255)
|
|
2671
|
+
// Value cell must be bare (no "Phase:" label prefix) — the column header already provides the label.
|
|
2672
|
+
const replaced = (0, state_document_cjs_1.stateReplaceField)(posBody, 'Phase', `${currentPhase} — COMPLETE`);
|
|
2673
|
+
if (replaced !== null)
|
|
2674
|
+
posBody = replaced;
|
|
2675
|
+
}
|
|
2676
|
+
// Update Status line if present
|
|
2677
|
+
const newStatus = `Status: Phase ${currentPhase} complete`;
|
|
2678
|
+
if (/^Status:/m.test(posBody)) {
|
|
2679
|
+
posBody = posBody.replace(/^Status:.*$/m, newStatus);
|
|
2680
|
+
}
|
|
2681
|
+
else {
|
|
2682
|
+
// Pipe-table format in Current Position (#1255)
|
|
2683
|
+
const replaced = (0, state_document_cjs_1.stateReplaceField)(posBody, 'Status', `Phase ${currentPhase} complete`);
|
|
2684
|
+
if (replaced !== null)
|
|
2685
|
+
posBody = replaced;
|
|
2686
|
+
}
|
|
2687
|
+
// Update Last activity line if present
|
|
2688
|
+
const newActivity = `Last activity: ${today} — Phase ${currentPhase} marked complete`;
|
|
2689
|
+
if (/^Last activity:/im.test(posBody)) {
|
|
2690
|
+
posBody = posBody.replace(/^Last activity:.*$/im, newActivity);
|
|
2691
|
+
}
|
|
2692
|
+
else {
|
|
2693
|
+
// Pipe-table format in Current Position (#1255)
|
|
2694
|
+
// Value must match the inline branch (date + narrative), not bare date.
|
|
2695
|
+
const activityValue = `${today} — Phase ${currentPhase} marked complete`;
|
|
2696
|
+
const replaced = (0, state_document_cjs_1.stateReplaceField)(posBody, 'Last Activity', activityValue)
|
|
2697
|
+
?? (0, state_document_cjs_1.stateReplaceField)(posBody, 'Last activity', activityValue);
|
|
2698
|
+
if (replaced !== null)
|
|
2699
|
+
posBody = replaced;
|
|
2700
|
+
}
|
|
2701
|
+
body = body.slice(0, cpBodyStart) + posBody + body.slice(cpBodyEnd);
|
|
2702
|
+
updated.push('Current Position');
|
|
2545
2703
|
}
|
|
2546
|
-
body = body.replace(positionPattern, () => `${header}${posBody}`);
|
|
2547
|
-
updated.push('Current Position');
|
|
2548
2704
|
}
|
|
2549
2705
|
return reassemble(body);
|
|
2550
2706
|
}, cwd);
|