@smartmemory/compose 0.2.48-beta → 0.2.50-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 +2 -0
- package/.compose-deps.json +13 -0
- package/bin/compose.js +35 -20
- package/dist/assets/{App-D7E7S49Q.js → App-Bxyif1yv.js} +160 -160
- package/dist/assets/{arc-LOW60tiI.js → arc-DeHak63Z.js} +1 -1
- package/dist/assets/{architectureDiagram-3BPJPVTR-BDY00hRy.js → architectureDiagram-3BPJPVTR-C5Nkyl9y.js} +1 -1
- package/dist/assets/{blockDiagram-GPEHLZMM-DiC_6ktq.js → blockDiagram-GPEHLZMM-BilnUBG2.js} +1 -1
- package/dist/assets/{c4Diagram-AAUBKEIU-CXZC8woU.js → c4Diagram-AAUBKEIU-BsFJmPuS.js} +1 -1
- package/dist/assets/channel-ZgbQ1k0u.js +1 -0
- package/dist/assets/{chunk-2J33WTMH-b8Rdk4S-.js → chunk-2J33WTMH-AEu6HIoY.js} +1 -1
- package/dist/assets/{chunk-4BX2VUAB-BSxn03f-.js → chunk-4BX2VUAB-XO4S2_89.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-uUk1WzZ1.js → chunk-55IACEB6-CtFSLInj.js} +1 -1
- package/dist/assets/{chunk-727SXJPM-zvXhT6iZ.js → chunk-727SXJPM-CZeBRpM9.js} +1 -1
- package/dist/assets/{chunk-AQP2D5EJ-B6GCyrIp.js → chunk-AQP2D5EJ-_vmxcBc4.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-DlhrqkBp.js → chunk-FMBD7UC4-BMGd_Y9a.js} +1 -1
- package/dist/assets/{chunk-ND2GUHAM-lo_8kNzw.js → chunk-ND2GUHAM-BlrOECEr.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-on-yD-C0.js → chunk-QZHKN3VN-DHhUJ8RK.js} +1 -1
- package/dist/assets/classDiagram-4FO5ZUOK-KXxOorUx.js +1 -0
- package/dist/assets/classDiagram-v2-Q7XG4LA2-KXxOorUx.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-DxV7Y2bU.js → cose-bilkent-S5V4N54A-C3M0mZqk.js} +1 -1
- package/dist/assets/{dagre-BM42HDAG-BHf9sURy.js → dagre-BM42HDAG-DJlm4fEk.js} +1 -1
- package/dist/assets/{diagram-2AECGRRQ-R9Puxzlh.js → diagram-2AECGRRQ-BmcONQG5.js} +1 -1
- package/dist/assets/{diagram-5GNKFQAL-BbjlClKx.js → diagram-5GNKFQAL-5JbWGIwd.js} +1 -1
- package/dist/assets/{diagram-KO2AKTUF-r89Mx4m1.js → diagram-KO2AKTUF-DAnIvyI0.js} +1 -1
- package/dist/assets/{diagram-LMA3HP47-CstVDJZ3.js → diagram-LMA3HP47-Bmh7tvmI.js} +1 -1
- package/dist/assets/{diagram-OG6HWLK6-xmX57FKh.js → diagram-OG6HWLK6-hnHVMNnk.js} +1 -1
- package/dist/assets/{erDiagram-TEJ5UH35-DhLPxvrl.js → erDiagram-TEJ5UH35-AMDitV0b.js} +1 -1
- package/dist/assets/{flowDiagram-I6XJVG4X-BExoUaNm.js → flowDiagram-I6XJVG4X-C0t2ZGQv.js} +1 -1
- package/dist/assets/{ganttDiagram-6RSMTGT7-BEXa5a22.js → ganttDiagram-6RSMTGT7-BPlM-BEN.js} +1 -1
- package/dist/assets/{gitGraphDiagram-PVQCEYII-Cbes9api.js → gitGraphDiagram-PVQCEYII-Lv_YTxnb.js} +1 -1
- package/dist/assets/index-COq21Zym.js +119 -0
- package/dist/assets/{infoDiagram-5YYISTIA-UBm4ssfm.js → infoDiagram-5YYISTIA-Dul1vdUm.js} +1 -1
- package/dist/assets/{ishikawaDiagram-YF4QCWOH-CksBYHCF.js → ishikawaDiagram-YF4QCWOH-CCBwgjFs.js} +1 -1
- package/dist/assets/{journeyDiagram-JHISSGLW-CKHkIHky.js → journeyDiagram-JHISSGLW-zUJN38EU.js} +1 -1
- package/dist/assets/{kanban-definition-UN3LZRKU-CEahMFzE.js → kanban-definition-UN3LZRKU-BNxWi-8J.js} +1 -1
- package/dist/assets/{linear-CqLVtYRk.js → linear-CRH9b4g7.js} +1 -1
- package/dist/assets/{mindmap-definition-RKZ34NQL-C61zWt4M.js → mindmap-definition-RKZ34NQL-DolYwq7Y.js} +1 -1
- package/dist/assets/{pieDiagram-4H26LBE5-BIpZHyJW.js → pieDiagram-4H26LBE5-oxNfr2TX.js} +1 -1
- package/dist/assets/{quadrantDiagram-W4KKPZXB-Db7nRbZm.js → quadrantDiagram-W4KKPZXB-BmZCvD-z.js} +1 -1
- package/dist/assets/{requirementDiagram-4Y6WPE33-BQqFCAr0.js → requirementDiagram-4Y6WPE33-DsXZ05jo.js} +1 -1
- package/dist/assets/{sankeyDiagram-5OEKKPKP-D-oAAMci.js → sankeyDiagram-5OEKKPKP-DsnYaawP.js} +1 -1
- package/dist/assets/{sequenceDiagram-3UESZ5HK-Dc4Cp0Om.js → sequenceDiagram-3UESZ5HK-Dt5mu3g8.js} +1 -1
- package/dist/assets/{stateDiagram-AJRCARHV-B5DkQ8Pr.js → stateDiagram-AJRCARHV-BFR5ZINQ.js} +1 -1
- package/dist/assets/stateDiagram-v2-BHNVJYJU-BDrQD8fR.js +1 -0
- package/dist/assets/{timeline-definition-PNZ67QCA-DU9Fl-FW.js → timeline-definition-PNZ67QCA-Bvnhw58e.js} +1 -1
- package/dist/assets/{vennDiagram-CIIHVFJN-Bthz7siw.js → vennDiagram-CIIHVFJN-5WnyjZqf.js} +1 -1
- package/dist/assets/{wardley-L42UT6IY-nqdlFSIA.js → wardley-L42UT6IY-CTxW4xow.js} +1 -1
- package/dist/assets/{wardleyDiagram-YWT4CUSO-BJ1BC5zc.js → wardleyDiagram-YWT4CUSO-K8Y1EOvb.js} +1 -1
- package/dist/assets/{xychartDiagram-2RQKCTM6-Bib5JrbR.js → xychartDiagram-2RQKCTM6-BCShes6B.js} +1 -1
- package/dist/index.html +1 -1
- package/lib/build-all.js +2 -1
- package/lib/build.js +96 -6
- package/lib/checkpoint/checkpoint-writer.js +3 -2
- package/lib/completion-writer.js +38 -14
- package/lib/deps.js +98 -1
- package/lib/feature-json.js +14 -3
- package/lib/feature-validator.js +22 -9
- 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/ideabox.js +3 -2
- package/lib/journal-writer.js +3 -3
- 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/rtk.js +66 -0
- package/lib/state-migrations.js +211 -42
- package/lib/tracker/local-provider.js +2 -2
- package/lib/triage.js +7 -5
- package/lib/vision-writer.js +6 -13
- package/lib/xref-push.js +4 -4
- package/lib/xref-sync.js +4 -4
- package/package.json +1 -1
- package/server/compose-mcp-tools.js +9 -1
- package/server/design-routes.js +9 -4
- package/server/drift-axes.js +5 -3
- package/server/feature-scan.js +8 -2
- package/server/file-watcher.js +13 -7
- package/server/ideabox-routes.js +12 -11
- package/server/project-root.js +21 -4
- package/server/session-routes.js +2 -2
- package/server/stratum-sync.js +85 -9
- package/server/vision-routes.js +17 -2
- package/server/vision-server.js +4 -3
- package/server/vision-store.js +6 -11
- package/server/vision-utils.js +23 -4
- package/dist/assets/channel-Dh96pu1k.js +0 -1
- package/dist/assets/classDiagram-4FO5ZUOK-BCqpDcaS.js +0 -1
- package/dist/assets/classDiagram-v2-Q7XG4LA2-BCqpDcaS.js +0 -1
- package/dist/assets/index-DNuHtZwR.js +0 -123
- package/dist/assets/stateDiagram-v2-BHNVJYJU-BM1Uf6a_.js +0 -1
package/lib/build.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync, renameSync, mkdtempSync, rmSync, symlinkSync } from 'node:fs';
|
|
12
|
-
import { join, resolve, dirname } from 'node:path';
|
|
12
|
+
import { join, resolve, dirname, relative } from 'node:path';
|
|
13
13
|
import { fileURLToPath } from 'node:url';
|
|
14
14
|
import { homedir, tmpdir } from 'node:os';
|
|
15
15
|
import { execSync, execFileSync } from 'node:child_process';
|
|
@@ -31,10 +31,11 @@ import { resolveAgentConfig } from './agent-string.js';
|
|
|
31
31
|
import { installFactoryShim } from './connector-factory-shim.js';
|
|
32
32
|
import { emitSections as emitPlanSections, appendTrailers as appendSectionTrailers, analyzeRollup, writeRollup } from './sections.js';
|
|
33
33
|
import { SECTIONS_DIR } from './constants.js';
|
|
34
|
+
import { rtkPrefix } from './rtk.js';
|
|
34
35
|
|
|
35
36
|
import YAML from 'yaml';
|
|
36
37
|
// feature-json direct imports removed — mutations now go through TrackerProvider (T9)
|
|
37
|
-
import { loadFeaturesDir } from './project-paths.js';
|
|
38
|
+
import { loadFeaturesDir, resolveContextPath, resolveRoadmapPath, resolveFeaturesPath } from './project-paths.js';
|
|
38
39
|
|
|
39
40
|
// Lazy provider accessor — avoids circular import risk (factory → local-provider
|
|
40
41
|
// does NOT import build.js, so a static import is safe, but lazy is used for
|
|
@@ -214,7 +215,10 @@ export async function maybeRunEscalation(stratum, context, progress, streamWrite
|
|
|
214
215
|
}
|
|
215
216
|
let currentDiff = '';
|
|
216
217
|
try {
|
|
217
|
-
|
|
218
|
+
// COMP-RTK-INTEROP: this diff is fed to Codex (LLM) for the tier-1 review, so
|
|
219
|
+
// route it through RTK when available to compress before the 8000-char cap.
|
|
220
|
+
// rtkPrefix is a no-op (byte-identical) when rtk is absent.
|
|
221
|
+
currentDiff = execSync(rtkPrefix('git diff --no-color HEAD'), {
|
|
218
222
|
cwd: context.cwd, encoding: 'utf-8', timeout: 10_000,
|
|
219
223
|
}).slice(0, 8000);
|
|
220
224
|
} catch { /* not a git repo or no diff */ }
|
|
@@ -601,10 +605,19 @@ export async function runBuild(featureCode, opts = {}) {
|
|
|
601
605
|
// `docs/features/<featureCode>/`. Callers must use this (not inline
|
|
602
606
|
// string concatenation) so the bug-mode path stays in sync.
|
|
603
607
|
// COMP-MCP-MIGRATION-2: feature-mode honors paths.features override.
|
|
608
|
+
// COMP-PATHS-EXTERNAL D7: two representations on purpose.
|
|
609
|
+
// - `featuresDir` stays RELATIVE (loadFeaturesDir): it flows into
|
|
610
|
+
// context.featuresDir → the MCP-enforcement guard, which matches against
|
|
611
|
+
// repo-relative `git status` output. External artifacts live in another
|
|
612
|
+
// repo and are out of that guard's scope by construction; making this
|
|
613
|
+
// absolute would break the relative match. triage consumers are
|
|
614
|
+
// absolute-safe (resolvePathValue) so the relative value is fine there too.
|
|
615
|
+
// - `resolveItemDir` resolves the actual FILESYSTEM dir via
|
|
616
|
+
// resolveFeaturesPath (absolute — handles absolute/../-escaping config).
|
|
604
617
|
const featuresDir = loadFeaturesDir(cwd);
|
|
605
618
|
const resolveItemDir = (code) => isBugMode
|
|
606
619
|
? join(cwd, 'docs', 'bugs', code)
|
|
607
|
-
: join(cwd,
|
|
620
|
+
: join(resolveFeaturesPath(cwd), code);
|
|
608
621
|
|
|
609
622
|
// COMP-MCP-MIGRATION-1: per-build correlation ID stamped onto every audit
|
|
610
623
|
// row written during this run, so `executeShipStep`'s pre-stage scan can
|
|
@@ -659,7 +672,7 @@ export async function runBuild(featureCode, opts = {}) {
|
|
|
659
672
|
}
|
|
660
673
|
let composeConfig = {};
|
|
661
674
|
try { composeConfig = JSON.parse(readFileSync(configPath, 'utf-8')); } catch { /* use defaults */ }
|
|
662
|
-
const contextDirPath =
|
|
675
|
+
const contextDirPath = resolveContextPath(cwd);
|
|
663
676
|
|
|
664
677
|
// COMP-PAR-MERGE-QUEUE-CONSUMER-RETRY (D5): per-task pre-merge gate is opt-in.
|
|
665
678
|
// Resolve ONCE and thread into startFresh's planInputs only when the capability
|
|
@@ -2215,6 +2228,43 @@ export function maybeEmitSectionsAfterPlanGate(stepId, featureDir, opts = {}) {
|
|
|
2215
2228
|
// Ship step — runs git commit in-process (not via agent)
|
|
2216
2229
|
// ---------------------------------------------------------------------------
|
|
2217
2230
|
|
|
2231
|
+
/**
|
|
2232
|
+
* The git repository toplevel that contains `dir`, or null if `dir` is not
|
|
2233
|
+
* inside any git work tree. Used so ship decides commit ownership per-file by
|
|
2234
|
+
* containing repo, not by assuming the workspace root == the repo
|
|
2235
|
+
* (COMP-PATHS-EXTERNAL D6a).
|
|
2236
|
+
*/
|
|
2237
|
+
function gitToplevel(dir) {
|
|
2238
|
+
try {
|
|
2239
|
+
return execSync('git rev-parse --show-toplevel', {
|
|
2240
|
+
cwd: dir, encoding: 'utf-8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'],
|
|
2241
|
+
}).trim() || null;
|
|
2242
|
+
} catch { return null; }
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
/**
|
|
2246
|
+
* For each owned artifact (ROADMAP, the feature folder) that resolves into a
|
|
2247
|
+
* git repo OTHER than the build's repo, log a one-line "commit it there"
|
|
2248
|
+
* notice. v1 does not auto-commit other repos (that is COMP-PATHS-EXTERNAL-1).
|
|
2249
|
+
* Artifacts in the build's own repo, or in no repo at all, produce no notice.
|
|
2250
|
+
*/
|
|
2251
|
+
function noticeExternalArtifacts(cwd, featureCode, buildToplevel) {
|
|
2252
|
+
try {
|
|
2253
|
+
const candidates = [
|
|
2254
|
+
['ROADMAP.md', resolveRoadmapPath(cwd)],
|
|
2255
|
+
['feature folder', join(resolveFeaturesPath(cwd), featureCode)],
|
|
2256
|
+
];
|
|
2257
|
+
for (const [label, abs] of candidates) {
|
|
2258
|
+
if (!existsSync(abs)) continue;
|
|
2259
|
+
const top = gitToplevel(dirname(abs));
|
|
2260
|
+
if (top && top !== buildToplevel) {
|
|
2261
|
+
// eslint-disable-next-line no-console
|
|
2262
|
+
console.warn(`[build/ship] 📝 wrote ${label} in ${top} — commit it there (Compose does not auto-commit other repos in v1)`);
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
} catch { /* notice is best-effort */ }
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2218
2268
|
/**
|
|
2219
2269
|
* Execute the ship step: run tests, stage feature files, commit.
|
|
2220
2270
|
* Returns a PhaseResult-shaped object.
|
|
@@ -2236,11 +2286,34 @@ export async function executeShipStep(featureCode, agentCwd, cwd, context, descr
|
|
|
2236
2286
|
} catch { /* not a git repo */ }
|
|
2237
2287
|
|
|
2238
2288
|
if (!isGitRepo) {
|
|
2289
|
+
// COMP-PATHS-EXTERNAL D6b: there is no repo to commit into (e.g. a
|
|
2290
|
+
// forge-top-shaped workspace), but the lifecycle must still advance —
|
|
2291
|
+
// record a commit-less completion (null-SHA) so status flips to COMPLETE.
|
|
2292
|
+
// Best-effort: a completion failure must not fail ship.
|
|
2293
|
+
let completionWarning = null;
|
|
2294
|
+
if (featureCode) {
|
|
2295
|
+
try {
|
|
2296
|
+
const { recordCompletion } = await import('./completion-writer.js');
|
|
2297
|
+
await recordCompletion(cwd, {
|
|
2298
|
+
feature_code: featureCode,
|
|
2299
|
+
// commit_sha omitted — non-git workspace, stamped with the null-SHA
|
|
2300
|
+
tests_pass: true,
|
|
2301
|
+
files_changed: [],
|
|
2302
|
+
notes: description.split('\n')[0].slice(0, 72),
|
|
2303
|
+
});
|
|
2304
|
+
} catch (err) {
|
|
2305
|
+
completionWarning = `completion record failed (${err.code || 'UNKNOWN'}): ${err.message}`;
|
|
2306
|
+
// eslint-disable-next-line no-console
|
|
2307
|
+
console.warn(`[build/ship] ${featureCode}: ${completionWarning}`);
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2239
2310
|
return {
|
|
2240
2311
|
phase: 'ship',
|
|
2241
2312
|
artifact: 'no-git',
|
|
2242
2313
|
outcome: 'complete',
|
|
2243
|
-
summary: 'No git repository — commit skipped
|
|
2314
|
+
summary: 'No git repository — wrote artifacts, recorded completion (commit skipped)',
|
|
2315
|
+
commit: null,
|
|
2316
|
+
...(completionWarning ? { completionWarning } : {}),
|
|
2244
2317
|
};
|
|
2245
2318
|
}
|
|
2246
2319
|
|
|
@@ -2312,6 +2385,18 @@ export async function executeShipStep(featureCode, agentCwd, cwd, context, descr
|
|
|
2312
2385
|
const dataDir = join(cwd, '.compose', 'data');
|
|
2313
2386
|
const mode = readEnforcementMode(dataDir);
|
|
2314
2387
|
if (mode !== 'off') {
|
|
2388
|
+
// COMP-PATHS-EXTERNAL: the pre-stage guard matches repo-relative
|
|
2389
|
+
// `git status`, so relocated canon (an external paths.roadmap /
|
|
2390
|
+
// paths.features) is OUT of its scope — unauthorized edits there are
|
|
2391
|
+
// NOT enforced. Surface that visibly instead of failing silently.
|
|
2392
|
+
// Full external-canon enforcement needs cross-repo dirty detection
|
|
2393
|
+
// (tracked with COMP-PATHS-EXTERNAL-1).
|
|
2394
|
+
for (const [label, abs] of [['paths.roadmap', resolveRoadmapPath(cwd)], ['paths.features', resolveFeaturesPath(cwd)]]) {
|
|
2395
|
+
if (relative(cwd, abs).startsWith('..')) {
|
|
2396
|
+
// eslint-disable-next-line no-console
|
|
2397
|
+
console.warn(`[ship] MCP enforcement (${mode}) does NOT cover external ${label} (${abs}) — edits there are unguarded in v1.`);
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2315
2400
|
const events = readEvents(cwd, { since: context.buildStartedAt });
|
|
2316
2401
|
const { violations } = scanGuarded({
|
|
2317
2402
|
dirtyFiles: featureFiles,
|
|
@@ -2437,6 +2522,11 @@ export async function executeShipStep(featureCode, agentCwd, cwd, context, descr
|
|
|
2437
2522
|
}
|
|
2438
2523
|
}
|
|
2439
2524
|
|
|
2525
|
+
// COMP-PATHS-EXTERNAL D6a: if ROADMAP / the feature folder resolved into a
|
|
2526
|
+
// DIFFERENT git repo, they were written but not committed here — tell the
|
|
2527
|
+
// user to commit them there (v1 does not auto-commit other repos).
|
|
2528
|
+
noticeExternalArtifacts(cwd, featureCode, gitToplevel(agentCwd));
|
|
2529
|
+
|
|
2440
2530
|
return {
|
|
2441
2531
|
phase: 'ship',
|
|
2442
2532
|
artifact: sha ?? '',
|
|
@@ -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) {
|
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
|
|
@@ -257,13 +273,21 @@ export async function recordCompletion(cwd, args) {
|
|
|
257
273
|
// 1. Validate
|
|
258
274
|
validate(args);
|
|
259
275
|
|
|
260
|
-
// 2. Normalize SHA
|
|
261
|
-
const commit_sha = args.commit_sha.
|
|
276
|
+
// 2. Normalize SHA (omitted/null ⇒ NULL_SHA sentinel — commit-less completion)
|
|
277
|
+
const commit_sha = (args.commit_sha === undefined || args.commit_sha === null)
|
|
278
|
+
? NULL_SHA
|
|
279
|
+
: args.commit_sha.trim().toLowerCase();
|
|
262
280
|
const commit_sha_short = commit_sha.slice(0, SHORT_LEN);
|
|
263
281
|
const feature_code = args.feature_code;
|
|
264
282
|
|
|
265
|
-
// 3. Compute completion_id
|
|
266
|
-
|
|
283
|
+
// 3. Compute completion_id. For a real commit the SHA makes it stable and
|
|
284
|
+
// dedup-able. For a commit-less completion (NULL_SHA) the SHA is constant,
|
|
285
|
+
// so id off the sha would alias every no-commit completion of a feature
|
|
286
|
+
// onto one record (a re-completion would idempotent-no-op). Derive a
|
|
287
|
+
// distinct id from a timestamp instead. (COMP-PATHS-EXTERNAL D6b)
|
|
288
|
+
const completion_id = (commit_sha === NULL_SHA)
|
|
289
|
+
? `${feature_code}:nocommit:${Date.now().toString(36)}-${(_nocommitSeq++).toString(36)}`
|
|
290
|
+
: `${feature_code}:${commit_sha}`;
|
|
267
291
|
|
|
268
292
|
// 4. Wrap in maybeIdempotent for caller-key path
|
|
269
293
|
return maybeIdempotent({ ...args, cwd }, async () => {
|
package/lib/deps.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
14
14
|
import { join } from 'node:path'
|
|
15
15
|
import { homedir } from 'node:os'
|
|
16
|
+
import { spawnSync } from 'node:child_process'
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Load .compose-deps.json from `packageRoot`. Returns the parsed manifest with
|
|
@@ -50,7 +51,34 @@ export function loadDeps(packageRoot) {
|
|
|
50
51
|
}
|
|
51
52
|
valid.push(dep)
|
|
52
53
|
}
|
|
53
|
-
|
|
54
|
+
|
|
55
|
+
// COMP-RTK-INTEROP: external_binaries is optional and additive. A missing or
|
|
56
|
+
// malformed array degrades to [] — it never nulls the whole manifest (the
|
|
57
|
+
// required external_skills check above remains the only fatal gate).
|
|
58
|
+
const validBinaries = []
|
|
59
|
+
if (raw.external_binaries !== undefined) {
|
|
60
|
+
if (!Array.isArray(raw.external_binaries)) {
|
|
61
|
+
console.warn('Warning: .compose-deps.json external_binaries must be an array — ignoring')
|
|
62
|
+
} else {
|
|
63
|
+
for (const bin of raw.external_binaries) {
|
|
64
|
+
const idOk = typeof bin?.id === 'string'
|
|
65
|
+
const detectOk = typeof bin?.detect === 'string'
|
|
66
|
+
const installOk = typeof bin?.install === 'string'
|
|
67
|
+
const optOk = typeof bin?.optional === 'boolean'
|
|
68
|
+
// required_for / recommend are optional; validate only when present.
|
|
69
|
+
const reqOk = bin?.required_for === undefined
|
|
70
|
+
|| (Array.isArray(bin.required_for) && bin.required_for.every(v => typeof v === 'string'))
|
|
71
|
+
const recOk = bin?.recommend === undefined || bin?.recommend === null || typeof bin?.recommend === 'string'
|
|
72
|
+
if (!(idOk && detectOk && installOk && optOk && reqOk && recOk)) {
|
|
73
|
+
console.warn(`Warning: skipping invalid binary entry in .compose-deps.json: ${JSON.stringify(bin)}`)
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
validBinaries.push(bin)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { ...raw, external_skills: valid, external_binaries: validBinaries }
|
|
54
82
|
}
|
|
55
83
|
|
|
56
84
|
/**
|
|
@@ -188,6 +216,75 @@ export function buildDepReport(result) {
|
|
|
188
216
|
}
|
|
189
217
|
}
|
|
190
218
|
|
|
219
|
+
/**
|
|
220
|
+
* COMP-RTK-INTEROP — probe each declared external *binary* (e.g. rtk) on PATH.
|
|
221
|
+
*
|
|
222
|
+
* Returns { present: [...], missing: [...] }. Optional binaries never make the
|
|
223
|
+
* doctor fail; this only reports availability + install/recommend hints.
|
|
224
|
+
*
|
|
225
|
+
* @param {object} deps - from loadDeps (uses deps.external_binaries)
|
|
226
|
+
* @param {object} [opts]
|
|
227
|
+
* @param {(detect:string)=>boolean} [opts.probe] - injectable for tests; default
|
|
228
|
+
* splits the `detect` string on whitespace and runs it, treating exit 0 as present.
|
|
229
|
+
*/
|
|
230
|
+
export function checkExternalBinaries(deps, opts = {}) {
|
|
231
|
+
const binaries = deps?.external_binaries ?? []
|
|
232
|
+
const probe = opts.probe ?? ((detect) => {
|
|
233
|
+
const parts = detect.trim().split(/\s+/)
|
|
234
|
+
const [bin, ...binArgs] = parts
|
|
235
|
+
try {
|
|
236
|
+
return spawnSync(bin, binArgs, { encoding: 'utf-8', timeout: 3000, stdio: 'pipe' }).status === 0
|
|
237
|
+
} catch {
|
|
238
|
+
return false
|
|
239
|
+
}
|
|
240
|
+
})
|
|
241
|
+
const present = []
|
|
242
|
+
const missing = []
|
|
243
|
+
for (const bin of binaries) {
|
|
244
|
+
if (probe(bin.detect)) present.push(bin); else missing.push(bin)
|
|
245
|
+
}
|
|
246
|
+
return { present, missing }
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Build a JSON-serializable binary report (mirrors buildDepReport for binaries).
|
|
251
|
+
*/
|
|
252
|
+
export function buildBinaryReport(result) {
|
|
253
|
+
const projectBin = (b) => ({
|
|
254
|
+
id: b.id,
|
|
255
|
+
detect: b.detect,
|
|
256
|
+
install: b.install,
|
|
257
|
+
recommend: b.recommend ?? null,
|
|
258
|
+
optional: b.optional,
|
|
259
|
+
})
|
|
260
|
+
return {
|
|
261
|
+
present: result.present.map(projectBin),
|
|
262
|
+
missing: result.missing.map(projectBin),
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Print a human-readable external-binary report. Returns true if all required
|
|
268
|
+
* (non-optional) binaries are present. Prints nothing when no binaries are declared.
|
|
269
|
+
*/
|
|
270
|
+
export function printBinaryReport(result) {
|
|
271
|
+
const total = result.present.length + result.missing.length
|
|
272
|
+
if (total === 0) return true
|
|
273
|
+
|
|
274
|
+
console.log('\nExternal binaries:')
|
|
275
|
+
for (const bin of result.present) {
|
|
276
|
+
const rec = bin.recommend ? ` — recommend: ${bin.recommend}` : ''
|
|
277
|
+
console.log(` ✓ ${bin.id}${rec}`)
|
|
278
|
+
}
|
|
279
|
+
for (const bin of result.missing) {
|
|
280
|
+
const mark = bin.optional ? '○' : '✗'
|
|
281
|
+
const tag = bin.optional ? ' (optional)' : ''
|
|
282
|
+
const rec = bin.recommend ? ` — recommend: ${bin.recommend}` : ''
|
|
283
|
+
console.log(` ${mark} ${bin.id}${tag} — install: ${bin.install}${rec}`)
|
|
284
|
+
}
|
|
285
|
+
return result.missing.every(b => b.optional)
|
|
286
|
+
}
|
|
287
|
+
|
|
191
288
|
export function printDepReport(result, opts = {}) {
|
|
192
289
|
if (opts.json) {
|
|
193
290
|
console.log(JSON.stringify(buildDepReport(result), null, 2))
|
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
|
}
|
|
@@ -1125,6 +1128,18 @@ export async function validateProject(cwd, options = {}) {
|
|
|
1125
1128
|
const ctx = loadValidationContext(cwd, options);
|
|
1126
1129
|
const findings = [];
|
|
1127
1130
|
|
|
1131
|
+
// COMP-PATHS-EXTERNAL: a relocated artifact path may resolve into a parent
|
|
1132
|
+
// that does not exist (e.g. the external docs repo isn't checked out). A
|
|
1133
|
+
// not-yet-created leaf dir is fine — writers mkdir it — but an unreachable
|
|
1134
|
+
// PARENT is a configuration error worth surfacing with the resolved path.
|
|
1135
|
+
for (const [key, p] of [['features', ctx.paths.features], ['roadmap', ctx.paths.roadmap]]) {
|
|
1136
|
+
const isExternal = path.relative(cwd, p).startsWith('..');
|
|
1137
|
+
if (isExternal && !fs.existsSync(path.dirname(p))) {
|
|
1138
|
+
findings.push(finding('error', 'CONFIGURED_PATH_UNREACHABLE', null,
|
|
1139
|
+
`configured ${key} path parent does not exist: ${path.dirname(p)} (resolved ${key} = ${p})`));
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1128
1143
|
const allCodes = new Set([
|
|
1129
1144
|
...ctx.roadmapByCode.keys(),
|
|
1130
1145
|
...ctx.visionByCode.keys(),
|
|
@@ -1146,9 +1161,7 @@ export async function validateProject(cwd, options = {}) {
|
|
|
1146
1161
|
|
|
1147
1162
|
// --- COMP-ROADMAP-RT: roundtrip + hierarchy ---
|
|
1148
1163
|
if (ctx.featureJsonMode) {
|
|
1149
|
-
const
|
|
1150
|
-
const featuresDir = (cfg && cfg.paths && cfg.paths.features) || DEFAULT_PATHS.features;
|
|
1151
|
-
const features = listFeatures(cwd, featuresDir);
|
|
1164
|
+
const features = listFeatures(cwd, ctx.paths.features);
|
|
1152
1165
|
const roadmapText = fs.existsSync(ctx.paths.roadmap)
|
|
1153
1166
|
? fs.readFileSync(ctx.paths.roadmap, 'utf8')
|
|
1154
1167
|
: '';
|
|
@@ -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}. ` +
|
package/lib/ideabox.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'
|
|
18
18
|
import { join, dirname } from 'node:path'
|
|
19
|
+
import { resolvePathValue } from './paths-core.js'
|
|
19
20
|
|
|
20
21
|
// ---------------------------------------------------------------------------
|
|
21
22
|
// Default template
|
|
@@ -540,7 +541,7 @@ export function loadLens(cwd, lensName) {
|
|
|
540
541
|
* Read and parse the ideabox file from the project.
|
|
541
542
|
*/
|
|
542
543
|
export function readIdeabox(cwd, ideaboxPath) {
|
|
543
|
-
const fullPath =
|
|
544
|
+
const fullPath = resolvePathValue(cwd, ideaboxPath, 'ideabox')
|
|
544
545
|
if (!existsSync(fullPath)) {
|
|
545
546
|
// Return empty state
|
|
546
547
|
return { ideas: [], killed: [], nextId: 1 }
|
|
@@ -553,7 +554,7 @@ export function readIdeabox(cwd, ideaboxPath) {
|
|
|
553
554
|
* Write serialized ideabox back to disk.
|
|
554
555
|
*/
|
|
555
556
|
export function writeIdeabox(cwd, ideaboxPath, parsedData) {
|
|
556
|
-
const fullPath =
|
|
557
|
+
const fullPath = resolvePathValue(cwd, ideaboxPath, 'ideabox')
|
|
557
558
|
mkdirSync(dirname(fullPath), { recursive: true })
|
|
558
559
|
writeFileSync(fullPath, serializeIdeabox(parsedData))
|
|
559
560
|
}
|