@ktpartners/dgs-platform 3.5.0 → 3.5.3

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.
Files changed (36) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/bin/install.js +22 -0
  3. package/deliver-great-systems/bin/lib/core.cjs +21 -0
  4. package/deliver-great-systems/bin/lib/core.test.cjs +66 -0
  5. package/deliver-great-systems/bin/lib/ideas.cjs +39 -11
  6. package/deliver-great-systems/bin/lib/ideas.test.cjs +32 -3
  7. package/deliver-great-systems/bin/lib/init.cjs +23 -0
  8. package/deliver-great-systems/bin/lib/init.test.cjs +78 -0
  9. package/deliver-great-systems/bin/lib/jobs.cjs +194 -83
  10. package/deliver-great-systems/bin/lib/jobs.test.cjs +272 -15
  11. package/deliver-great-systems/bin/lib/overlap.cjs +14 -4
  12. package/deliver-great-systems/bin/lib/overlap.test.cjs +13 -0
  13. package/deliver-great-systems/bin/lib/package-scan-report.cjs +19 -0
  14. package/deliver-great-systems/bin/lib/package-scan-report.test.cjs +18 -0
  15. package/deliver-great-systems/bin/lib/phase.cjs +12 -4
  16. package/deliver-great-systems/bin/lib/phase.test.cjs +69 -0
  17. package/deliver-great-systems/bin/lib/projects.cjs +13 -3
  18. package/deliver-great-systems/bin/lib/projects.test.cjs +8 -0
  19. package/deliver-great-systems/bin/lib/roadmap.cjs +26 -5
  20. package/deliver-great-systems/bin/lib/roadmap.test.cjs +86 -0
  21. package/deliver-great-systems/bin/lib/search.cjs +62 -15
  22. package/deliver-great-systems/bin/lib/search.test.cjs +94 -0
  23. package/deliver-great-systems/bin/lib/verify.cjs +37 -20
  24. package/deliver-great-systems/bin/lib/verify.test.cjs +58 -0
  25. package/deliver-great-systems/references/git-integration.md +1 -1
  26. package/deliver-great-systems/workflows/audit-milestone.md +1 -1
  27. package/deliver-great-systems/workflows/cleanup.md +1 -1
  28. package/deliver-great-systems/workflows/codereview.md +1 -1
  29. package/deliver-great-systems/workflows/complete-milestone.md +3 -3
  30. package/deliver-great-systems/workflows/discuss-phase.md +5 -1
  31. package/deliver-great-systems/workflows/execute-phase.md +2 -2
  32. package/deliver-great-systems/workflows/execute-plan.md +2 -2
  33. package/deliver-great-systems/workflows/pause-work.md +1 -1
  34. package/deliver-great-systems/workflows/plan-phase.md +6 -2
  35. package/deliver-great-systems/workflows/resume-project.md +2 -2
  36. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -8,6 +8,33 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [3.5.3] - 2026-06-28
