@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/server/file-watcher.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
199
|
-
// Also broadcast as fileChanged (features are docs)
|
|
200
|
-
|
|
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');
|
package/server/ideabox-routes.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
},
|
|
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:
|
|
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 })
|
package/server/project-root.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
}
|
package/server/session-routes.js
CHANGED
|
@@ -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(
|
|
143
|
+
featureDir: path.join(resolveProjectPath('features'), featureCode),
|
|
144
144
|
composeDir: path.join(targetRoot, '.compose'),
|
|
145
145
|
dataDir,
|
|
146
146
|
store: cpStore,
|
package/server/stratum-sync.js
CHANGED
|
@@ -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
|
|
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
|
|
77
|
+
/** Start the self-scheduling, sleep-aware poll loop (default 60s). */
|
|
32
78
|
start() {
|
|
33
|
-
this.#
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
});
|
|
37
|
-
}, 15_000);
|
|
79
|
+
this.#stopped = false;
|
|
80
|
+
this.#lastTickAt = Date.now();
|
|
81
|
+
this.#scheduleNext();
|
|
38
82
|
}
|
|
39
83
|
|
|
40
|
-
/** Stop the
|
|
84
|
+
/** Stop the poll loop. */
|
|
41
85
|
stop() {
|
|
86
|
+
this.#stopped = true;
|
|
42
87
|
if (this.#pollTimer) {
|
|
43
|
-
|
|
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.
|
package/server/vision-routes.js
CHANGED
|
@@ -42,7 +42,8 @@ function getSchemaValidator() {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
import { randomUUID } from 'node:crypto';
|
|
45
|
-
import { getTargetRoot, resolveProjectPath
|
|
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
|
-
?
|
|
185
|
+
? resolveFeaturesPathFromConfig(projectRoot, loadComposeConfig(projectRoot))
|
|
171
186
|
: resolveProjectPath('features');
|
|
172
187
|
|
|
173
188
|
// Phase graph + SKIPPABLE + TERMINAL are owned by lifecycle-guard.js (single
|
package/server/vision-server.js
CHANGED
|
@@ -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 =
|
|
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
|
-
?
|
|
461
|
+
? relForDisplay(getTargetRoot(), filePath)
|
|
461
462
|
: filePath.replace(/^\.\//, '');
|
|
462
463
|
const matches = [];
|
|
463
464
|
const matchType = new Map();
|
package/server/vision-store.js
CHANGED
|
@@ -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.
|
|
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
|
|
116
|
-
if (
|
|
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
|
}
|
package/server/vision-utils.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
75
|
-
|
|
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(
|
|
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};
|