@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.
Files changed (96) hide show
  1. package/.claude/skills/compose/SKILL.md +2 -0
  2. package/.compose-deps.json +13 -0
  3. package/bin/compose.js +35 -20
  4. package/dist/assets/{App-D7E7S49Q.js → App-Bxyif1yv.js} +160 -160
  5. package/dist/assets/{arc-LOW60tiI.js → arc-DeHak63Z.js} +1 -1
  6. package/dist/assets/{architectureDiagram-3BPJPVTR-BDY00hRy.js → architectureDiagram-3BPJPVTR-C5Nkyl9y.js} +1 -1
  7. package/dist/assets/{blockDiagram-GPEHLZMM-DiC_6ktq.js → blockDiagram-GPEHLZMM-BilnUBG2.js} +1 -1
  8. package/dist/assets/{c4Diagram-AAUBKEIU-CXZC8woU.js → c4Diagram-AAUBKEIU-BsFJmPuS.js} +1 -1
  9. package/dist/assets/channel-ZgbQ1k0u.js +1 -0
  10. package/dist/assets/{chunk-2J33WTMH-b8Rdk4S-.js → chunk-2J33WTMH-AEu6HIoY.js} +1 -1
  11. package/dist/assets/{chunk-4BX2VUAB-BSxn03f-.js → chunk-4BX2VUAB-XO4S2_89.js} +1 -1
  12. package/dist/assets/{chunk-55IACEB6-uUk1WzZ1.js → chunk-55IACEB6-CtFSLInj.js} +1 -1
  13. package/dist/assets/{chunk-727SXJPM-zvXhT6iZ.js → chunk-727SXJPM-CZeBRpM9.js} +1 -1
  14. package/dist/assets/{chunk-AQP2D5EJ-B6GCyrIp.js → chunk-AQP2D5EJ-_vmxcBc4.js} +1 -1
  15. package/dist/assets/{chunk-FMBD7UC4-DlhrqkBp.js → chunk-FMBD7UC4-BMGd_Y9a.js} +1 -1
  16. package/dist/assets/{chunk-ND2GUHAM-lo_8kNzw.js → chunk-ND2GUHAM-BlrOECEr.js} +1 -1
  17. package/dist/assets/{chunk-QZHKN3VN-on-yD-C0.js → chunk-QZHKN3VN-DHhUJ8RK.js} +1 -1
  18. package/dist/assets/classDiagram-4FO5ZUOK-KXxOorUx.js +1 -0
  19. package/dist/assets/classDiagram-v2-Q7XG4LA2-KXxOorUx.js +1 -0
  20. package/dist/assets/{cose-bilkent-S5V4N54A-DxV7Y2bU.js → cose-bilkent-S5V4N54A-C3M0mZqk.js} +1 -1
  21. package/dist/assets/{dagre-BM42HDAG-BHf9sURy.js → dagre-BM42HDAG-DJlm4fEk.js} +1 -1
  22. package/dist/assets/{diagram-2AECGRRQ-R9Puxzlh.js → diagram-2AECGRRQ-BmcONQG5.js} +1 -1
  23. package/dist/assets/{diagram-5GNKFQAL-BbjlClKx.js → diagram-5GNKFQAL-5JbWGIwd.js} +1 -1
  24. package/dist/assets/{diagram-KO2AKTUF-r89Mx4m1.js → diagram-KO2AKTUF-DAnIvyI0.js} +1 -1
  25. package/dist/assets/{diagram-LMA3HP47-CstVDJZ3.js → diagram-LMA3HP47-Bmh7tvmI.js} +1 -1
  26. package/dist/assets/{diagram-OG6HWLK6-xmX57FKh.js → diagram-OG6HWLK6-hnHVMNnk.js} +1 -1
  27. package/dist/assets/{erDiagram-TEJ5UH35-DhLPxvrl.js → erDiagram-TEJ5UH35-AMDitV0b.js} +1 -1
  28. package/dist/assets/{flowDiagram-I6XJVG4X-BExoUaNm.js → flowDiagram-I6XJVG4X-C0t2ZGQv.js} +1 -1
  29. package/dist/assets/{ganttDiagram-6RSMTGT7-BEXa5a22.js → ganttDiagram-6RSMTGT7-BPlM-BEN.js} +1 -1
  30. package/dist/assets/{gitGraphDiagram-PVQCEYII-Cbes9api.js → gitGraphDiagram-PVQCEYII-Lv_YTxnb.js} +1 -1
  31. package/dist/assets/index-COq21Zym.js +119 -0
  32. package/dist/assets/{infoDiagram-5YYISTIA-UBm4ssfm.js → infoDiagram-5YYISTIA-Dul1vdUm.js} +1 -1
  33. package/dist/assets/{ishikawaDiagram-YF4QCWOH-CksBYHCF.js → ishikawaDiagram-YF4QCWOH-CCBwgjFs.js} +1 -1
  34. package/dist/assets/{journeyDiagram-JHISSGLW-CKHkIHky.js → journeyDiagram-JHISSGLW-zUJN38EU.js} +1 -1
  35. package/dist/assets/{kanban-definition-UN3LZRKU-CEahMFzE.js → kanban-definition-UN3LZRKU-BNxWi-8J.js} +1 -1
  36. package/dist/assets/{linear-CqLVtYRk.js → linear-CRH9b4g7.js} +1 -1
  37. package/dist/assets/{mindmap-definition-RKZ34NQL-C61zWt4M.js → mindmap-definition-RKZ34NQL-DolYwq7Y.js} +1 -1
  38. package/dist/assets/{pieDiagram-4H26LBE5-BIpZHyJW.js → pieDiagram-4H26LBE5-oxNfr2TX.js} +1 -1
  39. package/dist/assets/{quadrantDiagram-W4KKPZXB-Db7nRbZm.js → quadrantDiagram-W4KKPZXB-BmZCvD-z.js} +1 -1
  40. package/dist/assets/{requirementDiagram-4Y6WPE33-BQqFCAr0.js → requirementDiagram-4Y6WPE33-DsXZ05jo.js} +1 -1
  41. package/dist/assets/{sankeyDiagram-5OEKKPKP-D-oAAMci.js → sankeyDiagram-5OEKKPKP-DsnYaawP.js} +1 -1
  42. package/dist/assets/{sequenceDiagram-3UESZ5HK-Dc4Cp0Om.js → sequenceDiagram-3UESZ5HK-Dt5mu3g8.js} +1 -1
  43. package/dist/assets/{stateDiagram-AJRCARHV-B5DkQ8Pr.js → stateDiagram-AJRCARHV-BFR5ZINQ.js} +1 -1
  44. package/dist/assets/stateDiagram-v2-BHNVJYJU-BDrQD8fR.js +1 -0
  45. package/dist/assets/{timeline-definition-PNZ67QCA-DU9Fl-FW.js → timeline-definition-PNZ67QCA-Bvnhw58e.js} +1 -1
  46. package/dist/assets/{vennDiagram-CIIHVFJN-Bthz7siw.js → vennDiagram-CIIHVFJN-5WnyjZqf.js} +1 -1
  47. package/dist/assets/{wardley-L42UT6IY-nqdlFSIA.js → wardley-L42UT6IY-CTxW4xow.js} +1 -1
  48. package/dist/assets/{wardleyDiagram-YWT4CUSO-BJ1BC5zc.js → wardleyDiagram-YWT4CUSO-K8Y1EOvb.js} +1 -1
  49. package/dist/assets/{xychartDiagram-2RQKCTM6-Bib5JrbR.js → xychartDiagram-2RQKCTM6-BCShes6B.js} +1 -1
  50. package/dist/index.html +1 -1
  51. package/lib/build-all.js +2 -1
  52. package/lib/build.js +96 -6
  53. package/lib/checkpoint/checkpoint-writer.js +3 -2
  54. package/lib/completion-writer.js +38 -14
  55. package/lib/deps.js +98 -1
  56. package/lib/feature-json.js +14 -3
  57. package/lib/feature-validator.js +22 -9
  58. package/lib/feature-write-guard.js +5 -7
  59. package/lib/feature-writer.js +3 -3
  60. package/lib/followup-writer.js +2 -2
  61. package/lib/get-roadmap.js +2 -2
  62. package/lib/gsd.js +2 -1
  63. package/lib/ideabox.js +3 -2
  64. package/lib/journal-writer.js +3 -3
  65. package/lib/migrate-roadmap.js +2 -3
  66. package/lib/paths-core.js +47 -0
  67. package/lib/project-paths.js +46 -37
  68. package/lib/roadmap-gen.js +3 -4
  69. package/lib/roadmap-graph/collect.js +5 -5
  70. package/lib/roadmap-graph/index.js +2 -2
  71. package/lib/rtk.js +66 -0
  72. package/lib/state-migrations.js +211 -42
  73. package/lib/tracker/local-provider.js +2 -2
  74. package/lib/triage.js +7 -5
  75. package/lib/vision-writer.js +6 -13
  76. package/lib/xref-push.js +4 -4
  77. package/lib/xref-sync.js +4 -4
  78. package/package.json +1 -1
  79. package/server/compose-mcp-tools.js +9 -1
  80. package/server/design-routes.js +9 -4
  81. package/server/drift-axes.js +5 -3
  82. package/server/feature-scan.js +8 -2
  83. package/server/file-watcher.js +13 -7
  84. package/server/ideabox-routes.js +12 -11
  85. package/server/project-root.js +21 -4
  86. package/server/session-routes.js +2 -2
  87. package/server/stratum-sync.js +85 -9
  88. package/server/vision-routes.js +17 -2
  89. package/server/vision-server.js +4 -3
  90. package/server/vision-store.js +6 -11
  91. package/server/vision-utils.js +23 -4
  92. package/dist/assets/channel-Dh96pu1k.js +0 -1
  93. package/dist/assets/classDiagram-4FO5ZUOK-BCqpDcaS.js +0 -1
  94. package/dist/assets/classDiagram-v2-Q7XG4LA2-BCqpDcaS.js +0 -1
  95. package/dist/assets/index-DNuHtZwR.js +0 -123
  96. package/dist/assets/stateDiagram-v2-BHNVJYJU-BM1Uf6a_.js +0 -1