12
+
13
+ ### Fixed
14
+ - **Flat-first jobs tests (quick-260628-r5i)** — aligned the root `tests/jobs.test.cjs` with the flat-first `listJobs` behavior introduced in 3.5.2 (which had updated `bin/lib/jobs.test.cjs` but left the root copy stale, leaving the suite 61/2 on that file). The grouped-jobs test now writes flat `jobs/milestone-*.md` files grouped by frontmatter status, and the directory test asserts the legacy `jobs/pending|in-progress|completed/` subdirs are NOT auto-created. Suite back to green.
15
+
16
+ ## [3.5.2] - 2026-06-28
17
+
18
+ ### Fixed
19
+ - **Flat-store readers flat-first (quick-260628-otc)** — `search.cjs scanIdeas`, `jobs.cjs listJobs`, and `ideas.cjs cmdIdeasFindRelated` now read the migrated flat frontmatter-status layout (`ideas/*.md`, `jobs/milestone-*.md`) first, so `/dgs:search`, `/dgs:list-jobs`, `/dgs:diff-report`, and `/dgs:find-related-ideas` no longer silently return nothing on flat installs. `listJobs` is now read-only (stops recreating abandoned `jobs/{pending,in-progress,completed}` subdirs) and `cmdIdeasUpdate` returns the file's real flat path. Legacy status-subdirs remain an existence-gated fallback.
20
+ - **Versioned phases-dir scanners (quick-260628-p59)** — `verify.cjs` (consistency + health W010), `overlap.cjs`, `projects.cjs`, and `package-scan-report.cjs` now descend `phases/<version>/NN-slug/` (via `core.phasesDir` / `isValidMilestoneVersion`) instead of treating the `vN.N` version dir as a phase dir, so consistency validation, `/dgs:overlap-check`, repos-touched listings, and package-scan active-phase reports work under per-milestone numbering. Flat layout still supported.
21
+ - **Versioned phases-dir workflow globs (quick-260628-pn3)** — 9 phases-dir globs/heuristics across 7 workflows (`complete-milestone`, `audit-milestone`, `plan-phase`, `discuss-phase`, `pause-work`, `resume-project`, `cleanup`) are now dual-layout (flat + `phases/<version>/`), so milestone-completion accomplishment extraction, SUMMARY cross-checks, prior-phase continuity, pause/resume detection, and cleanup no longer come up empty on versioned milestones. Also drops a GNU-only `grep -oP`.
22
+ - **Roadmap milestone parsers recognize all formats (quick-260628-pxx)** — `detectActiveMilestone`, `getMilestoneInfo`, `parseProjectMilestone`, `cmdPhaseAdd` range-bump, and `cmdRoadmapAnalyze` now recognize the ad-hoc `## Active Milestone: vX.Y`, single-milestone bold-line `**Milestone:** vX.Y`, and per-milestone-namespaced `Phases vX.Y/N-M` formats (additively, milestone-name-preserving), gated against multi-milestone over-match. Fixes blank dashboard milestone, `create-milestone` "No active milestone found", and stale `add-phase` ranges on the newer roadmap formats.
23
+
24
+ ## [3.5.1] - 2026-06-28
25
+
26
+ ### Added
27
+ - **`dgs-tools state reconcile-milestone` (quick-260627-m3k)** — self-heals a project whose milestone shipped (a `## <version>` heading in MILESTONES.md AND a matching git tag both present) but whose STATE.md was never flipped, so `/dgs:list-projects` stops showing a stale in-progress phase or false `executing` status. Conservative (no-op unless both shipped markers are present), idempotent, and current-project scoped.
28
+
29
+ ### Fixed
30
+ - **Dashboard staleness after milestone completion (quick-260627-m3k)** — `markMilestoneComplete` now resets the STATE.md `Phase:` line (via a shared `_finalizeMilestoneStateBody` helper) instead of leaving it frozen at the last in-progress value, so a shipped milestone no longer surfaces a stale phase / `executing` status on the dashboard.
31
+ - **Per-milestone finalize matcher (quick-260628-ikd)** — `roadmapUpdatePlanProgressInternal` is now format-agnostic: it updates progress-table rows keyed in the composite `<version>/N.` form (and bare `N.`) and preserves the optional Milestone column, so `phase finalize` / `plan finalize` update per-milestone ROADMAPs instead of silently no-op'ing.
32
+ - **Branch-bounded diff-ref and self-check lookups (quick-260628-mlk)** — the codereview diff-ref, phase-prefix self-check probes, and the milestone-stats query are now bounded to the milestone branch, so per-milestone commit-prefix collisions (`feat(NN-NN):` reused across milestones) no longer reach unrelated commits from a different milestone.
33
+ - **Jobs healthCheck flat-first (quick-260628-kxr-01)** — reworked the jobs `healthCheck` to be flat-first.
34
+
35
+ ### Documentation
36
+ - **Stale-dashboard recovery guide (quick-260627-kvy)** — added a "Recovering a stale milestone dashboard" section to `docs/USER-GUIDE.md` documenting `state reconcile-milestone` and the per-project recovery playbook.
37
+
11
38
  ## [3.5.0] - 2026-06-27
