@smartmemory/compose 0.1.7-beta → 0.1.9-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 (84) hide show
  1. package/README.md +32 -5
  2. package/bin/compose.js +294 -34
  3. package/bin/git-hooks/post-commit.template +2 -1
  4. package/bin/git-hooks/pre-push.template +2 -1
  5. package/dist/assets/{_baseUniq-D-avYfn5.js → _baseUniq-3jW4HAOf.js} +1 -1
  6. package/dist/assets/{arc-BC4dfQ-X.js → arc-DzzDimyd.js} +1 -1
  7. package/dist/assets/{architectureDiagram-Q4EWVU46-BZmFXnGI.js → architectureDiagram-Q4EWVU46-CtAgwORz.js} +1 -1
  8. package/dist/assets/{blockDiagram-DXYQGD6D-DlfWSuux.js → blockDiagram-DXYQGD6D-Bryby0c_.js} +1 -1
  9. package/dist/assets/{c4Diagram-AHTNJAMY-Y__uJrRx.js → c4Diagram-AHTNJAMY-C7N9RTJ8.js} +1 -1
  10. package/dist/assets/channel-DDkv7DUd.js +1 -0
  11. package/dist/assets/{chunk-4BX2VUAB-BfMePfTp.js → chunk-4BX2VUAB-wijkFgZY.js} +1 -1
  12. package/dist/assets/{chunk-4TB4RGXK-BdlMSdEA.js → chunk-4TB4RGXK-zdSZGRS2.js} +1 -1
  13. package/dist/assets/{chunk-55IACEB6-vrQHZTdv.js → chunk-55IACEB6-6zqzTZQQ.js} +1 -1
  14. package/dist/assets/{chunk-EDXVE4YY-B8wioVlW.js → chunk-EDXVE4YY-frd1Vwf-.js} +1 -1
  15. package/dist/assets/{chunk-FMBD7UC4-Cd6Hrux2.js → chunk-FMBD7UC4-CdkRK5Hx.js} +1 -1
  16. package/dist/assets/{chunk-OYMX7WX6-CfrhdQXY.js → chunk-OYMX7WX6-C6bMB0cf.js} +1 -1
  17. package/dist/assets/{chunk-QZHKN3VN-B9JQerOU.js → chunk-QZHKN3VN-4vsxN3jq.js} +1 -1
  18. package/dist/assets/{chunk-YZCP3GAM-DFN9X99H.js → chunk-YZCP3GAM-DbNARKip.js} +1 -1
  19. package/dist/assets/classDiagram-6PBFFD2Q-J6ZTeCbW.js +1 -0
  20. package/dist/assets/classDiagram-v2-HSJHXN6E-J6ZTeCbW.js +1 -0
  21. package/dist/assets/clone-5MVZ89iV.js +1 -0
  22. package/dist/assets/{cose-bilkent-S5V4N54A-BAn0ap_E.js → cose-bilkent-S5V4N54A-BpXeV7Vj.js} +1 -1
  23. package/dist/assets/{dagre-KV5264BT-DyxnVq1g.js → dagre-KV5264BT-DQLu_W8r.js} +1 -1
  24. package/dist/assets/{diagram-5BDNPKRD-XCrzqski.js → diagram-5BDNPKRD-skaOoe5A.js} +1 -1
  25. package/dist/assets/{diagram-G4DWMVQ6-MBCAXft_.js → diagram-G4DWMVQ6-DezlfFH4.js} +1 -1
  26. package/dist/assets/{diagram-MMDJMWI5-DbtB2yS6.js → diagram-MMDJMWI5-BUu-v-wT.js} +1 -1
  27. package/dist/assets/{diagram-TYMM5635-Bb5NzX61.js → diagram-TYMM5635-CziQ6LPs.js} +1 -1
  28. package/dist/assets/{erDiagram-SMLLAGMA-CpIeCOh2.js → erDiagram-SMLLAGMA-BsAyOVTI.js} +1 -1
  29. package/dist/assets/{flowDiagram-DWJPFMVM-CHyoKnhW.js → flowDiagram-DWJPFMVM-CbYWJOLq.js} +1 -1
  30. package/dist/assets/{ganttDiagram-T4ZO3ILL-DErKteO_.js → ganttDiagram-T4ZO3ILL-CAwgDkLl.js} +1 -1
  31. package/dist/assets/{gitGraphDiagram-UUTBAWPF-KFVAtj2F.js → gitGraphDiagram-UUTBAWPF-DK4RlkjO.js} +1 -1
  32. package/dist/assets/{graph-CRnO_ifT.js → graph-orv1XHGx.js} +1 -1
  33. package/dist/assets/{index-DkRKLuNr.js → index-Ceywghsu.js} +143 -143
  34. package/dist/assets/{infoDiagram-42DDH7IO-BZFnuSp5.js → infoDiagram-42DDH7IO-DQyA75sK.js} +1 -1
  35. package/dist/assets/{ishikawaDiagram-UXIWVN3A-4Xe2Szde.js → ishikawaDiagram-UXIWVN3A-C-F_5q4k.js} +1 -1
  36. package/dist/assets/{journeyDiagram-VCZTEJTY-CZRByfS-.js → journeyDiagram-VCZTEJTY-Bj8UIvK-.js} +1 -1
  37. package/dist/assets/{kanban-definition-6JOO6SKY-B95sk6Fk.js → kanban-definition-6JOO6SKY-DZYr8Dp1.js} +1 -1
  38. package/dist/assets/{layout-BqNQzxWT.js → layout-CBaTKjpX.js} +1 -1
  39. package/dist/assets/{linear-CUh7qb64.js → linear-j1sI_SiN.js} +1 -1
  40. package/dist/assets/{min-wXgOS3ig.js → min-DtJISjld.js} +1 -1
  41. package/dist/assets/{mindmap-definition-QFDTVHPH-DB6iaAbO.js → mindmap-definition-QFDTVHPH-Bulb64RS.js} +1 -1
  42. package/dist/assets/{pieDiagram-DEJITSTG-CHkZHrTW.js → pieDiagram-DEJITSTG-D11keQxr.js} +1 -1
  43. package/dist/assets/{quadrantDiagram-34T5L4WZ-DoTEO8e3.js → quadrantDiagram-34T5L4WZ-BEcWQiEG.js} +1 -1
  44. package/dist/assets/{requirementDiagram-MS252O5E-Dn8peXYp.js → requirementDiagram-MS252O5E-Cbp23uDf.js} +1 -1
  45. package/dist/assets/{sankeyDiagram-XADWPNL6-DRXs6Ipb.js → sankeyDiagram-XADWPNL6-Dae1hMc5.js} +1 -1
  46. package/dist/assets/{sequenceDiagram-FGHM5R23-wBBYZ0aq.js → sequenceDiagram-FGHM5R23-C16abORi.js} +1 -1
  47. package/dist/assets/{stateDiagram-FHFEXIEX-DPlBNGmf.js → stateDiagram-FHFEXIEX-CbEtfhbx.js} +1 -1
  48. package/dist/assets/stateDiagram-v2-QKLJ7IA2-CyY84hEA.js +1 -0
  49. package/dist/assets/{timeline-definition-GMOUNBTQ-CbbyTlHk.js → timeline-definition-GMOUNBTQ-BV7JTNMI.js} +1 -1
  50. package/dist/assets/{vennDiagram-DHZGUBPP-Bj4GaFfj.js → vennDiagram-DHZGUBPP-DBZiT48j.js} +1 -1
  51. package/dist/assets/{wardley-RL74JXVD-RtNzq8KU.js → wardley-RL74JXVD-Cc8uoiL3.js} +37 -37
  52. package/dist/assets/{wardleyDiagram-NUSXRM2D-CDfE3zSj.js → wardleyDiagram-NUSXRM2D-DEYcWGo5.js} +1 -1
  53. package/dist/assets/{xychartDiagram-5P7HB3ND-CZXHHYD5.js → xychartDiagram-5P7HB3ND-bFhLXv2b.js} +1 -1
  54. package/dist/index.html +1 -1
  55. package/lib/build.js +193 -19
  56. package/lib/completion-writer.js +7 -4
  57. package/lib/deps.js +17 -6
  58. package/lib/discover-workspaces.js +109 -0
  59. package/lib/feature-events.js +3 -0
  60. package/lib/feature-writer.js +34 -22
  61. package/lib/followup-writer.js +556 -0
  62. package/lib/mcp-enforcement.js +173 -0
  63. package/lib/migrate-roadmap.js +4 -1
  64. package/lib/project-paths.js +36 -0
  65. package/lib/resolve-workspace.js +166 -0
  66. package/lib/review-lenses.js +23 -8
  67. package/lib/review-normalize.js +42 -3
  68. package/lib/roadmap-drift.js +54 -0
  69. package/lib/roadmap-gen.js +297 -27
  70. package/lib/roadmap-preservers.js +353 -0
  71. package/lib/step-prompt.js +15 -0
  72. package/lib/triage.js +2 -1
  73. package/lib/version-check.js +110 -0
  74. package/package.json +1 -2
  75. package/server/compose-mcp-tools.js +44 -8
  76. package/server/compose-mcp.js +66 -1
  77. package/server/project-root.js +4 -0
  78. package/server/vision-routes.js +51 -2
  79. package/templates/ROADMAP.md +6 -0
  80. package/dist/assets/channel-LRG9kHqJ.js +0 -1
  81. package/dist/assets/classDiagram-6PBFFD2Q-BC9a6pDE.js +0 -1
  82. package/dist/assets/classDiagram-v2-HSJHXN6E-BC9a6pDE.js +0 -1
  83. package/dist/assets/clone-dRxgFrBv.js +0 -1
  84. package/dist/assets/stateDiagram-v2-QKLJ7IA2-BW0ezXb4.js +0 -1
