@ktpartners/dgs-platform 3.4.2 → 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 +28 -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 +34 -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 +132 -67
- package/deliver-great-systems/bin/lib/jobs.test.cjs +157 -13
- 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 +21 -11
- 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/references/git-integration.md +1 -1
- 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/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/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
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,34 @@ 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
|
+
|
|
25
|
+
## [3.5.0] - 2026-06-27
|
|
26
|
+
|
|
27
|
+
### Added — v25.0 Per-Milestone Phase Numbering via Directory Namespacing (Phases 163-170)
|
|
28
|
+
- **Phase numbers now restart at `01` per milestone**, stored in version-namespaced directories `phases/<version>/NN-slug/` instead of one global flat `phases/`. The product-global version sequence (`vX.Y`) is unchanged — only phase *numbers* became milestone-local. Pre-existing flat-layout projects keep working (dual-mode), so no migration is forced.
|
|
29
|
+
- **Authoritative version signal** — the active milestone version is sourced from STATE.md `current_milestone` (grammar-validated `^v\d+\.\d+$`, fail-loud, never silently defaulting to `v1.0`); the ROADMAP marker is advisory, and on mismatch the resolver warns and trusts STATE.md. `new-milestone` + `init.cjs` are the sole setters.
|
|
30
|
+
- **Canonical version-aware resolver** — a single `phasesDir(cwd)` returns `phases/<version>/` when that directory exists and falls back to flat `phases/` otherwise, consolidating phase-path logic that was previously duplicated across the init/commands/context/state/phase/roadmap/jobs/governance/milestone modules.
|
|
31
|
+
- **Version-aware lookup, state context, and structural archival**, with dual-mode (versioned + legacy-flat) test fixtures and the full library test suite greened.
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
- **Ad-hoc milestones support the full phase lifecycle (quick-260627-mv4)** — `milestone create-adhoc` now atomically seeds the planning scaffolding it previously skipped: it writes `current_milestone` to STATE.md, creates the versioned `phases/<version>/` directory, and seeds an `## Active Milestone` ROADMAP section. A hand-added phase therefore flows through `/dgs:add-phase` → `plan-phase` → `execute-phase` inside an ad-hoc container, with no requirements/roadmapper ceremony. `abandon-milestone` mirrors the teardown (removes the seeded versioned dir and restores the docs), and the three new artifacts fold into create-adhoc's existing atomic rollback so a failed creation leaves no residue.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
- **Workflow-discipline hook self-heals stale installs (quick-260627-o37)** — the installer's PreToolUse registration now upgrades an existing `dgs-enforce-discipline` entry whose matcher lacks `Bash` by appending `|Bash` in place, instead of only adding the entry when absent. Previously a machine that installed before the `Bash` matcher existed kept the stale `Edit|Write|Skill` matcher forever — the marker-on-init branch never fired for inline `/dgs:*` flows, so Edit/Write was falsely blocked, and `/dgs:update` could not fix it. Idempotent once `Bash` is present.
|
|
38
|
+
|
|
11
39
|
## [3.4.2] - 2026-06-25
|
|
12
40
|
|
|
13
41
|
### Fixed
|
package/README.md
CHANGED
|
@@ -612,6 +612,8 @@ See the [User Guide](docs/USER-GUIDE.md#context-tiers) for the complete command-
|
|
|
612
612
|
|
|
613
613
|
### Testing & Dependency Scanning
|
|
614
614
|
|
|
615
|
+
Run the full automated test suite (repo-root `tests/` plus the nested `deliver-great-systems/bin/lib/` library tree) with a single command: `npm test`. To run only the library tree: `npm run test:lib`.
|
|
616
|
+
|
|
615
617
|
| Command | What it does |
|
|
616
618
|
|---------|--------------|
|
|
617
619
|
| `/dgs:package-scan [--threshold critical\|high\|medium\|low] [--repo <name>] [--json] [--include-dev-deps\|--no-include-dev-deps]` | Scan every registered repo + product root for known dependency vulnerabilities and licence issues. Cascades Snyk → OSV-Scanner → ecosystem-native tool (`npm audit`, `pip-audit`, `govulncheck`, `bundler-audit`). Report is committed to the active phase dir, active milestone dir, or a timestamped project-root file. See `deliver-great-systems/references/package-scan-config.md` for config keys and installation. |
|
|
@@ -11,7 +11,7 @@ You are a DGS codebase cross-analyzer. You read per-repo codebase documents and
|
|
|
11
11
|
You are spawned by `/dgs:map-codebase` after all synthesizer agents have completed. There is one cross-analyzer instance per mapping run.
|
|
12
12
|
|
|
13
13
|
You receive these prompt variables from the orchestrator:
|
|
14
|
-
- **codebase_dir**: Path to the codebase directory (e.g.,
|
|
14
|
+
- **codebase_dir**: Path to the codebase directory (e.g., `codebase/` under the planning root)
|
|
15
15
|
- **repo_names**: JSON array of repo names that were mapped (e.g., `["business", "newarch"]`)
|
|
16
16
|
|
|
17
17
|
Your job: Read per-repo maps, compare across repos, write CROSS-REPO.md with comparison tables, return confirmation only.
|
|
@@ -156,7 +156,7 @@ Read key files identified during exploration. Use Glob and Grep with paths under
|
|
|
156
156
|
<step name="write_documents">
|
|
157
157
|
Write document(s) to `${codebase_dir}/` using the templates below.
|
|
158
158
|
|
|
159
|
-
**Note:** `${codebase_dir}` is set by the orchestrator and already includes the repo subdirectory path (e.g.,
|
|
159
|
+
**Note:** `${codebase_dir}` is set by the orchestrator and already includes the repo subdirectory path (e.g., `codebase/business/`). Write directly to it.
|
|
160
160
|
|
|
161
161
|
**Document naming:** UPPERCASE.md (e.g., STACK.md, ARCHITECTURE.md)
|
|
162
162
|
|
|
@@ -11,7 +11,7 @@ You are a DGS codebase synthesizer. You read per-repo codebase documents and pro
|
|
|
11
11
|
You are spawned by `/dgs:map-codebase` after all per-repo mapper agents have completed. Each synthesizer instance handles one document type.
|
|
12
12
|
|
|
13
13
|
You receive these prompt variables from the orchestrator:
|
|
14
|
-
- **codebase_dir**: Path to the codebase directory (e.g.,
|
|
14
|
+
- **codebase_dir**: Path to the codebase directory (e.g., `codebase/` under the planning root)
|
|
15
15
|
- **repo_names**: List of repo names that were mapped (e.g., `["business", "newarch"]`)
|
|
16
16
|
- **doc_type**: Which document to synthesize (one of: ARCHITECTURE, STACK, STRUCTURE, CONVENTIONS, TESTING, INTEGRATIONS, CONCERNS)
|
|
17
17
|
|
|
@@ -306,7 +306,7 @@ Verified patterns from official sources:
|
|
|
306
306
|
|
|
307
307
|
## Validation Architecture
|
|
308
308
|
|
|
309
|
-
> Skip this section entirely if workflow.nyquist_validation is explicitly set to false in
|
|
309
|
+
> Skip this section entirely if workflow.nyquist_validation is explicitly set to false in `${config_path}`. If the key is absent, treat as enabled.
|
|
310
310
|
|
|
311
311
|
### Test Framework
|
|
312
312
|
| Property | Value |
|
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) {
|
|
@@ -1816,11 +1838,11 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
1816
1838
|
settings.hooks.PreToolUse = [];
|
|
1817
1839
|
}
|
|
1818
1840
|
|
|
1819
|
-
const
|
|
1841
|
+
const disciplineHookEntry = settings.hooks.PreToolUse.find(entry =>
|
|
1820
1842
|
entry.hooks && entry.hooks.some(h => h.command && h.command.includes('dgs-enforce-discipline'))
|
|
1821
1843
|
);
|
|
1822
1844
|
|
|
1823
|
-
if (!
|
|
1845
|
+
if (!disciplineHookEntry) {
|
|
1824
1846
|
settings.hooks.PreToolUse.push({
|
|
1825
1847
|
matcher: 'Edit|Write|Skill|Bash',
|
|
1826
1848
|
hooks: [
|
|
@@ -1831,6 +1853,16 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
1831
1853
|
]
|
|
1832
1854
|
});
|
|
1833
1855
|
console.log(` ${green}✓${reset} Configured workflow discipline hook`);
|
|
1856
|
+
} else if (
|
|
1857
|
+
typeof disciplineHookEntry.matcher === 'string' &&
|
|
1858
|
+
!disciplineHookEntry.matcher.split('|').includes('Bash')
|
|
1859
|
+
) {
|
|
1860
|
+
// Self-heal stale installs: older versions registered this matcher without
|
|
1861
|
+
// Bash, so the marker-on-init branch never fired for inline /dgs:* flows and
|
|
1862
|
+
// Edit/Write got falsely blocked. Upgrade the existing entry in place so a
|
|
1863
|
+
// plain /dgs:update fixes it. Idempotent once Bash is present.
|
|
1864
|
+
disciplineHookEntry.matcher = disciplineHookEntry.matcher + '|Bash';
|
|
1865
|
+
console.log(` ${green}✓${reset} Upgraded workflow discipline hook matcher (added Bash)`);
|
|
1834
1866
|
}
|
|
1835
1867
|
|
|
1836
1868
|
// Configure PostToolUse hook for discipline marker cleanup (Skill completion)
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* state patch --field val ... Batch update STATE.md fields
|
|
16
16
|
* state archive-quick-tasks Archive excess quick task rows to HISTORY.md
|
|
17
17
|
* state mark-milestone-complete Mark milestone complete in STATE.md
|
|
18
|
+
* state reconcile-milestone Self-heal a shipped-but-not-flipped STATE.md
|
|
18
19
|
* resolve-model <agent-type> Get model for agent based on profile
|
|
19
20
|
* find-phase <phase> Find phase directory by number
|
|
20
21
|
* commit <message> [--files f1 f2] Commit planning docs
|
|
@@ -37,6 +38,7 @@
|
|
|
37
38
|
* Phase Operations:
|
|
38
39
|
* phase next-decimal <phase> Calculate next decimal phase number
|
|
39
40
|
* phase add <description> Append new phase to roadmap + create dir
|
|
41
|
+
* phase init-versioned-dir Create current milestone's phases/<version>/ (fail-loud; new-milestone only)
|
|
40
42
|
* phase insert <after> <description> Insert decimal phase after existing
|
|
41
43
|
* phase remove <phase> [--force] Remove phase, renumber all subsequent
|
|
42
44
|
* phase complete <phase> Mark phase done, update state + roadmap
|
|
@@ -708,6 +710,8 @@ async function main() {
|
|
|
708
710
|
state.cmdStateArchiveQuickTasks(cwd, raw);
|
|
709
711
|
} else if (subcommand === 'mark-milestone-complete') {
|
|
710
712
|
state.cmdMarkMilestoneComplete(cwd, raw);
|
|
713
|
+
} else if (subcommand === 'reconcile-milestone') {
|
|
714
|
+
state.cmdReconcileMilestone(cwd, raw);
|
|
711
715
|
} else if (subcommand === 'read-adhoc') {
|
|
712
716
|
state.cmdStateReadAdhoc(cwd, raw);
|
|
713
717
|
} else if (subcommand === 'adhoc-readiness') {
|
|
@@ -1178,6 +1182,8 @@ async function main() {
|
|
|
1178
1182
|
phase.cmdPhaseNextDecimal(cwd, args[2], raw);
|
|
1179
1183
|
} else if (subcommand === 'add') {
|
|
1180
1184
|
phase.cmdPhaseAdd(cwd, args.slice(2).join(' '), raw);
|
|
1185
|
+
} else if (subcommand === 'init-versioned-dir') {
|
|
1186
|
+
phase.cmdPhaseInitVersionedDir(cwd, raw);
|
|
1181
1187
|
} else if (subcommand === 'insert') {
|
|
1182
1188
|
phase.cmdPhaseInsert(cwd, args[2], args.slice(3).join(' '), raw);
|
|
1183
1189
|
} else if (subcommand === 'remove') {
|
|
@@ -1189,7 +1195,7 @@ async function main() {
|
|
|
1189
1195
|
const push = args.includes('--push');
|
|
1190
1196
|
phase.cmdPhaseFinalize(cwd, args[2], { push }, raw);
|
|
1191
1197
|
} else {
|
|
1192
|
-
error('Unknown phase subcommand. Available: next-decimal, add, insert, remove, complete, finalize');
|
|
1198
|
+
error('Unknown phase subcommand. Available: next-decimal, add, init-versioned-dir, insert, remove, complete, finalize');
|
|
1193
1199
|
}
|
|
1194
1200
|
break;
|
|
1195
1201
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { execSync } = require('child_process');
|
|
7
|
-
const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, getProjectRoot, resolveModelInternal, MODEL_PROFILES, toPosixPath, output, error, findPhaseInternal } = require('./core.cjs');
|
|
7
|
+
const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, getProjectRoot, phasesDir, resolveMilestoneVersion, resolveModelInternal, MODEL_PROFILES, toPosixPath, output, error, findPhaseInternal } = require('./core.cjs');
|
|
8
8
|
const { extractFrontmatter, spliceFrontmatter, reconstructFrontmatter } = require('./frontmatter.cjs');
|
|
9
9
|
const { getPlanningRoot } = require('./paths.cjs');
|
|
10
10
|
|
|
@@ -146,15 +146,35 @@ function cmdVerifyPathExists(cwd, targetPath, raw) {
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
function cmdHistoryDigest(cwd, raw) {
|
|
149
|
-
let
|
|
149
|
+
let phasesAbs;
|
|
150
150
|
try {
|
|
151
|
-
|
|
152
|
-
phasesDir = path.join(cwd, projectRoot, 'phases');
|
|
151
|
+
phasesAbs = path.join(cwd, phasesDir(cwd));
|
|
153
152
|
} catch {
|
|
154
|
-
|
|
153
|
+
phasesAbs = path.join(getPlanningRoot(cwd), 'phases');
|
|
155
154
|
}
|
|
156
155
|
const digest = { phases: {}, decisions: [], tech_stack: new Set() };
|
|
157
156
|
|
|
157
|
+
// Output shape (LOOK-02, composite {milestone, phase} key):
|
|
158
|
+
// Phases are keyed by a composite of milestone + phase number so a restarted
|
|
159
|
+
// phase number that exists in two milestones is NEVER conflated into one bucket.
|
|
160
|
+
// - Milestone-qualified entries (archived dirs, and current-phase entries once
|
|
161
|
+
// the current milestone is resolvable) → key = "<milestone>/<phase>"
|
|
162
|
+
// e.g. digest.phases['v4.0/03']
|
|
163
|
+
// - Entries with no resolvable milestone (flat-layout repos) → key = "<phase>"
|
|
164
|
+
// e.g. digest.phases['03'] — byte-identical to the pre-LOOK-02 shape, so
|
|
165
|
+
// flat installs see no data loss and no key change.
|
|
166
|
+
// decisions[] entries carry both `phase` (readable) and `milestone` so two
|
|
167
|
+
// milestones' same-numbered decisions are unambiguous.
|
|
168
|
+
|
|
169
|
+
// Resolve the current milestone once (non-required: validated vN.N or null,
|
|
170
|
+
// never throws on a read path — flat repos must keep working).
|
|
171
|
+
let currentMilestone = null;
|
|
172
|
+
try {
|
|
173
|
+
currentMilestone = resolveMilestoneVersion(cwd);
|
|
174
|
+
} catch {
|
|
175
|
+
currentMilestone = null;
|
|
176
|
+
}
|
|
177
|
+
|
|
158
178
|
// Collect all phase directories: archived + current
|
|
159
179
|
const allPhaseDirs = [];
|
|
160
180
|
|
|
@@ -164,15 +184,16 @@ function cmdHistoryDigest(cwd, raw) {
|
|
|
164
184
|
allPhaseDirs.push({ name: a.name, fullPath: a.fullPath, milestone: a.milestone });
|
|
165
185
|
}
|
|
166
186
|
|
|
167
|
-
// Add current phases
|
|
168
|
-
|
|
187
|
+
// Add current phases — assign the current milestone (archived entries already
|
|
188
|
+
// carry their own milestone; current entries are milestone: null until now).
|
|
189
|
+
if (fs.existsSync(phasesAbs)) {
|
|
169
190
|
try {
|
|
170
|
-
const currentDirs = fs.readdirSync(
|
|
191
|
+
const currentDirs = fs.readdirSync(phasesAbs, { withFileTypes: true })
|
|
171
192
|
.filter(e => e.isDirectory())
|
|
172
193
|
.map(e => e.name)
|
|
173
194
|
.sort();
|
|
174
195
|
for (const dir of currentDirs) {
|
|
175
|
-
allPhaseDirs.push({ name: dir, fullPath: path.join(
|
|
196
|
+
allPhaseDirs.push({ name: dir, fullPath: path.join(phasesAbs, dir), milestone: currentMilestone });
|
|
176
197
|
}
|
|
177
198
|
} catch {}
|
|
178
199
|
}
|
|
@@ -184,7 +205,7 @@ function cmdHistoryDigest(cwd, raw) {
|
|
|
184
205
|
}
|
|
185
206
|
|
|
186
207
|
try {
|
|
187
|
-
for (const { name: dir, fullPath: dirPath } of allPhaseDirs) {
|
|
208
|
+
for (const { name: dir, fullPath: dirPath, milestone } of allPhaseDirs) {
|
|
188
209
|
const summaries = fs.readdirSync(dirPath).filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
|
|
189
210
|
|
|
190
211
|
for (const summary of summaries) {
|
|
@@ -193,9 +214,12 @@ function cmdHistoryDigest(cwd, raw) {
|
|
|
193
214
|
const fm = extractFrontmatter(content);
|
|
194
215
|
|
|
195
216
|
const phaseNum = fm.phase || dir.split('-')[0];
|
|
217
|
+
// Composite key: "<milestone>/<phase>" when a milestone is present,
|
|
218
|
+
// else the bare phase number (flat-layout byte-identical behaviour).
|
|
219
|
+
const phaseKey = milestone ? `${milestone}/${phaseNum}` : phaseNum;
|
|
196
220
|
|
|
197
|
-
if (!digest.phases[
|
|
198
|
-
digest.phases[
|
|
221
|
+
if (!digest.phases[phaseKey]) {
|
|
222
|
+
digest.phases[phaseKey] = {
|
|
199
223
|
name: fm.name || dir.split('-').slice(1).join(' ') || 'Unknown',
|
|
200
224
|
provides: new Set(),
|
|
201
225
|
affects: new Set(),
|
|
@@ -205,25 +229,26 @@ function cmdHistoryDigest(cwd, raw) {
|
|
|
205
229
|
|
|
206
230
|
// Merge provides
|
|
207
231
|
if (fm['dependency-graph'] && fm['dependency-graph'].provides) {
|
|
208
|
-
fm['dependency-graph'].provides.forEach(p => digest.phases[
|
|
232
|
+
fm['dependency-graph'].provides.forEach(p => digest.phases[phaseKey].provides.add(p));
|
|
209
233
|
} else if (fm.provides) {
|
|
210
|
-
fm.provides.forEach(p => digest.phases[
|
|
234
|
+
fm.provides.forEach(p => digest.phases[phaseKey].provides.add(p));
|
|
211
235
|
}
|
|
212
236
|
|
|
213
237
|
// Merge affects
|
|
214
238
|
if (fm['dependency-graph'] && fm['dependency-graph'].affects) {
|
|
215
|
-
fm['dependency-graph'].affects.forEach(a => digest.phases[
|
|
239
|
+
fm['dependency-graph'].affects.forEach(a => digest.phases[phaseKey].affects.add(a));
|
|
216
240
|
}
|
|
217
241
|
|
|
218
242
|
// Merge patterns
|
|
219
243
|
if (fm['patterns-established']) {
|
|
220
|
-
fm['patterns-established'].forEach(p => digest.phases[
|
|
244
|
+
fm['patterns-established'].forEach(p => digest.phases[phaseKey].patterns.add(p));
|
|
221
245
|
}
|
|
222
246
|
|
|
223
|
-
// Merge decisions
|
|
247
|
+
// Merge decisions — tag with milestone so two milestones' same-numbered
|
|
248
|
+
// decisions are unambiguous; keep `phase` for readability.
|
|
224
249
|
if (fm['key-decisions']) {
|
|
225
250
|
fm['key-decisions'].forEach(d => {
|
|
226
|
-
digest.decisions.push({ phase: phaseNum, decision: d });
|
|
251
|
+
digest.decisions.push({ milestone: milestone || null, phase: phaseNum, decision: d });
|
|
227
252
|
});
|
|
228
253
|
}
|
|
229
254
|
|
|
@@ -622,7 +647,10 @@ function cmdPlanFinalize(cwd, phaseNum, planNum, options, raw) {
|
|
|
622
647
|
|
|
623
648
|
// Resolve phase dir via findPhaseInternal (returns phases/NN-name relative path)
|
|
624
649
|
const phaseInfo = findPhaseInternal(cwd, phaseNum);
|
|
625
|
-
|
|
650
|
+
// Guard on `found`: a LOOK-01 ambiguity object is truthy but has no .directory,
|
|
651
|
+
// so a bare `!phaseInfo` guard would deref undefined and crash. Surface its
|
|
652
|
+
// milestone-qualified message instead.
|
|
653
|
+
if (!phaseInfo || !phaseInfo.found) error(phaseInfo?.message || `Phase ${phaseNum} not found`);
|
|
626
654
|
const phaseDir = path.join(cwd, phaseInfo.directory);
|
|
627
655
|
|
|
628
656
|
// Locate PLAN.md + SUMMARY.md within the phase dir using `${phaseNum}-${planNum}` prefix
|
|
@@ -862,15 +890,15 @@ async function cmdWebsearch(query, options, raw) {
|
|
|
862
890
|
}
|
|
863
891
|
|
|
864
892
|
function cmdProgressRender(cwd, format, raw) {
|
|
865
|
-
let
|
|
893
|
+
let phasesAbs, roadmapPath;
|
|
866
894
|
try {
|
|
867
895
|
const projectRoot = getProjectRoot(cwd);
|
|
868
896
|
const projectAbs = path.join(cwd, projectRoot);
|
|
869
|
-
|
|
897
|
+
phasesAbs = path.join(cwd, phasesDir(cwd));
|
|
870
898
|
roadmapPath = path.join(projectAbs, 'ROADMAP.md');
|
|
871
899
|
} catch {
|
|
872
900
|
const planRoot = getPlanningRoot(cwd);
|
|
873
|
-
|
|
901
|
+
phasesAbs = path.join(planRoot, 'phases');
|
|
874
902
|
roadmapPath = path.join(planRoot, 'ROADMAP.md');
|
|
875
903
|
}
|
|
876
904
|
const milestone = getMilestoneInfo(cwd);
|
|
@@ -880,14 +908,14 @@ function cmdProgressRender(cwd, format, raw) {
|
|
|
880
908
|
let totalSummaries = 0;
|
|
881
909
|
|
|
882
910
|
try {
|
|
883
|
-
const entries = fs.readdirSync(
|
|
911
|
+
const entries = fs.readdirSync(phasesAbs, { withFileTypes: true });
|
|
884
912
|
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
|
|
885
913
|
|
|
886
914
|
for (const dir of dirs) {
|
|
887
915
|
const dm = dir.match(/^(\d+(?:\.\d+)*)-?(.*)/);
|
|
888
916
|
const phaseNum = dm ? dm[1] : dir;
|
|
889
917
|
const phaseName = dm && dm[2] ? dm[2].replace(/-/g, ' ') : '';
|
|
890
|
-
const phaseFiles = fs.readdirSync(path.join(
|
|
918
|
+
const phaseFiles = fs.readdirSync(path.join(phasesAbs, dir));
|
|
891
919
|
const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').length;
|
|
892
920
|
const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md').length;
|
|
893
921
|
|
|
@@ -1029,10 +1057,12 @@ function cmdScaffold(cwd, type, options, raw) {
|
|
|
1029
1057
|
|
|
1030
1058
|
// Find phase directory
|
|
1031
1059
|
const phaseInfo = phase ? findPhaseInternal(cwd, phase) : null;
|
|
1032
|
-
|
|
1060
|
+
// Gate phaseDir on `found`+`directory`: a LOOK-01 ambiguity object is truthy
|
|
1061
|
+
// but carries no .directory, so `path.join(cwd, undefined)` would throw.
|
|
1062
|
+
const phaseDir = (phaseInfo && phaseInfo.found && phaseInfo.directory) ? path.join(cwd, phaseInfo.directory) : null;
|
|
1033
1063
|
|
|
1034
1064
|
if (phase && !phaseDir && type !== 'phase-dir') {
|
|
1035
|
-
error(`Phase ${phase} directory not found`);
|
|
1065
|
+
error(phaseInfo?.message || `Phase ${phase} directory not found`);
|
|
1036
1066
|
}
|
|
1037
1067
|
|
|
1038
1068
|
let filePath, content;
|
|
@@ -1059,12 +1089,19 @@ function cmdScaffold(cwd, type, options, raw) {
|
|
|
1059
1089
|
}
|
|
1060
1090
|
const slug = generateSlugInternal(name);
|
|
1061
1091
|
const dirName = `${padded}-${slug}`;
|
|
1062
|
-
const
|
|
1092
|
+
const planRootRel = path.relative(cwd, getPlanningRoot(cwd)) || '.';
|
|
1093
|
+
// RSLV-03 residue (Phase 170): create the phase dir through the canonical
|
|
1094
|
+
// version-aware resolver so the dir we CREATE is the dir we REPORT. Under a
|
|
1095
|
+
// versioned layout this mkdir's phases/<version>/NN-slug/, not the flat root.
|
|
1096
|
+
let phasesRel;
|
|
1097
|
+
try { phasesRel = phasesDir(cwd); }
|
|
1098
|
+
catch { phasesRel = path.join(planRootRel, 'phases'); }
|
|
1099
|
+
const phasesParent = path.join(cwd, phasesRel);
|
|
1063
1100
|
fs.mkdirSync(phasesParent, { recursive: true });
|
|
1064
1101
|
const dirPath = path.join(phasesParent, dirName);
|
|
1065
1102
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
1066
|
-
const
|
|
1067
|
-
output({ created: true, directory:
|
|
1103
|
+
const dirRel = path.join(phasesRel, dirName);
|
|
1104
|
+
output({ created: true, directory: dirRel, path: dirPath }, raw, dirPath);
|
|
1068
1105
|
return;
|
|
1069
1106
|
}
|
|
1070
1107
|
default:
|