@@ -29,12 +29,12 @@ import { join, dirname } from 'node:path';
29
29
 
30
30
  import { appendEvent, normalizeSince } from './feature-events.js';
31
31
  import { checkOrInsert } from './idempotency.js';
32
+ import { resolveJournalPath } from './project-paths.js';
32
33
 
33
34
  // ---------------------------------------------------------------------------
34
35
  // Constants & regexes
35
36
  // ---------------------------------------------------------------------------
36
37
 
37
- const JOURNAL_DIR = 'docs/journal';
38
38
  const INDEX_FILE = 'README.md';
39
39
  const ENTRIES_HEADING = '## Entries';
40
40
 
@@ -649,7 +649,7 @@ export async function writeJournalEntry(cwd, args) {
649
649
  validateArgs(args);
650
650
 
651
651
  return maybeIdempotent({ ...args, cwd }, async () => {
652
- const journalDir = join(cwd, JOURNAL_DIR);
652
+ const journalDir = resolveJournalPath(cwd);
653
653
  const indexPath = join(journalDir, INDEX_FILE);
654
654
 
655
655
  // Pre-flight: read + parse index before any disk mutation.
@@ -857,7 +857,7 @@ export async function writeJournalEntry(cwd, args) {
857
857
  * @returns {{ entries: Array, count: number }}
858
858
  */
859
859
  export function getJournalEntries(cwd, opts = {}) {
860
- const journalDir = join(cwd, JOURNAL_DIR);
860
+ const journalDir = resolveJournalPath(cwd);
861
861
  if (!existsSync(journalDir)) return { entries: [], count: 0 };
862
862
 
863
863
  const sinceMs = opts.since !== undefined ? normalizeSince(opts.since) : null;
@@ -6,10 +6,9 @@
6
6
  */
7
7
 
8
8
  import { readFileSync, existsSync } from 'fs';
9
- import { join } from 'path';
10
9
  import { parseRoadmap } from './roadmap-parser.js';
11
10
  import { readFeature, writeFeature } from './feature-json.js';
12
- import { loadFeaturesDir } from './project-paths.js';
11
+ import { loadFeaturesDir, resolveRoadmapPath } from './project-paths.js';
13
12
 
14
13
  /**
15
14
  * Migrate ROADMAP.md entries to feature.json files.
@@ -31,7 +30,7 @@ export function migrateRoadmap(cwd, opts = {}) {
31
30
  const featuresDir = opts.featuresDir ?? loadFeaturesDir(cwd);
32
31
  const externalPrefixes = opts.externalPrefixes ?? [];
33
32
  const isExternal = (code) => externalPrefixes.some((p) => code.startsWith(p));
34
- const roadmapPath = join(cwd, 'ROADMAP.md');
33
+ const roadmapPath = resolveRoadmapPath(cwd);
35
34
 
36
35
  if (!existsSync(roadmapPath)) {
37
36
  throw new Error(`No ROADMAP.md found at ${roadmapPath}`);
@@ -0,0 +1,47 @@
1
+ /**
2
+ * paths-core.js — PURE artifact-path resolution. No fs, no config reading.
3
+ * Single source of truth for default artifact locations (COMP-PATHS-EXTERNAL).
4
+ *
5
+ * `resolvePathValue` uses path.resolve (NOT path.join) so an absolute or
6
+ * ../-escaping `paths.*` override resolves correctly instead of being
7
+ * silently re-rooted under cwd.
8
+ */
9
+ import path from 'node:path';
10
+
11
+ export const DEFAULT_PATHS = Object.freeze({
12
+ docs: 'docs',
13
+ roadmap: 'ROADMAP.md',
14
+ features: 'docs/features',
15
+ journal: 'docs/journal',
16
+ context: 'docs/context',
17
+ ideabox: 'docs/product/ideabox.md',
18
+ });
19
+
20
+ /**
21
+ * Resolve a configured `paths[key]` value to an absolute, normalized path.
22
+ *
23
+ * @param {string} root Absolute workspace root.
24
+ * @param {*} value The configured paths[key] value (any type).
25
+ * @param {string} key Fallback key into DEFAULT_PATHS when value is unusable.
26
+ * @returns {string} Absolute, normalized path.
27
+ */
28
+ export function resolvePathValue(root, value, key) {
29
+ const v = (typeof value === 'string' && value.trim().length > 0)
30
+ ? value
31
+ : DEFAULT_PATHS[key];
32
+ return path.isAbsolute(v) ? path.normalize(v) : path.resolve(root, v);
33
+ }
34
+
35
+ /**
36
+ * Display-safe relativization: a clean root-relative string when `abs` is
37
+ * inside `root`, else the absolute path (never a `../`-prefixed string).
38
+ *
39
+ * @param {string} root Absolute workspace root.
40
+ * @param {string} abs Absolute path to relativize for display.
41
+ * @returns {string}
42
+ */
43
+ export function relForDisplay(root, abs) {
44
+ const rel = path.relative(root, abs);
45
+ if (rel === '') return '.';
46
+ return rel.startsWith('..') || path.isAbsolute(rel) ? abs : rel;
47
+ }
@@ -1,52 +1,61 @@
1
1
  /**
2
- * project-paths.js — read .compose/compose.json `paths.features` override
3
- * for lib-side writers.
2
+ * project-paths.js — read .compose/compose.json and resolve artifact paths
3
+ * for lib-side code. All path math is delegated to lib/paths-core.js so the
4
+ * server and lib resolvers can never diverge.
4
5
  *
5
- * Server-side code uses `server/project-root.js`'s cached `loadProjectConfig`,
6
- * but lib code may run outside the server process (CLI, tests, MCP stdio)
7
- * and shouldn't share that cache. This is a tiny per-call read; the file is
8
- * a few hundred bytes.
9
- *
10
- * Introduced by COMP-MCP-MIGRATION-2.
6
+ * Introduced by COMP-MCP-MIGRATION-2; extended to relocatable artifact paths
7
+ * by COMP-PATHS-EXTERNAL (absolute readers + *FromConfig alternate-root form).
11
8
  */
12
9
  import { existsSync, readFileSync } from 'fs';
13
10
  import { join } from 'path';
11
+ import { DEFAULT_PATHS, resolvePathValue, relForDisplay } from './paths-core.js';
12
+
13
+ function readConfig(cwd) {
14
+ const cfgPath = join(cwd, '.compose', 'compose.json');
15
+ if (!existsSync(cfgPath)) return {};
16
+ try { return JSON.parse(readFileSync(cfgPath, 'utf-8')); }
17
+ catch { return {}; }
18
+ }
19
+
20
+ /** Resolve one artifact key against the config on disk at `cwd`. Absolute. */
21
+ function resolveKey(cwd, key) {
22
+ return resolvePathValue(cwd, readConfig(cwd)?.paths?.[key], key);
23
+ }
24
+ /** Resolve one artifact key against an already-loaded config + arbitrary root. Absolute. */
25
+ function resolveKeyFromConfig(root, config, key) {
26
+ return resolvePathValue(root, config?.paths?.[key], key);
27
+ }
14
28
 
15
- const DEFAULT_FEATURES_DIR = 'docs/features';
29
+ export const resolveDocsPath = (cwd) => resolveKey(cwd, 'docs');
30
+ export const resolveRoadmapPath = (cwd) => resolveKey(cwd, 'roadmap');
31
+ export const resolveFeaturesPath = (cwd) => resolveKey(cwd, 'features');
32
+ export const resolveJournalPath = (cwd) => resolveKey(cwd, 'journal');
33
+ export const resolveContextPath = (cwd) => resolveKey(cwd, 'context');
34
+ export const resolveIdeaboxPath = (cwd) => resolveKey(cwd, 'ideabox');
35
+
36
+ export const resolveDocsPathFromConfig = (root, config) => resolveKeyFromConfig(root, config, 'docs');
37
+ export const resolveRoadmapPathFromConfig = (root, config) => resolveKeyFromConfig(root, config, 'roadmap');
38
+ export const resolveFeaturesPathFromConfig = (root, config) => resolveKeyFromConfig(root, config, 'features');
39
+ export const resolveJournalPathFromConfig = (root, config) => resolveKeyFromConfig(root, config, 'journal');
40
+ export const resolveContextPathFromConfig = (root, config) => resolveKeyFromConfig(root, config, 'context');
41
+ export const resolveIdeaboxPathFromConfig = (root, config) => resolveKeyFromConfig(root, config, 'ideabox');
42
+
43
+ export { relForDisplay };
16
44
 
17
45
  /**
18
- * Resolve the project's features directory, respecting `.compose/compose.json`'s
19
- * `paths.features` override. Returns the relative path (joined onto cwd by callers).
20
- *
21
- * @param {string} cwd
22
- * @returns {string} Relative features dir, e.g. 'docs/features' or 'specs/features'.
46
+ * @deprecated relative form prefer resolveFeaturesPath (absolute). Kept for
47
+ * any caller that genuinely needs the configured RELATIVE string. Callers that
48
+ * pass the result into feature-json.js should migrate to resolveFeaturesPath,
49
+ * which is absolute-safe (COMP-PATHS-EXTERNAL S2).
23
50
  */
24
51
  export function loadFeaturesDir(cwd) {
25
- const cfgPath = join(cwd, '.compose', 'compose.json');
26
- if (!existsSync(cfgPath)) return DEFAULT_FEATURES_DIR;
27
- try {
28
- const cfg = JSON.parse(readFileSync(cfgPath, 'utf-8'));
29
- const rel = cfg?.paths?.features;
30
- return (typeof rel === 'string' && rel.length > 0) ? rel : DEFAULT_FEATURES_DIR;
31
- } catch {
32
- return DEFAULT_FEATURES_DIR;
33
- }
52
+ const rel = readConfig(cwd)?.paths?.features;
53
+ return (typeof rel === 'string' && rel.length > 0) ? rel : DEFAULT_PATHS.features;
34
54
  }
35
55
 
36
- /**
37
- * Read `externalPrefixes` from .compose/compose.json — code prefixes (e.g.
38
- * ["STRAT-"]) for features owned by OTHER projects, present in this roadmap
39
- * only as cross-project references. Returns [] if absent/unreadable.
40
- * @param {string} cwd
41
- * @returns {string[]}
42
- */
43
56
  export function loadExternalPrefixes(cwd) {
44
- try {
45
- const cfgPath = join(cwd, '.compose', 'compose.json');
46
- if (!existsSync(cfgPath)) return [];
47
- const cfg = JSON.parse(readFileSync(cfgPath, 'utf-8'));
48
- return Array.isArray(cfg.externalPrefixes) ? cfg.externalPrefixes : [];
49
- } catch { return []; }
57
+ const arr = readConfig(cwd)?.externalPrefixes;
58
+ return Array.isArray(arr) ? arr : [];
50
59
  }
51
60
 
52
- export const _internals = { DEFAULT_FEATURES_DIR };
61
+ export const _internals = { DEFAULT_FEATURES_DIR: DEFAULT_PATHS.features };
@@ -5,10 +5,9 @@
5
5
  */
6
6
 
7
7
  import { readFileSync, writeFileSync, existsSync } from 'fs';
8
- import { join } from 'path';
9
8
  import { listFeatures, positionSortKey } from './feature-json.js';
10
9
  import { parseStatusToken } from './roadmap-parser.js';
11
- import { loadFeaturesDir } from './project-paths.js';
10
+ import { loadFeaturesDir, resolveRoadmapPath } from './project-paths.js';
12
11
  import {
13
12
  readPhaseOverrides,
14
13
  readAnonymousRows,
@@ -207,7 +206,7 @@ export function generateRoadmapFromBase(baseText, features, opts = {}) {
207
206
  * @returns {string} - Generated ROADMAP.md content
208
207
  */
209
208
  export function generateRoadmap(cwd, opts = {}) {
210
- const roadmapPath = join(cwd, 'ROADMAP.md');
209
+ const roadmapPath = resolveRoadmapPath(cwd);
211
210
  const existingText = existsSync(roadmapPath) ? readFileSync(roadmapPath, 'utf-8') : '';
212
211
 
213
212
  // Narrative-owned workspace: ROADMAP.md is hand-authored. Return it verbatim
@@ -513,7 +512,7 @@ function buildKeyDocs(features, featuresDir) {
513
512
  * @param {object} [opts]
514
513
  */
515
514
  export function writeRoadmap(cwd, opts = {}) {
516
- const roadmapPath = join(cwd, 'ROADMAP.md');
515
+ const roadmapPath = resolveRoadmapPath(cwd);
517
516
 
518
517
  // Narrative-owned workspace: never overwrite hand-authored ROADMAP.md (#39).
519
518
  // No-op + warn; the existing file on disk is the source of truth.
@@ -12,7 +12,7 @@ import { join, dirname, resolve } from 'node:path';
12
12
  import { fileURLToPath } from 'node:url';
13
13
  import { parse as parseYaml } from 'yaml';
14
14
  import { listFeatures } from '../feature-json.js';
15
- import { loadFeaturesDir, loadExternalPrefixes } from '../project-paths.js';
15
+ import { resolveFeaturesPath, resolveRoadmapPath, loadExternalPrefixes } from '../project-paths.js';
16
16
  import { parseRoadmap } from '../roadmap-parser.js';
17
17
  import { SchemaValidator } from '../../server/schema-validator.js';
18
18
  import { depsToEdges } from './model.js';
@@ -28,7 +28,7 @@ const FM_KEYS = ['name', 'priority', 'track', 'desc'];
28
28
  * @param {string} [featuresDir] relative features dir (default from compose.json)
29
29
  * @returns {{ nodes: object[], rawEdges: object[], knownCodes: Set<string>, warnings: string[] }}
30
30
  */
31
- export function collectGraphInputs(cwd, featuresDir = loadFeaturesDir(cwd)) {
31
+ export function collectGraphInputs(cwd, featuresDir = resolveFeaturesPath(cwd)) {
32
32
  const warnings = [];
33
33
  const depsValidator = new SchemaValidator(DEPS_SCHEMA);
34
34
  const fmValidator = new SchemaValidator(FRONTMATTER_SCHEMA);
@@ -48,7 +48,7 @@ export function collectGraphInputs(cwd, featuresDir = loadFeaturesDir(cwd)) {
48
48
  // An external-prefixed folder is a cross-project reference living here —
49
49
  // known (so edges resolve) but never rendered as one of THIS project's nodes.
50
50
  if (isExternal(f.code)) { externalCodes.add(f.code); continue; }
51
- const folder = join(cwd, featuresDir, f.code);
51
+ const folder = join(featuresDir, f.code);
52
52
  const fm = readDisplayMetadata(folder, f, fmValidator, warnings);
53
53
  universe.set(f.code, {
54
54
  id: f.code,
@@ -62,7 +62,7 @@ export function collectGraphInputs(cwd, featuresDir = loadFeaturesDir(cwd)) {
62
62
  }
63
63
 
64
64
  // (b) ROADMAP.md rows — fallback for features not registered as folders.
65
- const roadmapPath = join(cwd, 'ROADMAP.md');
65
+ const roadmapPath = resolveRoadmapPath(cwd);
66
66
  if (existsSync(roadmapPath)) {
67
67
  let entries = [];
68
68
  try { entries = parseRoadmap(readFileSync(roadmapPath, 'utf-8')); } catch { /* ignore */ }
@@ -88,7 +88,7 @@ export function collectGraphInputs(cwd, featuresDir = loadFeaturesDir(cwd)) {
88
88
  for (const f of features) {
89
89
  if (!f || typeof f.code !== 'string') continue;
90
90
  if (isExternal(f.code)) continue;
91
- const depsPath = join(cwd, featuresDir, f.code, 'deps.yaml');
91
+ const depsPath = join(featuresDir, f.code, 'deps.yaml');
92
92
  if (!existsSync(depsPath)) continue;
93
93
  let deps;
94
94
  try {
@@ -10,7 +10,7 @@
10
10
  */
11
11
  import { existsSync, readFileSync, writeFileSync, renameSync, unlinkSync, mkdirSync } from 'node:fs';
12
12
  import { join, isAbsolute, dirname } from 'node:path';
13
- import { loadFeaturesDir } from '../project-paths.js';
13
+ import { resolveFeaturesPath } from '../project-paths.js';
14
14
  import { collectGraphInputs } from './collect.js';
15
15
  import { buildGraph } from './model.js';
16
16
  import { renderGraphHtml } from './render.js';
@@ -22,7 +22,7 @@ import { loadGraphConfig } from './config.js';
22
22
  * @returns {{ html: string, nodes: object[], edges: object[], dropped: string[], warnings: string[], config: object }}
23
23
  */
24
24
  export function buildRoadmapGraph(cwd) {
25
- const featuresDir = loadFeaturesDir(cwd);
25
+ const featuresDir = resolveFeaturesPath(cwd);
26
26
  const inputs = collectGraphInputs(cwd, featuresDir);
27
27
  const graph = buildGraph(inputs); // throws DanglingEdgeError
28
28
  const config = loadGraphConfig(cwd);
package/lib/rtk.js ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * COMP-RTK-INTEROP — optional RTK (Rust Token Killer) interop.
3
+ *
4
+ * RTK (https://github.com/rtk-ai/rtk) is a LOSSY command-output compressor invoked
5
+ * as a command-PREFIX wrapper: `rtk git diff` runs git diff itself and rewrites
6
+ * stdout, compressing it 60–90% for LLM consumption. There is no stdin-filter mode.
7
+ *
8
+ * INTEGRATION RULE — read before adding a call site:
9
+ * Wrap a command with rtkPrefix() ONLY when its stdout is fed into an LLM prompt
10
+ * (e.g. a diff shown to a Codex reviewer). NEVER wrap a command whose output is
11
+ * parsed by code (filename lists via split('\n'), SHAs/shortstat via regex/.trim())
12
+ * or re-applied mechanically (`git apply` patches) — RTK's lossy rewrite would
13
+ * silently corrupt the parse/patch.
14
+ *
15
+ * Also pass only operator-free bare commands (no `;`, `&&`, `||`, `2>`): RTK runs the
16
+ * command, so shell operators would not be wrapped meaningfully.
17
+ *
18
+ * Detection is memoized once per process. Set COMPOSE_DISABLE_RTK=1 to force-disable
19
+ * (e.g. CI, or if RTK ever misbehaves) — degrades to raw, byte-identical commands.
20
+ */
21
+ import { spawnSync } from 'node:child_process'
22
+
23
+ let _cached = null // memoized availability; null = not yet probed
24
+
25
+ // Default probe: `rtk --version` exits 0 when installed. Swappable in tests.
26
+ let _prober = () => {
27
+ try {
28
+ const r = spawnSync('rtk', ['--version'], { encoding: 'utf-8', timeout: 3000, stdio: 'pipe' })
29
+ return r.status === 0
30
+ } catch {
31
+ return false
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Whether `rtk` is usable in this process. Memoized after the first call.
37
+ * COMPOSE_DISABLE_RTK=1 short-circuits to false (and is cached like any other result).
38
+ */
39
+ export function isRtkAvailable() {
40
+ if (_cached !== null) return _cached
41
+ if (process.env.COMPOSE_DISABLE_RTK === '1') {
42
+ _cached = false
43
+ return _cached
44
+ }
45
+ _cached = _prober()
46
+ return _cached
47
+ }
48
+
49
+ /**
50
+ * Prefix an LLM-bound command with `rtk` when RTK is available; otherwise return it
51
+ * unchanged (byte-identical degrade). Pass only operator-free bare commands.
52
+ */
53
+ export function rtkPrefix(command) {
54
+ return isRtkAvailable() ? `rtk ${command}` : command
55
+ }
56
+
57
+ // --- test seams ------------------------------------------------------------
58
+ /** Replace the availability prober and clear the memoized result. Tests only. */
59
+ export function _setRtkProber(fn) {
60
+ _prober = fn
61
+ _cached = null
62
+ }
63
+ /** Clear the memoized availability result so the next call re-probes. Tests only. */
64
+ export function _resetRtkCache() {
65
+ _cached = null
66
+ }