@ktpartners/dgs-platform 3.4.2 → 3.5.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/CHANGELOG.md +14 -0
- package/README.md +2 -0
- package/agents/dgs-codebase-cross-analyzer.md +1 -1
- package/agents/dgs-codebase-mapper.md +1 -1
- package/agents/dgs-codebase-synthesizer.md +1 -1
- package/agents/dgs-phase-researcher.md +1 -1
- package/bin/install.js +12 -2
- package/deliver-great-systems/bin/dgs-tools.cjs +7 -1
- package/deliver-great-systems/bin/lib/commands.cjs +66 -29
- package/deliver-great-systems/bin/lib/commands.test.cjs +221 -1
- package/deliver-great-systems/bin/lib/context.cjs +6 -6
- package/deliver-great-systems/bin/lib/context.test.cjs +9 -9
- package/deliver-great-systems/bin/lib/core.cjs +199 -9
- package/deliver-great-systems/bin/lib/core.test.cjs +242 -0
- package/deliver-great-systems/bin/lib/execution.cjs +7 -0
- package/deliver-great-systems/bin/lib/governance.cjs +7 -7
- package/deliver-great-systems/bin/lib/init.cjs +25 -17
- package/deliver-great-systems/bin/lib/init.test.cjs +69 -10
- package/deliver-great-systems/bin/lib/jobs.cjs +16 -10
- package/deliver-great-systems/bin/lib/jobs.test.cjs +17 -1
- package/deliver-great-systems/bin/lib/migration.test.cjs +8 -0
- package/deliver-great-systems/bin/lib/milestone-archival.test.cjs +186 -0
- package/deliver-great-systems/bin/lib/milestone.cjs +168 -37
- package/deliver-great-systems/bin/lib/milestone.test.cjs +113 -1
- package/deliver-great-systems/bin/lib/path-audit.test.cjs +128 -0
- package/deliver-great-systems/bin/lib/paths.cjs +1 -2
- package/deliver-great-systems/bin/lib/paths.test.cjs +3 -4
- package/deliver-great-systems/bin/lib/phase-versioned.test.cjs +134 -0
- package/deliver-great-systems/bin/lib/phase.cjs +60 -7
- package/deliver-great-systems/bin/lib/phase.test.cjs +168 -1
- package/deliver-great-systems/bin/lib/projects.test.cjs +38 -0
- package/deliver-great-systems/bin/lib/repos.cjs +8 -4
- package/deliver-great-systems/bin/lib/repos.test.cjs +6 -2
- package/deliver-great-systems/bin/lib/roadmap.cjs +9 -6
- package/deliver-great-systems/bin/lib/state-snapshot.test.cjs +134 -0
- package/deliver-great-systems/bin/lib/state.cjs +173 -26
- package/deliver-great-systems/templates/milestone-archive.md +1 -1
- package/deliver-great-systems/templates/roadmap.md +12 -10
- package/deliver-great-systems/workflows/abandon-milestone.md +8 -1
- package/deliver-great-systems/workflows/abandon-quick.md +1 -1
- package/deliver-great-systems/workflows/new-milestone.md +46 -12
- package/deliver-great-systems/workflows/quick-abandon.md +1 -1
- package/deliver-great-systems/workflows/quick.md +3 -3
- package/package.json +3 -2
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State snapshot milestone-context test (LOOK-03).
|
|
3
|
+
*
|
|
4
|
+
* Dedicated file (no state test file existed before) proving that
|
|
5
|
+
* `cmdStateSnapshot` ("state-snapshot") surfaces `current_milestone` alongside
|
|
6
|
+
* the existing `current_phase` so a bare "phase 3" in the snapshot is
|
|
7
|
+
* unambiguous about which milestone it belongs to.
|
|
8
|
+
*
|
|
9
|
+
* The milestone value is read from the AUTHORITATIVE STATE.md frontmatter
|
|
10
|
+
* signal (Phase 163) with the pinned precedence `current_milestone` then
|
|
11
|
+
* `milestone`, grammar-validated via isValidMilestoneVersion, and surfaced as
|
|
12
|
+
* `null` (never coerced, never thrown) when absent/out-of-grammar — existing
|
|
13
|
+
* snapshot consumers keep working in the flat / pre-versioned case.
|
|
14
|
+
*
|
|
15
|
+
* Uses Node.js built-in test runner (node:test) + assert.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const { describe, it } = require('node:test');
|
|
19
|
+
const assert = require('node:assert');
|
|
20
|
+
const { createFixture } = require('./test-helpers.cjs');
|
|
21
|
+
const { cmdStateSnapshot } = require('./state.cjs');
|
|
22
|
+
|
|
23
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Capture stdout from CLI commands that call output() (process.stdout.write +
|
|
27
|
+
* process.exit). Mirrors phase-versioned.test.cjs's captureStdout so multiple
|
|
28
|
+
* invocations can run in sequence. Returns { stdout, exitCode, json }.
|
|
29
|
+
*/
|
|
30
|
+
function captureStdout(fn) {
|
|
31
|
+
const chunks = [];
|
|
32
|
+
const origWrite = process.stdout.write.bind(process.stdout);
|
|
33
|
+
const origExit = process.exit;
|
|
34
|
+
let exitCode = null;
|
|
35
|
+
process.stdout.write = (data) => { chunks.push(String(data)); return true; };
|
|
36
|
+
process.exit = (code) => {
|
|
37
|
+
exitCode = code == null ? 0 : code;
|
|
38
|
+
throw new Error('__EXIT__');
|
|
39
|
+
};
|
|
40
|
+
try {
|
|
41
|
+
fn();
|
|
42
|
+
} catch (e) {
|
|
43
|
+
if (e && e.message !== '__EXIT__') throw e;
|
|
44
|
+
} finally {
|
|
45
|
+
process.stdout.write = origWrite;
|
|
46
|
+
process.exit = origExit;
|
|
47
|
+
}
|
|
48
|
+
const stdout = chunks.join('');
|
|
49
|
+
let json = null;
|
|
50
|
+
try { json = JSON.parse(stdout); } catch { /* not JSON */ }
|
|
51
|
+
return { stdout, exitCode, json };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* v2 project fixture whose STATE.md frontmatter + body the snapshot reads.
|
|
56
|
+
* `current_project` in config.local.json makes resolveStatePath resolve to
|
|
57
|
+
* projects/<slug>/STATE.md (mirrors phase-versioned.test.cjs's layout).
|
|
58
|
+
*
|
|
59
|
+
* @param {string} frontmatter - YAML lines between the `---` fences (no fences).
|
|
60
|
+
* @param {string} [body] - markdown body after the frontmatter (defaults to a
|
|
61
|
+
* minimal "**Current Phase:** 3" body so current_phase is populated).
|
|
62
|
+
*/
|
|
63
|
+
function stateFixture(frontmatter, body = '\n# Project State\n\n## Current Position\n\n**Current Phase:** 3\n') {
|
|
64
|
+
const slug = 'demo';
|
|
65
|
+
const fm = frontmatter ? `---\n${frontmatter}\n---` : '';
|
|
66
|
+
return createFixture({
|
|
67
|
+
'config.json': JSON.stringify({}),
|
|
68
|
+
'config.local.json': JSON.stringify({ current_project: slug }),
|
|
69
|
+
'PROJECTS.md': '# Projects\n\n| Project | Status |\n',
|
|
70
|
+
'REPOS.md': '# Repos\n\n| Name | Path |\n',
|
|
71
|
+
[`projects/${slug}/PROJECT.md`]: '# Project',
|
|
72
|
+
[`projects/${slug}/STATE.md`]: `${fm}${body}`,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─── cmdStateSnapshot current_milestone (LOOK-03) ─────────────────────────────
|
|
77
|
+
|
|
78
|
+
describe('cmdStateSnapshot current_milestone (LOOK-03)', () => {
|
|
79
|
+
it('Test 1: surfaces current_milestone alongside current_phase (both present)', () => {
|
|
80
|
+
const fixture = stateFixture('current_milestone: v26.0');
|
|
81
|
+
try {
|
|
82
|
+
const { json } = captureStdout(() => cmdStateSnapshot(fixture.cwd, false));
|
|
83
|
+
assert.ok(json, 'snapshot should emit JSON');
|
|
84
|
+
// The milestone qualifies the phase: both surfaced, unambiguous together.
|
|
85
|
+
assert.equal(json.current_milestone, 'v26.0', 'current_milestone surfaced from frontmatter');
|
|
86
|
+
assert.equal(json.current_phase, '3', 'current_phase still surfaced from body');
|
|
87
|
+
} finally {
|
|
88
|
+
fixture.cleanup();
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('Test 2: falls back to milestone when current_milestone absent (real-repo shape)', () => {
|
|
93
|
+
// Mirrors the real repo: only the advisory `milestone:` field is present.
|
|
94
|
+
const fixture = stateFixture('milestone: v25.0');
|
|
95
|
+
try {
|
|
96
|
+
const { json } = captureStdout(() => cmdStateSnapshot(fixture.cwd, false));
|
|
97
|
+
assert.ok(json, 'snapshot should emit JSON');
|
|
98
|
+
// Phase-163 precedence: current_milestone first, milestone second.
|
|
99
|
+
assert.equal(json.current_milestone, 'v25.0', 'precedence fallback to milestone honoured');
|
|
100
|
+
} finally {
|
|
101
|
+
fixture.cleanup();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('Test 3: absent milestone → null, no throw, rest of snapshot still populated', () => {
|
|
106
|
+
// Flat / pre-versioned STATE: no milestone frontmatter at all.
|
|
107
|
+
const fixture = stateFixture('');
|
|
108
|
+
try {
|
|
109
|
+
let result;
|
|
110
|
+
assert.doesNotThrow(() => {
|
|
111
|
+
result = captureStdout(() => cmdStateSnapshot(fixture.cwd, false));
|
|
112
|
+
}, 'snapshot must not throw when no milestone signal is present');
|
|
113
|
+
const json = result.json;
|
|
114
|
+
assert.ok(json, 'snapshot should emit JSON');
|
|
115
|
+
assert.equal(json.current_milestone, null, 'current_milestone is null when absent');
|
|
116
|
+
// Backward-compat: the rest of the snapshot is unaffected.
|
|
117
|
+
assert.equal(json.current_phase, '3', 'current_phase still populated (no regression)');
|
|
118
|
+
} finally {
|
|
119
|
+
fixture.cleanup();
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('Test 4: out-of-grammar current_milestone rejected (null), never coerced', () => {
|
|
124
|
+
const fixture = stateFixture('current_milestone: garbage');
|
|
125
|
+
try {
|
|
126
|
+
const { json } = captureStdout(() => cmdStateSnapshot(fixture.cwd, false));
|
|
127
|
+
assert.ok(json, 'snapshot should emit JSON');
|
|
128
|
+
// isValidMilestoneVersion rejects non-^v\d+\.\d+$ → null, never the raw string.
|
|
129
|
+
assert.equal(json.current_milestone, null, 'out-of-grammar value rejected, not coerced');
|
|
130
|
+
} finally {
|
|
131
|
+
fixture.cleanup();
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
|
-
const { loadConfig, getMilestoneInfo, getMilestonePhaseFilter, output, error, getProjectRoot } = require('./core.cjs');
|
|
7
|
+
const { loadConfig, getMilestoneInfo, getMilestonePhaseFilter, output, error, getProjectRoot, phasesDir, isValidMilestoneVersion, execGit } = require('./core.cjs');
|
|
8
8
|
const { extractFrontmatter, reconstructFrontmatter, spliceFrontmatter } = require('./frontmatter.cjs');
|
|
9
9
|
const { getPlanningRoot } = require('./paths.cjs');
|
|
10
10
|
|
|
@@ -296,15 +296,17 @@ function stateUpdateProgressInternal(cwd) {
|
|
|
296
296
|
let content = fs.readFileSync(statePath, 'utf-8');
|
|
297
297
|
|
|
298
298
|
// Count summaries across all phases
|
|
299
|
-
|
|
299
|
+
let phasesRel;
|
|
300
|
+
try { phasesRel = phasesDir(cwd); } catch { phasesRel = path.join(projectRoot, 'phases'); }
|
|
301
|
+
const phasesAbs = path.join(cwd, phasesRel);
|
|
300
302
|
let totalPlans = 0;
|
|
301
303
|
let totalSummaries = 0;
|
|
302
304
|
|
|
303
|
-
if (fs.existsSync(
|
|
304
|
-
const phaseDirs = fs.readdirSync(
|
|
305
|
+
if (fs.existsSync(phasesAbs)) {
|
|
306
|
+
const phaseDirs = fs.readdirSync(phasesAbs, { withFileTypes: true })
|
|
305
307
|
.filter(e => e.isDirectory()).map(e => e.name);
|
|
306
308
|
for (const dir of phaseDirs) {
|
|
307
|
-
const files = fs.readdirSync(path.join(
|
|
309
|
+
const files = fs.readdirSync(path.join(phasesAbs, dir));
|
|
308
310
|
totalPlans += files.filter(f => f.match(/-PLAN\.md$/i)).length;
|
|
309
311
|
totalSummaries += files.filter(f => f.match(/-SUMMARY\.md$/i)).length;
|
|
310
312
|
}
|
|
@@ -502,6 +504,17 @@ function cmdStateSnapshot(cwd, raw) {
|
|
|
502
504
|
|
|
503
505
|
const content = fs.readFileSync(statePath, 'utf-8');
|
|
504
506
|
|
|
507
|
+
// Resolve the milestone signal that qualifies the current phase (LOOK-03).
|
|
508
|
+
// Read the AUTHORITATIVE structured frontmatter field (Phase 163) with the
|
|
509
|
+
// pinned precedence `current_milestone` then `milestone`, grammar-validated.
|
|
510
|
+
// Surface `null` (never coerced, never thrown) when absent/out-of-grammar so
|
|
511
|
+
// flat / pre-versioned STATE.md still produces a valid snapshot.
|
|
512
|
+
const fm = extractFrontmatter(content) || {};
|
|
513
|
+
let currentMilestone = null;
|
|
514
|
+
for (const v of [fm.current_milestone, fm.milestone]) {
|
|
515
|
+
if (isValidMilestoneVersion(v)) { currentMilestone = v; break; }
|
|
516
|
+
}
|
|
517
|
+
|
|
505
518
|
// Extract basic fields
|
|
506
519
|
const currentPhase = stateExtractField(content, 'Current Phase');
|
|
507
520
|
const currentPhaseName = stateExtractField(content, 'Current Phase Name');
|
|
@@ -571,6 +584,7 @@ function cmdStateSnapshot(cwd, raw) {
|
|
|
571
584
|
}
|
|
572
585
|
|
|
573
586
|
const result = {
|
|
587
|
+
current_milestone: currentMilestone,
|
|
574
588
|
current_phase: currentPhase,
|
|
575
589
|
current_phase_name: currentPhaseName,
|
|
576
590
|
total_phases: totalPhases,
|
|
@@ -596,7 +610,7 @@ function cmdStateSnapshot(cwd, raw) {
|
|
|
596
610
|
* a YAML frontmatter object. Allows hooks and scripts to read state
|
|
597
611
|
* reliably via `state json` instead of fragile regex parsing.
|
|
598
612
|
*/
|
|
599
|
-
function buildStateFrontmatter(bodyContent, cwd) {
|
|
613
|
+
function buildStateFrontmatter(bodyContent, cwd, options) {
|
|
600
614
|
const currentPhase = stateExtractField(bodyContent, 'Current Phase');
|
|
601
615
|
const currentPhaseName = stateExtractField(bodyContent, 'Current Phase Name');
|
|
602
616
|
const currentPlan = stateExtractField(bodyContent, 'Current Plan');
|
|
@@ -625,16 +639,15 @@ function buildStateFrontmatter(bodyContent, cwd) {
|
|
|
625
639
|
|
|
626
640
|
if (cwd) {
|
|
627
641
|
try {
|
|
628
|
-
let
|
|
642
|
+
let phasesAbs;
|
|
629
643
|
try {
|
|
630
|
-
|
|
631
|
-
phasesDir = path.join(cwd, projectRoot, 'phases');
|
|
644
|
+
phasesAbs = path.join(cwd, phasesDir(cwd));
|
|
632
645
|
} catch {
|
|
633
|
-
|
|
646
|
+
phasesAbs = path.join(getPlanningRoot(cwd), 'phases');
|
|
634
647
|
}
|
|
635
|
-
if (fs.existsSync(
|
|
648
|
+
if (fs.existsSync(phasesAbs)) {
|
|
636
649
|
const isDirInMilestone = getMilestonePhaseFilter(cwd);
|
|
637
|
-
const phaseDirs = fs.readdirSync(
|
|
650
|
+
const phaseDirs = fs.readdirSync(phasesAbs, { withFileTypes: true })
|
|
638
651
|
.filter(e => e.isDirectory()).map(e => e.name)
|
|
639
652
|
.filter(isDirInMilestone);
|
|
640
653
|
let diskTotalPlans = 0;
|
|
@@ -642,7 +655,7 @@ function buildStateFrontmatter(bodyContent, cwd) {
|
|
|
642
655
|
let diskCompletedPhases = 0;
|
|
643
656
|
|
|
644
657
|
for (const dir of phaseDirs) {
|
|
645
|
-
const files = fs.readdirSync(path.join(
|
|
658
|
+
const files = fs.readdirSync(path.join(phasesAbs, dir));
|
|
646
659
|
const plans = files.filter(f => f.match(/-PLAN\.md$/i)).length;
|
|
647
660
|
const summaries = files.filter(f => f.match(/-SUMMARY\.md$/i)).length;
|
|
648
661
|
diskTotalPlans += plans;
|
|
@@ -689,6 +702,17 @@ function buildStateFrontmatter(bodyContent, cwd) {
|
|
|
689
702
|
|
|
690
703
|
const fm = { dgs_state_version: '1.0' };
|
|
691
704
|
|
|
705
|
+
// Preserve the authoritative structured current_milestone signal through the
|
|
706
|
+
// rebuild. The STATE sync re-derives the advisory `milestone`/`milestone_name`
|
|
707
|
+
// fields from the ROADMAP prose scrape, but the structured `current_milestone`
|
|
708
|
+
// field is set only by the milestone-lifecycle writers (new-milestone flow +
|
|
709
|
+
// init.cjs). If the existing frontmatter carries a grammar-valid value, carry
|
|
710
|
+
// it forward verbatim; reject (omit) out-of-grammar values — never coerce.
|
|
711
|
+
const existingCM = options && options.existingFm ? options.existingFm.current_milestone : undefined;
|
|
712
|
+
if (isValidMilestoneVersion(existingCM)) {
|
|
713
|
+
fm.current_milestone = existingCM;
|
|
714
|
+
}
|
|
715
|
+
|
|
692
716
|
if (milestone) fm.milestone = milestone;
|
|
693
717
|
if (milestoneName) fm.milestone_name = milestoneName;
|
|
694
718
|
if (currentPhase) fm.current_phase = currentPhase;
|
|
@@ -716,8 +740,12 @@ function stripFrontmatter(content) {
|
|
|
716
740
|
}
|
|
717
741
|
|
|
718
742
|
function syncStateFrontmatter(content, cwd) {
|
|
743
|
+
// Read the existing frontmatter BEFORE stripping so the structured
|
|
744
|
+
// current_milestone signal (set only by milestone-lifecycle writers) is
|
|
745
|
+
// carried through the rebuild rather than silently dropped.
|
|
746
|
+
const existingFm = extractFrontmatter(content);
|
|
719
747
|
const body = stripFrontmatter(content);
|
|
720
|
-
const fm = buildStateFrontmatter(body, cwd);
|
|
748
|
+
const fm = buildStateFrontmatter(body, cwd, { existingFm });
|
|
721
749
|
const yamlStr = reconstructFrontmatter(fm);
|
|
722
750
|
return `---\n${yamlStr}\n---\n\n${body}`;
|
|
723
751
|
}
|
|
@@ -910,6 +938,37 @@ function cmdStateArchiveQuickTasks(cwd, raw) {
|
|
|
910
938
|
* @param {string} cwd - Planning root directory
|
|
911
939
|
* @returns {{ success: boolean, milestone: string, completed_date: string }}
|
|
912
940
|
*/
|
|
941
|
+
/**
|
|
942
|
+
* Shared STATE-body finalization for milestone completion.
|
|
943
|
+
*
|
|
944
|
+
* Performs all the "milestone shipped" body rewrites: the progress bar, the
|
|
945
|
+
* Current focus / Status / Last activity lines, AND the Current Position
|
|
946
|
+
* `Phase:` reset that keeps the dashboard from showing a frozen, mid-execution
|
|
947
|
+
* phase after a milestone ships (DASH-STALE-01).
|
|
948
|
+
*
|
|
949
|
+
* Used by both markMilestoneComplete (normal completion) and reconcileMilestone
|
|
950
|
+
* (self-heal) so the two paths produce byte-identical STATE bodies.
|
|
951
|
+
*
|
|
952
|
+
* @param {string} body - STATE.md body (frontmatter already stripped)
|
|
953
|
+
* @param {{ milestone: string, today: string, totalPhases: (number|string), totalPlans: (number|string) }} opts
|
|
954
|
+
* @returns {string} the rewritten body
|
|
955
|
+
*/
|
|
956
|
+
function _finalizeMilestoneStateBody(body, { milestone, today, totalPhases, totalPlans }) {
|
|
957
|
+
// Update progress bar
|
|
958
|
+
body = body.replace(/Progress:\s*\[[^\]]*\]\s*\d+%/, 'Progress: [██████████] 100%');
|
|
959
|
+
// Update current focus
|
|
960
|
+
body = body.replace(/\*\*Current focus:\*\*\s*.+/, `**Current focus:** Milestone ${milestone} complete — shipped ${today}`);
|
|
961
|
+
// Update status line in Current Position
|
|
962
|
+
body = body.replace(/Status:\s*.+/, `Status: Milestone ${milestone} shipped ${today}`);
|
|
963
|
+
// Update last activity in Current Position
|
|
964
|
+
body = body.replace(/(Last activity:\s*).+/, `$1${today} -- Milestone ${milestone} shipped (${totalPhases} phases, ${totalPlans} plans)`);
|
|
965
|
+
// Reset the Current Position Phase: line so the dashboard no longer references
|
|
966
|
+
// an in-progress phase. Multiline-anchored so it targets the line-leading
|
|
967
|
+
// `Phase:` entry, not substrings like "Phases completed".
|
|
968
|
+
body = body.replace(/^Phase:\s*.+$/m, `Phase: — (between milestones; ${milestone} shipped ${today})`);
|
|
969
|
+
return body;
|
|
970
|
+
}
|
|
971
|
+
|
|
913
972
|
function markMilestoneComplete(cwd) {
|
|
914
973
|
const statePath = resolveStatePath(cwd);
|
|
915
974
|
if (!fs.existsSync(statePath)) {
|
|
@@ -933,20 +992,10 @@ function markMilestoneComplete(cwd) {
|
|
|
933
992
|
// Reconstruct frontmatter and preserve body
|
|
934
993
|
let body = content.replace(/^---\n[\s\S]*?\n---\n*/, '');
|
|
935
994
|
|
|
936
|
-
// Update markdown body to reflect completion
|
|
937
|
-
const milestoneName = fm.milestone_name || fm.milestone || 'unknown';
|
|
995
|
+
// Update markdown body to reflect completion (shared with reconcileMilestone)
|
|
938
996
|
const totalPhases = (fm.progress && fm.progress.total_phases) || '?';
|
|
939
997
|
const totalPlans = (fm.progress && fm.progress.total_plans) || '?';
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
// Update progress bar
|
|
943
|
-
body = body.replace(/Progress:\s*\[[^\]]*\]\s*\d+%/, 'Progress: [██████████] 100%');
|
|
944
|
-
// Update current focus
|
|
945
|
-
body = body.replace(/\*\*Current focus:\*\*\s*.+/, `**Current focus:** Milestone ${milestone} complete — shipped ${today}`);
|
|
946
|
-
// Update status line in Current Position
|
|
947
|
-
body = body.replace(/Status:\s*.+/, `Status: Milestone ${milestone} shipped ${today}`);
|
|
948
|
-
// Update last activity in Current Position
|
|
949
|
-
body = body.replace(/(Last activity:\s*).+/, `$1${today} -- Milestone ${milestone} shipped (${totalPhases} phases, ${totalPlans} plans)`);
|
|
998
|
+
body = _finalizeMilestoneStateBody(body, { milestone, today, totalPhases, totalPlans });
|
|
950
999
|
|
|
951
1000
|
const yamlStr = reconstructFrontmatter(fm);
|
|
952
1001
|
content = `---\n${yamlStr}\n---\n\n${body}`;
|
|
@@ -961,6 +1010,99 @@ function cmdMarkMilestoneComplete(cwd, raw) {
|
|
|
961
1010
|
output(result, raw);
|
|
962
1011
|
}
|
|
963
1012
|
|
|
1013
|
+
/**
|
|
1014
|
+
* Self-heal a shipped-but-not-flipped project (DASH-STALE-02).
|
|
1015
|
+
*
|
|
1016
|
+
* Completion's STATE flip is sometimes skipped, leaving a project that has a
|
|
1017
|
+
* MILESTONES.md entry AND a matching git tag (i.e. genuinely shipped) but whose
|
|
1018
|
+
* STATE.md still reports status `executing` and a mid-execution `Phase:` line.
|
|
1019
|
+
* This applies the SAME finalization as markMilestoneComplete to bring STATE in
|
|
1020
|
+
* line. It is idempotent — a no-op on an already-correct or not-yet-shipped
|
|
1021
|
+
* project. Scope is the CURRENT project (the resolved STATE.md) only.
|
|
1022
|
+
*
|
|
1023
|
+
* @param {string} cwd - Planning root directory
|
|
1024
|
+
* @returns {{ healed: boolean, milestone: (string|null), changes?: string[], reason?: string }}
|
|
1025
|
+
*/
|
|
1026
|
+
function reconcileMilestone(cwd) {
|
|
1027
|
+
const statePath = resolveStatePath(cwd);
|
|
1028
|
+
if (!fs.existsSync(statePath)) {
|
|
1029
|
+
error('STATE.md not found');
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
const content = fs.readFileSync(statePath, 'utf-8');
|
|
1033
|
+
const fm = extractFrontmatter(content) || {};
|
|
1034
|
+
|
|
1035
|
+
// Determine milestone version: structured fields first, then ROADMAP scrape.
|
|
1036
|
+
let version = null;
|
|
1037
|
+
for (const v of [fm.current_milestone, fm.milestone]) {
|
|
1038
|
+
if (isValidMilestoneVersion(v)) { version = v; break; }
|
|
1039
|
+
}
|
|
1040
|
+
if (!version) {
|
|
1041
|
+
try {
|
|
1042
|
+
const info = getMilestoneInfo(cwd);
|
|
1043
|
+
if (isValidMilestoneVersion(info.version)) version = info.version;
|
|
1044
|
+
} catch {}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// Detect "shipped": MILESTONES.md heading AND a matching git tag — both required.
|
|
1048
|
+
let shipped = false;
|
|
1049
|
+
if (version) {
|
|
1050
|
+
let hasMilestoneEntry = false;
|
|
1051
|
+
try {
|
|
1052
|
+
const milestonesPath = path.join(getPlanningRoot(cwd), 'MILESTONES.md');
|
|
1053
|
+
const mContent = fs.readFileSync(milestonesPath, 'utf-8');
|
|
1054
|
+
const headingRe = new RegExp(`^##\\s+${escapeRegex(version)}\\b`, 'm');
|
|
1055
|
+
hasMilestoneEntry = headingRe.test(mContent);
|
|
1056
|
+
} catch {}
|
|
1057
|
+
const tagResult = execGit(cwd, ['tag', '-l', version]);
|
|
1058
|
+
const hasTag = tagResult.exitCode === 0 &&
|
|
1059
|
+
tagResult.stdout.split('\n').map((s) => s.trim()).includes(version);
|
|
1060
|
+
shipped = hasMilestoneEntry && hasTag;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
if (!shipped) {
|
|
1064
|
+
return { healed: false, milestone: version, reason: 'not_shipped' };
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// Detect "not flipped": status not milestone_shipped OR the Phase: line still
|
|
1068
|
+
// references an in-progress phase.
|
|
1069
|
+
const statusFlipped = fm.status === 'milestone_shipped';
|
|
1070
|
+
const body = content.replace(/^---\n[\s\S]*?\n---\n*/, '');
|
|
1071
|
+
const phaseLine = body.match(/^Phase:\s*(.+)$/m);
|
|
1072
|
+
const phaseInProgress = phaseLine ? /\d+\s+of\s+\d+/.test(phaseLine[1]) : false;
|
|
1073
|
+
|
|
1074
|
+
if (statusFlipped && !phaseInProgress) {
|
|
1075
|
+
return { healed: false, milestone: version, reason: 'already_correct' };
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// Heal: apply the same finalization as markMilestoneComplete.
|
|
1079
|
+
const today = new Date().toISOString().split('T')[0];
|
|
1080
|
+
const now = new Date().toISOString();
|
|
1081
|
+
const changes = [];
|
|
1082
|
+
if (!statusFlipped) changes.push('status');
|
|
1083
|
+
if (phaseInProgress) changes.push('phase');
|
|
1084
|
+
|
|
1085
|
+
fm.status = 'milestone_shipped';
|
|
1086
|
+
if (!fm.progress) fm.progress = {};
|
|
1087
|
+
fm.progress.percent = 100;
|
|
1088
|
+
fm.last_updated = now;
|
|
1089
|
+
fm.completed_date = today;
|
|
1090
|
+
|
|
1091
|
+
const totalPhases = (fm.progress && fm.progress.total_phases) || '?';
|
|
1092
|
+
const totalPlans = (fm.progress && fm.progress.total_plans) || '?';
|
|
1093
|
+
const newBody = _finalizeMilestoneStateBody(body, { milestone: version, today, totalPhases, totalPlans });
|
|
1094
|
+
|
|
1095
|
+
const yamlStr = reconstructFrontmatter(fm);
|
|
1096
|
+
fs.writeFileSync(statePath, `---\n${yamlStr}\n---\n\n${newBody}`, 'utf-8');
|
|
1097
|
+
|
|
1098
|
+
return { healed: true, milestone: version, changes };
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
function cmdReconcileMilestone(cwd, raw) {
|
|
1102
|
+
const result = reconcileMilestone(cwd);
|
|
1103
|
+
output(result, raw, JSON.stringify(result));
|
|
1104
|
+
}
|
|
1105
|
+
|
|
964
1106
|
// ─── Ad-hoc milestone marker (Phase 159, ADH-04 primary) ─────────────────────
|
|
965
1107
|
|
|
966
1108
|
/**
|
|
@@ -1109,6 +1251,8 @@ function cmdStateAdhocReadiness(cwd, raw) {
|
|
|
1109
1251
|
module.exports = {
|
|
1110
1252
|
stateExtractField,
|
|
1111
1253
|
stateReplaceField,
|
|
1254
|
+
buildStateFrontmatter,
|
|
1255
|
+
syncStateFrontmatter,
|
|
1112
1256
|
writeStateMd,
|
|
1113
1257
|
cmdStateLoad,
|
|
1114
1258
|
cmdStateGet,
|
|
@@ -1125,8 +1269,11 @@ module.exports = {
|
|
|
1125
1269
|
cmdStateSnapshot,
|
|
1126
1270
|
cmdStateJson,
|
|
1127
1271
|
cmdStateArchiveQuickTasks,
|
|
1272
|
+
_finalizeMilestoneStateBody,
|
|
1128
1273
|
markMilestoneComplete,
|
|
1129
1274
|
cmdMarkMilestoneComplete,
|
|
1275
|
+
reconcileMilestone,
|
|
1276
|
+
cmdReconcileMilestone,
|
|
1130
1277
|
stateReadAdhoc,
|
|
1131
1278
|
stateSetAdhoc,
|
|
1132
1279
|
cmdStateReadAdhoc,
|
|
@@ -121,5 +121,5 @@ _For current project status, see ROADMAP.md_
|
|
|
121
121
|
|
|
122
122
|
- Update ROADMAP.md to collapse completed milestone in `<details>` tag
|
|
123
123
|
- Update PROJECT.md to brownfield format with Current State section
|
|
124
|
-
-
|
|
124
|
+
- Restart phase numbering at `01` in the next milestone — numbering is per-milestone, with the milestone version disambiguating (active phases live under `phases/<version>/NN-slug/`). Already-archived milestones keep their original numbers; no renumbering.
|
|
125
125
|
</guidelines>
|
|
@@ -95,7 +95,7 @@ Plans:
|
|
|
95
95
|
## Progress
|
|
96
96
|
|
|
97
97
|
**Execution Order:**
|
|
98
|
-
Phases execute in numeric order: 2 → 2.1 → 2.2 → 3 → 3.1 → 4
|
|
98
|
+
Phases execute in numeric order within a milestone: 2 → 2.1 → 2.2 → 3 → 3.1 → 4
|
|
99
99
|
|
|
100
100
|
| Phase | Plans Complete | Status | Completed |
|
|
101
101
|
|-------|----------------|--------|-----------|
|
|
@@ -125,7 +125,7 @@ Phases execute in numeric order: 2 → 2.1 → 2.2 → 3 → 3.1 → 4
|
|
|
125
125
|
**After milestones ship:**
|
|
126
126
|
- Collapse completed milestones in `<details>` tags
|
|
127
127
|
- Add new milestone sections for upcoming work
|
|
128
|
-
-
|
|
128
|
+
- Restart phase numbering at `01` for each new milestone — numbering is per-milestone, scoped under `phases/<version>/NN-slug/` with the milestone version as the disambiguator (mirrors the `milestones/<version>-phases/` archive layout). Pre-existing flat-layout projects keep their existing global numbers — numbering is never rewritten.
|
|
129
129
|
</guidelines>
|
|
130
130
|
|
|
131
131
|
<status_values>
|
|
@@ -145,8 +145,8 @@ After completing first milestone, reorganize with milestone groupings:
|
|
|
145
145
|
## Milestones
|
|
146
146
|
|
|
147
147
|
- ✅ **v1.0 MVP** - Phases 1-4 (shipped YYYY-MM-DD)
|
|
148
|
-
- 🚧 **v1.1 [Name]** - Phases
|
|
149
|
-
- 📋 **v2.0 [Name]** - Phases
|
|
148
|
+
- 🚧 **v1.1 [Name]** - Phases 1-2 (in progress)
|
|
149
|
+
- 📋 **v2.0 [Name]** - Phases 1-4 (planned)
|
|
150
150
|
|
|
151
151
|
## Phases
|
|
152
152
|
|
|
@@ -170,14 +170,16 @@ Plans:
|
|
|
170
170
|
|
|
171
171
|
**Milestone Goal:** [What v1.1 delivers]
|
|
172
172
|
|
|
173
|
-
|
|
173
|
+
**Phase directory:** `phases/v1.1/` (numbering restarts at `01` for this milestone)
|
|
174
|
+
|
|
175
|
+
#### Phase 1: [Name]
|
|
174
176
|
**Goal**: [What this phase delivers]
|
|
175
|
-
**Depends on**:
|
|
177
|
+
**Depends on**: v1.0 milestone (first phase of v1.1 — numbering restarts at 01)
|
|
176
178
|
**Plans**: 2 plans
|
|
177
179
|
|
|
178
180
|
Plans:
|
|
179
|
-
- [ ]
|
|
180
|
-
- [ ]
|
|
181
|
+
- [ ] 01-01: [Brief description]
|
|
182
|
+
- [ ] 01-02: [Brief description]
|
|
181
183
|
|
|
182
184
|
[... remaining v1.1 phases ...]
|
|
183
185
|
|
|
@@ -193,12 +195,12 @@ Plans:
|
|
|
193
195
|
|-------|-----------|----------------|--------|-----------|
|
|
194
196
|
| 1. Foundation | v1.0 | 3/3 | Complete | YYYY-MM-DD |
|
|
195
197
|
| 2. Features | v1.0 | 2/2 | Complete | YYYY-MM-DD |
|
|
196
|
-
|
|
|
198
|
+
| 1. Security | v1.1 | 0/2 | Not started | - |
|
|
197
199
|
```
|
|
198
200
|
|
|
199
201
|
**Notes:**
|
|
200
202
|
- Milestone emoji: ✅ shipped, 🚧 in progress, 📋 planned
|
|
201
203
|
- Completed milestones collapsed in `<details>` for readability
|
|
202
204
|
- Current/future milestones expanded
|
|
203
|
-
-
|
|
205
|
+
- Per-milestone phase numbering: each milestone restarts at `01` (zero-padded), namespaced under `phases/<version>/`
|
|
204
206
|
- Progress table includes milestone column
|
|
@@ -85,7 +85,13 @@ EXIT_CODE=$?
|
|
|
85
85
|
**Step 4: Display result**
|
|
86
86
|
|
|
87
87
|
**If exit code is 0 (success):**
|
|
88
|
-
Parse the restored-docs list and any warnings from RESULT (JSON: `{ abandoned, slug, base_ref, restored, reverted, warnings }`).
|
|
88
|
+
Parse the restored-docs list and any warnings from RESULT (JSON: `{ abandoned, slug, base_ref, restored, reverted, phases_dir_removed, warnings }`).
|
|
89
|
+
|
|
90
|
+
Abandon also removes the seeded versioned phases directory `phases/<version>/`
|
|
91
|
+
that `create-adhoc` created, so no versioned phases residue remains after the
|
|
92
|
+
planning docs are restored. Any tracked phase files under it are staged for
|
|
93
|
+
deletion in the same attributed reversion commit. The removed path is reported as
|
|
94
|
+
`phases_dir_removed` (null when there was nothing to remove).
|
|
89
95
|
```
|
|
90
96
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
91
97
|
DGS ► MILESTONE ABANDONED ✓
|
|
@@ -116,5 +122,6 @@ Display the error / reverted-vs-not report from RESULT verbatim (ADH-20 — it s
|
|
|
116
122
|
- [ ] The 5 project docs path-scoped-restored to the base ref (never a whole-tree reset) (ADH-09)
|
|
117
123
|
- [ ] Attributed reversion commit recorded (ADH-20)
|
|
118
124
|
- [ ] active_context cleared
|
|
125
|
+
- [ ] Seeded versioned phases dir `phases/<version>/` removed; no versioned residue (reported as `phases_dir_removed`)
|
|
119
126
|
- [ ] Owned remote milestone branch deleted during teardown (non-fatal on failure); failures surfaced as warnings (ADH-20)
|
|
120
127
|
</success_criteria>
|
|
@@ -13,7 +13,7 @@ Requires explicit confirmation before proceeding. This is destructive and cannot
|
|
|
13
13
|
Check for active product-level quick:
|
|
14
14
|
```bash
|
|
15
15
|
ACTIVE=$(node -e "
|
|
16
|
-
const q = require('
|
|
16
|
+
const q = require(process.env.HOME + '/.claude/deliver-great-systems/bin/lib/quick.cjs');
|
|
17
17
|
const a = q.getActiveQuick(process.cwd());
|
|
18
18
|
process.stdout.write(JSON.stringify(a || { none: true }));
|
|
19
19
|
")
|