12
39
 
13
40
  ### Added — v25.0 Per-Milestone Phase Numbering via Directory Namespacing (Phases 163-170)
package/bin/install.js CHANGED
@@ -615,6 +615,22 @@ function convertClaudeToGeminiToml(content) {
615
615
  return toml;
616
616
  }
617
617
 
618
+ /**
619
+ * Rewrite HOME-relative ".claude" references to the target config dir basename.
620
+ * Swaps ONLY the ".claude" basename, preserving the $HOME / process.env.HOME prefix.
621
+ * For a default ".claude" install cfgBase === ".claude" => both replaces are no-ops.
622
+ * @param {string} content - file content
623
+ * @param {string} cfgBase - target config dir basename, e.g. ".claude-v25-test" or ".claude"
624
+ */
625
+ function rewriteHomeRelativeConfigPaths(content, cfgBase) {
626
+ // shell form: $HOME/.claude/ -> $HOME/<cfgBase>/
627
+ content = content.replace(/\$HOME\/\.claude\//g, '$HOME/' + cfgBase + '/');
628
+ // JS concat form: process.env.HOME + '/.claude/' (both quote styles)
629
+ // matches a quote char followed by /.claude/ ; $1 backreference preserves the quote
630
+ content = content.replace(/(["'])\/\.claude\//g, '$1/' + cfgBase + '/');
631
+ return content;
632
+ }
633
+
618
634
  /**
619
635
  * Copy commands to a flat structure for OpenCode
620
636
  * OpenCode expects: command/dgs-help.md (invoked as /dgs-help)
@@ -664,6 +680,8 @@ function copyFlattenedCommands(srcDir, destDir, prefix, pathPrefix, runtime) {
664
680
  content = content.replace(globalClaudeRegex, pathPrefix);
665
681
  content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`);
666
682
  content = content.replace(opencodeDirRegex, pathPrefix);
683
+ const cfgBase = pathPrefix.replace(/\/$/, '').split('/').pop();
684
+ content = rewriteHomeRelativeConfigPaths(content, cfgBase);
667
685
  content = processAttribution(content, getCommitAttribution(runtime));
668
686
  content = convertClaudeToOpencodeFrontmatter(content);
669
687
 
@@ -705,6 +723,8 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
705
723
  const localClaudeRegex = /\.\/\.claude\//g;
706
724
  content = content.replace(globalClaudeRegex, pathPrefix);
707
725
  content = content.replace(localClaudeRegex, `./${dirName}/`);
726
+ const cfgBase = pathPrefix.replace(/\/$/, '').split('/').pop();
727
+ content = rewriteHomeRelativeConfigPaths(content, cfgBase);
708
728
  content = processAttribution(content, getCommitAttribution(runtime));
709
729
 
710
730
  // Convert frontmatter for opencode compatibility
@@ -1657,6 +1677,8 @@ function install(isGlobal, runtime = 'claude') {
1657
1677
  // Always replace ~/.claude/ as it is the source of truth in the repo
1658
1678
  const dirRegex = /~\/\.claude\//g;
1659
1679
  content = content.replace(dirRegex, pathPrefix);
1680
+ const cfgBase = pathPrefix.replace(/\/$/, '').split('/').pop();
1681
+ content = rewriteHomeRelativeConfigPaths(content, cfgBase);
1660
1682
  content = processAttribution(content, getCommitAttribution(runtime));
1661
1683
  // Convert frontmatter for runtime compatibility
1662
1684
  if (isOpencode) {
@@ -534,6 +534,27 @@ function getMilestoneInfo(cwd) {
534
534
  name: headingMatch[2].trim(),
535
535
  };
536
536
  }
537
+ // Ad-hoc container format: "## Active Milestone: v25.1 add-prs" — version
538
+ // and real NAME live on the heading. Runs after the heading branch so a
539
+ // legacy "### vX.Y Name" heading still wins; the literal "Active Milestone:"
540
+ // prefix is specific enough to run un-gated.
541
+ const adhoc = cleaned.match(/^##\s+Active Milestone:\s*v(\d+\.\d+)\s+(.+?)\s*$/im);
542
+ if (adhoc) {
543
+ return { version: 'v' + adhoc[1], name: adhoc[2].trim() };
544
+ }
545
+
546
+ // Bold-line single-milestone format: "**Milestone:** v30.1 — Name".
547
+ // GATED single-milestone (mirror analyzeMilestonePhases): only when at most
548
+ // one version-bearing heading exists, so a stray bold line never hijacks a
549
+ // multi-milestone heading roadmap.
550
+ const versionHeadings = cleaned.match(/^#{2,4}\s+.*v\d+\.\d+/gim) || [];
551
+ if (versionHeadings.length <= 1) {
552
+ const bold = cleaned.match(/^\*\*Milestone:\*\*\s*v(\d+\.\d+)\s*(?:[—-]\s*)?(.*)$/im);
553
+ if (bold) {
554
+ return { version: 'v' + bold[1], name: bold[2].trim() || 'milestone' };
555
+ }
556
+ }
557
+
537
558
  // Fallback: try bare version match
538
559
  const versionMatch = cleaned.match(/v(\d+\.\d+)/);
539
560
  return {
@@ -28,6 +28,7 @@ const {
28
28
  resolveModelInternal,
29
29
  MODEL_PROFILES,
30
30
  findPhaseInternal,
31
+ getMilestoneInfo,
31
32
  } = require('./core.cjs');
32
33
 
33
34
  // ─── Root layout (no v2 markers) Tests ───────────────────────────────────────
@@ -1157,3 +1158,68 @@ describe('MODEL_PROFILES dgs-idea-researcher tiering', () => {
1157
1158
  });
1158
1159
 
1159
1160
  });
1161
+
1162
+ // ─── getMilestoneInfo format coverage ───────────────────────────────────────
1163
+ describe('getMilestoneInfo all-format coverage', () => {
1164
+ let fixture;
1165
+
1166
+ afterEach(() => {
1167
+ if (fixture) fixture.cleanup();
1168
+ });
1169
+
1170
+ const ADHOC_ROADMAP = `# Roadmap
1171
+
1172
+ ## Active Milestone: v25.1 add-prs
1173
+
1174
+ **Goal:** add-prs
1175
+ **Status:** In progress (ad-hoc container — phases added on demand via /dgs:add-phase)
1176
+
1177
+ Phases:
1178
+
1179
+ ---
1180
+ `;
1181
+
1182
+ const BOLD_LINE_ROADMAP = `# Roadmap: Tenant data-quality metrics in Admin UI (v30.1)
1183
+
1184
+ **Milestone:** v30.1 — Tenant data-quality metrics in Admin UI
1185
+
1186
+ ## Overview
1187
+
1188
+ Single-milestone roadmap. Version is only in the title and the bold line.
1189
+ `;
1190
+
1191
+ // Multi-milestone heading roadmap WITH a stray bold line: the heading branch
1192
+ // must win, the gated bold-line branch must stay inert.
1193
+ const MULTI_MILESTONE_WITH_STRAY_BOLD = `# Roadmap
1194
+
1195
+ **Milestone:** v9.9 — stray bold line
1196
+
1197
+ ## v1.0 Foundation
1198
+ Some shipped work.
1199
+
1200
+ ## v2.0 Next
1201
+ Current work here.
1202
+ `;
1203
+
1204
+ it('returns the real name for an ad-hoc roadmap (not generic milestone)', () => {
1205
+ fixture = createFixture({ 'ROADMAP.md': ADHOC_ROADMAP });
1206
+ assert.deepEqual(getMilestoneInfo(fixture.cwd), { version: 'v25.1', name: 'add-prs' });
1207
+ });
1208
+
1209
+ it('returns the real name for a bold-line single-milestone roadmap', () => {
1210
+ fixture = createFixture({ 'ROADMAP.md': BOLD_LINE_ROADMAP });
1211
+ assert.deepEqual(getMilestoneInfo(fixture.cwd), {
1212
+ version: 'v30.1',
1213
+ name: 'Tenant data-quality metrics in Admin UI',
1214
+ });
1215
+ });
1216
+
1217
+ it('resolves a multi-milestone heading roadmap via the heading branch (bold line does not hijack)', () => {
1218
+ fixture = createFixture({ 'ROADMAP.md': MULTI_MILESTONE_WITH_STRAY_BOLD });
1219
+ const info = getMilestoneInfo(fixture.cwd);
1220
+ // Heading branch wins → version is the first heading's (v1.0), NOT the stray
1221
+ // bold line's v9.9. The key assertion is that the bold line did not hijack.
1222
+ assert.notEqual(info.version, 'v9.9');
1223
+ assert.equal(info.version, 'v1.0');
1224
+ });
1225
+ });
@@ -475,11 +475,12 @@ function cmdIdeasUpdate(cwd, idOrFilename, field, value, raw, author) {
475
475
  }
476
476
  }
477
477
 
478
- const planRootRel = path.relative(cwd, getPlanningRoot(cwd)) || '.';
479
478
  const result = {
480
479
  id: fm.id,
481
480
  filename: newFilename,
482
- path: path.join(planRootRel, 'ideas', idea.state, newFilename),
481
+ // Derive the returned path from the file's REAL location so it is correct
482
+ // for both flat (dirname is ideas/) and legacy (dirname is ideas/<state>/).
483
+ path: path.relative(cwd, path.join(path.dirname(idea.path), newFilename)),
483
484
  field,
484
485
  old_value: oldValue,
485
486
  new_value: value,
@@ -1294,23 +1295,50 @@ function cmdIdeasFindRelated(cwd, options, raw) {
1294
1295
 
1295
1296
  const anchorTags = anchor.frontmatter.tags || [];
1296
1297
 
1297
- // 2. Load all other pending ideas
1298
- const pendingDir = path.join(getPlanningRoot(cwd), 'ideas', 'pending');
1299
- let pendingFiles;
1298
+ // 2. Load all other pending ideas — flat-first (mirror cmdIdeasList's flat
1299
+ // scan), with legacy ideas/pending/ as an existence-gated dedup fallback.
1300
+ const candidateContents = [];
1301
+ const seenIds = new Set();
1302
+
1303
+ const flatDir = path.join(getPlanningRoot(cwd), 'ideas');
1300
1304
  try {
1301
- pendingFiles = fs.readdirSync(pendingDir).filter(f => f.endsWith('.md'));
1305
+ const flatFiles = fs.readdirSync(flatDir).filter(f => f.endsWith('.md') && !fs.statSync(path.join(flatDir, f)).isDirectory());
1306
+ for (const file of flatFiles) {
1307
+ const content = safeReadFile(path.join(flatDir, file));
1308
+ if (!content) continue;
1309
+ const parsed = parseIdeaFrontmatter(content);
1310
+ if ((parsed.frontmatter.status || 'pending') !== 'pending') continue;
1311
+ candidateContents.push(content);
1312
+ seenIds.add(parsed.frontmatter.id);
1313
+ }
1302
1314
  } catch {
1303
- pendingFiles = [];
1315
+ // Flat directory may not exist — continue to legacy scan
1316
+ }
1317
+
1318
+ // Legacy fallback: ideas/pending/ (existence-gated, deduped by id)
1319
+ const pendingDir = path.join(getPlanningRoot(cwd), 'ideas', 'pending');
1320
+ if (fs.existsSync(pendingDir)) {
1321
+ let pendingFiles;
1322
+ try {
1323
+ pendingFiles = fs.readdirSync(pendingDir).filter(f => f.endsWith('.md'));
1324
+ } catch {
1325
+ pendingFiles = [];
1326
+ }
1327
+ for (const file of pendingFiles) {
1328
+ const content = safeReadFile(path.join(pendingDir, file));
1329
+ if (!content) continue;
1330
+ const parsed = parseIdeaFrontmatter(content);
1331
+ if (seenIds.has(parsed.frontmatter.id)) continue;
1332
+ candidateContents.push(content);
1333
+ seenIds.add(parsed.frontmatter.id);
1334
+ }
1304
1335
  }
1305
1336
 
1306
1337
  // 3. Score each candidate for tag overlap
1307
1338
  const matchLevelOrder = { HIGH: 0, MEDIUM: 1, LOW: 2, NONE: 3 };
1308
1339
  const candidates = [];
1309
1340
 
1310
- for (const file of pendingFiles) {
1311
- const content = safeReadFile(path.join(pendingDir, file));
1312
- if (!content) continue;
1313
-
1341
+ for (const content of candidateContents) {
1314
1342
  const parsed = parseIdeaFrontmatter(content);
1315
1343
  const candidateId = parsed.frontmatter.id;
1316
1344
 
@@ -10,7 +10,7 @@ const path = require('path');
10
10
  const { execSync } = require('child_process');
11
11
  const { createTempDir, cleanupDir, createTempProject, initGitRepo } = require('./test-helpers.cjs');
12
12
  const { resetPaths } = require('./paths.cjs');
13
- const { findIdeaFile, parseIdeaFrontmatter, ensureIdeasDirs, cmdIdeasCreate, cmdIdeasList, loadManifest, IDEA_STATES, IDEA_STATUSES, setIdeaStatus, buildIdeaContent, cmdIdeasConsolidate, cmdIdeasFindRelated, cmdIdeasUndoConsolidation } = require('./ideas.cjs');
13
+ const { findIdeaFile, parseIdeaFrontmatter, ensureIdeasDirs, cmdIdeasCreate, cmdIdeasList, cmdIdeasUpdate, loadManifest, IDEA_STATES, IDEA_STATUSES, setIdeaStatus, buildIdeaContent, cmdIdeasConsolidate, cmdIdeasFindRelated, cmdIdeasUndoConsolidation } = require('./ideas.cjs');
14
14
 
15
15
  // ─── Helpers ──────────────────────────────────────────────────────────────────
16
16
 
@@ -949,8 +949,9 @@ describe('cmdIdeasFindRelated', () => {
949
949
  const filename = `${paddedId}-${slug}.md`;
950
950
  const now = new Date().toISOString();
951
951
  const tagsStr = tags.length > 0 ? `[${tags.map(t => `"${t}"`).join(', ')}]` : '[]';
952
- const content = `---\nid: ${id}\ntitle: "${title}"\ntags: ${tagsStr}\ncreated: ${now}\nupdated: ${now}\n---\n\n${body || title + ' body.'}\n`;
953
- const dir2 = path.join(dir, 'ideas', 'pending');
952
+ const content = `---\nid: ${id}\ntitle: "${title}"\nstatus: pending\ntags: ${tagsStr}\ncreated: ${now}\nupdated: ${now}\n---\n\n${body || title + ' body.'}\n`;
953
+ // Flat layout: ideas/<paddedId>-<slug>.md (status in frontmatter)
954
+ const dir2 = path.join(dir, 'ideas');
954
955
  fs.mkdirSync(dir2, { recursive: true });
955
956
  fs.writeFileSync(path.join(dir2, filename), content, 'utf-8');
956
957
  // Update manifest so allocateId returns next ID correctly
@@ -1168,6 +1169,34 @@ describe('cmdIdeasFindRelated', () => {
1168
1169
  `match_level should be HIGH, MEDIUM, or LOW, got: ${r.match_level}`);
1169
1170
  }
1170
1171
  });
1172
+
1173
+ it('finds flat-layout pending candidates', () => {
1174
+ // createPendingIdea now seeds FLAT ideas/*.md with status: pending.
1175
+ createPendingIdea(tmpDir, 1, 'Anchor idea', ['api', 'reliability']);
1176
+ createPendingIdea(tmpDir, 2, 'Flat candidate', ['api', 'reliability']);
1177
+ const output = captureOutput(() => {
1178
+ cmdIdeasFindRelated(tmpDir, { id: '1', threshold: 'low' }, true);
1179
+ });
1180
+ const result = JSON.parse(output);
1181
+ assert.ok(result.results.length >= 1, 'should find at least one flat candidate');
1182
+ assert.strictEqual(result.results[0].id, 2);
1183
+ assert.strictEqual(result.counts.total, 1);
1184
+ });
1185
+
1186
+ it('ideas update returns the real flat path (no state subdir segment)', () => {
1187
+ // Seed a FLAT pending idea, then rename via title update.
1188
+ createPendingIdea(tmpDir, 1, 'Original title', ['api']);
1189
+ const output = captureOutput(() => {
1190
+ cmdIdeasUpdate(tmpDir, '1', 'title', 'Renamed title', true);
1191
+ });
1192
+ const result = JSON.parse(output);
1193
+ // Path must point to the flat location ideas/<file>, never ideas/pending/<file>.
1194
+ assert.ok(!result.path.includes(`${path.sep}pending${path.sep}`),
1195
+ `path should not contain a pending/ segment, got: ${result.path}`);
1196
+ assert.match(result.path, /^ideas[\\/]001-renamed-title\.md$/);
1197
+ assert.ok(fs.existsSync(path.join(tmpDir, result.path)),
1198
+ `renamed file should exist at ${result.path}`);
1199
+ });
1171
1200
  });
1172
1201
 
1173
1202
  // ─── cmdIdeasUndoConsolidation ────────────────────────────────────────────────
@@ -1472,6 +1472,29 @@ function parseProjectMilestone(roadmapPath) {
1472
1472
  return { milestone_version: 'v' + headingMatch[1], milestone_name: headingMatch[2].trim() };
1473
1473
  }
1474
1474
 
1475
+ // Ad-hoc container: "## Active Milestone: v25.1 add-prs" (mirror getMilestoneInfo)
1476
+ const adhoc = cleaned.match(/^##\s+Active Milestone:\s*v(\d+\.\d+)\s+(.+?)\s*$/im);
1477
+ if (adhoc) {
1478
+ return { milestone_version: 'v' + adhoc[1], milestone_name: adhoc[2].trim() };
1479
+ }
1480
+
1481
+ // Bold-line single-milestone: "**Milestone:** v30.1 — Name", GATED to <=1
1482
+ // version-bearing heading so a stray bold line never hijacks a multi-milestone
1483
+ // roadmap (mirror getMilestoneInfo / analyzeMilestonePhases).
1484
+ const versionHeadings = cleaned.match(/^#{2,4}\s+.*v\d+\.\d+/gim) || [];
1485
+ if (versionHeadings.length <= 1) {
1486
+ const bold = cleaned.match(/^\*\*Milestone:\*\*\s*v(\d+\.\d+)\s*(?:[—-]\s*)?(.*)$/im);
1487
+ if (bold) {
1488
+ return { milestone_version: 'v' + bold[1], milestone_name: bold[2].trim() || 'milestone' };
1489
+ }
1490
+ }
1491
+
1492
+ // Bare-version fallback (mirror getMilestoneInfo): any vX.Y rather than {null,null}.
1493
+ const versionMatch = cleaned.match(/v(\d+\.\d+)/);
1494
+ if (versionMatch) {
1495
+ return { milestone_version: versionMatch[0], milestone_name: 'milestone' };
1496
+ }
1497
+
1475
1498
  return { milestone_version: null, milestone_name: null };
1476
1499
  }
1477
1500
 
@@ -2255,3 +2255,81 @@ describe('init execute-phase coerces ambiguous archive number to not-found (LOOK
2255
2255
  }
2256
2256
  });
2257
2257
  });
2258
+
2259
+ // ─── progress-all milestone parsing (parseProjectMilestone) ─────────────────
2260
+ describe('init progress-all: parseProjectMilestone format coverage', () => {
2261
+ let fixture;
2262
+
2263
+ afterEach(() => {
2264
+ if (fixture) fixture.cleanup();
2265
+ });
2266
+
2267
+ // Build a v2 fixture whose single active project's ROADMAP.md carries the
2268
+ // supplied content, so progress-all → parseProjectMilestone exercises it.
2269
+ function v2FixtureWithRoadmap(roadmapContent) {
2270
+ return createFixture({
2271
+ 'config.json': JSON.stringify({ current_project: 'test-project' }),
2272
+ 'PROJECTS.md': '# Projects\n\n| Project | Status |\n|---------|--------|\n| test-project | Active |\n',
2273
+ 'REPOS.md': '# Repos\n\n| Name | Path |\n|------|------|\n',
2274
+ 'projects/test-project/STATE.md': '# State',
2275
+ 'projects/test-project/ROADMAP.md': roadmapContent,
2276
+ 'projects/test-project/REQUIREMENTS.md': '# Requirements',
2277
+ 'projects/test-project/PROJECT.md': '# Project',
2278
+ });
2279
+ }
2280
+
2281
+ const ADHOC_ROADMAP = `# Roadmap
2282
+
2283
+ ## Active Milestone: v25.1 add-prs
2284
+
2285
+ **Goal:** add-prs
2286
+ **Status:** In progress (ad-hoc container — phases added on demand via /dgs:add-phase)
2287
+
2288
+ Phases:
2289
+
2290
+ ---
2291
+ `;
2292
+
2293
+ const BOLD_LINE_ROADMAP = `# Roadmap: Tenant data-quality metrics in Admin UI (v30.1)
2294
+
2295
+ **Milestone:** v30.1 — Tenant data-quality metrics in Admin UI
2296
+
2297
+ ## Overview
2298
+
2299
+ Single-milestone roadmap.
2300
+ `;
2301
+
2302
+ // Only a bare vX.Y appears anywhere — exercises the new bare-version fallback.
2303
+ const BARE_VERSION_ROADMAP = `# Roadmap
2304
+
2305
+ Some prose mentioning v42.7 without any heading or bold milestone line.
2306
+ `;
2307
+
2308
+ function projectEntry(result) {
2309
+ return (result.projects || []).find(p => p.name === 'test-project');
2310
+ }
2311
+
2312
+ it('populates milestone_version + milestone_name for an ad-hoc project roadmap', () => {
2313
+ fixture = v2FixtureWithRoadmap(ADHOC_ROADMAP);
2314
+ const p = projectEntry(runInit(fixture.cwd, 'progress-all --raw'));
2315
+ assert.ok(p, 'expected the active project in progress-all output');
2316
+ assert.equal(p.milestone_version, 'v25.1');
2317
+ assert.equal(p.milestone_name, 'add-prs');
2318
+ });
2319
+
2320
+ it('populates milestone_version + milestone_name for a bold-line project roadmap', () => {
2321
+ fixture = v2FixtureWithRoadmap(BOLD_LINE_ROADMAP);
2322
+ const p = projectEntry(runInit(fixture.cwd, 'progress-all --raw'));
2323
+ assert.ok(p, 'expected the active project in progress-all output');
2324
+ assert.equal(p.milestone_version, 'v30.1');
2325
+ assert.equal(p.milestone_name, 'Tenant data-quality metrics in Admin UI');
2326
+ });
2327
+
2328
+ it('bare-version fallback returns the version (no longer null) when only vX.Y appears', () => {
2329
+ fixture = v2FixtureWithRoadmap(BARE_VERSION_ROADMAP);
2330
+ const p = projectEntry(runInit(fixture.cwd, 'progress-all --raw'));
2331
+ assert.ok(p, 'expected the active project in progress-all output');
2332
+ assert.equal(p.milestone_version, 'v42.7');
2333
+ assert.notEqual(p.milestone_version, null);
2334
+ });
2335
+ });