@ktpartners/dgs-platform 3.5.0 → 3.5.1

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 CHANGED
@@ -8,6 +8,20 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [3.5.1] - 2026-06-28
12
+
13
+ ### Added
14
+ - **`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.
15
+
16
+ ### Fixed
17
+ - **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.
18
+ - **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.
19
+ - **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.
20
+ - **Jobs healthCheck flat-first (quick-260628-kxr-01)** — reworked the jobs `healthCheck` to be flat-first.
21
+
22
+ ### Documentation
23
+ - **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.
24
+
11
25
  ## [3.5.0] - 2026-06-27
12
26
 
13
27
  ### 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) {
@@ -1157,52 +1157,77 @@ function analyzeMilestonePhases(cwd, version) {
1157
1157
  );
1158
1158
  const sectionMatch = content.match(sectionHeadingPattern);
1159
1159
  if (!sectionMatch) {
1160
- throw new Error(`Milestone ${version} not found in ROADMAP.md`);
1161
- }
1160
+ // No ##-#### heading carries the version. Support the newer
1161
+ // single-milestone ROADMAP format where the version appears ONLY in the
1162
+ // `# Roadmap: ... (vX.Y)` title and a `**Milestone:** vX.Y` bold line.
1163
+ // Gate the whole-roadmap phase scan behind a single-milestone check so a
1164
+ // stray bold line on a multi-milestone roadmap does not mis-derive a range.
1165
+ const boldMilestonePattern = new RegExp(
1166
+ '^\\*\\*Milestone:\\*\\*\\s*' + escapedVersion + '\\b',
1167
+ 'im'
1168
+ );
1169
+ const versionHeadings = content.match(/^#{2,4}\s+.*v\d+\.\d+/gim) || [];
1170
+ const isSingleMilestone = versionHeadings.length <= 1;
1171
+ if (boldMilestonePattern.test(content) && isSingleMilestone) {
1172
+ const fullPhasePattern = /Phase\s+(\d+)/gi;
1173
+ const phaseNumbers = [];
1174
+ let fpMatch;
1175
+ while ((fpMatch = fullPhasePattern.exec(content)) !== null) {
1176
+ phaseNumbers.push(parseInt(fpMatch[1], 10));
1177
+ }
1178
+ if (phaseNumbers.length === 0) {
1179
+ throw new Error(`Milestone ${version} not found in ROADMAP.md`);
1180
+ }
1181
+ phaseStart = Math.min(...phaseNumbers);
1182
+ phaseEnd = Math.max(...phaseNumbers);
1183
+ } else {
1184
+ throw new Error(`Milestone ${version} not found in ROADMAP.md`);
1185
+ }
1186
+ } else {
1187
+ const headingLevel = sectionMatch[1].length;
1188
+ const sectionStart = sectionMatch.index + sectionMatch[0].length;
1162
1189
 
1163
- const headingLevel = sectionMatch[1].length;
1164
- const sectionStart = sectionMatch.index + sectionMatch[0].length;
1190
+ // Find the end of this section (next heading at same or higher level)
1191
+ const endPattern = new RegExp(
1192
+ `^#{2,${headingLevel}}\\s+`,
1193
+ 'im'
1194
+ );
1195
+ const restContent = content.slice(sectionStart);
1196
+ const endMatch = restContent.match(endPattern);
1197
+ const sectionContent = endMatch
1198
+ ? restContent.slice(0, endMatch.index)
1199
+ : restContent;
1200
+
1201
+ // Collect Phase N: headers within this section
1202
+ const phaseHeaderPattern = /Phase\s+(\d+)\s*:/gi;
1203
+ const phaseNumbers = [];
1204
+ let phMatch;
1205
+ while ((phMatch = phaseHeaderPattern.exec(sectionContent)) !== null) {
1206
+ phaseNumbers.push(parseInt(phMatch[1], 10));
1207
+ }
1165
1208
 
1166
- // Find the end of this section (next heading at same or higher level)
1167
- const endPattern = new RegExp(
1168
- `^#{2,${headingLevel}}\\s+`,
1169
- 'im'
1170
- );
1171
- const restContent = content.slice(sectionStart);
1172
- const endMatch = restContent.match(endPattern);
1173
- const sectionContent = endMatch
1174
- ? restContent.slice(0, endMatch.index)
1175
- : restContent;
1176
-
1177
- // Collect Phase N: headers within this section
1178
- const phaseHeaderPattern = /Phase\s+(\d+)\s*:/gi;
1179
- const phaseNumbers = [];
1180
- let phMatch;
1181
- while ((phMatch = phaseHeaderPattern.exec(sectionContent)) !== null) {
1182
- phaseNumbers.push(parseInt(phMatch[1], 10));
1183
- }
1209
+ // If section-based search found nothing (common when ROADMAP has peer-level
1210
+ // ## headings like ## v1.0, ## Overview, ## Phase Details), search the entire
1211
+ // ROADMAP for Phase N: headings. For single-milestone ROADMAPs all phases
1212
+ // belong to the milestone.
1213
+ if (phaseNumbers.length === 0) {
1214
+ const fullPhasePattern = new RegExp(
1215
+ `Phase\\s+(\\d+)`,
1216
+ 'gi'
1217
+ );
1218
+ let fpMatch;
1219
+ while ((fpMatch = fullPhasePattern.exec(content)) !== null) {
1220
+ phaseNumbers.push(parseInt(fpMatch[1], 10));
1221
+ }
1222
+ }
1184
1223
 
1185
- // If section-based search found nothing (common when ROADMAP has peer-level
1186
- // ## headings like ## v1.0, ## Overview, ## Phase Details), search the entire
1187
- // ROADMAP for Phase N: headings. For single-milestone ROADMAPs all phases
1188
- // belong to the milestone.
1189
- if (phaseNumbers.length === 0) {
1190
- const fullPhasePattern = new RegExp(
1191
- `Phase\\s+(\\d+)`,
1192
- 'gi'
1193
- );
1194
- let fpMatch;
1195
- while ((fpMatch = fullPhasePattern.exec(content)) !== null) {
1196
- phaseNumbers.push(parseInt(fpMatch[1], 10));
1224
+ if (phaseNumbers.length === 0) {
1225
+ throw new Error(`Milestone ${version} not found in ROADMAP.md`);
1197
1226
  }
1198
- }
1199
1227
 
1200
- if (phaseNumbers.length === 0) {
1201
- throw new Error(`Milestone ${version} not found in ROADMAP.md`);
1228
+ phaseStart = Math.min(...phaseNumbers);
1229
+ phaseEnd = Math.max(...phaseNumbers);
1202
1230
  }
1203
-
1204
- phaseStart = Math.min(...phaseNumbers);
1205
- phaseEnd = Math.max(...phaseNumbers);
1206
1231
  }
1207
1232
 
1208
1233
  // Extract all phase detail sections: ### Phase N: Name
@@ -1272,7 +1297,13 @@ function detectActiveMilestone(content) {
1272
1297
  if (match) return match[1];
1273
1298
  // Fallback: milestone list item with (in progress)
1274
1299
  const listMatch = content.match(/-\s+.*?(v\d+\.\d+).*?\((?:in\s+progress)\)/i);
1275
- return listMatch ? listMatch[1] : null;
1300
+ if (listMatch) return listMatch[1];
1301
+ // Final fallback: newer single-milestone format with a `**Milestone:** vX.Y`
1302
+ // bold line. Runs LAST so legacy In-Progress heading/list signals win when
1303
+ // present on multi-milestone roadmaps.
1304
+ const boldMatch = content.match(/^\*\*Milestone:\*\*\s*(v\d+\.\d+)/im);
1305
+ if (boldMatch) return boldMatch[1];
1306
+ return null;
1276
1307
  }
1277
1308
 
1278
1309
  // ─── Milestone Job CLI Wrappers ─────────────────────────────────────────────
@@ -1519,37 +1550,63 @@ function cancelJob(cwd, version) {
1519
1550
  }
1520
1551
 
1521
1552
  /**
1522
- * Validate jobs directory structure. Auto-create missing directories.
1553
+ * Validate jobs directory structure (flat-first).
1554
+ *
1555
+ * Scans the flat `jobs/milestone-*.md` layout first. Legacy
1556
+ * `pending/in-progress/completed` subdirs are an existence-gated optional
1557
+ * fallback: they are counted + parse-validated when present, but are never
1558
+ * created and their absence is not an issue. Only genuine parse failures
1559
+ * flip `healthy` to false.
1523
1560
  *
1524
1561
  * @param {string} cwd - Working directory
1525
1562
  * @returns {{ healthy: boolean, directories: Array, job_count: number, issues?: Array }}
1526
1563
  */
1527
1564
  function healthCheck(cwd) {
1528
1565
  const jobsDir = path.join(getPlanningRoot(cwd), 'jobs');
1529
- const requiredDirs = ['pending', 'in-progress', 'completed'];
1530
1566
  const directories = [];
1531
1567
  const issues = [];
1532
1568
  let jobCount = 0;
1533
1569
 
1534
- for (const dirName of requiredDirs) {
1535
- const dirPath = path.join(jobsDir, dirName);
1536
- const exists = fs.existsSync(dirPath);
1537
- let created = false;
1570
+ // An absent/empty jobs dir is healthy. Ensure it exists, but do not flag it.
1571
+ let jobsCreated = false;
1572
+ if (!fs.existsSync(jobsDir)) {
1573
+ fs.mkdirSync(jobsDir, { recursive: true });
1574
+ jobsCreated = true;
1575
+ }
1538
1576
 
1539
- if (!exists) {
1540
- fs.mkdirSync(dirPath, { recursive: true });
1541
- created = true;
1542
- issues.push(`Missing directory auto-created: ${dirName}/`);
1577
+ // Shared flat-job predicate (identical to findJobFile/listJobs). Excludes
1578
+ // summary files (job-*-SUMMARY.md) since they do not start with 'milestone-'.
1579
+ const isJobFile = f => f.startsWith('milestone-') && f.endsWith('.md');
1580
+
1581
+ // Flat-first scan: jobs/milestone-*.md
1582
+ try {
1583
+ const flatFiles = fs.readdirSync(jobsDir).filter(isJobFile);
1584
+ jobCount += flatFiles.length;
1585
+ for (const file of flatFiles) {
1586
+ const filePath = path.join(jobsDir, file);
1587
+ try {
1588
+ parseJobFile(filePath);
1589
+ } catch (err) {
1590
+ issues.push(`Parse failure in ${file}: ${err.message}`);
1591
+ }
1543
1592
  }
1593
+ } catch {
1594
+ // jobs dir unreadable; treat as empty.
1595
+ }
1544
1596
 
1545
- directories.push({ name: dirName, exists: exists || created, created });
1597
+ directories.push({ name: 'jobs', exists: true, created: jobsCreated });
1598
+
1599
+ // Legacy subdirs: existence-gated optional fallback. Never created.
1600
+ const legacyDirs = ['pending', 'in-progress', 'completed'];
1601
+ for (const dirName of legacyDirs) {
1602
+ const dirPath = path.join(jobsDir, dirName);
1603
+ if (!fs.existsSync(dirPath)) continue;
1604
+
1605
+ directories.push({ name: dirName, exists: true, created: false });
1546
1606
 
1547
- // Scan for job files
1548
1607
  try {
1549
- const files = fs.readdirSync(dirPath).filter(f => f.startsWith('milestone-') && f.endsWith('.md'));
1608
+ const files = fs.readdirSync(dirPath).filter(isJobFile);
1550
1609
  jobCount += files.length;
1551
-
1552
- // Validate each file
1553
1610
  for (const file of files) {
1554
1611
  const filePath = path.join(dirPath, file);
1555
1612
  try {
@@ -1559,7 +1616,7 @@ function healthCheck(cwd) {
1559
1616
  }
1560
1617
  }
1561
1618
  } catch {
1562
- // Directory just created, no files
1619
+ // Subdir unreadable; skip.
1563
1620
  }
1564
1621
  }
1565
1622
 
@@ -2117,6 +2174,8 @@ module.exports = {
2117
2174
  cmdJobsInsertPhaseGapFix,
2118
2175
  cmdJobsCreateMilestone,
2119
2176
  cmdJobsMilestonePreview,
2177
+ analyzeMilestonePhases,
2178
+ detectActiveMilestone,
2120
2179
  cmdJobsListJobs,
2121
2180
  cmdJobsCancelJob,
2122
2181
  cmdJobsRecordStartShas,
@@ -18,6 +18,7 @@ const {
18
18
  parseJobFile, updateJobStep, moveJobFile,
19
19
  generateMilestoneSteps, buildJobFileContent,
20
20
  cmdJobsCreateMilestone, cmdJobsMilestonePreview,
21
+ analyzeMilestonePhases, detectActiveMilestone,
21
22
  findJobFile, updateJobHeader, insertJobSteps,
22
23
  buildGapFixSteps, insertGapFixSection,
23
24
  listJobs, cancelJob, recordStartShas, rollbackJob,
@@ -1379,6 +1380,105 @@ Plans:
1379
1380
  });
1380
1381
  });
1381
1382
 
1383
+ // ─── Bold-line single-milestone format Tests ────────────────────────────
1384
+
1385
+ describe('bold-line single-milestone format', () => {
1386
+ let fixture;
1387
+
1388
+ afterEach(() => {
1389
+ if (fixture) fixture.cleanup();
1390
+ });
1391
+
1392
+ const BOLD_LINE_ROADMAP = `# Roadmap: Tenant data-quality metrics in Admin UI (v30.1)
1393
+
1394
+ **Milestone:** v30.1 — Tenant data-quality metrics in Admin UI
1395
+
1396
+ ## Overview
1397
+
1398
+ Single-milestone roadmap. Version is only in the title and the bold line.
1399
+
1400
+ ### Phase 1: Metrics schema
1401
+ **Goal**: Define metrics tables
1402
+ Plans:
1403
+ - [ ] 01-01-PLAN.md
1404
+
1405
+ ### Phase 2: Admin UI surface
1406
+ **Goal**: Surface metrics in admin
1407
+ Plans:
1408
+ - [ ] 02-01-PLAN.md
1409
+ `;
1410
+
1411
+ const MULTI_MILESTONE_WITH_BOLD = `# Roadmap (v9.9)
1412
+
1413
+ **Milestone:** v9.9 — stray bold line
1414
+
1415
+ ## v1.0 Foundation (SHIPPED)
1416
+ ### Phase 1: Base
1417
+ ### Phase 2: More base
1418
+
1419
+ ## v2.0 Next (In Progress)
1420
+ ### Phase 3: New
1421
+ ### Phase 4: Newer
1422
+ `;
1423
+
1424
+ it('detectActiveMilestone returns v30.1 from the bold line', () => {
1425
+ assert.equal(detectActiveMilestone(BOLD_LINE_ROADMAP), 'v30.1');
1426
+ });
1427
+
1428
+ it('analyzeMilestonePhases derives phases 1 and 2 from the bold-line roadmap', () => {
1429
+ fixture = createFixture({
1430
+ 'ROADMAP.md': BOLD_LINE_ROADMAP,
1431
+ 'phases/01-metrics-schema/01-01-PLAN.md': '',
1432
+ 'phases/02-admin-ui/02-01-PLAN.md': '',
1433
+ });
1434
+
1435
+ const phases = analyzeMilestonePhases(fixture.cwd, 'v30.1');
1436
+ assert.equal(phases.length, 2);
1437
+ assert.deepEqual(phases.map(p => p.number), ['1', '2']);
1438
+ });
1439
+
1440
+ it('cmdJobsMilestonePreview succeeds with an explicit version', () => {
1441
+ fixture = createFixture({
1442
+ 'ROADMAP.md': BOLD_LINE_ROADMAP,
1443
+ 'phases/01-metrics-schema/01-01-PLAN.md': '',
1444
+ 'phases/02-admin-ui/02-01-PLAN.md': '',
1445
+ });
1446
+
1447
+ const result = cmdJobsMilestonePreview(fixture.cwd, 'v30.1', true, false);
1448
+ assert.equal(result.preview, true);
1449
+ assert.equal(result.version, 'v30.1');
1450
+ assert.ok(result.step_count > 0, 'expected at least one step');
1451
+ });
1452
+
1453
+ it('cmdJobsMilestonePreview auto-detects the bold-line version', () => {
1454
+ fixture = createFixture({
1455
+ 'ROADMAP.md': BOLD_LINE_ROADMAP,
1456
+ 'phases/01-metrics-schema/01-01-PLAN.md': '',
1457
+ 'phases/02-admin-ui/02-01-PLAN.md': '',
1458
+ });
1459
+
1460
+ const result = cmdJobsMilestonePreview(fixture.cwd, null, true, false);
1461
+ assert.equal(result.version, 'v30.1');
1462
+ });
1463
+
1464
+ it('analyzeMilestonePhases throws on a multi-milestone roadmap with a stray bold line', () => {
1465
+ fixture = createFixture({
1466
+ 'ROADMAP.md': MULTI_MILESTONE_WITH_BOLD,
1467
+ 'phases/01-base/01-01-PLAN.md': '',
1468
+ 'phases/03-new/03-01-PLAN.md': '',
1469
+ });
1470
+
1471
+ assert.throws(
1472
+ () => analyzeMilestonePhases(fixture.cwd, 'v9.9'),
1473
+ (err) => /v9\.9|not found/i.test(err.message)
1474
+ );
1475
+ });
1476
+
1477
+ it('detectActiveMilestone prefers the In-Progress heading over the bold line', () => {
1478
+ assert.equal(detectActiveMilestone(MULTI_MILESTONE_WITH_BOLD), 'v2.0');
1479
+ });
1480
+ });
1481
+
1382
1482
  // ─── findJobFile Tests ──────────────────────────────────────────────────
1383
1483
 
1384
1484
  describe('findJobFile', () => {
@@ -2182,18 +2282,46 @@ Plans:
2182
2282
  assert.equal(result.job_count, 0);
2183
2283
  });
2184
2284
 
2185
- it('returns healthy: false listing issues for missing directories, then auto-creates them', () => {
2285
+ it('does not create legacy subdirs and stays healthy when they are absent', () => {
2186
2286
  fixture = createFixture({
2187
2287
  '': null,
2188
2288
  });
2189
2289
  const result = healthCheck(fixture.cwd);
2190
2290
 
2191
- // Should report missing directories but auto-create them
2291
+ // An empty project (no legacy subdirs) is healthy with no jobs.
2292
+ assert.equal(result.healthy, true);
2293
+ assert.equal(result.job_count, 0);
2192
2294
  assert.ok(Array.isArray(result.directories));
2193
- // After auto-create, directories should exist
2194
- assert.ok(fs.existsSync(path.join(fixture.cwd, 'jobs', 'pending')));
2195
- assert.ok(fs.existsSync(path.join(fixture.cwd, 'jobs', 'in-progress')));
2196
- assert.ok(fs.existsSync(path.join(fixture.cwd, 'jobs', 'completed')));
2295
+ // Legacy subdirs must NOT be auto-created.
2296
+ assert.equal(fs.existsSync(path.join(fixture.cwd, 'jobs', 'pending')), false);
2297
+ assert.equal(fs.existsSync(path.join(fixture.cwd, 'jobs', 'in-progress')), false);
2298
+ assert.equal(fs.existsSync(path.join(fixture.cwd, 'jobs', 'completed')), false);
2299
+ });
2300
+
2301
+ it('reports healthy: true for a flat jobs layout and does not create legacy subdirs', () => {
2302
+ fixture = createFixture({
2303
+ 'jobs/milestone-v6.0.md': WELL_FORMED_JOB,
2304
+ });
2305
+ const result = healthCheck(fixture.cwd);
2306
+
2307
+ assert.equal(result.healthy, true);
2308
+ assert.equal(result.job_count, 1);
2309
+ assert.equal(result.issues.length, 0);
2310
+ // No legacy subdirs created for a flat layout.
2311
+ assert.equal(fs.existsSync(path.join(fixture.cwd, 'jobs', 'pending')), false);
2312
+ assert.equal(fs.existsSync(path.join(fixture.cwd, 'jobs', 'in-progress')), false);
2313
+ assert.equal(fs.existsSync(path.join(fixture.cwd, 'jobs', 'completed')), false);
2314
+ });
2315
+
2316
+ it('excludes job-*-SUMMARY.md files from job_count in a flat layout', () => {
2317
+ fixture = createFixture({
2318
+ 'jobs/milestone-v6.0.md': WELL_FORMED_JOB,
2319
+ 'jobs/job-v6.0-SUMMARY.md': '# Summary',
2320
+ });
2321
+ const result = healthCheck(fixture.cwd);
2322
+
2323
+ assert.equal(result.job_count, 1);
2324
+ assert.equal(result.healthy, true);
2197
2325
  });
2198
2326
 
2199
2327
  it('validates each job file parses successfully; reports parse failures as issues', () => {
@@ -2546,14 +2674,14 @@ describe('jobs root-layout', () => {
2546
2674
 
2547
2675
  });
2548
2676
 
2549
- it('healthCheck creates jobs dirs at root layout', () => {
2677
+ it('healthCheck does not create legacy subdirs at root layout', () => {
2550
2678
  fixture = createTempProject({ layout: 'root' });
2551
2679
  const result = healthCheck(fixture.cwd);
2552
- assert.ok(result.directories.length >= 3);
2553
- // Verify dirs were created at root
2554
- assert.ok(fs.existsSync(path.join(fixture.cwd, 'jobs', 'pending')));
2555
- assert.ok(fs.existsSync(path.join(fixture.cwd, 'jobs', 'in-progress')));
2556
- assert.ok(fs.existsSync(path.join(fixture.cwd, 'jobs', 'completed')));
2680
+ assert.equal(result.healthy, true);
2681
+ // Legacy subdirs must NOT be auto-created.
2682
+ assert.equal(fs.existsSync(path.join(fixture.cwd, 'jobs', 'pending')), false);
2683
+ assert.equal(fs.existsSync(path.join(fixture.cwd, 'jobs', 'in-progress')), false);
2684
+ assert.equal(fs.existsSync(path.join(fixture.cwd, 'jobs', 'completed')), false);
2557
2685
  });
2558
2686
 
2559
2687
  it('listJobs works in root layout', () => {
@@ -268,17 +268,24 @@ function roadmapUpdatePlanProgressInternal(cwd, phaseNum) {
268
268
  }
269
269
 
270
270
  let roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
271
- const phaseEscaped = escapeRegex(phaseNum);
272
-
273
- // Progress table row: update Plans column (summaries/plans) and Status column
271
+ // Derive a BARE numeric phase key by stripping any leading `vX.Y/` prefix.
272
+ // Checklist lines and detail headings are always bare, so the bare key drives
273
+ // the heading/checkbox regexes; the progress-row matcher allows an optional
274
+ // version prefix and Milestone column on top of the same bare key.
275
+ const bareKey = String(phaseNum).replace(/^v\d+\.\d+\//, '');
276
+ const phaseEscaped = escapeRegex(bareKey);
277
+
278
+ // Progress table row: update Plans column (summaries/plans) and Status column.
279
+ // Format-agnostic: optional `vX.Y/` prefix on the Phase cell and an optional
280
+ // Milestone column (a pure-version cell) between Phase and Plans, both preserved.
274
281
  const tablePattern = new RegExp(
275
- `(\\|\\s*${phaseEscaped}\\.?\\s[^|]*\\|)[^|]*(\\|)\\s*[^|]*(\\|)\\s*[^|]*(\\|)`,
282
+ `(\\|\\s*(?:v\\d+\\.\\d+/)?${phaseEscaped}\\.?\\s[^|]*\\|)(\\s*v\\d+\\.\\d+\\s*\\|)?[^|]*(\\|)\\s*[^|]*(\\|)\\s*[^|]*(\\|)`,
276
283
  'i'
277
284
  );
278
285
  const dateField = isComplete ? ` ${today} ` : ' ';
279
286
  roadmapContent = roadmapContent.replace(
280
287
  tablePattern,
281
- `$1 ${summaryCount}/${planCount} $2 ${status.padEnd(11)}$3${dateField}$4`
288
+ `$1$2 ${summaryCount}/${planCount} $3 ${status.padEnd(11)}$4${dateField}$5`
282
289
  );
283
290
 
284
291
  // Update plan count in phase detail section
@@ -228,7 +228,7 @@ Each plan produces 2-4 commits (tasks + metadata). Clear, granular, bisectable.
228
228
 
229
229
  **Context engineering for AI:**
230
230
  - Git history becomes primary context source for future Claude sessions
231
- - `git log --grep="{phase}-{plan}"` shows all work for a plan
231
+ - `git log $(git merge-base main HEAD)..HEAD --grep="{phase}-{plan}"` shows all work for a plan on the current milestone branch
232
232
  - `git diff <hash>^..<hash>` shows exact changes per task
233
233
  - Less reliance on parsing SUMMARY.md = more context for actual work
234
234
 
@@ -21,7 +21,7 @@ Multi-agent code review that runs 3 passes of 3 parallel agents each (9 total re
21
21
  Compute the diff from the plan's task commits.
22
22
 
23
23
  ```bash
24
- FIRST_TASK_COMMIT=$(git -C "${CODE_REPO_PATH}" log --oneline --grep="feat(${PHASE}-${PLAN}):" --grep="fix(${PHASE}-${PLAN}):" --grep="test(${PHASE}-${PLAN}):" --grep="refactor(${PHASE}-${PLAN}):" --reverse | head -1 | cut -d' ' -f1)
24
+ FIRST_TASK_COMMIT=$(git -C "${CODE_REPO_PATH}" log $(git -C "${CODE_REPO_PATH}" merge-base main HEAD)..HEAD --oneline --grep="feat(${PHASE}-${PLAN}):" --grep="fix(${PHASE}-${PLAN}):" --grep="test(${PHASE}-${PLAN}):" --grep="refactor(${PHASE}-${PLAN}):" --reverse | head -1 | cut -d' ' -f1)
25
25
  ```
26
26
 
27
27
  If FIRST_TASK_COMMIT is empty, exit with message: "No task commits found for ${PHASE}-${PLAN}, skipping code review."
@@ -433,7 +433,7 @@ If mark-milestone-complete fails, log a warning but continue to gather_stats (no
433
433
  Calculate milestone statistics:
434
434
 
435
435
  ```bash
436
- git log --oneline --grep="feat(" | head -20
436
+ git log --oneline FIRST_COMMIT..LAST_COMMIT --grep="feat(" | head -20
437
437
  git diff --stat FIRST_COMMIT..LAST_COMMIT | tail -1
438
438
  find . -name "*.swift" -o -name "*.ts" -o -name "*.py" | xargs wc -l 2>/dev/null
439
439
  git log --format="%ai" FIRST_COMMIT | tail -1
@@ -377,7 +377,7 @@ Execute each wave in sequence. Within a wave: parallel if `PARALLELIZATION=true`
377
377
 
378
378
  For each SUMMARY.md:
379
379
  - Verify first 2 files from `key-files.created` exist on disk
380
- - Check `git log --oneline --all --grep="{phase}-{plan}"` returns ≥1 commit
380
+ - Check `git log --oneline $(git merge-base main HEAD)..HEAD --grep="{phase}-{plan}"` returns ≥1 commit
381
381
  - Check for `## Self-Check: FAILED` marker
382
382
 
383
383
  If ANY spot-check fails: report which plan failed, route to failure handler.
@@ -450,7 +450,7 @@ Execute each wave in sequence. Within a wave: parallel if `PARALLELIZATION=true`
450
450
 
451
451
  Compute diff reference for the plan's task commits:
452
452
  ```bash
453
- FIRST_TASK_COMMIT=$(git -C "${CODE_REPO_PATH}" log --oneline --grep="feat(${PHASE}-${PLAN}):" --grep="fix(${PHASE}-${PLAN}):" --grep="test(${PHASE}-${PLAN}):" --grep="refactor(${PHASE}-${PLAN}):" --reverse | head -1 | cut -d' ' -f1)
453
+ FIRST_TASK_COMMIT=$(git -C "${CODE_REPO_PATH}" log $(git -C "${CODE_REPO_PATH}" merge-base main HEAD)..HEAD --oneline --grep="feat(${PHASE}-${PLAN}):" --grep="fix(${PHASE}-${PLAN}):" --grep="test(${PHASE}-${PLAN}):" --grep="refactor(${PHASE}-${PLAN}):" --reverse | head -1 | cut -d' ' -f1)
454
454
  ```
455
455
 
456
456
  If FIRST_TASK_COMMIT is empty (no task commits found), skip codereview for this plan with message: "No task commits found for {phase}-{plan}, skipping code review."
@@ -114,7 +114,7 @@ Pattern B only (verify-only checkpoints). Skip for A/C.
114
114
  - Main route: execute tasks using standard flow (step name="execute")
115
115
  3. After ALL segments: aggregate files/deviations/decisions → create SUMMARY.md → commit → self-check:
116
116
  - Verify key-files.created exist on disk with `[ -f ]`
117
- - Check `git log --oneline --all --grep="{phase}-{plan}"` returns ≥1 commit
117
+ - Check `git log --oneline $(git merge-base main HEAD)..HEAD --grep="{phase}-{plan}"` returns ≥1 commit
118
118
  - Append `## Self-Check: PASSED` or `## Self-Check: FAILED` to SUMMARY
119
119
 
120
120
  **Known Claude Code bug (classifyHandoffIfNeeded):** If any segment agent reports "failed" with `classifyHandoffIfNeeded is not defined`, this is a Claude Code runtime bug — not a real failure. Run spot-checks; if they pass, treat as successful.
@@ -490,7 +490,7 @@ The plan name is auto-extracted from the PLAN.md `plan_name` frontmatter field (
490
490
  If ${project_root}/codebase/ doesn't exist: skip.
491
491
 
492
492
  ```bash
493
- FIRST_TASK=$(git log --oneline --grep="feat({phase}-{plan}):" --grep="fix({phase}-{plan}):" --grep="test({phase}-{plan}):" --reverse | head -1 | cut -d' ' -f1)
493
+ FIRST_TASK=$(git log $(git merge-base main HEAD)..HEAD --oneline --grep="feat({phase}-{plan}):" --grep="fix({phase}-{plan}):" --grep="test({phase}-{plan}):" --reverse | head -1 | cut -d' ' -f1)
494
494
  git diff --name-only ${FIRST_TASK}^..HEAD 2>/dev/null
495
495
  ```
496
496
 
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "bugs": {
9
9
  "url": "https://github.com/KT-Partners-Ltd/dgs-platform-docs/issues"
10
10
  },
11
- "version": "3.5.0",
11
+ "version": "3.5.1",
12
12
  "description": "Deliver Great Systems Platform — A meta-prompting, context engineering and spec-driven development system for Claude Code and Gemini by KT Partners.",
13
13
  "bin": {
14
14
  "dgs": "bin/install.js"