@smartmemory/compose 0.1.0 → 0.1.2-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/bug-fix/SKILL.md +143 -0
- package/.claude/skills/compose/SKILL.md +604 -0
- package/.compose-deps.json +89 -0
- package/README.md +14 -3
- package/bin/compose.js +473 -0
- package/contracts/comp-obs-contract.schema.json +362 -0
- package/contracts/cross-model-review-result.json +78 -0
- package/contracts/review-result.json +126 -0
- package/dist/assets/{_baseUniq-CQwX6VLz.js → _baseUniq-D-avYfn5.js} +1 -1
- package/dist/assets/{arc-SxJ2J1sh.js → arc-BC4dfQ-X.js} +1 -1
- package/dist/assets/{architectureDiagram-Q4EWVU46-BykunY1F.js → architectureDiagram-Q4EWVU46-BZmFXnGI.js} +1 -1
- package/dist/assets/{blockDiagram-DXYQGD6D-ohAKBOUw.js → blockDiagram-DXYQGD6D-DlfWSuux.js} +1 -1
- package/dist/assets/{c4Diagram-AHTNJAMY-DBDC3ENB.js → c4Diagram-AHTNJAMY-Y__uJrRx.js} +1 -1
- package/dist/assets/channel-LRG9kHqJ.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-Cv93Z7uM.js → chunk-4BX2VUAB-BfMePfTp.js} +1 -1
- package/dist/assets/{chunk-4TB4RGXK-DE0WBDkj.js → chunk-4TB4RGXK-BdlMSdEA.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-CE1EXenG.js → chunk-55IACEB6-vrQHZTdv.js} +1 -1
- package/dist/assets/{chunk-EDXVE4YY-DA7Ana6H.js → chunk-EDXVE4YY-B8wioVlW.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-CTDIPA3p.js → chunk-FMBD7UC4-Cd6Hrux2.js} +1 -1
- package/dist/assets/{chunk-OYMX7WX6-uGBaPaTX.js → chunk-OYMX7WX6-CfrhdQXY.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-CYlnXuUO.js → chunk-QZHKN3VN-B9JQerOU.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-ojGkzcZK.js → chunk-YZCP3GAM-DFN9X99H.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-BC9a6pDE.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-BC9a6pDE.js +1 -0
- package/dist/assets/clone-dRxgFrBv.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-Bktn9hL-.js → cose-bilkent-S5V4N54A-BAn0ap_E.js} +1 -1
- package/dist/assets/{dagre-KV5264BT-DFaSzuRF.js → dagre-KV5264BT-DyxnVq1g.js} +1 -1
- package/dist/assets/{diagram-5BDNPKRD-DnfmDzEm.js → diagram-5BDNPKRD-XCrzqski.js} +1 -1
- package/dist/assets/{diagram-G4DWMVQ6-Bm8W9YnG.js → diagram-G4DWMVQ6-MBCAXft_.js} +1 -1
- package/dist/assets/{diagram-MMDJMWI5-B5-TSKvp.js → diagram-MMDJMWI5-DbtB2yS6.js} +1 -1
- package/dist/assets/{diagram-TYMM5635-ls4rqlky.js → diagram-TYMM5635-Bb5NzX61.js} +1 -1
- package/dist/assets/{erDiagram-SMLLAGMA-giG6WO-r.js → erDiagram-SMLLAGMA-CpIeCOh2.js} +1 -1
- package/dist/assets/{flowDiagram-DWJPFMVM-XvlUuz-7.js → flowDiagram-DWJPFMVM-CHyoKnhW.js} +1 -1
- package/dist/assets/{ganttDiagram-T4ZO3ILL-hLBV57oV.js → ganttDiagram-T4ZO3ILL-DErKteO_.js} +1 -1
- package/dist/assets/{gitGraphDiagram-UUTBAWPF-BHu3s_Gn.js → gitGraphDiagram-UUTBAWPF-KFVAtj2F.js} +1 -1
- package/dist/assets/{graph-D0Cfv00Y.js → graph-CRnO_ifT.js} +1 -1
- package/dist/assets/index-DKBsEUJ-.css +1 -0
- package/dist/assets/index-DkRKLuNr.js +1144 -0
- package/dist/assets/{infoDiagram-42DDH7IO-DbqRsOo3.js → infoDiagram-42DDH7IO-BZFnuSp5.js} +1 -1
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-DnCdx7zb.js → ishikawaDiagram-UXIWVN3A-4Xe2Szde.js} +1 -1
- package/dist/assets/{journeyDiagram-VCZTEJTY-CfD7eNcP.js → journeyDiagram-VCZTEJTY-CZRByfS-.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-BYaO9-mK.js → kanban-definition-6JOO6SKY-B95sk6Fk.js} +1 -1
- package/dist/assets/{layout-Bj72wOEB.js → layout-BqNQzxWT.js} +1 -1
- package/dist/assets/{linear-BRFo114D.js → linear-CUh7qb64.js} +1 -1
- package/dist/assets/{min-GCHnKlJS.js → min-wXgOS3ig.js} +1 -1
- package/dist/assets/{mindmap-definition-QFDTVHPH-n0PMebY4.js → mindmap-definition-QFDTVHPH-DB6iaAbO.js} +1 -1
- package/dist/assets/{pieDiagram-DEJITSTG-pN4CljHF.js → pieDiagram-DEJITSTG-CHkZHrTW.js} +1 -1
- package/dist/assets/{quadrantDiagram-34T5L4WZ-DNoAy8-D.js → quadrantDiagram-34T5L4WZ-DoTEO8e3.js} +1 -1
- package/dist/assets/{requirementDiagram-MS252O5E-BhtY05PT.js → requirementDiagram-MS252O5E-Dn8peXYp.js} +1 -1
- package/dist/assets/{sankeyDiagram-XADWPNL6-B6AD-16A.js → sankeyDiagram-XADWPNL6-DRXs6Ipb.js} +1 -1
- package/dist/assets/{sequenceDiagram-FGHM5R23-DShHM-uk.js → sequenceDiagram-FGHM5R23-wBBYZ0aq.js} +1 -1
- package/dist/assets/{stateDiagram-FHFEXIEX-DMxn7HTo.js → stateDiagram-FHFEXIEX-DPlBNGmf.js} +1 -1
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-BW0ezXb4.js +1 -0
- package/dist/assets/{timeline-definition-GMOUNBTQ-Cdu6uq52.js → timeline-definition-GMOUNBTQ-CbbyTlHk.js} +1 -1
- package/dist/assets/{vennDiagram-DHZGUBPP-CpK29iRe.js → vennDiagram-DHZGUBPP-Bj4GaFfj.js} +1 -1
- package/dist/assets/{wardley-RL74JXVD-BQgSkdcO.js → wardley-RL74JXVD-RtNzq8KU.js} +55 -55
- package/dist/assets/{wardleyDiagram-NUSXRM2D-DJHYev6O.js → wardleyDiagram-NUSXRM2D-CDfE3zSj.js} +1 -1
- package/dist/assets/{xychartDiagram-5P7HB3ND-1d75pbaO.js → xychartDiagram-5P7HB3ND-CZXHHYD5.js} +1 -1
- package/dist/index.html +2 -2
- package/lib/budget-ledger.js +45 -0
- package/lib/bug-bisect.js +292 -0
- package/lib/bug-checkpoint.js +191 -0
- package/lib/bug-escalation.js +306 -0
- package/lib/bug-index-gen.js +136 -0
- package/lib/bug-ledger.js +126 -0
- package/lib/build-stream-schema.js +176 -0
- package/lib/build-stream-writer.js +3 -1
- package/lib/build.js +854 -284
- package/lib/connector-factory-shim.js +167 -0
- package/lib/constants.js +18 -0
- package/lib/debug-discipline.js +176 -27
- package/lib/deps.js +205 -0
- package/lib/health-score.js +4 -4
- package/lib/import.js +26 -13
- package/lib/inject-schema.js +21 -0
- package/lib/new.js +27 -53
- package/lib/result-normalizer.js +160 -144
- package/lib/review-lenses.js +5 -5
- package/lib/review-normalize.js +413 -0
- package/lib/review-prompt.js +163 -0
- package/lib/sections.js +325 -0
- package/lib/step-prompt.js +21 -1
- package/lib/step-validator.js +5 -3
- package/lib/stratum-mcp-client.js +172 -7
- package/package.json +14 -3
- package/pipelines/bug-fix.stratum.yaml +39 -1
- package/pipelines/build.stratum.yaml +28 -45
- package/pipelines/review-fix.stratum.yaml +1 -1
- package/presets/team-review.stratum.yaml +21 -14
- package/server/build-stream-bridge.js +28 -0
- package/server/cc-session-feature-resolver.js +111 -0
- package/server/cc-session-reader.js +327 -0
- package/server/cc-session-watcher.js +318 -0
- package/server/compose-mcp-tools.js +0 -125
- package/server/compose-mcp.js +2 -4
- package/server/contract-diff.js +192 -0
- package/server/decision-event-emit.js +175 -0
- package/server/decision-event-id.js +64 -0
- package/server/decision-events-snapshot.js +166 -0
- package/server/design-routes.js +92 -49
- package/server/drift-axes.js +365 -0
- package/server/drift-emit.js +121 -0
- package/server/gate-log-store.js +102 -0
- package/server/lifecycle-phase-history.js +44 -0
- package/server/open-loops-store.js +102 -0
- package/server/schema-validator.js +49 -0
- package/server/status-emit.js +27 -0
- package/server/status-snapshot.js +218 -0
- package/server/vision-routes.js +332 -4
- package/server/vision-server.js +104 -12
- package/server/vision-store.js +21 -0
- package/dist/assets/channel-DGElom1e.js +0 -1
- package/dist/assets/classDiagram-6PBFFD2Q-KqWP9wWZ.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-KqWP9wWZ.js +0 -1
- package/dist/assets/clone-DUJKJXd7.js +0 -1
- package/dist/assets/index-CUd6pFGF.css +0 -1
- package/dist/assets/index-DReRlzZI.js +0 -1144
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-o6PnCs4e.js +0 -1
- package/server/connectors/agent-connector.js +0 -78
- package/server/connectors/claude-sdk-connector.js +0 -198
- package/server/connectors/codex-connector.js +0 -240
- package/server/connectors/connector-discovery.js +0 -18
- package/server/connectors/connector-runtime.js +0 -13
- package/server/connectors/opencode-connector.js +0 -200
package/server/vision-routes.js
CHANGED
|
@@ -31,9 +31,24 @@ import path from 'node:path';
|
|
|
31
31
|
import { fileURLToPath } from 'node:url';
|
|
32
32
|
import { extractFilePaths } from './vision-utils.js';
|
|
33
33
|
import { ArtifactManager } from './artifact-manager.js';
|
|
34
|
-
import { recordIteration, checkCumulativeBudget } from '../lib/budget-ledger.js';
|
|
34
|
+
import { recordIteration, checkCumulativeBudget, readBudget } from '../lib/budget-ledger.js';
|
|
35
|
+
import { SchemaValidator } from './schema-validator.js';
|
|
36
|
+
import { appendPhaseHistory } from './lifecycle-phase-history.js';
|
|
37
|
+
import { emitDecisionEvent, buildPhaseTransitionEvent, buildIterationEvent, buildGateEvent } from './decision-event-emit.js';
|
|
38
|
+
import { emitStatusSnapshot } from './status-emit.js';
|
|
39
|
+
import { computeStatusSnapshot } from './status-snapshot.js';
|
|
40
|
+
import { emitDriftAxes } from './drift-emit.js';
|
|
41
|
+
|
|
42
|
+
let _schemaValidator = null;
|
|
43
|
+
function getSchemaValidator() {
|
|
44
|
+
if (!_schemaValidator) _schemaValidator = new SchemaValidator();
|
|
45
|
+
return _schemaValidator;
|
|
46
|
+
}
|
|
35
47
|
|
|
48
|
+
import { randomUUID } from 'node:crypto';
|
|
36
49
|
import { getTargetRoot, resolveProjectPath, loadProjectConfig } from './project-root.js';
|
|
50
|
+
import { appendGateLogEntry, readGateLog, mapResolveOutcomeToSchema } from './gate-log-store.js';
|
|
51
|
+
import { addOpenLoop, resolveOpenLoop, listOpenLoops } from './open-loops-store.js';
|
|
37
52
|
|
|
38
53
|
const PROJECT_ROOT = getTargetRoot();
|
|
39
54
|
|
|
@@ -154,6 +169,19 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
154
169
|
}
|
|
155
170
|
});
|
|
156
171
|
|
|
172
|
+
// COMP-OBS-STATUS: GET /api/lifecycle/status?featureCode=<FC>
|
|
173
|
+
// Returns the latest StatusSnapshot for the given featureCode.
|
|
174
|
+
// Missing or unknown featureCode returns the no-feature snapshot (not 400/404).
|
|
175
|
+
app.get('/api/lifecycle/status', (req, res) => {
|
|
176
|
+
try {
|
|
177
|
+
const fc = req.query.featureCode || null;
|
|
178
|
+
const snapshot = computeStatusSnapshot(store, fc, new Date().toISOString());
|
|
179
|
+
res.json({ snapshot });
|
|
180
|
+
} catch (err) {
|
|
181
|
+
res.status(500).json({ error: err.message });
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
157
185
|
app.post('/api/vision/items/:id/lifecycle/start', (req, res) => {
|
|
158
186
|
try {
|
|
159
187
|
const { featureCode } = req.body;
|
|
@@ -171,9 +199,17 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
171
199
|
killedAt: null,
|
|
172
200
|
killReason: null,
|
|
173
201
|
};
|
|
202
|
+
// COMP-OBS-TIMELINE: populate phaseHistory before storing
|
|
203
|
+
appendPhaseHistory({ lifecycle }, { from: null, to: 'explore_design', outcome: null, timestamp: now });
|
|
174
204
|
store.updateLifecycle(req.params.id, lifecycle);
|
|
175
205
|
scheduleBroadcast();
|
|
176
206
|
broadcastMessage({ type: 'lifecycleStarted', itemId: req.params.id, phase: 'explore_design', featureCode, timestamp: now });
|
|
207
|
+
emitDecisionEvent(broadcastMessage, buildPhaseTransitionEvent({ featureCode, from: null, to: 'explore_design', outcome: null, timestamp: now }));
|
|
208
|
+
// COMP-OBS-DRIFT: emit drift axes before status so STATUS reads fresh drift_axes
|
|
209
|
+
const startedItem = store.items.get(req.params.id);
|
|
210
|
+
if (startedItem) emitDriftAxes(broadcastMessage, store, startedItem, projectRoot, now);
|
|
211
|
+
// COMP-OBS-STATUS: emit status snapshot after lifecycle started
|
|
212
|
+
emitStatusSnapshot(broadcastMessage, store, featureCode, now);
|
|
177
213
|
res.json(lifecycle);
|
|
178
214
|
} catch (err) {
|
|
179
215
|
const status = err.message.includes('not found') ? 404 : 400;
|
|
@@ -181,6 +217,38 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
181
217
|
}
|
|
182
218
|
});
|
|
183
219
|
|
|
220
|
+
// COMP-OBS-BRANCH: accept BranchLineage payloads from the CC-session watcher.
|
|
221
|
+
app.post('/api/vision/items/:id/lifecycle/branch-lineage', (req, res) => {
|
|
222
|
+
try {
|
|
223
|
+
const item = store.items.get(req.params.id);
|
|
224
|
+
if (!item) return res.status(404).json({ error: `Item not found: ${req.params.id}` });
|
|
225
|
+
|
|
226
|
+
const body = req.body || {};
|
|
227
|
+
const { valid, errors } = getSchemaValidator().validate('BranchLineage', body);
|
|
228
|
+
if (!valid) {
|
|
229
|
+
return res.status(400).json({ error: 'Invalid BranchLineage payload', details: errors });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const itemFC = item.lifecycle?.featureCode;
|
|
233
|
+
if (!itemFC) {
|
|
234
|
+
return res.status(400).json({ error: 'Item has no lifecycle.featureCode; cannot attach branch lineage.' });
|
|
235
|
+
}
|
|
236
|
+
if (body.feature_code !== itemFC) {
|
|
237
|
+
return res.status(400).json({
|
|
238
|
+
error: `Lineage feature_code (${body.feature_code}) does not match item featureCode (${itemFC}).`,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
store.updateLifecycleExt(req.params.id, 'branch_lineage', body);
|
|
243
|
+
scheduleBroadcast();
|
|
244
|
+
broadcastMessage({ type: 'branchLineageUpdate', itemId: req.params.id, ...body });
|
|
245
|
+
res.json({ ok: true, branch_lineage: body });
|
|
246
|
+
} catch (err) {
|
|
247
|
+
const status = err.message.includes('not found') ? 404 : 400;
|
|
248
|
+
res.status(status).json({ error: err.message });
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
184
252
|
app.post('/api/vision/items/:id/lifecycle/advance', (req, res) => {
|
|
185
253
|
try {
|
|
186
254
|
const { targetPhase, outcome } = req.body;
|
|
@@ -192,10 +260,17 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
192
260
|
if (!valid?.includes(targetPhase)) return res.status(400).json({ error: `Invalid transition: ${from} → ${targetPhase}` });
|
|
193
261
|
|
|
194
262
|
item.lifecycle.currentPhase = targetPhase;
|
|
195
|
-
|
|
263
|
+
// COMP-OBS-TIMELINE: populate phaseHistory + emit phase_transition DecisionEvent
|
|
196
264
|
const now = new Date().toISOString();
|
|
265
|
+
appendPhaseHistory(item, { from, to: targetPhase, outcome: outcome ?? null, timestamp: now });
|
|
266
|
+
store.updateLifecycle(req.params.id, item.lifecycle);
|
|
197
267
|
scheduleBroadcast();
|
|
198
268
|
broadcastMessage({ type: 'lifecycleTransition', itemId: req.params.id, from, to: targetPhase, outcome, timestamp: now });
|
|
269
|
+
emitDecisionEvent(broadcastMessage, buildPhaseTransitionEvent({ featureCode: item.lifecycle.featureCode, from, to: targetPhase, outcome, timestamp: now }));
|
|
270
|
+
// COMP-OBS-DRIFT: emit drift axes before status so STATUS reads fresh drift_axes
|
|
271
|
+
emitDriftAxes(broadcastMessage, store, item, projectRoot, now);
|
|
272
|
+
// COMP-OBS-STATUS: emit status snapshot after lifecycle transition (advance)
|
|
273
|
+
emitStatusSnapshot(broadcastMessage, store, item.lifecycle.featureCode, now);
|
|
199
274
|
res.json({ from, to: targetPhase, outcome });
|
|
200
275
|
} catch (err) {
|
|
201
276
|
const status = err.message.includes('not found') ? 404 : 400;
|
|
@@ -215,10 +290,17 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
215
290
|
if (!valid?.includes(targetPhase)) return res.status(400).json({ error: `Invalid transition: ${from} → ${targetPhase}` });
|
|
216
291
|
|
|
217
292
|
item.lifecycle.currentPhase = targetPhase;
|
|
218
|
-
|
|
293
|
+
// COMP-OBS-TIMELINE: populate phaseHistory + emit phase_transition DecisionEvent
|
|
219
294
|
const now = new Date().toISOString();
|
|
295
|
+
appendPhaseHistory(item, { from, to: targetPhase, outcome: 'skipped', timestamp: now });
|
|
296
|
+
store.updateLifecycle(req.params.id, item.lifecycle);
|
|
220
297
|
scheduleBroadcast();
|
|
221
298
|
broadcastMessage({ type: 'lifecycleTransition', itemId: req.params.id, from, to: targetPhase, outcome: 'skipped', timestamp: now });
|
|
299
|
+
emitDecisionEvent(broadcastMessage, buildPhaseTransitionEvent({ featureCode: item.lifecycle.featureCode, from, to: targetPhase, outcome: 'skipped', timestamp: now }));
|
|
300
|
+
// COMP-OBS-DRIFT: emit drift axes before status so STATUS reads fresh drift_axes
|
|
301
|
+
emitDriftAxes(broadcastMessage, store, item, projectRoot, now);
|
|
302
|
+
// COMP-OBS-STATUS: emit status snapshot after lifecycle transition (skip)
|
|
303
|
+
emitStatusSnapshot(broadcastMessage, store, item.lifecycle.featureCode, now);
|
|
222
304
|
res.json({ from, to: targetPhase, outcome: 'skipped', reason });
|
|
223
305
|
} catch (err) {
|
|
224
306
|
const status = err.message.includes('not found') ? 404 : 400;
|
|
@@ -238,10 +320,17 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
238
320
|
item.lifecycle.currentPhase = 'killed';
|
|
239
321
|
item.lifecycle.killedAt = now;
|
|
240
322
|
item.lifecycle.killReason = reason;
|
|
323
|
+
// COMP-OBS-TIMELINE: populate phaseHistory + emit phase_transition DecisionEvent
|
|
324
|
+
appendPhaseHistory(item, { from, to: 'killed', outcome: 'killed', timestamp: now });
|
|
241
325
|
store.updateLifecycle(req.params.id, item.lifecycle);
|
|
242
326
|
store.updateItem(req.params.id, { status: 'killed' });
|
|
243
327
|
scheduleBroadcast();
|
|
244
328
|
broadcastMessage({ type: 'lifecycleTransition', itemId: req.params.id, from, to: 'killed', outcome: 'killed', timestamp: now });
|
|
329
|
+
emitDecisionEvent(broadcastMessage, buildPhaseTransitionEvent({ featureCode: item.lifecycle.featureCode, from, to: 'killed', outcome: 'killed', timestamp: now }));
|
|
330
|
+
// COMP-OBS-DRIFT: emit drift axes before status so STATUS reads fresh drift_axes
|
|
331
|
+
emitDriftAxes(broadcastMessage, store, item, projectRoot, now);
|
|
332
|
+
// COMP-OBS-STATUS: emit status snapshot after lifecycle transition (kill)
|
|
333
|
+
emitStatusSnapshot(broadcastMessage, store, item.lifecycle.featureCode, now);
|
|
245
334
|
res.json({ phase: from, reason });
|
|
246
335
|
} catch (err) {
|
|
247
336
|
const status = err.message.includes('not found') ? 404 : 400;
|
|
@@ -260,10 +349,17 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
260
349
|
const now = new Date().toISOString();
|
|
261
350
|
item.lifecycle.currentPhase = 'complete';
|
|
262
351
|
item.lifecycle.completedAt = now;
|
|
352
|
+
// COMP-OBS-TIMELINE: populate phaseHistory + emit phase_transition DecisionEvent
|
|
353
|
+
appendPhaseHistory(item, { from: 'ship', to: 'complete', outcome: 'approved', timestamp: now });
|
|
263
354
|
store.updateLifecycle(req.params.id, item.lifecycle);
|
|
264
355
|
store.updateItem(req.params.id, { status: 'complete' });
|
|
265
356
|
scheduleBroadcast();
|
|
266
357
|
broadcastMessage({ type: 'lifecycleTransition', itemId: req.params.id, from: 'ship', to: 'complete', outcome: 'approved', timestamp: now });
|
|
358
|
+
emitDecisionEvent(broadcastMessage, buildPhaseTransitionEvent({ featureCode: item.lifecycle.featureCode, from: 'ship', to: 'complete', outcome: 'approved', timestamp: now }));
|
|
359
|
+
// COMP-OBS-DRIFT: emit drift axes before status so STATUS reads fresh drift_axes
|
|
360
|
+
emitDriftAxes(broadcastMessage, store, item, projectRoot, now);
|
|
361
|
+
// COMP-OBS-STATUS: emit status snapshot after lifecycle transition (complete)
|
|
362
|
+
emitStatusSnapshot(broadcastMessage, store, item.lifecycle.featureCode, now);
|
|
267
363
|
res.json({ completedAt: now });
|
|
268
364
|
} catch (err) {
|
|
269
365
|
const status = err.message.includes('not found') ? 404 : 400;
|
|
@@ -316,6 +412,22 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
316
412
|
store.updateLifecycle(req.params.id, item.lifecycle);
|
|
317
413
|
scheduleBroadcast();
|
|
318
414
|
broadcastMessage({ type: 'iterationStarted', itemId: req.params.id, loopId: item.lifecycle.iterationState.loopId, loopType, maxIterations: max, timestamp: now, startedAt: now, wallClockTimeout: timeoutMinutes, maxActions: maxActions ?? null });
|
|
415
|
+
// COMP-OBS-TIMELINE: emit iteration DecisionEvent at loop start
|
|
416
|
+
if (item.lifecycle.featureCode) {
|
|
417
|
+
emitDecisionEvent(broadcastMessage, buildIterationEvent({
|
|
418
|
+
featureCode: item.lifecycle.featureCode,
|
|
419
|
+
loopId: item.lifecycle.iterationState.loopId,
|
|
420
|
+
loopType,
|
|
421
|
+
stage: 'start',
|
|
422
|
+
attempt: null,
|
|
423
|
+
outcome: 'retry',
|
|
424
|
+
timestamp: now,
|
|
425
|
+
}));
|
|
426
|
+
// COMP-OBS-DRIFT: emit drift axes before status so STATUS reads fresh drift_axes
|
|
427
|
+
emitDriftAxes(broadcastMessage, store, item, projectRoot, now);
|
|
428
|
+
// COMP-OBS-STATUS: emit status snapshot after iteration started
|
|
429
|
+
emitStatusSnapshot(broadcastMessage, store, item.lifecycle.featureCode, now);
|
|
430
|
+
}
|
|
319
431
|
res.json(item.lifecycle.iterationState);
|
|
320
432
|
} catch (err) {
|
|
321
433
|
res.status(400).json({ error: err.message });
|
|
@@ -375,8 +487,31 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
375
487
|
const completeMsg = { type: 'iterationComplete', itemId: req.params.id, loopId: iter.loopId, loopType: iter.loopType, outcome: iter.outcome, finalCount: iter.count, timestamp: now };
|
|
376
488
|
if (iter.elapsedMinutes != null) completeMsg.elapsedMinutes = iter.elapsedMinutes;
|
|
377
489
|
broadcastMessage(completeMsg);
|
|
490
|
+
// COMP-OBS-TIMELINE: emit iteration DecisionEvent at loop complete (not per-attempt)
|
|
491
|
+
if (item.lifecycle.featureCode) {
|
|
492
|
+
emitDecisionEvent(broadcastMessage, buildIterationEvent({
|
|
493
|
+
featureCode: item.lifecycle.featureCode,
|
|
494
|
+
loopId: iter.loopId,
|
|
495
|
+
loopType: iter.loopType,
|
|
496
|
+
stage: 'complete',
|
|
497
|
+
attempt: iter.count,
|
|
498
|
+
outcome: iter.outcome,
|
|
499
|
+
timestamp: now,
|
|
500
|
+
}));
|
|
501
|
+
// COMP-OBS-DRIFT: emit drift axes before status so STATUS reads fresh drift_axes
|
|
502
|
+
emitDriftAxes(broadcastMessage, store, item, projectRoot, now);
|
|
503
|
+
// COMP-OBS-STATUS: emit status snapshot after iteration complete
|
|
504
|
+
emitStatusSnapshot(broadcastMessage, store, item.lifecycle.featureCode, now);
|
|
505
|
+
}
|
|
378
506
|
} else {
|
|
507
|
+
// per-attempt update: NO DecisionEvent (Decision 2 — would flood strip)
|
|
379
508
|
broadcastMessage({ type: 'iterationUpdate', itemId: req.params.id, loopId: iter.loopId, loopType: iter.loopType, count: iter.count, maxIterations: iter.maxIterations, exitCriteriaMet: false, findingsCount: result.findings?.length ?? 0, timestamp: now });
|
|
509
|
+
// COMP-OBS-STATUS: STATUS broadcasts on iterationUpdate per Decision 4 (TIMELINE does not)
|
|
510
|
+
if (item.lifecycle.featureCode) {
|
|
511
|
+
// COMP-OBS-DRIFT: emit drift axes before status so STATUS reads fresh drift_axes
|
|
512
|
+
emitDriftAxes(broadcastMessage, store, item, projectRoot, now);
|
|
513
|
+
emitStatusSnapshot(broadcastMessage, store, item.lifecycle.featureCode, now);
|
|
514
|
+
}
|
|
380
515
|
}
|
|
381
516
|
res.json({ continue: shouldContinue, count: iter.count, maxIterations: iter.maxIterations, outcome: iter.outcome });
|
|
382
517
|
} catch (err) {
|
|
@@ -405,6 +540,22 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
405
540
|
store.updateLifecycle(req.params.id, item.lifecycle);
|
|
406
541
|
scheduleBroadcast();
|
|
407
542
|
broadcastMessage({ type: 'iterationComplete', itemId: req.params.id, loopId: iter.loopId, loopType: iter.loopType, outcome: 'aborted', finalCount: iter.count, timestamp: now });
|
|
543
|
+
// COMP-OBS-TIMELINE: emit iteration DecisionEvent on abort (outcome maps to 'fail')
|
|
544
|
+
if (item.lifecycle.featureCode) {
|
|
545
|
+
emitDecisionEvent(broadcastMessage, buildIterationEvent({
|
|
546
|
+
featureCode: item.lifecycle.featureCode,
|
|
547
|
+
loopId: iter.loopId,
|
|
548
|
+
loopType: iter.loopType,
|
|
549
|
+
stage: 'complete',
|
|
550
|
+
attempt: iter.count,
|
|
551
|
+
outcome: 'aborted',
|
|
552
|
+
timestamp: now,
|
|
553
|
+
}));
|
|
554
|
+
// COMP-OBS-DRIFT: emit drift axes before status so STATUS reads fresh drift_axes
|
|
555
|
+
emitDriftAxes(broadcastMessage, store, item, projectRoot, now);
|
|
556
|
+
// COMP-OBS-STATUS: emit status snapshot after iteration abort
|
|
557
|
+
emitStatusSnapshot(broadcastMessage, store, item.lifecycle.featureCode, now);
|
|
558
|
+
}
|
|
408
559
|
res.json({ aborted: true });
|
|
409
560
|
} catch (err) {
|
|
410
561
|
res.status(400).json({ error: err.message });
|
|
@@ -511,6 +662,15 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
511
662
|
store.createGate(gate);
|
|
512
663
|
scheduleBroadcast();
|
|
513
664
|
broadcastMessage({ type: 'gateCreated', gateId: id, itemId: itemId || null, timestamp: gate.createdAt });
|
|
665
|
+
// COMP-OBS-STATUS: emit status snapshot after gate created
|
|
666
|
+
if (itemId) {
|
|
667
|
+
const gateItem = store.items.get(itemId);
|
|
668
|
+
if (gateItem?.lifecycle?.featureCode) {
|
|
669
|
+
// COMP-OBS-DRIFT: emit drift axes before status so STATUS reads fresh drift_axes
|
|
670
|
+
emitDriftAxes(broadcastMessage, store, gateItem, projectRoot, gate.createdAt);
|
|
671
|
+
emitStatusSnapshot(broadcastMessage, store, gateItem.lifecycle.featureCode, gate.createdAt);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
514
674
|
res.status(201).json(gate);
|
|
515
675
|
} catch (err) {
|
|
516
676
|
res.status(400).json({ error: err.message });
|
|
@@ -540,8 +700,21 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
540
700
|
// Normalize legacy outcome values
|
|
541
701
|
const outcomeMap = { approved: 'approve', killed: 'kill', revised: 'revise' };
|
|
542
702
|
const outcome = outcomeMap[rawOutcome] || rawOutcome;
|
|
703
|
+
// Whitelist — anything outside the route vocabulary is rejected so we
|
|
704
|
+
// can't persist contract-invalid GateLogEntry.decision values.
|
|
705
|
+
const VALID_RESOLVE_OUTCOMES = new Set(['approve', 'revise', 'kill']);
|
|
706
|
+
if (!VALID_RESOLVE_OUTCOMES.has(outcome)) {
|
|
707
|
+
return res.status(400).json({ error: `outcome must be one of: approve, revise, kill (got '${rawOutcome}')` });
|
|
708
|
+
}
|
|
543
709
|
const gate = store.gates.get(req.params.id);
|
|
544
710
|
if (!gate) return res.status(404).json({ error: `Gate not found: ${req.params.id}` });
|
|
711
|
+
// Lazy expiry parity with GET — expired gates can't be resolved or audited.
|
|
712
|
+
const gateTimeout = Number(process.env.COMPOSE_GATE_TIMEOUT) || 30 * 60 * 1000;
|
|
713
|
+
if (gate.status === 'pending' && (Date.now() - new Date(gate.createdAt).getTime()) > gateTimeout) {
|
|
714
|
+
gate.status = 'expired';
|
|
715
|
+
store._save();
|
|
716
|
+
return res.status(409).json({ error: `Gate ${req.params.id} has expired and cannot be resolved` });
|
|
717
|
+
}
|
|
545
718
|
// Idempotent: already-resolved gates return 200
|
|
546
719
|
if (gate.status !== 'pending') {
|
|
547
720
|
return res.status(200).json({ gateId: req.params.id, gateOutcome: gate.outcome });
|
|
@@ -551,7 +724,60 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
551
724
|
store.resolveGate(req.params.id, { outcome, comment, resolvedBy });
|
|
552
725
|
|
|
553
726
|
scheduleBroadcast();
|
|
554
|
-
|
|
727
|
+
const resolvedAt = new Date().toISOString();
|
|
728
|
+
broadcastMessage({ type: 'gateResolved', gateId: req.params.id, itemId: gate.itemId, outcome, timestamp: resolvedAt });
|
|
729
|
+
|
|
730
|
+
// COMP-OBS-GATELOG: persist audit entry + emit gate DecisionEvent (Decision 3 — emit-first-then-append).
|
|
731
|
+
// Skip when gate has no resolvable feature_code (Decision 1b).
|
|
732
|
+
const gateLogItem = gate.itemId ? store.items.get(gate.itemId) : null;
|
|
733
|
+
const gateFeatureCode = gateLogItem?.lifecycle?.featureCode ?? null;
|
|
734
|
+
if (gateFeatureCode) {
|
|
735
|
+
const entryId = randomUUID();
|
|
736
|
+
const schemaDecision = mapResolveOutcomeToSchema(outcome);
|
|
737
|
+
const createdAtMs = gate.createdAt ? Date.parse(gate.createdAt) : Date.parse(resolvedAt);
|
|
738
|
+
const durationMs = Date.parse(resolvedAt) - createdAtMs;
|
|
739
|
+
|
|
740
|
+
const gateEvent = buildGateEvent({
|
|
741
|
+
featureCode: gateFeatureCode,
|
|
742
|
+
gateLogEntryId: entryId,
|
|
743
|
+
gateId: req.params.id,
|
|
744
|
+
decision: outcome,
|
|
745
|
+
timestamp: resolvedAt,
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
// Emit first — if it throws, we still write but with decision_event_id: null
|
|
749
|
+
let emittedEventId = null;
|
|
750
|
+
try {
|
|
751
|
+
emitDecisionEvent(broadcastMessage, gateEvent);
|
|
752
|
+
emittedEventId = gateEvent.id;
|
|
753
|
+
} catch (emitErr) {
|
|
754
|
+
console.warn('[vision-routes] gate DecisionEvent emit failed:', emitErr.message);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/** @type {import('./gate-log-store.js').GateLogEntry} */
|
|
758
|
+
const entry = {
|
|
759
|
+
id: entryId,
|
|
760
|
+
gate_id: req.params.id,
|
|
761
|
+
decision: schemaDecision,
|
|
762
|
+
operator: gate.resolvedBy ?? null,
|
|
763
|
+
duration_to_decide_ms: durationMs >= 0 ? durationMs : null,
|
|
764
|
+
timestamp: resolvedAt,
|
|
765
|
+
feature_code: gateFeatureCode,
|
|
766
|
+
decision_event_id: emittedEventId,
|
|
767
|
+
};
|
|
768
|
+
appendGateLogEntry(entry);
|
|
769
|
+
}
|
|
770
|
+
// else: featureless gate — skip both writes (Decision 1b)
|
|
771
|
+
|
|
772
|
+
// COMP-OBS-STATUS: emit status snapshot after gate resolved
|
|
773
|
+
if (gate.itemId) {
|
|
774
|
+
const resolvedItem = store.items.get(gate.itemId);
|
|
775
|
+
if (resolvedItem?.lifecycle?.featureCode) {
|
|
776
|
+
// COMP-OBS-DRIFT: emit drift axes before status so STATUS reads fresh drift_axes
|
|
777
|
+
emitDriftAxes(broadcastMessage, store, resolvedItem, projectRoot, resolvedAt);
|
|
778
|
+
emitStatusSnapshot(broadcastMessage, store, resolvedItem.lifecycle.featureCode, resolvedAt);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
555
781
|
res.json({ gateId: req.params.id, gateOutcome: outcome });
|
|
556
782
|
} catch (err) {
|
|
557
783
|
const status = err.message.includes('not found') ? 404 : 400;
|
|
@@ -559,6 +785,94 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
559
785
|
}
|
|
560
786
|
});
|
|
561
787
|
|
|
788
|
+
// ── COMP-OBS-LOOPS routes ─────────────────────────────────────────────────
|
|
789
|
+
|
|
790
|
+
// GET /api/vision/items/:id/loops — list open loops for an item
|
|
791
|
+
app.get('/api/vision/items/:id/loops', (req, res) => {
|
|
792
|
+
try {
|
|
793
|
+
const item = store.items.get(req.params.id);
|
|
794
|
+
if (!item) return res.status(404).json({ error: `Item not found: ${req.params.id}` });
|
|
795
|
+
const includeResolved = req.query.includeResolved === 'true';
|
|
796
|
+
const loops = listOpenLoops(item, { includeResolved });
|
|
797
|
+
res.json({ loops });
|
|
798
|
+
} catch (err) {
|
|
799
|
+
res.status(500).json({ error: err.message });
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
// POST /api/vision/items/:id/loops — add a new open loop
|
|
804
|
+
app.post('/api/vision/items/:id/loops', (req, res) => {
|
|
805
|
+
try {
|
|
806
|
+
const item = store.items.get(req.params.id);
|
|
807
|
+
if (!item) return res.status(404).json({ error: `Item not found: ${req.params.id}` });
|
|
808
|
+
if (!item.lifecycle?.featureCode) {
|
|
809
|
+
return res.status(400).json({ error: 'item has no lifecycle.featureCode — cannot create feature-scoped open loop' });
|
|
810
|
+
}
|
|
811
|
+
const { kind, summary, parent_branch, ttl_days } = req.body;
|
|
812
|
+
// Schema-aligned validation (CONTRACT v0.2.3 OpenLoop)
|
|
813
|
+
const VALID_KINDS = new Set(['deferred', 'blocked', 'open_question']);
|
|
814
|
+
if (!kind) return res.status(400).json({ error: 'kind is required' });
|
|
815
|
+
if (!VALID_KINDS.has(kind)) {
|
|
816
|
+
return res.status(400).json({ error: `kind must be one of: deferred, blocked, open_question (got '${kind}')` });
|
|
817
|
+
}
|
|
818
|
+
if (typeof summary !== 'string' || summary.length === 0) {
|
|
819
|
+
return res.status(400).json({ error: 'summary is required (non-empty string)' });
|
|
820
|
+
}
|
|
821
|
+
if (summary.length > 280) {
|
|
822
|
+
return res.status(400).json({ error: 'summary exceeds 280-char schema limit' });
|
|
823
|
+
}
|
|
824
|
+
if (ttl_days !== undefined && (!Number.isInteger(ttl_days) || ttl_days < 0)) {
|
|
825
|
+
return res.status(400).json({ error: 'ttl_days must be a non-negative integer' });
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const { loop, nextLoops } = addOpenLoop(item, { kind, summary, parent_branch, ttl_days });
|
|
829
|
+
store.updateLifecycleExt(item.id, 'open_loops', nextLoops);
|
|
830
|
+
|
|
831
|
+
const updatedItem = store.items.get(item.id);
|
|
832
|
+
broadcastMessage({ type: 'openLoopsUpdate', itemId: item.id, loops: nextLoops });
|
|
833
|
+
const featureCode = updatedItem.lifecycle.featureCode;
|
|
834
|
+
const now = new Date().toISOString();
|
|
835
|
+
// COMP-OBS-DRIFT: emit drift axes before status so STATUS reads fresh drift_axes
|
|
836
|
+
emitDriftAxes(broadcastMessage, store, updatedItem, projectRoot, now);
|
|
837
|
+
emitStatusSnapshot(broadcastMessage, store, featureCode, now);
|
|
838
|
+
|
|
839
|
+
res.status(201).json({ loop });
|
|
840
|
+
} catch (err) {
|
|
841
|
+
const status = err.status || (err.message.includes('not found') ? 404 : 400);
|
|
842
|
+
res.status(status).json({ error: err.message });
|
|
843
|
+
}
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
// POST /api/vision/items/:id/loops/:loopId/resolve — resolve a loop
|
|
847
|
+
app.post('/api/vision/items/:id/loops/:loopId/resolve', (req, res) => {
|
|
848
|
+
try {
|
|
849
|
+
const item = store.items.get(req.params.id);
|
|
850
|
+
if (!item) return res.status(404).json({ error: `Item not found: ${req.params.id}` });
|
|
851
|
+
const { note, resolved_by } = req.body;
|
|
852
|
+
|
|
853
|
+
const { loop, nextLoops } = resolveOpenLoop(item, req.params.loopId, {
|
|
854
|
+
note,
|
|
855
|
+
resolved_by: resolved_by || process.env.USER || 'unknown',
|
|
856
|
+
});
|
|
857
|
+
store.updateLifecycleExt(item.id, 'open_loops', nextLoops);
|
|
858
|
+
|
|
859
|
+
broadcastMessage({ type: 'openLoopsUpdate', itemId: item.id, loops: nextLoops });
|
|
860
|
+
const featureCode = item.lifecycle?.featureCode;
|
|
861
|
+
if (featureCode) {
|
|
862
|
+
const now = new Date().toISOString();
|
|
863
|
+
// COMP-OBS-DRIFT: emit drift axes before status so STATUS reads fresh drift_axes
|
|
864
|
+
const resolvedLoopItem = store.items.get(item.id);
|
|
865
|
+
if (resolvedLoopItem) emitDriftAxes(broadcastMessage, store, resolvedLoopItem, projectRoot, now);
|
|
866
|
+
emitStatusSnapshot(broadcastMessage, store, featureCode, now);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
res.json({ loop });
|
|
870
|
+
} catch (err) {
|
|
871
|
+
const status = err.status || (err.message.includes('not found') ? 404 : 400);
|
|
872
|
+
res.status(status).json({ error: err.message });
|
|
873
|
+
}
|
|
874
|
+
});
|
|
875
|
+
|
|
562
876
|
// GET /api/vision/summary — structured board summary
|
|
563
877
|
app.get('/api/vision/summary', (_req, res) => {
|
|
564
878
|
const { items, connections } = store.getState();
|
|
@@ -635,6 +949,20 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
|
|
|
635
949
|
res.json({ ok: true });
|
|
636
950
|
});
|
|
637
951
|
|
|
952
|
+
// GET /api/lifecycle/budget?featureCode=<FC> — COMP-OBS-STEPDETAIL
|
|
953
|
+
// Returns per-loop-type budget breakdown from ledger + settings.
|
|
954
|
+
// v1 limitation: usedIterations is feature-wide (ledger doesn't split by loopType).
|
|
955
|
+
app.get('/api/lifecycle/budget', (req, res) => {
|
|
956
|
+
const { featureCode } = req.query;
|
|
957
|
+
if (!featureCode) {
|
|
958
|
+
return res.status(400).json({ error: 'featureCode query param is required' });
|
|
959
|
+
}
|
|
960
|
+
const composeDir = path.join(projectRoot, '.compose');
|
|
961
|
+
const settings = settingsStore?.get();
|
|
962
|
+
const budget = readBudget(composeDir, featureCode, settings);
|
|
963
|
+
res.json(budget);
|
|
964
|
+
});
|
|
965
|
+
|
|
638
966
|
// POST /api/plan/parse — extract file paths from plan/spec markdown
|
|
639
967
|
app.post('/api/plan/parse', (req, res) => {
|
|
640
968
|
const { filePath, itemId } = req.body || {};
|
package/server/vision-server.js
CHANGED
|
@@ -18,13 +18,17 @@ import { AgentRegistry } from './agent-registry.js';
|
|
|
18
18
|
import { HealthMonitor } from './agent-health.js';
|
|
19
19
|
import { WorktreeGC } from './worktree-gc.js';
|
|
20
20
|
import { attachVisionRoutes } from './vision-routes.js';
|
|
21
|
+
import { deriveDecisionEvents } from './decision-events-snapshot.js';
|
|
22
|
+
import { CCSessionWatcher } from './cc-session-watcher.js';
|
|
23
|
+
import { emitStatusSnapshot } from './status-emit.js';
|
|
24
|
+
import { emitDriftAxes } from './drift-emit.js';
|
|
25
|
+
import { SchemaValidator } from './schema-validator.js';
|
|
21
26
|
import { attachSessionRoutes } from './session-routes.js';
|
|
22
27
|
import { attachActivityRoutes } from './activity-routes.js';
|
|
23
28
|
import { SettingsStore } from './settings-store.js';
|
|
24
29
|
import { attachSettingsRoutes } from './settings-routes.js';
|
|
25
30
|
import { attachDesignRoutes } from './design-routes.js';
|
|
26
31
|
import { DesignSessionManager } from './design-session.js';
|
|
27
|
-
import { ClaudeSDKConnector } from './connectors/claude-sdk-connector.js';
|
|
28
32
|
import { attachPipelineRoutes } from './pipeline-routes.js';
|
|
29
33
|
import { attachIdeaboxRoutes } from './ideabox-routes.js';
|
|
30
34
|
import { CoalescingBuffer } from './coalescing-buffer.js';
|
|
@@ -126,8 +130,6 @@ export class VisionServer {
|
|
|
126
130
|
// Re-resolve on every call so project switches get fresh instances.
|
|
127
131
|
let _designSessionManager = null;
|
|
128
132
|
let _designDataDir = null;
|
|
129
|
-
let _designConnector = null;
|
|
130
|
-
let _designCwd = null;
|
|
131
133
|
|
|
132
134
|
attachDesignRoutes(app, {
|
|
133
135
|
getSessionManager: () => {
|
|
@@ -139,14 +141,6 @@ export class VisionServer {
|
|
|
139
141
|
}
|
|
140
142
|
return _designSessionManager;
|
|
141
143
|
},
|
|
142
|
-
getConnector: () => {
|
|
143
|
-
const cwd = getTargetRoot();
|
|
144
|
-
if (cwd !== _designCwd) {
|
|
145
|
-
_designConnector = new ClaudeSDKConnector({ cwd });
|
|
146
|
-
_designCwd = cwd;
|
|
147
|
-
}
|
|
148
|
-
return _designConnector;
|
|
149
|
-
},
|
|
150
144
|
getProjectRoot: () => getTargetRoot(),
|
|
151
145
|
});
|
|
152
146
|
|
|
@@ -244,6 +238,81 @@ export class VisionServer {
|
|
|
244
238
|
});
|
|
245
239
|
}
|
|
246
240
|
|
|
241
|
+
// ── COMP-OBS-BRANCH: CC-session watcher (opt-in) ──────────────────────
|
|
242
|
+
// Default OFF. Enable by setting `capabilities.cc_session_watcher: true` in compose.json
|
|
243
|
+
// or by setting the `CC_SESSION_WATCHER=1` env var. When enabled, Forge reads
|
|
244
|
+
// `~/.claude/projects/**/*.jsonl` and emits BranchLineage + DecisionEvents tied to the
|
|
245
|
+
// current feature via sessions.json's transcriptPath basename.
|
|
246
|
+
const ccWatcherEnabled =
|
|
247
|
+
this._config.capabilities?.cc_session_watcher === true ||
|
|
248
|
+
process.env.CC_SESSION_WATCHER === '1';
|
|
249
|
+
if (ccWatcherEnabled) {
|
|
250
|
+
try {
|
|
251
|
+
const projectsRoot = process.env.CC_PROJECTS_ROOT ||
|
|
252
|
+
path.join(process.env.HOME || process.env.USERPROFILE || '', '.claude', 'projects');
|
|
253
|
+
const sessionsFile = this.sessionManager?.sessionsFile || path.join(getDataDir(), 'sessions.json');
|
|
254
|
+
const featureRoot = path.join(getTargetRoot(), 'docs', 'features');
|
|
255
|
+
|
|
256
|
+
const findItemIdByFeatureCode = (fc) => {
|
|
257
|
+
for (const it of this.store.items.values()) {
|
|
258
|
+
if (it.lifecycle?.featureCode === fc) return it.id;
|
|
259
|
+
}
|
|
260
|
+
return null;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const schemaValidator = new SchemaValidator();
|
|
264
|
+
const postBranchLineage = async (itemId, lineage) => {
|
|
265
|
+
// Validate at the producer boundary — the watcher path bypasses the HTTP
|
|
266
|
+
// route, so we must re-check here to guarantee contract conformance.
|
|
267
|
+
const { valid, errors } = schemaValidator.validate('BranchLineage', lineage);
|
|
268
|
+
if (!valid) {
|
|
269
|
+
throw new Error(`Invalid BranchLineage payload: ${JSON.stringify(errors)}`);
|
|
270
|
+
}
|
|
271
|
+
const item = this.store.items.get(itemId);
|
|
272
|
+
const itemFC = item?.lifecycle?.featureCode;
|
|
273
|
+
if (!itemFC || itemFC !== lineage.feature_code) {
|
|
274
|
+
throw new Error(
|
|
275
|
+
`feature_code mismatch: lineage=${lineage.feature_code} item=${itemFC || '<none>'}`
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
this.store.updateLifecycleExt(itemId, 'branch_lineage', lineage);
|
|
279
|
+
this.scheduleBroadcast();
|
|
280
|
+
this.broadcastMessage({ type: 'branchLineageUpdate', itemId, ...lineage });
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
this._ccWatcher = new CCSessionWatcher({
|
|
284
|
+
projectsRoot,
|
|
285
|
+
sessionsFile,
|
|
286
|
+
featureRoot,
|
|
287
|
+
findItemIdByFeatureCode,
|
|
288
|
+
postBranchLineage,
|
|
289
|
+
broadcastMessage: (msg) => this.broadcastMessage(msg),
|
|
290
|
+
// COMP-OBS-STATUS: inject emitStatusSnapshot for post-lineage status broadcast
|
|
291
|
+
emitStatusSnapshot,
|
|
292
|
+
getState: () => this.store,
|
|
293
|
+
// COMP-OBS-DRIFT: inject emitDriftAxes for post-lineage drift broadcast
|
|
294
|
+
emitDriftAxes,
|
|
295
|
+
projectRoot: getTargetRoot(),
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// Seed emitted_event_ids from any existing lineage so startup doesn't replay.
|
|
299
|
+
for (const it of this.store.items.values()) {
|
|
300
|
+
const l = it.lifecycle?.lifecycle_ext?.branch_lineage;
|
|
301
|
+
if (l?.feature_code && Array.isArray(l.emitted_event_ids)) {
|
|
302
|
+
this._ccWatcher.seedEmittedEventIds(l.feature_code, l.emitted_event_ids);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
this._ccWatcher.fullScan().catch(err => {
|
|
307
|
+
console.warn('[vision] cc-session-watcher initial scan failed:', err.message);
|
|
308
|
+
});
|
|
309
|
+
this._ccWatcher.start();
|
|
310
|
+
console.log(`[vision] cc-session-watcher enabled (projectsRoot=${projectsRoot})`);
|
|
311
|
+
} catch (err) {
|
|
312
|
+
console.warn('[vision] cc-session-watcher failed to start:', err.message);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
247
316
|
// ── Haiku summary broadcast ─────────────────────────────────────────────
|
|
248
317
|
if (this.sessionManager) {
|
|
249
318
|
this.sessionManager.onSummary((summary) => {
|
|
@@ -307,10 +376,33 @@ export class VisionServer {
|
|
|
307
376
|
if ('type' in state) {
|
|
308
377
|
throw new Error('store.getState() must not include a `type` field — would collide with hydrate envelope');
|
|
309
378
|
}
|
|
379
|
+
|
|
380
|
+
// COMP-OBS-TIMELINE: derive DecisionEvents for the active feature
|
|
381
|
+
// (derive from all features present in state — client filters by featureCode)
|
|
382
|
+
let decisionEventsSnapshot = [];
|
|
383
|
+
try {
|
|
384
|
+
const internalState = this.store;
|
|
385
|
+
// Use internal items Map for deriveDecisionEvents (avoids serialization round-trip)
|
|
386
|
+
const featureCodes = new Set();
|
|
387
|
+
for (const item of (internalState.items?.values?.() || [])) {
|
|
388
|
+
if (item?.lifecycle?.featureCode) featureCodes.add(item.lifecycle.featureCode);
|
|
389
|
+
}
|
|
390
|
+
for (const fc of featureCodes) {
|
|
391
|
+
decisionEventsSnapshot = decisionEventsSnapshot.concat(
|
|
392
|
+
deriveDecisionEvents(internalState, fc)
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
} catch (snapshotErr) {
|
|
396
|
+
console.error('[vision] decisionEventsSnapshot derivation error:', snapshotErr.message);
|
|
397
|
+
}
|
|
398
|
+
|
|
310
399
|
const snapshot = Object.assign(
|
|
311
400
|
{ type: 'hydrate' },
|
|
312
401
|
state,
|
|
313
|
-
{
|
|
402
|
+
{
|
|
403
|
+
sessions: this.sessionManager?.getRecentSessions?.() || [],
|
|
404
|
+
decisionEventsSnapshot,
|
|
405
|
+
}
|
|
314
406
|
);
|
|
315
407
|
ws.send(JSON.stringify(snapshot));
|
|
316
408
|
} catch (err) {
|
package/server/vision-store.js
CHANGED
|
@@ -217,7 +217,13 @@ export class VisionStore {
|
|
|
217
217
|
updateLifecycle(id, lifecycle) {
|
|
218
218
|
const item = this.items.get(id);
|
|
219
219
|
if (!item) throw new Error(`Item not found: ${id}`);
|
|
220
|
+
const priorExt = item.lifecycle?.lifecycle_ext;
|
|
220
221
|
item.lifecycle = lifecycle;
|
|
222
|
+
// Preserve lifecycle_ext across partial-update callers (Wave 6 additive slot).
|
|
223
|
+
// A caller that truly wants to clear it passes `lifecycle_ext: {}` or `null` explicitly.
|
|
224
|
+
if (priorExt && lifecycle && !('lifecycle_ext' in lifecycle)) {
|
|
225
|
+
item.lifecycle.lifecycle_ext = priorExt;
|
|
226
|
+
}
|
|
221
227
|
// Re-derive group when lifecycle gains a featureCode
|
|
222
228
|
if (lifecycle?.featureCode && !item.group) {
|
|
223
229
|
item.group = deriveGroup(item.title, lifecycle.featureCode);
|
|
@@ -228,6 +234,21 @@ export class VisionStore {
|
|
|
228
234
|
return item;
|
|
229
235
|
}
|
|
230
236
|
|
|
237
|
+
/** Merge a single lifecycle_ext.<key> = value slot without touching other extension keys.
|
|
238
|
+
* Wave 6 callers (BRANCH, OPEN-LOOPS, DRIFT, STATUS) use this rather than reaching into
|
|
239
|
+
* item.lifecycle.lifecycle_ext directly. */
|
|
240
|
+
updateLifecycleExt(id, key, value) {
|
|
241
|
+
const item = this.items.get(id);
|
|
242
|
+
if (!item) throw new Error(`Item not found: ${id}`);
|
|
243
|
+
if (!item.lifecycle) item.lifecycle = {};
|
|
244
|
+
if (!item.lifecycle.lifecycle_ext) item.lifecycle.lifecycle_ext = {};
|
|
245
|
+
item.lifecycle.lifecycle_ext[key] = value;
|
|
246
|
+
item.updatedAt = new Date().toISOString();
|
|
247
|
+
this.items.set(id, item);
|
|
248
|
+
this._save();
|
|
249
|
+
return item;
|
|
250
|
+
}
|
|
251
|
+
|
|
231
252
|
/** Delete an item and all its connections and gates */
|
|
232
253
|
deleteItem(id) {
|
|
233
254
|
if (!this.items.has(id)) throw new Error(`Item not found: ${id}`);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{aq as o,ar as n}from"./index-DReRlzZI.js";const t=(r,a)=>o.lang.round(n.parse(r)[a]);export{t as c};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{s as a,c as s,a as e,C as t}from"./chunk-4TB4RGXK-DE0WBDkj.js";import{_ as i}from"./index-DReRlzZI.js";import"./chunk-FMBD7UC4-CTDIPA3p.js";import"./chunk-YZCP3GAM-ojGkzcZK.js";import"./chunk-55IACEB6-CE1EXenG.js";import"./chunk-EDXVE4YY-DA7Ana6H.js";var u={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{u as diagram};
|