@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
@@ -10,6 +10,7 @@ import path from 'node:path';
10
10
  import { fileURLToPath } from 'node:url';
11
11
 
12
12
  import { getTargetRoot, loadProjectConfig, ensureDataDir } from './project-root.js';
13
+ import { resolveDocsPathFromConfig, resolveFeaturesPathFromConfig } from '../lib/project-paths.js';
13
14
 
14
15
  const PROJECT_ROOT = getTargetRoot();
15
16
 
@@ -69,7 +70,8 @@ export class FileWatcherServer {
69
70
  app.get('/api/files', (_req, res) => {
70
71
  const config = loadProjectConfig();
71
72
  const docsPrefix = config.paths?.docs || 'docs';
72
- const docsDir = path.join(PROJECT_ROOT, docsPrefix);
73
+ // COMP-PATHS-EXTERNAL: list the RESOLVED docs dir (may be relocated).
74
+ const docsDir = resolveDocsPathFromConfig(PROJECT_ROOT, config);
73
75
  try {
74
76
  const files = this.listMarkdownFiles(docsDir, docsPrefix);
75
77
  res.json({ files });
@@ -164,7 +166,10 @@ export class FileWatcherServer {
164
166
  if (!filename || !fileFilter(filename)) return;
165
167
 
166
168
  const relativePath = path.join(prefix, filename);
167
- const fullPath = path.join(PROJECT_ROOT, relativePath);
169
+ // COMP-PATHS-EXTERNAL: derive the real path from the WATCHED dir, not
170
+ // by re-rooting under PROJECT_ROOT — the dir may be relocated outside
171
+ // the workspace. Byte-identical to the old form for an in-root dir.
172
+ const fullPath = path.join(dir, filename);
168
173
 
169
174
  // Debounce: ignore events within 100ms of each other for the same file
170
175
  const now = Date.now();
@@ -180,10 +185,11 @@ export class FileWatcherServer {
180
185
  }
181
186
  };
182
187
 
183
- // Watch docs/ — broadcast fileChanged events
188
+ // Watch docs/ — broadcast fileChanged events. COMP-PATHS-EXTERNAL: watch
189
+ // the RESOLVED absolute dir (may be relocated outside PROJECT_ROOT).
184
190
  const config = loadProjectConfig();
185
191
  const docsPrefix = config.paths?.docs || 'docs';
186
- watchDir(path.join(PROJECT_ROOT, docsPrefix), docsPrefix, (relativePath, fullPath) => {
192
+ watchDir(resolveDocsPathFromConfig(PROJECT_ROOT, config), docsPrefix, (relativePath, fullPath) => {
187
193
  try {
188
194
  if (!fs.existsSync(fullPath)) return;
189
195
  const content = fs.readFileSync(fullPath, 'utf-8');
@@ -195,9 +201,9 @@ export class FileWatcherServer {
195
201
 
196
202
  // Watch features/ — notify for auto-reseed into vision store
197
203
  const featuresPrefix = config.paths?.features || 'docs/features';
198
- watchDir(path.join(PROJECT_ROOT, featuresPrefix), featuresPrefix, (relativePath) => {
199
- // Also broadcast as fileChanged (features are docs)
200
- const fullPath = path.join(PROJECT_ROOT, relativePath);
204
+ watchDir(resolveFeaturesPathFromConfig(PROJECT_ROOT, config), featuresPrefix, (relativePath, fullPath) => {
205
+ // Also broadcast as fileChanged (features are docs). fullPath comes from
206
+ // the watched dir (COMP-PATHS-EXTERNAL) — do not re-root under PROJECT_ROOT.
201
207
  try {
202
208
  if (fs.existsSync(fullPath)) {
203
209
  const content = fs.readFileSync(fullPath, 'utf-8');
@@ -25,6 +25,7 @@ import {
25
25
  } from '../lib/ideabox.js'
26
26
  import { writeFeature } from '../lib/feature-json.js'
27
27
  import { IdeaboxCache } from './ideabox-cache.js'
28
+ import { resolveIdeaboxPathFromConfig, resolveFeaturesPathFromConfig, relForDisplay } from '../lib/project-paths.js'
28
29
 
29
30
  /**
30
31
  * @param {object} app — Express app
@@ -41,8 +42,7 @@ export function attachIdeaboxRoutes(app, { getProjectRoot, getDataDir, broadcast
41
42
  const dataDir = getDataDir()
42
43
  if (!_cache || _lastProjectRoot !== projectRoot || _lastDataDir !== dataDir) {
43
44
  const config = loadConfig(projectRoot)
44
- const ideaboxRel = config?.paths?.ideabox || 'docs/product/ideabox.md'
45
- const sourceFile = path.join(projectRoot, ideaboxRel)
45
+ const sourceFile = resolveIdeaboxPathFromConfig(projectRoot, config)
46
46
  _cache = new IdeaboxCache(dataDir, sourceFile)
47
47
  _lastProjectRoot = projectRoot
48
48
  _lastDataDir = dataDir
@@ -172,16 +172,17 @@ export function attachIdeaboxRoutes(app, { getProjectRoot, getDataDir, broadcast
172
172
  resolvedCode = `IDEA-${sourceIdea.num}-${slug}`.toUpperCase()
173
173
  }
174
174
 
175
- // Create feature folder + feature.json (same logic as CLI promote)
175
+ // Create feature folder + feature.json (same logic as CLI promote).
176
+ // COMP-PATHS-EXTERNAL: resolve the ABSOLUTE features base (may be
177
+ // relocated outside projectRoot). Re-rooting under projectRoot would make
178
+ // existsSync check the wrong path and risk overwriting external canon.
176
179
  const composeJsonPath = path.join(projectRoot, '.compose', 'compose.json')
177
- let featuresRel = 'docs/features'
180
+ let cfg = {}
178
181
  if (fs.existsSync(composeJsonPath)) {
179
- try {
180
- const cfg = JSON.parse(fs.readFileSync(composeJsonPath, 'utf-8'))
181
- featuresRel = cfg?.paths?.features || 'docs/features'
182
- } catch {}
182
+ try { cfg = JSON.parse(fs.readFileSync(composeJsonPath, 'utf-8')) } catch {}
183
183
  }
184
- const featuresDir = path.join(projectRoot, featuresRel, resolvedCode)
184
+ const featuresBase = resolveFeaturesPathFromConfig(projectRoot, cfg) // absolute
185
+ const featuresDir = path.join(featuresBase, resolvedCode)
185
186
  if (!fs.existsSync(featuresDir)) {
186
187
  // COMP-MCP-VALIDATE-1: route through the validated writer instead of a
187
188
  // raw fs.writeFileSync so the promoted feature.json is schema-guarded.
@@ -191,7 +192,7 @@ export function attachIdeaboxRoutes(app, { getProjectRoot, getDataDir, broadcast
191
192
  status: 'PLANNED',
192
193
  promotedFrom: sourceIdea.id,
193
194
  createdAt: new Date().toISOString(),
194
- }, featuresRel)
195
+ }, featuresBase)
195
196
  }
196
197
 
197
198
  promoteIdea(parsed, id, resolvedCode)
@@ -200,7 +201,7 @@ export function attachIdeaboxRoutes(app, { getProjectRoot, getDataDir, broadcast
200
201
  broadcastUpdate()
201
202
 
202
203
  const updated = parsed.ideas.find(i => i.id.toUpperCase() === upper)
203
- res.json({ ...(updated || { id }), featureCode: resolvedCode, featurePath: `${featuresRel}/${resolvedCode}` })
204
+ res.json({ ...(updated || { id }), featureCode: resolvedCode, featurePath: relForDisplay(projectRoot, featuresDir) })
204
205
  } catch (err) {
205
206
  const status = err.message.includes('not found') ? 404 : 500
206
207
  res.status(status).json({ error: err.message })
@@ -15,6 +15,7 @@ import path from 'node:path';
15
15
  import fs from 'node:fs';
16
16
  import { fileURLToPath } from 'node:url';
17
17
  import { findProjectRoot } from './find-root.js';
18
+ import { DEFAULT_PATHS, resolvePathValue } from '../lib/paths-core.js';
18
19
 
19
20
  export { findProjectRoot } from './find-root.js';
20
21
 
@@ -99,7 +100,7 @@ export function ensureDataDir() {
99
100
  const DEFAULT_CONFIG = Object.freeze({
100
101
  version: 1,
101
102
  capabilities: Object.freeze({ stratum: true, lifecycle: true }),
102
- paths: Object.freeze({ docs: 'docs', features: 'docs/features', journal: 'docs/journal' }),
103
+ paths: DEFAULT_PATHS, // single source of truth (COMP-PATHS-EXTERNAL)
103
104
  });
104
105
 
105
106
  function cloneConfig(obj) {
@@ -120,7 +121,23 @@ export function loadProjectConfig() {
120
121
 
121
122
  export function resolveProjectPath(key) {
122
123
  const config = loadProjectConfig();
123
- const rel = config.paths?.[key];
124
- if (!rel) return path.join(getTargetRoot(), DEFAULT_CONFIG.paths[key] || key);
125
- return path.join(getTargetRoot(), rel);
124
+ // resolvePathValue handles in-root / ../-escaping / absolute overrides.
125
+ // Preserve the legacy `|| key` fallback for keys absent from DEFAULT_PATHS.
126
+ // (COMP-PATHS-EXTERNAL)
127
+ const value = config.paths?.[key] ?? DEFAULT_PATHS[key] ?? key;
128
+ return resolvePathValue(getTargetRoot(), value, key);
129
+ }
130
+
131
+ /**
132
+ * Whether the bound workspace runs the lifecycle/vision store.
133
+ *
134
+ * FORGE-ROADMAP-RETIRE-STORE: a workspace sets `capabilities.lifecycle: false`
135
+ * to RETIRE its vision store — used by narrative-owned workspaces (e.g. forge-top)
136
+ * where the prose ROADMAP is the single source of truth and a second, frozen
137
+ * `vision-state.json` would only be a drift-prone parallel answer. Default true
138
+ * (absent ⇒ enabled); only an explicit `false` disables it.
139
+ * @returns {boolean}
140
+ */
141
+ export function isLifecycleEnabled() {
142
+ return loadProjectConfig().capabilities?.lifecycle !== false;
126
143
  }
@@ -11,7 +11,7 @@
11
11
 
12
12
  import path from 'node:path';
13
13
  import { readSessionsByFeature } from './session-store.js';
14
- import { getTargetRoot, getDataDir } from './project-root.js';
14
+ import { getTargetRoot, getDataDir, resolveProjectPath } from './project-root.js';
15
15
  import { appendPhaseHistory } from './lifecycle-phase-history.js';
16
16
  import { reconcile } from '../lib/checkpoint/reconciler.js';
17
17
  import { checkpointConfig } from '../lib/checkpoint/checkpoint-writer.js';
@@ -140,7 +140,7 @@ export function attachSessionRoutes(app, { sessionManager, scheduleBroadcast, br
140
140
  featureCode,
141
141
  item,
142
142
  cwd: targetRoot,
143
- featureDir: path.join(targetRoot, 'docs', 'features', featureCode),
143
+ featureDir: path.join(resolveProjectPath('features'), featureCode),
144
144
  composeDir: path.join(targetRoot, '.compose'),
145
145
  dataDir,
146
146
  store: cpStore,
@@ -2,7 +2,19 @@
2
2
  * stratum-sync.js — Stratum flow poller + route registration.
3
3
  *
4
4
  * Polls stratum flow state via stratum-client (not direct file reads) and syncs
5
- * into the vision store every 15 seconds.
5
+ * into the vision store on a self-scheduling, sleep-aware loop.
6
+ *
7
+ * Sleep behavior: each poll cold-starts a `stratum-mcp` subprocess (Python
8
+ * interpreter spin-up — the expensive part, not the query itself). Doing that
9
+ * every few seconds generated enough periodic activity to keep the host from
10
+ * sleeping. Two mitigations:
11
+ * 1. Cadence widened to 60s (override with COMPOSE_STRATUM_POLL_MS).
12
+ * 2. The spawn is skipped entirely when no bound flow can still change state
13
+ * (`hasLiveFlows`), so an idle/finished Forge does zero periodic spawning
14
+ * and the host is free to sleep. The cheap in-memory scan that gates this
15
+ * holds no power assertion.
16
+ * On wake, a tick that fired far later than scheduled is treated as a
17
+ * post-sleep resync rather than business as usual.
6
18
  *
7
19
  * Routes: POST /api/stratum/bind, POST /api/stratum/audit/:itemId
8
20
  * (Flow/gate query routes are now in stratum-api.js)
@@ -10,6 +22,38 @@
10
22
 
11
23
  import { queryFlows } from './stratum-client.js';
12
24
 
25
+ // Poll cadence. 60s is plenty for a flow list (slower than any build step) and
26
+ // keeps periodic process-spawn activity low enough not to fight host sleep.
27
+ const DEFAULT_POLL_MS = 60_000;
28
+ const MIN_POLL_MS = 1_000;
29
+ const POLL_MS = (() => {
30
+ const raw = parseInt(process.env.COMPOSE_STRATUM_POLL_MS ?? '', 10);
31
+ return Number.isFinite(raw) && raw >= MIN_POLL_MS ? raw : DEFAULT_POLL_MS;
32
+ })();
33
+ // A tick that fires later than POLL_MS * this factor means the host slept
34
+ // through scheduled ticks — log it and treat the next sync as a wake resync.
35
+ const WAKE_GAP_FACTOR = 1.5;
36
+
37
+ /**
38
+ * Sleep-aware poll gate: true only when some bound vision item could still
39
+ * change state (i.e. is bound to a flow and not settled-`complete`). When this
40
+ * is false the poller skips the stratum-mcp spawn so an idle host can sleep.
41
+ *
42
+ * A bound `complete` item is steady-state (status only advances to `complete`
43
+ * via the audit route, never back), so polling it changes nothing. Anything
44
+ * else bound — `in_progress`, `blocked`, or mid-flight — can still transition
45
+ * and is worth a poll.
46
+ *
47
+ * @param {Iterable<{stratumFlowId?: string, status?: string}>} items
48
+ * @returns {boolean}
49
+ */
50
+ export function hasLiveFlows(items) {
51
+ for (const item of items) {
52
+ if (item.stratumFlowId && item.status !== 'complete') return true;
53
+ }
54
+ return false;
55
+ }
56
+
13
57
  // ---------------------------------------------------------------------------
14
58
  // StratumSync class
15
59
  // ---------------------------------------------------------------------------
@@ -18,6 +62,8 @@ export class StratumSync {
18
62
  #store;
19
63
  #scheduleBroadcast;
20
64
  #pollTimer = null;
65
+ #stopped = true;
66
+ #lastTickAt = 0;
21
67
 
22
68
  /**
23
69
  * @param {object} store — VisionStore instance
@@ -28,23 +74,53 @@ export class StratumSync {
28
74
  this.#scheduleBroadcast = scheduleBroadcast;
29
75
  }
30
76
 
31
- /** Start the 15s polling interval. */
77
+ /** Start the self-scheduling, sleep-aware poll loop (default 60s). */
32
78
  start() {
33
- this.#pollTimer = setInterval(() => {
34
- this.#syncFlows().catch(err => {
35
- console.error('[vision] Stratum poll error:', err.message);
36
- });
37
- }, 15_000);
79
+ this.#stopped = false;
80
+ this.#lastTickAt = Date.now();
81
+ this.#scheduleNext();
38
82
  }
39
83
 
40
- /** Stop the polling interval. */
84
+ /** Stop the poll loop. */
41
85
  stop() {
86
+ this.#stopped = true;
42
87
  if (this.#pollTimer) {
43
- clearInterval(this.#pollTimer);
88
+ clearTimeout(this.#pollTimer);
44
89
  this.#pollTimer = null;
45
90
  }
46
91
  }
47
92
 
93
+ /** Arm the next tick. No-op once stopped so stop() is final. */
94
+ #scheduleNext() {
95
+ if (this.#stopped) return;
96
+ this.#pollTimer = setTimeout(() => this.#tick(), POLL_MS);
97
+ }
98
+
99
+ /**
100
+ * One poll tick. Skips the stratum-mcp spawn when nothing is live (lets the
101
+ * host sleep); on a tick that fired far later than scheduled, notes the
102
+ * sleep gap before resyncing.
103
+ */
104
+ async #tick() {
105
+ if (this.#stopped) return;
106
+ const now = Date.now();
107
+ const gap = now - this.#lastTickAt;
108
+ this.#lastTickAt = now;
109
+
110
+ if (hasLiveFlows(this.#store.items.values())) {
111
+ if (gap > POLL_MS * WAKE_GAP_FACTOR) {
112
+ console.log(`[vision] Stratum sync resuming after ${Math.round(gap / 1000)}s gap (host likely slept) — resyncing`);
113
+ }
114
+ try {
115
+ await this.#syncFlows();
116
+ } catch (err) {
117
+ console.error('[vision] Stratum poll error:', err.message);
118
+ }
119
+ }
120
+
121
+ this.#scheduleNext();
122
+ }
123
+
48
124
  /**
49
125
  * Fetch flow summaries via stratum-client (stable contract, not direct file reads).
50
126
  * Maps stratum's query output to the shape the sync logic expects.
@@ -42,7 +42,8 @@ function getSchemaValidator() {
42
42
  }
43
43
 
44
44
  import { randomUUID } from 'node:crypto';
45
- import { getTargetRoot, resolveProjectPath, loadProjectConfig } from './project-root.js';
45
+ import { getTargetRoot, resolveProjectPath } from './project-root.js';
46
+ import { resolveFeaturesPathFromConfig } from '../lib/project-paths.js';
46
47
  import { anchorBoundary } from '../lib/checkpoint/checkpoint-writer.js';
47
48
  import { appendGateLogEntry, readGateLog, mapResolveOutcomeToSchema } from './gate-log-store.js';
48
49
  import { addOpenLoop, resolveOpenLoop, listOpenLoops } from './open-loops-store.js';
@@ -55,6 +56,20 @@ import { requireSensitiveOrPaired as requireSensitiveToken } from './security.js
55
56
 
56
57
  const PROJECT_ROOT = getTargetRoot();
57
58
 
59
+ /**
60
+ * Read the compose.json config for an ARBITRARY workspace root (not the bound
61
+ * target). The alternate-root lifecycle branch must resolve `paths.features`
62
+ * against that root's own config, not the process-global cache
63
+ * (COMP-PATHS-EXTERNAL, Decision 1 / Codex finding 3).
64
+ */
65
+ function loadComposeConfig(root) {
66
+ try {
67
+ return JSON.parse(fs.readFileSync(path.join(root, '.compose', 'compose.json'), 'utf-8'));
68
+ } catch {
69
+ return {};
70
+ }
71
+ }
72
+
58
73
  /**
59
74
  * Attach vision CRUD and lifecycle REST routes to an Express app.
60
75
  *
@@ -167,7 +182,7 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
167
182
 
168
183
  // ── Lifecycle endpoints (simplified — no state machine) ──────────────
169
184
  const featuresPath = projectRoot !== PROJECT_ROOT
170
- ? path.join(projectRoot, loadProjectConfig().paths?.features || 'docs/features')
185
+ ? resolveFeaturesPathFromConfig(projectRoot, loadComposeConfig(projectRoot))
171
186
  : resolveProjectPath('features');
172
187
 
173
188
  // Phase graph + SKIPPABLE + TERMINAL are owned by lifecycle-guard.js (single
@@ -55,7 +55,8 @@ const SETTINGS_DEFAULTS = {
55
55
  policyModes: ['gate', 'flag', 'skip'],
56
56
  };
57
57
 
58
- import { getTargetRoot, getDataDir } from './project-root.js';
58
+ import { getTargetRoot, getDataDir, resolveProjectPath } from './project-root.js';
59
+ import { relForDisplay } from '../lib/project-paths.js';
59
60
 
60
61
  export class VisionServer {
61
62
  constructor(store, sessionManager = null, { config } = {}) {
@@ -264,7 +265,7 @@ export class VisionServer {
264
265
  const projectsRoot = process.env.CC_PROJECTS_ROOT ||
265
266
  path.join(process.env.HOME || process.env.USERPROFILE || '', '.claude', 'projects');
266
267
  const sessionsFile = this.sessionManager?.sessionsFile || path.join(getDataDir(), 'sessions.json');
267
- const featureRoot = path.join(getTargetRoot(), 'docs', 'features');
268
+ const featureRoot = resolveProjectPath('features');
268
269
 
269
270
  const findItemIdByFeatureCode = (fc) => {
270
271
  for (const it of this.store.items.values()) {
@@ -457,7 +458,7 @@ export class VisionServer {
457
458
  /** Resolve a file path to matching tracker items */
458
459
  resolveItems(filePath) {
459
460
  const rel = filePath.startsWith(getTargetRoot())
460
- ? filePath.slice(getTargetRoot().length + 1)
461
+ ? relForDisplay(getTargetRoot(), filePath)
461
462
  : filePath.replace(/^\.\//, '');
462
463
  const matches = [];
463
464
  const matchType = new Map();
@@ -13,6 +13,7 @@ export const VALID_CONNECTION_TYPES = ['informs', 'blocks', 'supports', 'contrad
13
13
  export const VALID_PHASES = ['vision', 'specification', 'planning', 'implementation', 'verification', 'release'];
14
14
 
15
15
  import { getDataDir as getDefaultDataDir } from './project-root.js';
16
+ import { migrateVisionItemFeatureCode, normalizeGateOutcome } from '../lib/state-migrations.js';
16
17
 
17
18
  const DATA_FILE = path.join(getDefaultDataDir(), 'vision-state.json');
18
19
 
@@ -67,14 +68,8 @@ export class VisionStore {
67
68
  let migrated = false;
68
69
  if (Array.isArray(data.items)) {
69
70
  for (const item of data.items) {
70
- // Migration: legacy featureCode → lifecycle.featureCode
71
- if (item.featureCode && item.featureCode.startsWith('feature:') && !item.lifecycle?.featureCode) {
72
- const bare = item.featureCode.replace(/^feature:/, '');
73
- item.lifecycle = item.lifecycle || {};
74
- item.lifecycle.featureCode = bare;
75
- delete item.featureCode;
76
- migrated = true;
77
- }
71
+ // Migration: legacy featureCode → lifecycle.featureCode (shared impl)
72
+ if (migrateVisionItemFeatureCode(item).changed) migrated = true;
78
73
  if (!item.slug && item.title) item.slug = slugify(item.title);
79
74
  if (!item.files) item.files = [];
80
75
  // Group precedence (most-authoritative first):
@@ -110,10 +105,10 @@ export class VisionStore {
110
105
  if (seenPendingKeys.has(key)) { migrated = true; continue; }
111
106
  seenPendingKeys.add(key);
112
107
  }
113
- // Migration: normalize legacy gate outcomes
108
+ // Migration: normalize legacy gate outcomes (shared impl)
114
109
  if (gate.outcome) {
115
- const map = { approved: 'approve', killed: 'kill', revised: 'revise' };
116
- if (map[gate.outcome]) { gate.outcome = map[gate.outcome]; migrated = true; }
110
+ const normalized = normalizeGateOutcome(gate.outcome);
111
+ if (normalized !== gate.outcome) { gate.outcome = normalized; migrated = true; }
117
112
  }
118
113
  this.gates.set(gate.id, gate);
119
114
  }
@@ -10,10 +10,25 @@ import fs from 'node:fs';
10
10
  import path from 'node:path';
11
11
  import { fileURLToPath } from 'node:url';
12
12
 
13
- import { getTargetRoot, loadProjectConfig } from './project-root.js';
13
+ import { getTargetRoot } from './project-root.js';
14
+ import { resolveJournalPathFromConfig, relForDisplay } from '../lib/project-paths.js';
14
15
 
15
16
  const PROJECT_ROOT = getTargetRoot();
16
17
 
18
+ /**
19
+ * Read the compose.json config for an ARBITRARY workspace root (not the bound
20
+ * target). Routes/agents that serve an alternate `projectRoot` must resolve
21
+ * artifact paths against that root's own config, not the process-global cache
22
+ * (COMP-PATHS-EXTERNAL, Decision 1 / Codex finding 3).
23
+ */
24
+ function loadComposeConfig(root) {
25
+ try {
26
+ return JSON.parse(fs.readFileSync(path.join(root, '.compose', 'compose.json'), 'utf-8'));
27
+ } catch {
28
+ return {};
29
+ }
30
+ }
31
+
17
32
  // ---------------------------------------------------------------------------
18
33
  // Error detection
19
34
  // ---------------------------------------------------------------------------
@@ -71,8 +86,12 @@ export function detectError(tool, input, responseText) {
71
86
  * @param {string} [projectRoot] — defaults to PROJECT_ROOT
72
87
  */
73
88
  export function spawnJournalAgent(session, transcriptPath, projectRoot = PROJECT_ROOT) {
74
- const config = loadProjectConfig();
75
- const journalRel = config.paths?.journal || 'docs/journal';
89
+ // Resolve the journal path against the PASSED projectRoot's own config not
90
+ // the process-global cached config of the bound target (COMP-PATHS-EXTERNAL).
91
+ const journalAbs = resolveJournalPathFromConfig(projectRoot, loadComposeConfig(projectRoot));
92
+ // Display path for the agent prompt: clean relative when in-root (the agent
93
+ // runs with cwd: projectRoot), absolute when the journal escapes the root.
94
+ const journalRel = relForDisplay(projectRoot, journalAbs);
76
95
 
77
96
  const itemSummaries = Object.entries(session.items || {})
78
97
  .map(([_id, data]) => `- ${data.title}: ${data.writes} writes, ${data.reads} reads. ${(data.summaries || []).map(s => s.summary || '').filter(Boolean).join('. ')}`)
@@ -88,7 +107,7 @@ export function spawnJournalAgent(session, transcriptPath, projectRoot = PROJECT
88
107
  const today = new Date().toISOString().slice(0, 10);
89
108
  let sessionNum = 0;
90
109
  try {
91
- const entries = fs.readdirSync(path.join(projectRoot, journalRel));
110
+ const entries = fs.readdirSync(journalAbs);
92
111
  for (const f of entries) {
93
112
  const m = f.match(new RegExp(`^${today}-session-(\\d+)`));
94
113
  if (m) sessionNum = Math.max(sessionNum, parseInt(m[1]) + 1);
@@ -1 +0,0 @@
1
- import{ai as o,aj as n}from"./App-D7E7S49Q.js";const t=(a,r)=>o.lang.round(n.parse(a)[r]);export{t as c};
@@ -1 +0,0 @@
1
- import{s as a,c as s,a as e,C as t}from"./chunk-727SXJPM-zvXhT6iZ.js";import{_ as i}from"./App-D7E7S49Q.js";import"./chunk-FMBD7UC4-DlhrqkBp.js";import"./chunk-ND2GUHAM-lo_8kNzw.js";import"./chunk-55IACEB6-uUk1WzZ1.js";import"./chunk-2J33WTMH-b8Rdk4S-.js";import"./mobile-DwmxS_O4.js";import"./index-DNuHtZwR.js";import"./graph-BBXaecIU.js";var b={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{b as diagram};
@@ -1 +0,0 @@
1
- import{s as a,c as s,a as e,C as t}from"./chunk-727SXJPM-zvXhT6iZ.js";import{_ as i}from"./App-D7E7S49Q.js";import"./chunk-FMBD7UC4-DlhrqkBp.js";import"./chunk-ND2GUHAM-lo_8kNzw.js";import"./chunk-55IACEB6-uUk1WzZ1.js";import"./chunk-2J33WTMH-b8Rdk4S-.js";import"./mobile-DwmxS_O4.js";import"./index-DNuHtZwR.js";import"./graph-BBXaecIU.js";var b={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{b as diagram};