@smartmemory/compose 0.2.49-beta → 0.2.51-beta
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/.claude/skills/compose/SKILL.md +24 -1
- package/.claude/skills/compose/references/hermes-tools.md +80 -0
- package/README.md +1 -1
- package/bin/compose.js +340 -70
- package/dist/assets/App-CT0vXPgd.js +724 -0
- package/dist/assets/_baseUniq-CsOHc_iS.js +1 -0
- package/dist/assets/{arc-B1TqAC_R.js → arc-BXht3LyE.js} +1 -1
- package/dist/assets/architectureDiagram-Q4EWVU46-BV1r86-w.js +36 -0
- package/dist/assets/blockDiagram-DXYQGD6D-BgcMJjAR.js +132 -0
- package/dist/assets/{browser-DW2JlJCC.js → browser-CnKiSnlr.js} +6 -6
- package/dist/assets/{c4Diagram-AAUBKEIU-Dc6swoQo.js → c4Diagram-AHTNJAMY-nBIRLJTv.js} +1 -1
- package/dist/assets/channel-Dd4XaiYv.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-CdqXgd68.js → chunk-4BX2VUAB-CNmGyhrp.js} +1 -1
- package/dist/assets/chunk-4TB4RGXK-MLT7I7w4.js +206 -0
- package/dist/assets/{chunk-55IACEB6-D7VauOmU.js → chunk-55IACEB6-D8KuKVLa.js} +1 -1
- package/dist/assets/{chunk-2J33WTMH-6HKDSnOw.js → chunk-EDXVE4YY-TPlt1bS2.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-C1Z5m8DP.js → chunk-FMBD7UC4-tYY8xcjr.js} +1 -1
- package/dist/assets/chunk-OYMX7WX6-yBPwT1AT.js +231 -0
- package/dist/assets/{chunk-QZHKN3VN-DY9GU2uD.js → chunk-QZHKN3VN-B4Wfybww.js} +1 -1
- package/dist/assets/{chunk-ND2GUHAM-DmqZMEQe.js → chunk-YZCP3GAM-DE6_5aqh.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-DshSQDF9.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-DshSQDF9.js +1 -0
- package/dist/assets/clone-CBsbNGAa.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-B34g9xzV.js → cose-bilkent-S5V4N54A-DzzRyJtB.js} +1 -1
- package/dist/assets/dagre-KV5264BT-CTcTVGdU.js +4 -0
- package/dist/assets/diagram-5BDNPKRD-qR6mc8kJ.js +10 -0
- package/dist/assets/diagram-G4DWMVQ6-CUcGXyNb.js +24 -0
- package/dist/assets/diagram-MMDJMWI5-B68cPonH.js +43 -0
- package/dist/assets/diagram-TYMM5635-q8Id8eFG.js +24 -0
- package/dist/assets/{erDiagram-TEJ5UH35-C05_Iqmb.js → erDiagram-SMLLAGMA-CtxJi57K.js} +1 -1
- package/dist/assets/{flowDiagram-I6XJVG4X-lXpnb-Ch.js → flowDiagram-DWJPFMVM-BzzO4JYU.js} +1 -1
- package/dist/assets/ganttDiagram-T4ZO3ILL-Dn0wIyhR.js +292 -0
- package/dist/assets/gitGraphDiagram-UUTBAWPF-tnNFXfMM.js +106 -0
- package/dist/assets/graph-DZe55uk8.js +331 -0
- package/dist/assets/graph-Tq_bs_r0.js +1 -0
- package/dist/assets/index-8UhRLbGq.js +123 -0
- package/dist/assets/index-CRqB9els.css +1 -0
- package/dist/assets/infoDiagram-42DDH7IO-BXLvLeS0.js +2 -0
- package/dist/assets/{ishikawaDiagram-YF4QCWOH-CgdsWdgu.js → ishikawaDiagram-UXIWVN3A-DWx-7BBy.js} +5 -5
- package/dist/assets/{journeyDiagram-JHISSGLW-CyvHmE3z.js → journeyDiagram-VCZTEJTY-C42hXNha.js} +1 -1
- package/dist/assets/{kanban-definition-UN3LZRKU-Bqjdyv0T.js → kanban-definition-6JOO6SKY-CEq930ew.js} +1 -1
- package/dist/assets/katex-DkKDou_j.js +257 -0
- package/dist/assets/layout-qtUgN9BC.js +1 -0
- package/dist/assets/{linear-DecnRXf0.js → linear-D158F7OT.js} +1 -1
- package/dist/assets/min-hESxN-c0.js +1 -0
- package/dist/assets/{mindmap-definition-RKZ34NQL-DM1x9vNY.js → mindmap-definition-QFDTVHPH-d9SSh2nG.js} +7 -7
- package/dist/assets/mobile-1gVCT0OK.css +1 -0
- package/dist/assets/mobile-Chw8RWyH.js +17 -0
- package/dist/assets/pieDiagram-DEJITSTG-gglmeMav.js +30 -0
- package/dist/assets/quadrantDiagram-34T5L4WZ-CqFC_htU.js +7 -0
- package/dist/assets/{requirementDiagram-4Y6WPE33-DD-Gg_y5.js → requirementDiagram-MS252O5E-B1ZFIotK.js} +1 -1
- package/dist/assets/sankeyDiagram-XADWPNL6-BvItvZt5.js +10 -0
- package/dist/assets/sequenceDiagram-FGHM5R23-CAIn5Z3U.js +157 -0
- package/dist/assets/stateDiagram-FHFEXIEX-CyJrGzib.js +1 -0
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-DprhF_Ho.js +1 -0
- package/dist/assets/{timeline-definition-PNZ67QCA-BPm2AEQj.js → timeline-definition-GMOUNBTQ-k_0vJQAK.js} +1 -1
- package/dist/assets/{vennDiagram-CIIHVFJN-DQCDWwlj.js → vennDiagram-DHZGUBPP-TZAW60b0.js} +4 -4
- package/dist/assets/wardley-RL74JXVD-DzQEoG6X.js +162 -0
- package/dist/assets/wardleyDiagram-NUSXRM2D-BAoVVIjZ.js +20 -0
- package/dist/assets/{xychartDiagram-2RQKCTM6-Ba05SXXF.js → xychartDiagram-5P7HB3ND-CynkZcUb.js} +1 -1
- package/dist/index.html +4 -4
- package/lib/build-all.js +2 -1
- package/lib/build.js +363 -38
- package/lib/checkpoint/checkpoint-writer.js +3 -2
- package/lib/codex-preflight.js +151 -0
- package/lib/completion-writer.js +59 -15
- package/lib/feature-json.js +14 -3
- package/lib/feature-validator.js +27 -10
- package/lib/feature-write-guard.js +5 -7
- package/lib/feature-writer.js +3 -3
- package/lib/followup-writer.js +2 -2
- package/lib/get-roadmap.js +2 -2
- package/lib/gsd.js +2 -1
- package/lib/hooks-status.js +135 -0
- package/lib/ideabox.js +3 -2
- package/lib/install-agent-defs.js +22 -0
- package/lib/journal-writer.js +3 -3
- package/lib/migrate-anon.js +277 -0
- package/lib/migrate-roadmap.js +2 -3
- package/lib/paths-core.js +47 -0
- package/lib/project-paths.js +46 -37
- package/lib/roadmap-gen.js +3 -4
- package/lib/roadmap-graph/collect.js +5 -5
- package/lib/roadmap-graph/index.js +2 -2
- package/lib/roadmap-parser.js +1 -1
- package/lib/state-migrations.js +3 -3
- package/lib/test-bootstrap.js +165 -2
- package/lib/tracker/local-provider.js +2 -2
- package/lib/triage.js +7 -5
- package/lib/vocabulary-inject.js +103 -0
- package/lib/xref-push.js +4 -4
- package/lib/xref-sync.js +4 -4
- package/package.json +2 -1
- package/pipelines/build-quick.stratum.yaml +433 -0
- package/pipelines/build.stratum.yaml +60 -3
- package/scripts/watch-server.sh +32 -0
- package/server/build-routes.js +89 -9
- package/server/build-stream-bridge.js +12 -0
- package/server/compose-mcp-tools.js +73 -1
- package/server/compose-mcp.js +27 -0
- package/server/design-routes.js +9 -4
- package/server/drift-axes.js +5 -3
- package/server/feature-scaffold-routes.js +82 -0
- package/server/feature-scan.js +8 -2
- package/server/file-watcher.js +13 -7
- package/server/health-routes.js +261 -0
- package/server/ideabox-routes.js +12 -11
- package/server/project-root.js +7 -4
- package/server/qa-scope-routes.js +96 -0
- package/server/session-routes.js +2 -2
- package/server/stratum-sync.js +85 -9
- package/server/validate-routes.js +70 -0
- package/server/vision-routes.js +46 -2
- package/server/vision-server.js +25 -3
- package/server/vision-utils.js +23 -4
- package/dist/assets/App-DOerhoDp.js +0 -706
- package/dist/assets/architectureDiagram-3BPJPVTR-C4hfxNOw.js +0 -36
- package/dist/assets/blockDiagram-GPEHLZMM-DRqkGncy.js +0 -132
- package/dist/assets/channel-KsY1x-7s.js +0 -1
- package/dist/assets/chunk-727SXJPM-CeKpXDif.js +0 -206
- package/dist/assets/chunk-AQP2D5EJ-y2XL-YHV.js +0 -231
- package/dist/assets/classDiagram-4FO5ZUOK-zDEDoVPk.js +0 -1
- package/dist/assets/classDiagram-v2-Q7XG4LA2-zDEDoVPk.js +0 -1
- package/dist/assets/dagre-BM42HDAG-klZ_GOcw.js +0 -4
- package/dist/assets/diagram-2AECGRRQ-D0FmM6Wf.js +0 -43
- package/dist/assets/diagram-5GNKFQAL-DYuhby6q.js +0 -10
- package/dist/assets/diagram-KO2AKTUF-CCtQZgal.js +0 -3
- package/dist/assets/diagram-LMA3HP47-Cd1OqEln.js +0 -24
- package/dist/assets/diagram-OG6HWLK6-CiJrhoxo.js +0 -24
- package/dist/assets/ganttDiagram-6RSMTGT7-EPbL_2Hr.js +0 -292
- package/dist/assets/gitGraphDiagram-PVQCEYII-COCoF_vW.js +0 -106
- package/dist/assets/graph-BBXaecIU.js +0 -331
- package/dist/assets/graph-CAnANduQ.js +0 -1
- package/dist/assets/index-BwLfbbOu.css +0 -1
- package/dist/assets/index-DlgxA8dX.js +0 -123
- package/dist/assets/infoDiagram-5YYISTIA-B1sJw6j5.js +0 -2
- package/dist/assets/katex-C5jXJg4s.js +0 -257
- package/dist/assets/layout-DGIYPm2g.js +0 -1
- package/dist/assets/mobile-Cag5dHlF.css +0 -1
- package/dist/assets/mobile-DwmxS_O4.js +0 -17
- package/dist/assets/pieDiagram-4H26LBE5-CRtmCvwV.js +0 -30
- package/dist/assets/quadrantDiagram-W4KKPZXB-CsBLWg27.js +0 -7
- package/dist/assets/sankeyDiagram-5OEKKPKP-BPdMN8UU.js +0 -40
- package/dist/assets/sequenceDiagram-3UESZ5HK-D4AENdze.js +0 -162
- package/dist/assets/stateDiagram-AJRCARHV-DDplpCf_.js +0 -1
- package/dist/assets/stateDiagram-v2-BHNVJYJU-J4t9AVzF.js +0 -1
- package/dist/assets/wardley-L42UT6IY-WERqOmJt.js +0 -173
- package/dist/assets/wardleyDiagram-YWT4CUSO-5teUSOiI.js +0 -78
|
@@ -16,6 +16,7 @@ import { captureFingerprint } from './fingerprint.js';
|
|
|
16
16
|
import { captureAnchor } from './anchor.js';
|
|
17
17
|
import { scribePrompt } from './prompts.js';
|
|
18
18
|
import { createCheckpointStore } from './store/index.js';
|
|
19
|
+
import { resolveFeaturesPath } from '../project-paths.js';
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Resolve the checkpoint config block from .compose/compose.json with defaults
|
|
@@ -75,7 +76,7 @@ export function writeCheckpoint(targetRoot, {
|
|
|
75
76
|
const cfg = checkpointConfig(targetRoot);
|
|
76
77
|
const dataDir = path.join(targetRoot, '.compose', 'data');
|
|
77
78
|
const composeDir = path.join(targetRoot, '.compose');
|
|
78
|
-
const featureDir = path.join(targetRoot,
|
|
79
|
+
const featureDir = path.join(resolveFeaturesPath(targetRoot), featureCode);
|
|
79
80
|
|
|
80
81
|
const store = createCheckpointStore(cfg.backend, { dataDir });
|
|
81
82
|
// The prior checkpoint (read before writing) seeds the scribe prompt's context.
|
|
@@ -121,7 +122,7 @@ export function anchorBoundary(targetRoot, { item, trigger, flowId = null } = {}
|
|
|
121
122
|
if (!featureCode) return null;
|
|
122
123
|
const dataDir = path.join(targetRoot, '.compose', 'data');
|
|
123
124
|
const composeDir = path.join(targetRoot, '.compose');
|
|
124
|
-
const featureDir = path.join(targetRoot,
|
|
125
|
+
const featureDir = path.join(resolveFeaturesPath(targetRoot), featureCode);
|
|
125
126
|
const store = createCheckpointStore(cfg.backend, { dataDir });
|
|
126
127
|
return captureAnchor({ item, trigger, cwd: targetRoot, featureDir, composeDir, dataDir, store, flowId });
|
|
127
128
|
} catch (err) {
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* COMP-CODEX-IMPL — preflight probe for the Codex-as-implementer path.
|
|
3
|
+
*
|
|
4
|
+
* Codex self-applies a Seatbelt sandbox ([[project_codex_seatbelt_nonnesting]]).
|
|
5
|
+
* Compose's `execute` step runs each task agent inside a *detached git worktree*
|
|
6
|
+
* (`git worktree add --detach`), whose `.git` is a file pointer, not a directory.
|
|
7
|
+
* Whether Codex's Seatbelt tolerates writing inside such a worktree is UNVERIFIED —
|
|
8
|
+
* no Codex+worktree path exists in the repo today (Codex only ever ran read-only
|
|
9
|
+
* reviews with cwd at the repo root). If it cannot, a `--codex` build would
|
|
10
|
+
* hard-fail at `execute`. A runtime warning would only explain that after the fact.
|
|
11
|
+
*
|
|
12
|
+
* So before any real `--codex` work, we probe the exact primitive: create a throwaway
|
|
13
|
+
* detached worktree, have Codex write a sentinel file in it, and verify the write
|
|
14
|
+
* landed. On failure the build aborts fast with an actionable message rather than
|
|
15
|
+
* failing mid-execute. The result is cached per-repo (the primitive doesn't change
|
|
16
|
+
* between builds) and can be skipped via COMPOSE_SKIP_CODEX_PROBE for CI/tests.
|
|
17
|
+
*
|
|
18
|
+
* The proper fix for a probe-fails environment (a Codex `isolation: none` execution
|
|
19
|
+
* path) is deferred to COMP-CODEX-IMPL-SPIKE.
|
|
20
|
+
*/
|
|
21
|
+
import { execSync } from 'node:child_process';
|
|
22
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
23
|
+
import { join } from 'node:path';
|
|
24
|
+
import { homedir } from 'node:os';
|
|
25
|
+
|
|
26
|
+
export const PROBE_SENTINEL = 'COMPOSE_CODEX_PROBE_OK';
|
|
27
|
+
const WORKTREE_BASE = join(homedir(), '.stratum', 'worktrees');
|
|
28
|
+
const PROBE_CACHE = 'codex-worktree-probe.json';
|
|
29
|
+
// Bound the probe's agent run so a wedged Codex CLI can't hang the whole build
|
|
30
|
+
// before the flow even starts. On timeout we reject → the finally cleanup runs.
|
|
31
|
+
const PROBE_AGENT_TIMEOUT_MS = 180_000;
|
|
32
|
+
|
|
33
|
+
function cachePath(dataDir) {
|
|
34
|
+
return join(dataDir, PROBE_CACHE);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function readProbeCache(dataDir) {
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(readFileSync(cachePath(dataDir), 'utf-8'));
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function writeProbeCache(dataDir, result) {
|
|
46
|
+
try {
|
|
47
|
+
mkdirSync(dataDir, { recursive: true });
|
|
48
|
+
writeFileSync(cachePath(dataDir), JSON.stringify(result, null, 2));
|
|
49
|
+
} catch { /* best-effort cache; a miss just re-probes */ }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isGitRepo(cwd) {
|
|
53
|
+
try {
|
|
54
|
+
execSync('git rev-parse --is-inside-work-tree', { cwd, encoding: 'utf-8', timeout: 5000, stdio: 'pipe' });
|
|
55
|
+
return true;
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Verify Codex can write inside a detached git worktree under its sandbox.
|
|
63
|
+
*
|
|
64
|
+
* @param {object} args
|
|
65
|
+
* @param {string} args.cwd repo working directory
|
|
66
|
+
* @param {object} args.stratum stratum client (uses runAgentText('codex', ...))
|
|
67
|
+
* @param {string} args.dataDir .compose/data dir for the per-repo cache
|
|
68
|
+
* @param {string} args.ts caller-supplied timestamp string (unique worktree name)
|
|
69
|
+
* @param {boolean} [args.force] ignore the cache and re-probe
|
|
70
|
+
* @returns {Promise<{ok: boolean, reason: string, cached?: boolean, skipped?: boolean}>}
|
|
71
|
+
*/
|
|
72
|
+
export async function preflightCodexWorktreeProbe({ cwd, stratum, dataDir, ts, force = false }) {
|
|
73
|
+
if (process.env.COMPOSE_SKIP_CODEX_PROBE) {
|
|
74
|
+
return { ok: true, skipped: true, reason: 'COMPOSE_SKIP_CODEX_PROBE set — probe skipped' };
|
|
75
|
+
}
|
|
76
|
+
if (!force) {
|
|
77
|
+
const cached = readProbeCache(dataDir);
|
|
78
|
+
if (cached && cached.ok === true) return { ...cached, cached: true };
|
|
79
|
+
}
|
|
80
|
+
// Not a git repo → the execute step won't create worktrees (useWorktrees gates on
|
|
81
|
+
// isGitRepo), so there is no worktree primitive to probe. Treat as a pass.
|
|
82
|
+
if (!isGitRepo(cwd)) {
|
|
83
|
+
return { ok: true, reason: 'not a git repo — worktree isolation is not used, nothing to probe' };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try { mkdirSync(WORKTREE_BASE, { recursive: true }); } catch { /* best-effort */ }
|
|
87
|
+
const wtPath = join(WORKTREE_BASE, `codex-probe-${ts}`);
|
|
88
|
+
|
|
89
|
+
// Create the detached worktree (the exact primitive the execute step uses).
|
|
90
|
+
try {
|
|
91
|
+
execSync(`git worktree add "${wtPath}" --detach HEAD`, {
|
|
92
|
+
cwd, encoding: 'utf-8', timeout: 30_000, stdio: 'pipe',
|
|
93
|
+
});
|
|
94
|
+
} catch (err) {
|
|
95
|
+
try { execSync(`rm -rf "${wtPath}"`, { encoding: 'utf-8', timeout: 10_000 }); } catch { /* best-effort */ }
|
|
96
|
+
const result = { ok: false, reason: `could not create a probe worktree: ${err?.message ?? err}` };
|
|
97
|
+
writeProbeCache(dataDir, result);
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Unique per-run filename so a file committed in the repo (carried into the
|
|
102
|
+
// detached worktree from HEAD) can never make the probe false-pass.
|
|
103
|
+
const probeName = `codex-probe-${ts}.txt`;
|
|
104
|
+
try {
|
|
105
|
+
const prompt =
|
|
106
|
+
`Write a file named ${probeName} in the current working directory whose exact ` +
|
|
107
|
+
`contents are the single line: ${PROBE_SENTINEL}\n` +
|
|
108
|
+
`Do nothing else. This is an environment write-probe.`;
|
|
109
|
+
try {
|
|
110
|
+
// Bound the agent run — a hang here would otherwise stall the build and skip
|
|
111
|
+
// the finally cleanup. On timeout the race rejects and we fall to the catch.
|
|
112
|
+
await Promise.race([
|
|
113
|
+
stratum.runAgentText('codex', prompt, { cwd: wtPath }),
|
|
114
|
+
new Promise((_, reject) =>
|
|
115
|
+
setTimeout(() => reject(new Error(`probe timed out after ${PROBE_AGENT_TIMEOUT_MS}ms`)), PROBE_AGENT_TIMEOUT_MS).unref?.()
|
|
116
|
+
),
|
|
117
|
+
]);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
const result = { ok: false, reason: `Codex agent run failed/timed out inside the worktree: ${err?.message ?? err}` };
|
|
120
|
+
writeProbeCache(dataDir, result);
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
const probeFile = join(wtPath, probeName);
|
|
124
|
+
let landed = false;
|
|
125
|
+
try {
|
|
126
|
+
landed = existsSync(probeFile) && readFileSync(probeFile, 'utf-8').includes(PROBE_SENTINEL);
|
|
127
|
+
} catch { landed = false; }
|
|
128
|
+
const result = landed
|
|
129
|
+
? { ok: true, reason: 'Codex wrote the sentinel inside a detached worktree' }
|
|
130
|
+
: { ok: false, reason: 'Codex did not write the sentinel file inside the detached worktree (likely a Seatbelt/worktree write restriction)' };
|
|
131
|
+
writeProbeCache(dataDir, result);
|
|
132
|
+
return result;
|
|
133
|
+
} finally {
|
|
134
|
+
try {
|
|
135
|
+
execSync(`git worktree remove "${wtPath}" --force`, { cwd, encoding: 'utf-8', timeout: 30_000, stdio: 'pipe' });
|
|
136
|
+
} catch {
|
|
137
|
+
try { execSync(`rm -rf "${wtPath}"`, { encoding: 'utf-8', timeout: 10_000 }); } catch { /* give up */ }
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** The actionable abort message when the probe fails. */
|
|
143
|
+
export function codexProbeAbortMessage(reason) {
|
|
144
|
+
return (
|
|
145
|
+
`--codex aborted: Codex cannot write inside a detached git worktree in this environment.\n` +
|
|
146
|
+
` Reason: ${reason}\n` +
|
|
147
|
+
` This is the known COMP-CODEX-IMPL-SPIKE limitation (Codex Seatbelt vs worktree isolation).\n` +
|
|
148
|
+
` Re-run without --codex (Claude implements), or set COMPOSE_SKIP_CODEX_PROBE=1 to bypass the\n` +
|
|
149
|
+
` probe at your own risk. A Codex isolation:none execution path is tracked as the follow-up.`
|
|
150
|
+
);
|
|
151
|
+
}
|
package/lib/completion-writer.js
CHANGED
|
@@ -42,6 +42,13 @@ async function getProvider(cwd) {
|
|
|
42
42
|
// ---------------------------------------------------------------------------
|
|
43
43
|
const SHA_RE = /^[0-9a-f]{40}$/i; // FULL SHA only (Decision 9). Case-insensitive input; normalize to lowercase.
|
|
44
44
|
const SHORT_LEN = 8; // Display only — never the dedup key.
|
|
45
|
+
// git's null-SHA. Stamped for a commit-less completion (non-git workspace —
|
|
46
|
+
// COMP-PATHS-EXTERNAL Decision 6b). Schema-valid (matches SHA_RE) and a
|
|
47
|
+
// well-known "no commit" marker, so no schema or downstream change is needed.
|
|
48
|
+
const NULL_SHA = '0'.repeat(40);
|
|
49
|
+
// Monotonic per-process counter so distinct commit-less completions get
|
|
50
|
+
// distinct completion_ids even within the same millisecond.
|
|
51
|
+
let _nocommitSeq = 0;
|
|
45
52
|
const DEFAULT_LIMIT = 50;
|
|
46
53
|
const MAX_LIMIT = 500;
|
|
47
54
|
|
|
@@ -124,6 +131,10 @@ function validateRepoRelativePath(p) {
|
|
|
124
131
|
if (p.includes('\0')) {
|
|
125
132
|
throw inputError(`files_changed: entry contains NUL byte: "${p}"`);
|
|
126
133
|
}
|
|
134
|
+
// files_changed stays repo-relative on purpose — absolute paths are a
|
|
135
|
+
// path-safety hazard (e.g. "/etc/passwd") and no caller needs them: the
|
|
136
|
+
// commit-less (external) ship path passes []. (COMP-PATHS-EXTERNAL: kept the
|
|
137
|
+
// guard rather than widen it for an unused capability.)
|
|
127
138
|
if (p.startsWith('/')) {
|
|
128
139
|
throw inputError(`files_changed: absolute paths not allowed: "${p}"`);
|
|
129
140
|
}
|
|
@@ -151,16 +162,21 @@ function validate(args) {
|
|
|
151
162
|
);
|
|
152
163
|
}
|
|
153
164
|
|
|
154
|
-
// commit_sha —
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
)
|
|
165
|
+
// commit_sha — OPTIONAL (COMP-PATHS-EXTERNAL Decision 6b): omitted/null means
|
|
166
|
+
// a commit-less completion (non-git workspace) and is stamped with NULL_SHA.
|
|
167
|
+
// When PRESENT it must still be a full 40-char hex SHA (Decision 9) — a
|
|
168
|
+
// malformed value (short, non-hex, empty string, non-string) is rejected.
|
|
169
|
+
if (args.commit_sha !== undefined && args.commit_sha !== null) {
|
|
170
|
+
if (typeof args.commit_sha !== 'string' || args.commit_sha.trim().length === 0) {
|
|
171
|
+
throw inputError('completion-writer: commit_sha must be a non-empty 40-char hex SHA when provided');
|
|
172
|
+
}
|
|
173
|
+
const trimmedSha = args.commit_sha.trim();
|
|
174
|
+
if (!SHA_RE.test(trimmedSha)) {
|
|
175
|
+
throw inputError(
|
|
176
|
+
`completion-writer: commit_sha must be a full 40-char hex SHA (Decision 9). ` +
|
|
177
|
+
`Got "${trimmedSha}" (length ${trimmedSha.length}). Short prefixes are rejected on write.`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
164
180
|
}
|
|
165
181
|
|
|
166
182
|
// tests_pass — strict boolean
|
|
@@ -193,6 +209,18 @@ function validate(args) {
|
|
|
193
209
|
throw inputError('completion-writer: set_status must be a boolean');
|
|
194
210
|
}
|
|
195
211
|
|
|
212
|
+
// built_via — OPTIONAL marker recording the pipeline template a feature was
|
|
213
|
+
// built with (e.g. 'build-quick'). Persisted on feature.json so downstream
|
|
214
|
+
// consumers (e.g. the validator's report-completion check) can tell which
|
|
215
|
+
// lifecycle produced the feature. Must be a lowercase template slug.
|
|
216
|
+
if (args.built_via !== undefined && args.built_via !== null) {
|
|
217
|
+
if (typeof args.built_via !== 'string' || !/^[a-z][a-z0-9-]{0,63}$/.test(args.built_via)) {
|
|
218
|
+
throw inputError(
|
|
219
|
+
`completion-writer: built_via must be a lowercase template slug (e.g. "build-quick"), got "${args.built_via}"`
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
196
224
|
// force — boolean if present
|
|
197
225
|
if (args.force !== undefined && typeof args.force !== 'boolean') {
|
|
198
226
|
throw inputError('completion-writer: force must be a boolean');
|
|
@@ -248,6 +276,8 @@ async function safeAppendEvent(cwd, event) {
|
|
|
248
276
|
* @param {string} [args.notes]
|
|
249
277
|
* @param {boolean} [args.set_status] Default true — flip status to COMPLETE
|
|
250
278
|
* @param {boolean} [args.force] Replace existing same-(code,sha) record
|
|
279
|
+
* @param {string} [args.built_via] Optional template slug (e.g. 'build-quick')
|
|
280
|
+
* stamped onto feature.json (COMP-BUILD-QUICK-1)
|
|
251
281
|
* @param {string} [args.idempotency_key]
|
|
252
282
|
*
|
|
253
283
|
* @returns {{ feature_code, completion_id, commit_sha, commit_sha_short,
|
|
@@ -257,13 +287,21 @@ export async function recordCompletion(cwd, args) {
|
|
|
257
287
|
// 1. Validate
|
|
258
288
|
validate(args);
|
|
259
289
|
|
|
260
|
-
// 2. Normalize SHA
|
|
261
|
-
const commit_sha = args.commit_sha.
|
|
290
|
+
// 2. Normalize SHA (omitted/null ⇒ NULL_SHA sentinel — commit-less completion)
|
|
291
|
+
const commit_sha = (args.commit_sha === undefined || args.commit_sha === null)
|
|
292
|
+
? NULL_SHA
|
|
293
|
+
: args.commit_sha.trim().toLowerCase();
|
|
262
294
|
const commit_sha_short = commit_sha.slice(0, SHORT_LEN);
|
|
263
295
|
const feature_code = args.feature_code;
|
|
264
296
|
|
|
265
|
-
// 3. Compute completion_id
|
|
266
|
-
|
|
297
|
+
// 3. Compute completion_id. For a real commit the SHA makes it stable and
|
|
298
|
+
// dedup-able. For a commit-less completion (NULL_SHA) the SHA is constant,
|
|
299
|
+
// so id off the sha would alias every no-commit completion of a feature
|
|
300
|
+
// onto one record (a re-completion would idempotent-no-op). Derive a
|
|
301
|
+
// distinct id from a timestamp instead. (COMP-PATHS-EXTERNAL D6b)
|
|
302
|
+
const completion_id = (commit_sha === NULL_SHA)
|
|
303
|
+
? `${feature_code}:nocommit:${Date.now().toString(36)}-${(_nocommitSeq++).toString(36)}`
|
|
304
|
+
: `${feature_code}:${commit_sha}`;
|
|
267
305
|
|
|
268
306
|
// 4. Wrap in maybeIdempotent for caller-key path
|
|
269
307
|
return maybeIdempotent({ ...args, cwd }, async () => {
|
|
@@ -320,7 +358,13 @@ export async function recordCompletion(cwd, args) {
|
|
|
320
358
|
// 5h. Persist completion record BEFORE status flip (so flip failure doesn't lose the record)
|
|
321
359
|
// Use persistFeatureRaw (policy-free, no status-delta guard) — completion-writer
|
|
322
360
|
// owns its own ordering/locking and only mutates the completions array, not status.
|
|
323
|
-
await provider.persistFeatureRaw(feature_code, {
|
|
361
|
+
await provider.persistFeatureRaw(feature_code, {
|
|
362
|
+
...feature,
|
|
363
|
+
completions,
|
|
364
|
+
// COMP-BUILD-QUICK-1: stamp the build template (e.g. 'build-quick') so the
|
|
365
|
+
// validator can exempt quick-built features from MISSING_COMPLETION_REPORT.
|
|
366
|
+
...(args.built_via ? { built_via: args.built_via } : {}),
|
|
367
|
+
});
|
|
324
368
|
|
|
325
369
|
// 5i. Status flip (default on)
|
|
326
370
|
const set_status = args.set_status !== false;
|
package/lib/feature-json.js
CHANGED
|
@@ -10,6 +10,17 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync, unlinkS
|
|
|
10
10
|
import { join, basename } from 'path';
|
|
11
11
|
import { readdirSync } from 'fs';
|
|
12
12
|
import { assertValidLinkShape, assertLinkTargetsExist } from './feature-write-guard.js';
|
|
13
|
+
import { resolvePathValue } from './paths-core.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Resolve the features-dir base. `featuresDir` may be relative-to-cwd (legacy,
|
|
17
|
+
* incl. the default), ../-escaping, or absolute (COMP-PATHS-EXTERNAL D7).
|
|
18
|
+
* Identical to join(cwd, featuresDir) for plain relative dirs.
|
|
19
|
+
* @returns {string} absolute features dir
|
|
20
|
+
*/
|
|
21
|
+
function featuresBase(cwd, featuresDir) {
|
|
22
|
+
return resolvePathValue(cwd, featuresDir, 'features');
|
|
23
|
+
}
|
|
13
24
|
|
|
14
25
|
/**
|
|
15
26
|
* @typedef {object} FeatureJson
|
|
@@ -34,7 +45,7 @@ import { assertValidLinkShape, assertLinkTargetsExist } from './feature-write-gu
|
|
|
34
45
|
* @returns {FeatureJson|null}
|
|
35
46
|
*/
|
|
36
47
|
export function readFeature(cwd, code, featuresDir = 'docs/features') {
|
|
37
|
-
const path = join(cwd, featuresDir, code, 'feature.json');
|
|
48
|
+
const path = join(featuresBase(cwd, featuresDir), code, 'feature.json');
|
|
38
49
|
if (!existsSync(path)) return null;
|
|
39
50
|
try {
|
|
40
51
|
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
@@ -66,7 +77,7 @@ export function writeFeature(cwd, feature, featuresDir = 'docs/features', opts =
|
|
|
66
77
|
const priorLinks = hasTargets ? (readFeature(cwd, feature.code, featuresDir)?.links) : undefined;
|
|
67
78
|
assertLinkTargetsExist(cwd, feature, { allowForwardRefs: opts.allowForwardRefs, priorLinks });
|
|
68
79
|
}
|
|
69
|
-
const dir = join(cwd, featuresDir, feature.code);
|
|
80
|
+
const dir = join(featuresBase(cwd, featuresDir), feature.code);
|
|
70
81
|
mkdirSync(dir, { recursive: true });
|
|
71
82
|
const path = join(dir, 'feature.json');
|
|
72
83
|
feature.updated = new Date().toISOString().slice(0, 10);
|
|
@@ -115,7 +126,7 @@ export function positionSortKey(position) {
|
|
|
115
126
|
* @returns {FeatureJson[]}
|
|
116
127
|
*/
|
|
117
128
|
export function listFeatures(cwd, featuresDir = 'docs/features') {
|
|
118
|
-
const dir =
|
|
129
|
+
const dir = featuresBase(cwd, featuresDir);
|
|
119
130
|
if (!existsSync(dir)) return [];
|
|
120
131
|
|
|
121
132
|
const features = [];
|
package/lib/feature-validator.js
CHANGED
|
@@ -43,6 +43,9 @@ import { featureStatusToVisionStatus } from './status-projection.js';
|
|
|
43
43
|
import { GitHubApi } from './tracker/github-api.js';
|
|
44
44
|
import { ArtifactManager } from '../server/artifact-manager.js';
|
|
45
45
|
import { SchemaValidator } from '../server/schema-validator.js';
|
|
46
|
+
import {
|
|
47
|
+
resolveRoadmapPathFromConfig, resolveFeaturesPathFromConfig, resolveJournalPathFromConfig,
|
|
48
|
+
} from './project-paths.js';
|
|
46
49
|
|
|
47
50
|
const __filename = fileURLToPath(import.meta.url);
|
|
48
51
|
const __dirname = path.dirname(__filename);
|
|
@@ -51,7 +54,6 @@ const FEATURE_JSON_SCHEMA = path.resolve(__dirname, '../contracts/feature-json
|
|
|
51
54
|
const VISION_STATE_SCHEMA = path.resolve(__dirname, '../contracts/vision-state.schema.json');
|
|
52
55
|
const ROADMAP_ROW_SCHEMA = path.resolve(__dirname, '../contracts/roadmap-row.schema.json');
|
|
53
56
|
|
|
54
|
-
const DEFAULT_PATHS = { docs: 'docs', features: 'docs/features', journal: 'docs/journal' };
|
|
55
57
|
|
|
56
58
|
const TERMINAL_STATUSES = new Set(['KILLED', 'SUPERSEDED']);
|
|
57
59
|
const VALID_STATUSES = new Set(['PLANNED', 'IN_PROGRESS', 'PARTIAL', 'COMPLETE', 'SUPERSEDED', 'PARKED', 'BLOCKED', 'KILLED']);
|
|
@@ -109,13 +111,14 @@ function readProjectConfig(cwd) {
|
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
function resolveProjectPaths(cwd) {
|
|
112
|
-
const cfg = readProjectConfig(cwd);
|
|
113
|
-
|
|
114
|
+
const cfg = readProjectConfig(cwd) || {};
|
|
115
|
+
// Artifact paths may be relocated outside cwd (COMP-PATHS-EXTERNAL); resolve
|
|
116
|
+
// through the shared resolver. vision-state + changelog stay at the root.
|
|
114
117
|
return {
|
|
115
|
-
roadmap:
|
|
118
|
+
roadmap: resolveRoadmapPathFromConfig(cwd, cfg),
|
|
116
119
|
visionState: path.join(cwd, '.compose', 'data', 'vision-state.json'),
|
|
117
|
-
features:
|
|
118
|
-
journal:
|
|
120
|
+
features: resolveFeaturesPathFromConfig(cwd, cfg),
|
|
121
|
+
journal: resolveJournalPathFromConfig(cwd, cfg),
|
|
119
122
|
changelog: path.join(cwd, 'CHANGELOG.md'),
|
|
120
123
|
};
|
|
121
124
|
}
|
|
@@ -570,7 +573,11 @@ function runArtifactLinkChecks(fctx, ctx, findings) {
|
|
|
570
573
|
// PLANNED / SUPERSEDED / KILLED / PARKED — no finding (expected baseline)
|
|
571
574
|
}
|
|
572
575
|
// MISSING_COMPLETION_REPORT (warning, COMPLETE)
|
|
573
|
-
|
|
576
|
+
// COMP-BUILD-QUICK-1: the quick lifecycle (compose build --quick) omits the
|
|
577
|
+
// report phase by design and stamps built_via:'build-quick' on feature.json at
|
|
578
|
+
// ship. Exempt those features — a missing report.md is expected, not debt.
|
|
579
|
+
if (status === 'COMPLETE' && !folder.files.has('report.md') &&
|
|
580
|
+
featureJson?.built_via !== 'build-quick') {
|
|
574
581
|
findings.push(finding('warning', 'MISSING_COMPLETION_REPORT', code,
|
|
575
582
|
`Feature is COMPLETE but report.md is missing (current writers do not enforce this; warning only)`));
|
|
576
583
|
}
|
|
@@ -1125,6 +1132,18 @@ export async function validateProject(cwd, options = {}) {
|
|
|
1125
1132
|
const ctx = loadValidationContext(cwd, options);
|
|
1126
1133
|
const findings = [];
|
|
1127
1134
|
|
|
1135
|
+
// COMP-PATHS-EXTERNAL: a relocated artifact path may resolve into a parent
|
|
1136
|
+
// that does not exist (e.g. the external docs repo isn't checked out). A
|
|
1137
|
+
// not-yet-created leaf dir is fine — writers mkdir it — but an unreachable
|
|
1138
|
+
// PARENT is a configuration error worth surfacing with the resolved path.
|
|
1139
|
+
for (const [key, p] of [['features', ctx.paths.features], ['roadmap', ctx.paths.roadmap]]) {
|
|
1140
|
+
const isExternal = path.relative(cwd, p).startsWith('..');
|
|
1141
|
+
if (isExternal && !fs.existsSync(path.dirname(p))) {
|
|
1142
|
+
findings.push(finding('error', 'CONFIGURED_PATH_UNREACHABLE', null,
|
|
1143
|
+
`configured ${key} path parent does not exist: ${path.dirname(p)} (resolved ${key} = ${p})`));
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1128
1147
|
const allCodes = new Set([
|
|
1129
1148
|
...ctx.roadmapByCode.keys(),
|
|
1130
1149
|
...ctx.visionByCode.keys(),
|
|
@@ -1146,9 +1165,7 @@ export async function validateProject(cwd, options = {}) {
|
|
|
1146
1165
|
|
|
1147
1166
|
// --- COMP-ROADMAP-RT: roundtrip + hierarchy ---
|
|
1148
1167
|
if (ctx.featureJsonMode) {
|
|
1149
|
-
const
|
|
1150
|
-
const featuresDir = (cfg && cfg.paths && cfg.paths.features) || DEFAULT_PATHS.features;
|
|
1151
|
-
const features = listFeatures(cwd, featuresDir);
|
|
1168
|
+
const features = listFeatures(cwd, ctx.paths.features);
|
|
1152
1169
|
const roadmapText = fs.existsSync(ctx.paths.roadmap)
|
|
1153
1170
|
? fs.readFileSync(ctx.paths.roadmap, 'utf8')
|
|
1154
1171
|
: '';
|
|
@@ -20,6 +20,7 @@ import { fileURLToPath } from 'url';
|
|
|
20
20
|
import { SchemaValidator } from '../server/schema-validator.js';
|
|
21
21
|
import { FEATURE_CODE_RE_STRICT } from './feature-code.js';
|
|
22
22
|
import { splitRoadmapCells } from './roadmap-parser.js';
|
|
23
|
+
import { resolveFeaturesPath, resolveRoadmapPath } from './project-paths.js';
|
|
23
24
|
|
|
24
25
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
25
26
|
const SCHEMA_PATH = resolve(__dirname, '../contracts/feature-json.schema.json');
|
|
@@ -77,14 +78,11 @@ export function assertValidLinkShape(feature) {
|
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
function resolvePaths(cwd) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const cfg = JSON.parse(readFileSync(join(cwd, '.compose', 'compose.json'), 'utf-8'));
|
|
83
|
-
if (cfg?.paths?.features) featuresRel = cfg.paths.features;
|
|
84
|
-
} catch { /* no config — use defaults (mirrors resolveProjectPaths) */ }
|
|
81
|
+
// Artifact paths may be relocated outside cwd (COMP-PATHS-EXTERNAL); resolve
|
|
82
|
+
// through the shared resolver. vision-state stays at the root (D4).
|
|
85
83
|
return {
|
|
86
|
-
features:
|
|
87
|
-
roadmap:
|
|
84
|
+
features: resolveFeaturesPath(cwd),
|
|
85
|
+
roadmap: resolveRoadmapPath(cwd),
|
|
88
86
|
visionState: join(cwd, '.compose', 'data', 'vision-state.json'),
|
|
89
87
|
};
|
|
90
88
|
}
|
package/lib/feature-writer.js
CHANGED
|
@@ -21,7 +21,7 @@ import { resolve, normalize, sep, basename, dirname, join } from 'path';
|
|
|
21
21
|
|
|
22
22
|
import { readEvents } from './feature-events.js';
|
|
23
23
|
import { checkOrInsert } from './idempotency.js';
|
|
24
|
-
import { loadFeaturesDir } from './project-paths.js';
|
|
24
|
+
import { loadFeaturesDir, resolveRoadmapPath } from './project-paths.js';
|
|
25
25
|
import { checkRoundtrip } from './roadmap-roundtrip.js';
|
|
26
26
|
import { isNarrativeOwned, narrativeOwnedMessage } from './roadmap-config.js';
|
|
27
27
|
import { knownFeatureCodes, FeatureWriteValidationError } from './feature-write-guard.js';
|
|
@@ -238,7 +238,7 @@ function readRoadmapBase(roadmapPath) {
|
|
|
238
238
|
async function roundtripGuard(cwd, provider, mutate, { force, label }) {
|
|
239
239
|
const current = await provider.listFeatures();
|
|
240
240
|
const projected = mutate(current.map(f => ({ ...f })));
|
|
241
|
-
const roadmapPath =
|
|
241
|
+
const roadmapPath = resolveRoadmapPath(cwd);
|
|
242
242
|
// Read the existing ROADMAP as the regen base, but only when it's a readable
|
|
243
243
|
// regular file. If the path is missing, a directory, or otherwise unreadable,
|
|
244
244
|
// treat the base as empty: the guard must never mask the downstream
|
|
@@ -728,7 +728,7 @@ export async function setRoadmapRowStatus(cwd, args) {
|
|
|
728
728
|
throw new Error(`feature-writer: invalid status "${args.status}"`);
|
|
729
729
|
}
|
|
730
730
|
return maybeIdempotent({ ...args, cwd }, async () => {
|
|
731
|
-
const roadmapPath =
|
|
731
|
+
const roadmapPath = resolveRoadmapPath(cwd);
|
|
732
732
|
let text;
|
|
733
733
|
try { text = readFileSync(roadmapPath, 'utf-8'); }
|
|
734
734
|
catch { return { code: args.code, changed: false }; }
|
package/lib/followup-writer.js
CHANGED
|
@@ -28,7 +28,7 @@ import { writeRoadmap } from './roadmap-gen.js';
|
|
|
28
28
|
import { appendEvent } from './feature-events.js';
|
|
29
29
|
import { checkOrInsert } from './idempotency.js';
|
|
30
30
|
import { FEATURE_CODE_RE_STRICT } from './feature-code.js';
|
|
31
|
-
import { loadFeaturesDir } from './project-paths.js';
|
|
31
|
+
import { loadFeaturesDir, resolveRoadmapPath } from './project-paths.js';
|
|
32
32
|
|
|
33
33
|
const TERMINAL_STATUSES = new Set(['KILLED', 'SUPERSEDED']);
|
|
34
34
|
const VALID_STATUSES = new Set([
|
|
@@ -509,7 +509,7 @@ async function orchestrate({ cwd, parent_code, parent, args, phase, status, requ
|
|
|
509
509
|
parent_code,
|
|
510
510
|
phase: created?.phase ?? phase,
|
|
511
511
|
position: created?.position,
|
|
512
|
-
roadmap_path:
|
|
512
|
+
roadmap_path: resolveRoadmapPath(cwd),
|
|
513
513
|
scaffolded: scaffolded ?? { created: [], skipped: ['design.md'] },
|
|
514
514
|
link: { kind: 'surfaced_by', from_code: allocated_code, to_code: parent_code },
|
|
515
515
|
};
|
package/lib/get-roadmap.js
CHANGED
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
* - Row parsing reuses parseRoadmap — no second hand-rolled regex.
|
|
13
13
|
*/
|
|
14
14
|
import { existsSync, readFileSync } from 'node:fs';
|
|
15
|
-
import { join } from 'node:path';
|
|
16
15
|
|
|
17
16
|
import { generateRoadmap } from './roadmap-gen.js';
|
|
18
17
|
import { isNarrativeOwned } from './roadmap-config.js';
|
|
19
18
|
import { parseRoadmap, parseStatusToken } from './roadmap-parser.js';
|
|
19
|
+
import { resolveRoadmapPath } from './project-paths.js';
|
|
20
20
|
|
|
21
21
|
// Whole `**Last updated:** <date>` line — date-only and per-day, so it is
|
|
22
22
|
// stripped (not literal-matched) before drift comparison.
|
|
@@ -56,7 +56,7 @@ function stripVolatile(text) {
|
|
|
56
56
|
export function getRoadmap(root, opts = {}) {
|
|
57
57
|
const { status, phase, format = 'summary', check_drift = true, limit } = opts ?? {};
|
|
58
58
|
|
|
59
|
-
const roadmapPath =
|
|
59
|
+
const roadmapPath = resolveRoadmapPath(root);
|
|
60
60
|
const onDisk = existsSync(roadmapPath) ? readFileSync(roadmapPath, 'utf-8') : '';
|
|
61
61
|
|
|
62
62
|
// Branch on narrative ownership ourselves: read the file directly for narrative
|
package/lib/gsd.js
CHANGED
|
@@ -33,6 +33,7 @@ import { writeGsdState, readGsdState, gsdStatePath, pidAlive, clearGsdHaltArtifa
|
|
|
33
33
|
import { generateGsdMilestoneReport } from './gsd-milestone-report.js';
|
|
34
34
|
import { readHeadlessConfig } from './gsd-headless-config.js';
|
|
35
35
|
import { appendGsdEvent, clearGsdEvents } from './gsd-events.js';
|
|
36
|
+
import { resolveFeaturesPath } from './project-paths.js';
|
|
36
37
|
|
|
37
38
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
38
39
|
const PACKAGE_ROOT = resolve(__dirname, '..');
|
|
@@ -62,7 +63,7 @@ export async function runGsd(featureCode, opts = {}) {
|
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
// 1. Validate preconditions: blueprint exists + Boundary Map ok
|
|
65
|
-
const blueprintPath = join(cwd,
|
|
66
|
+
const blueprintPath = join(resolveFeaturesPath(cwd), featureCode, 'blueprint.md');
|
|
66
67
|
if (!existsSync(blueprintPath)) {
|
|
67
68
|
throw new Error(
|
|
68
69
|
`runGsd: blueprint missing at ${blueprintPath}. ` +
|