@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 +14 -0
- package/bin/install.js +22 -0
- package/deliver-great-systems/bin/lib/jobs.cjs +116 -57
- package/deliver-great-systems/bin/lib/jobs.test.cjs +140 -12
- package/deliver-great-systems/bin/lib/roadmap.cjs +12 -5
- package/deliver-great-systems/references/git-integration.md +1 -1
- package/deliver-great-systems/workflows/codereview.md +1 -1
- package/deliver-great-systems/workflows/complete-milestone.md +1 -1
- package/deliver-great-systems/workflows/execute-phase.md +2 -2
- package/deliver-great-systems/workflows/execute-plan.md +2 -2
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
1164
|
-
|
|
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
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
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
|
-
|
|
1186
|
-
|
|
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
|
-
|
|
1201
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
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
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
//
|
|
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('
|
|
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
|
-
//
|
|
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
|
-
//
|
|
2194
|
-
assert.
|
|
2195
|
-
assert.
|
|
2196
|
-
assert.
|
|
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
|
|
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.
|
|
2553
|
-
//
|
|
2554
|
-
assert.
|
|
2555
|
-
assert.
|
|
2556
|
-
assert.
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
//
|
|
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
|
|
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} $
|
|
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
|
|
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
|
|
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.
|
|
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"
|