@@ -0,0 +1,109 @@
1
+ /**
2
+ * discover-workspaces.js — bounded bidirectional discovery of compose workspaces.
3
+ *
4
+ * Walks upward to find an "anchor" (any of ANCHOR_MARKERS), then scans the anchor
5
+ * subtree to MAX_DEPTH for `.compose/` markers. Hard-capped at MAX_VISITED dirs;
6
+ * over-cap throws an Error with code='WorkspaceDiscoveryTooBroad'. Permission
7
+ * errors during readdir are skipped silently — discovery is best-effort, not
8
+ * authoritative for individual subtrees.
9
+ *
10
+ * Exports:
11
+ * - findAnchor(startDir) → string|null
12
+ * - discoverWorkspaces(startDir) → { anchor, candidates: [{id, root, configPath}] }
13
+ * - deriveId({root}) → {id, root, configPath}
14
+ */
15
+ import path from 'node:path';
16
+ import fs from 'node:fs';
17
+
18
+ export const ANCHOR_MARKERS = ['.compose', '.stratum.yaml', '.git'];
19
+ export const WORKSPACE_MARKER = '.compose';
20
+ export const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.turbo']);
21
+ export const MAX_DEPTH = 3;
22
+ export const MAX_VISITED = 500;
23
+
24
+ /**
25
+ * Walk upward from startDir; return the first directory containing any
26
+ * ANCHOR_MARKER, or null if none found before filesystem root.
27
+ */
28
+ export function findAnchor(startDir) {
29
+ let dir = path.resolve(startDir);
30
+ const { root } = path.parse(dir);
31
+ while (true) {
32
+ for (const marker of ANCHOR_MARKERS) {
33
+ if (fs.existsSync(path.join(dir, marker))) return dir;
34
+ }
35
+ if (dir === root) return null;
36
+ const parent = path.dirname(dir);
37
+ if (parent === dir) return null;
38
+ dir = parent;
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Discover candidate workspaces under the anchor for startDir.
44
+ * If no anchor exists upward, anchors at startDir itself.
45
+ */
46
+ export function discoverWorkspaces(startDir) {
47
+ const anchor = findAnchor(startDir) ?? path.resolve(startDir);
48
+ const visited = { count: 0 };
49
+ const candidates = [];
50
+ walkDescendants(anchor, 0, candidates, visited);
51
+ if (fs.existsSync(path.join(anchor, WORKSPACE_MARKER))) {
52
+ if (!candidates.find((c) => c.root === anchor)) {
53
+ candidates.unshift({ root: anchor });
54
+ }
55
+ }
56
+ return { anchor, candidates: candidates.map(deriveId) };
57
+ }
58
+
59
+ function walkDescendants(dir, depth, out, visited) {
60
+ if (depth > MAX_DEPTH) return;
61
+ if (++visited.count > MAX_VISITED) {
62
+ const e = new Error(
63
+ `Workspace discovery exceeded ${MAX_VISITED} directories from anchor. ` +
64
+ 'Set COMPOSE_TARGET=/absolute/path to bypass discovery.',
65
+ );
66
+ e.code = 'WorkspaceDiscoveryTooBroad';
67
+ throw e;
68
+ }
69
+ let entries;
70
+ try {
71
+ entries = fs.readdirSync(dir, { withFileTypes: true });
72
+ } catch (err) {
73
+ // EACCES, EPERM, ENOENT (race with rm), ENOTDIR (symlink target gone) —
74
+ // skip silently. Discovery is best-effort; missing perms aren't fatal.
75
+ return;
76
+ }
77
+ for (const entry of entries) {
78
+ if (!entry.isDirectory() || SKIP_DIRS.has(entry.name)) continue;
79
+ const child = path.join(dir, entry.name);
80
+ if (fs.existsSync(path.join(child, WORKSPACE_MARKER))) {
81
+ out.push({ root: child });
82
+ }
83
+ walkDescendants(child, depth + 1, out, visited);
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Resolve {id, root, configPath} for a candidate workspace root.
89
+ * Honors `.compose/compose.json#workspaceId` if it matches the canonical regex;
90
+ * otherwise falls back to path.basename(root).
91
+ *
92
+ * Exported so resolve-workspace.js can derive ids without re-running discovery.
93
+ */
94
+ export function deriveId({ root }) {
95
+ const configPath = path.join(root, '.compose', 'compose.json');
96
+ let id = path.basename(root);
97
+ try {
98
+ const cfg = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
99
+ if (
100
+ typeof cfg.workspaceId === 'string' &&
101
+ /^[a-z][a-z0-9-]{1,63}$/.test(cfg.workspaceId)
102
+ ) {
103
+ id = cfg.workspaceId;
104
+ }
105
+ } catch {
106
+ // missing/unreadable/malformed → basename is fine
107
+ }
108
+ return { id, root, configPath };
109
+ }
@@ -52,6 +52,9 @@ export function appendEvent(cwd, event) {
52
52
  const row = {
53
53
  ts: new Date().toISOString(),
54
54
  actor: actor(),
55
+ // COMP-MCP-MIGRATION-1: stamp build correlation ID when running inside a
56
+ // build runner. Null outside of a build (manual CLI/MCP invocations).
57
+ build_id: process.env.COMPOSE_BUILD_ID || null,
55
58
  ...event,
56
59
  };
57
60
  appendFileSync(path, JSON.stringify(row) + '\n');
@@ -24,6 +24,7 @@ const _listFeatures = listFeatures;
24
24
  import { writeRoadmap } from './roadmap-gen.js';
25
25
  import { appendEvent, readEvents } from './feature-events.js';
26
26
  import { checkOrInsert } from './idempotency.js';
27
+ import { loadFeaturesDir } from './project-paths.js';
27
28
 
28
29
  // ---------------------------------------------------------------------------
29
30
  // Status / transition policy
@@ -98,8 +99,9 @@ export async function addRoadmapEntry(cwd, args) {
98
99
  throw new Error(`feature-writer: invalid status "${status}"`);
99
100
  }
100
101
 
102
+ const featuresDir = loadFeaturesDir(cwd);
101
103
  return maybeIdempotent({ ...args, cwd }, () => {
102
- const existing = readFeature(cwd, args.code);
104
+ const existing = readFeature(cwd, args.code, featuresDir);
103
105
  if (existing) {
104
106
  throw new Error(`feature-writer: feature "${args.code}" already exists`);
105
107
  }
@@ -116,14 +118,14 @@ export async function addRoadmapEntry(cwd, args) {
116
118
  if (args.complexity) feature.complexity = args.complexity;
117
119
  feature.position = args.position !== undefined
118
120
  ? args.position
119
- : nextPositionInPhase(cwd, args.phase);
121
+ : nextPositionInPhase(cwd, args.phase, featuresDir);
120
122
  if (args.parent) feature.parent = args.parent;
121
123
  if (args.tags && args.tags.length) feature.tags = args.tags;
122
124
 
123
- writeFeature(cwd, feature);
125
+ writeFeature(cwd, feature, featuresDir);
124
126
  let roadmapPath;
125
127
  try {
126
- roadmapPath = writeRoadmap(cwd);
128
+ roadmapPath = writeRoadmap(cwd, { featuresDir });
127
129
  } catch (err) {
128
130
  throw partialWriteError(
129
131
  `add_roadmap_entry: feature.json for "${args.code}" was written but ROADMAP.md regeneration failed. ` +
@@ -151,8 +153,8 @@ export async function addRoadmapEntry(cwd, args) {
151
153
 
152
154
  // Default position for a new feature: max existing position in the same
153
155
  // phase, plus 1. Falls back to 1 when the phase is empty.
154
- function nextPositionInPhase(cwd, phase) {
155
- const peers = _listFeatures(cwd).filter(f => f.phase === phase);
156
+ function nextPositionInPhase(cwd, phase, featuresDir) {
157
+ const peers = _listFeatures(cwd, featuresDir).filter(f => f.phase === phase);
156
158
  if (peers.length === 0) return 1;
157
159
  const maxPos = peers.reduce((m, f) => {
158
160
  const p = typeof f.position === 'number' ? f.position : 0;
@@ -205,8 +207,9 @@ export async function setFeatureStatus(cwd, args) {
205
207
  throw new Error(`feature-writer: invalid status "${args.status}" — must be one of ${[...STATUSES].join(', ')}`);
206
208
  }
207
209
 
210
+ const featuresDir = loadFeaturesDir(cwd);
208
211
  return maybeIdempotent({ ...args, cwd }, () => {
209
- const feature = readFeature(cwd, args.code);
212
+ const feature = readFeature(cwd, args.code, featuresDir);
210
213
  if (!feature) {
211
214
  throw new Error(`feature-writer: feature "${args.code}" not found`);
212
215
  }
@@ -229,9 +232,9 @@ export async function setFeatureStatus(cwd, args) {
229
232
 
230
233
  const updates = { status: to };
231
234
  if (args.commit_sha) updates.commit_sha = args.commit_sha;
232
- updateFeature(cwd, args.code, updates);
235
+ updateFeature(cwd, args.code, updates, featuresDir);
233
236
  try {
234
- writeRoadmap(cwd);
237
+ writeRoadmap(cwd, { featuresDir });
235
238
  } catch (err) {
236
239
  throw partialWriteError(
237
240
  `set_feature_status: feature.json for "${args.code}" was updated (${from} → ${to}) but ROADMAP.md regeneration failed. ` +
@@ -269,12 +272,17 @@ export async function setFeatureStatus(cwd, args) {
269
272
  */
270
273
  export function roadmapDiff(cwd, args = {}) {
271
274
  const since = args.since ?? '24h';
272
- const events = readEvents(cwd, {
275
+ const rawEvents = readEvents(cwd, {
273
276
  since,
274
277
  code: args.feature_code,
275
278
  tool: args.tool,
276
279
  });
277
280
 
281
+ // Filter out internal reconciliation events that aren't user-driven mutations.
282
+ // `roadmap_drift` events fire when a curated phase override diverges from
283
+ // the auto-rollup; they're observability output, not roadmap changes.
284
+ const events = rawEvents.filter(e => e.tool !== 'roadmap_drift');
285
+
278
286
  const added = [];
279
287
  const status_changed = [];
280
288
  for (const e of events) {
@@ -336,14 +344,14 @@ function validateRepoPath(cwd, path) {
336
344
  return normalized;
337
345
  }
338
346
 
339
- function rejectCanonicalArtifact(featureCode, normalizedPath) {
340
- // Reject paths like docs/features/<CODE>/design.md, prd.md, etc.
347
+ function rejectCanonicalArtifact(featuresDir, featureCode, normalizedPath) {
348
+ // Reject paths like <featuresDir>/<CODE>/design.md, prd.md, etc.
341
349
  const file = basename(normalizedPath);
342
350
  if (!CANONICAL_ARTIFACT_NAMES.has(file)) return;
343
351
  // The canonical files live under the feature folder. If this path points
344
352
  // inside the feature's own folder, refuse — those are auto-discovered.
345
353
  const parent = dirname(normalizedPath);
346
- if (parent.endsWith(`docs/features/${featureCode}`)) {
354
+ if (parent.endsWith(`${featuresDir}/${featureCode}`)) {
347
355
  throw new Error(
348
356
  `feature-writer: "${file}" inside the feature folder is a canonical artifact; ` +
349
357
  `it is auto-discovered by assess_feature_artifacts and should not be linked explicitly.`
@@ -371,10 +379,11 @@ export async function linkArtifact(cwd, args) {
371
379
  throw new Error('feature-writer: artifact_type is required (non-empty string)');
372
380
  }
373
381
  const normalizedPath = validateRepoPath(cwd, args.path);
374
- rejectCanonicalArtifact(args.feature_code, normalizedPath);
382
+ const featuresDir = loadFeaturesDir(cwd);
383
+ rejectCanonicalArtifact(featuresDir, args.feature_code, normalizedPath);
375
384
 
376
385
  return maybeIdempotent({ ...args, cwd }, () => {
377
- const feature = readFeature(cwd, args.feature_code);
386
+ const feature = readFeature(cwd, args.feature_code, featuresDir);
378
387
  if (!feature) {
379
388
  throw new Error(`feature-writer: feature "${args.feature_code}" not found`);
380
389
  }
@@ -399,7 +408,7 @@ export async function linkArtifact(cwd, args) {
399
408
  if (matchIdx !== -1) artifacts[matchIdx] = entry;
400
409
  else artifacts.push(entry);
401
410
 
402
- updateFeature(cwd, args.feature_code, { artifacts });
411
+ updateFeature(cwd, args.feature_code, { artifacts }, featuresDir);
403
412
 
404
413
  safeAppendEvent(cwd, {
405
414
  tool: 'link_artifact',
@@ -443,8 +452,9 @@ export async function linkFeatures(cwd, args) {
443
452
  );
444
453
  }
445
454
 
455
+ const featuresDir = loadFeaturesDir(cwd);
446
456
  return maybeIdempotent({ ...args, cwd }, () => {
447
- const feature = readFeature(cwd, args.from_code);
457
+ const feature = readFeature(cwd, args.from_code, featuresDir);
448
458
  if (!feature) {
449
459
  throw new Error(`feature-writer: feature "${args.from_code}" not found`);
450
460
  }
@@ -464,7 +474,7 @@ export async function linkFeatures(cwd, args) {
464
474
  if (matchIdx !== -1) links[matchIdx] = entry;
465
475
  else links.push(entry);
466
476
 
467
- updateFeature(cwd, args.from_code, { links });
477
+ updateFeature(cwd, args.from_code, { links }, featuresDir);
468
478
 
469
479
  safeAppendEvent(cwd, {
470
480
  tool: 'link_features',
@@ -498,7 +508,8 @@ export async function linkFeatures(cwd, args) {
498
508
  */
499
509
  export async function getFeatureArtifacts(cwd, args) {
500
510
  validateCode(args.feature_code);
501
- const feature = readFeature(cwd, args.feature_code);
511
+ const featuresDir = loadFeaturesDir(cwd);
512
+ const feature = readFeature(cwd, args.feature_code, featuresDir);
502
513
  if (!feature) {
503
514
  throw new Error(`feature-writer: feature "${args.feature_code}" not found`);
504
515
  }
@@ -514,7 +525,7 @@ export async function getFeatureArtifacts(cwd, args) {
514
525
  let canonical = null;
515
526
  try {
516
527
  const { ArtifactManager } = await import('../server/artifact-manager.js');
517
- const featureRoot = resolve(realCwd, 'docs', 'features');
528
+ const featureRoot = resolve(realCwd, featuresDir);
518
529
  if (existsSync(featureRoot)) {
519
530
  const manager = new ArtifactManager(featureRoot);
520
531
  canonical = manager.assess(args.feature_code);
@@ -549,10 +560,11 @@ export function getFeatureLinks(cwd, args) {
549
560
  }
550
561
  const kind = args.kind;
551
562
 
563
+ const featuresDir = loadFeaturesDir(cwd);
552
564
  const out = { feature_code: args.feature_code };
553
565
 
554
566
  if (direction === 'outgoing' || direction === 'both') {
555
- const feature = readFeature(cwd, args.feature_code);
567
+ const feature = readFeature(cwd, args.feature_code, featuresDir);
556
568
  if (!feature) {
557
569
  throw new Error(`feature-writer: feature "${args.feature_code}" not found`);
558
570
  }
@@ -562,7 +574,7 @@ export function getFeatureLinks(cwd, args) {
562
574
  }
563
575
 
564
576
  if (direction === 'incoming' || direction === 'both') {
565
- const all = _listFeatures(cwd);
577
+ const all = _listFeatures(cwd, featuresDir);
566
578
  const incoming = [];
567
579
  for (const f of all) {
568
580
  if (f.code === args.feature_code) continue;