@smartmemory/compose 0.1.0
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/LICENSE +21 -0
- package/README.md +1014 -0
- package/bin/compose.js +1515 -0
- package/dist/assets/_baseUniq-CQwX6VLz.js +1 -0
- package/dist/assets/arc-SxJ2J1sh.js +1 -0
- package/dist/assets/architectureDiagram-Q4EWVU46-BykunY1F.js +36 -0
- package/dist/assets/blockDiagram-DXYQGD6D-ohAKBOUw.js +132 -0
- package/dist/assets/c4Diagram-AHTNJAMY-DBDC3ENB.js +10 -0
- package/dist/assets/channel-DGElom1e.js +1 -0
- package/dist/assets/chunk-4BX2VUAB-Cv93Z7uM.js +1 -0
- package/dist/assets/chunk-4TB4RGXK-DE0WBDkj.js +206 -0
- package/dist/assets/chunk-55IACEB6-CE1EXenG.js +1 -0
- package/dist/assets/chunk-EDXVE4YY-DA7Ana6H.js +1 -0
- package/dist/assets/chunk-FMBD7UC4-CTDIPA3p.js +15 -0
- package/dist/assets/chunk-OYMX7WX6-uGBaPaTX.js +231 -0
- package/dist/assets/chunk-QZHKN3VN-CYlnXuUO.js +1 -0
- package/dist/assets/chunk-YZCP3GAM-ojGkzcZK.js +1 -0
- package/dist/assets/classDiagram-6PBFFD2Q-KqWP9wWZ.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-KqWP9wWZ.js +1 -0
- package/dist/assets/clone-DUJKJXd7.js +1 -0
- package/dist/assets/cose-bilkent-S5V4N54A-Bktn9hL-.js +1 -0
- package/dist/assets/dagre-KV5264BT-DFaSzuRF.js +4 -0
- package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/assets/diagram-5BDNPKRD-DnfmDzEm.js +10 -0
- package/dist/assets/diagram-G4DWMVQ6-Bm8W9YnG.js +24 -0
- package/dist/assets/diagram-MMDJMWI5-B5-TSKvp.js +43 -0
- package/dist/assets/diagram-TYMM5635-ls4rqlky.js +24 -0
- package/dist/assets/erDiagram-SMLLAGMA-giG6WO-r.js +85 -0
- package/dist/assets/flowDiagram-DWJPFMVM-XvlUuz-7.js +162 -0
- package/dist/assets/ganttDiagram-T4ZO3ILL-hLBV57oV.js +292 -0
- package/dist/assets/gitGraphDiagram-UUTBAWPF-BHu3s_Gn.js +106 -0
- package/dist/assets/graph-D0Cfv00Y.js +1 -0
- package/dist/assets/index-CUd6pFGF.css +1 -0
- package/dist/assets/index-DReRlzZI.js +1144 -0
- package/dist/assets/infoDiagram-42DDH7IO-DbqRsOo3.js +2 -0
- package/dist/assets/init-Gi6I4Gst.js +1 -0
- package/dist/assets/ishikawaDiagram-UXIWVN3A-DnCdx7zb.js +70 -0
- package/dist/assets/journeyDiagram-VCZTEJTY-CfD7eNcP.js +139 -0
- package/dist/assets/kanban-definition-6JOO6SKY-BYaO9-mK.js +89 -0
- package/dist/assets/katex-DkKDou_j.js +257 -0
- package/dist/assets/layout-Bj72wOEB.js +1 -0
- package/dist/assets/linear-BRFo114D.js +1 -0
- package/dist/assets/min-GCHnKlJS.js +1 -0
- package/dist/assets/mindmap-definition-QFDTVHPH-n0PMebY4.js +96 -0
- package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/dist/assets/pieDiagram-DEJITSTG-pN4CljHF.js +30 -0
- package/dist/assets/quadrantDiagram-34T5L4WZ-DNoAy8-D.js +7 -0
- package/dist/assets/requirementDiagram-MS252O5E-BhtY05PT.js +84 -0
- package/dist/assets/sankeyDiagram-XADWPNL6-B6AD-16A.js +10 -0
- package/dist/assets/sequenceDiagram-FGHM5R23-DShHM-uk.js +157 -0
- package/dist/assets/stateDiagram-FHFEXIEX-DMxn7HTo.js +1 -0
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-o6PnCs4e.js +1 -0
- package/dist/assets/timeline-definition-GMOUNBTQ-Cdu6uq52.js +120 -0
- package/dist/assets/vennDiagram-DHZGUBPP-CpK29iRe.js +34 -0
- package/dist/assets/wardley-RL74JXVD-BQgSkdcO.js +162 -0
- package/dist/assets/wardleyDiagram-NUSXRM2D-DJHYev6O.js +20 -0
- package/dist/assets/xychartDiagram-5P7HB3ND-1d75pbaO.js +7 -0
- package/dist/index.html +30 -0
- package/lib/agent-chains.js +65 -0
- package/lib/agent-string.js +86 -0
- package/lib/budget-ledger.js +86 -0
- package/lib/build-all.js +162 -0
- package/lib/build-dag.js +120 -0
- package/lib/build-stream-writer.js +190 -0
- package/lib/build.js +2997 -0
- package/lib/capability-checker.js +53 -0
- package/lib/cert-inject.js +38 -0
- package/lib/cli-progress.js +483 -0
- package/lib/constants.js +69 -0
- package/lib/cross-layer-audit.js +84 -0
- package/lib/debug-discipline.js +173 -0
- package/lib/feature-json.js +106 -0
- package/lib/gate-prompt.js +291 -0
- package/lib/gate-tiers.js +194 -0
- package/lib/health-history.js +119 -0
- package/lib/health-score.js +227 -0
- package/lib/ideabox.js +570 -0
- package/lib/import.js +244 -0
- package/lib/migrate-roadmap.js +94 -0
- package/lib/model-pricing.js +67 -0
- package/lib/new.js +413 -0
- package/lib/pipeline-cli.js +489 -0
- package/lib/plan-parser.js +103 -0
- package/lib/qa-scoping.js +474 -0
- package/lib/questionnaire.js +200 -0
- package/lib/resolve-port.js +7 -0
- package/lib/result-normalizer.js +349 -0
- package/lib/review-lenses.js +166 -0
- package/lib/roadmap-gen.js +210 -0
- package/lib/roadmap-parser.js +176 -0
- package/lib/server-probe.js +23 -0
- package/lib/staleness.js +87 -0
- package/lib/step-prompt.js +260 -0
- package/lib/step-validator.js +49 -0
- package/lib/stratum-mcp-client.js +365 -0
- package/lib/team-flag.js +46 -0
- package/lib/test-bootstrap.js +401 -0
- package/lib/triage.js +274 -0
- package/lib/vision-writer.js +391 -0
- package/package.json +111 -0
- package/pipelines/bug-fix.stratum.yaml +230 -0
- package/pipelines/build.stratum.yaml +498 -0
- package/pipelines/content.stratum.yaml +112 -0
- package/pipelines/coverage-sweep.stratum.yaml +52 -0
- package/pipelines/refactor.stratum.yaml +169 -0
- package/pipelines/research.stratum.yaml +88 -0
- package/pipelines/review-fix.stratum.yaml +109 -0
- package/presets/team-feature.stratum.yaml +105 -0
- package/presets/team-research.stratum.yaml +108 -0
- package/presets/team-review.stratum.yaml +106 -0
- package/scripts/agent-activity-hook.sh +31 -0
- package/scripts/agent-error-hook.sh +28 -0
- package/scripts/analyze-orphans.mjs +50 -0
- package/scripts/find-orphans.mjs +26 -0
- package/scripts/fix-phases.mjs +49 -0
- package/scripts/generate-stratum-spec.mjs +137 -0
- package/scripts/import-roadmap.mjs +116 -0
- package/scripts/phase-audit.mjs +33 -0
- package/scripts/run-pipeline.mjs +314 -0
- package/scripts/session-end-hook.sh +18 -0
- package/scripts/session-start-hook.sh +38 -0
- package/scripts/vision-hook.sh +104 -0
- package/scripts/vision-track.mjs +554 -0
- package/scripts/wire-all-orphans.mjs +108 -0
- package/scripts/wire-orphans.mjs +164 -0
- package/server/activity-routes.js +123 -0
- package/server/agent-health.js +197 -0
- package/server/agent-hooks.js +102 -0
- package/server/agent-mcp.js +10 -0
- package/server/agent-registry.js +95 -0
- package/server/agent-server.js +290 -0
- package/server/agent-spawn.js +251 -0
- package/server/agent-templates.js +77 -0
- package/server/artifact-manager.js +247 -0
- package/server/artifact-templates/architecture.md +28 -0
- package/server/artifact-templates/blueprint.md +21 -0
- package/server/artifact-templates/design.md +36 -0
- package/server/artifact-templates/plan.md +25 -0
- package/server/artifact-templates/prd.md +43 -0
- package/server/artifact-templates/report.md +40 -0
- package/server/block-tracker.js +90 -0
- package/server/build-stream-bridge.js +502 -0
- package/server/coalescing-buffer.js +46 -0
- package/server/compose-mcp-tools.js +479 -0
- package/server/compose-mcp.js +324 -0
- package/server/connectors/agent-connector.js +78 -0
- package/server/connectors/claude-sdk-connector.js +198 -0
- package/server/connectors/codex-connector.js +240 -0
- package/server/connectors/connector-discovery.js +18 -0
- package/server/connectors/connector-runtime.js +13 -0
- package/server/connectors/opencode-connector.js +200 -0
- package/server/design-routes.js +540 -0
- package/server/design-session.js +161 -0
- package/server/feature-scan.js +593 -0
- package/server/file-watcher.js +284 -0
- package/server/find-root.js +29 -0
- package/server/graph-export.js +343 -0
- package/server/ideabox-cache.js +77 -0
- package/server/ideabox-routes.js +294 -0
- package/server/index.js +156 -0
- package/server/model-tiers.js +49 -0
- package/server/pipeline-routes.js +288 -0
- package/server/policy-evaluator.js +36 -0
- package/server/project-root.js +122 -0
- package/server/security.js +23 -0
- package/server/session-manager.js +403 -0
- package/server/session-routes.js +190 -0
- package/server/session-store.js +107 -0
- package/server/settings-routes.js +35 -0
- package/server/settings-store.js +234 -0
- package/server/stratum-api.js +102 -0
- package/server/stratum-client.js +192 -0
- package/server/stratum-sync.js +193 -0
- package/server/summarizer.js +139 -0
- package/server/supervisor.js +196 -0
- package/server/vision-routes.js +668 -0
- package/server/vision-server.js +393 -0
- package/server/vision-store.js +360 -0
- package/server/vision-utils.js +179 -0
- package/server/worktree-gc.js +137 -0
- package/templates/ROADMAP.md +46 -0
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vision-routes.js — Vision CRUD + plan/parse routes.
|
|
3
|
+
*
|
|
4
|
+
* Routes:
|
|
5
|
+
* GET /api/vision/items
|
|
6
|
+
* POST /api/vision/items
|
|
7
|
+
* PATCH /api/vision/items/:id
|
|
8
|
+
* DELETE /api/vision/items/:id
|
|
9
|
+
* GET /api/vision/items/:id
|
|
10
|
+
* POST /api/vision/connections
|
|
11
|
+
* DELETE /api/vision/connections/:id
|
|
12
|
+
* GET /api/vision/summary
|
|
13
|
+
* GET /api/vision/blocked
|
|
14
|
+
* POST /api/vision/ui
|
|
15
|
+
* POST /api/plan/parse
|
|
16
|
+
* GET /api/vision/items/:id/lifecycle
|
|
17
|
+
* POST /api/vision/items/:id/lifecycle/start
|
|
18
|
+
* POST /api/vision/items/:id/lifecycle/advance
|
|
19
|
+
* POST /api/vision/items/:id/lifecycle/skip
|
|
20
|
+
* POST /api/vision/items/:id/lifecycle/kill
|
|
21
|
+
* POST /api/vision/items/:id/lifecycle/complete
|
|
22
|
+
* GET /api/vision/gates
|
|
23
|
+
* GET /api/vision/gates/:id
|
|
24
|
+
* POST /api/vision/gates/:id/resolve
|
|
25
|
+
* GET /api/vision/items/:id/artifacts
|
|
26
|
+
* POST /api/vision/items/:id/artifacts/scaffold
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import fs from 'node:fs';
|
|
30
|
+
import path from 'node:path';
|
|
31
|
+
import { fileURLToPath } from 'node:url';
|
|
32
|
+
import { extractFilePaths } from './vision-utils.js';
|
|
33
|
+
import { ArtifactManager } from './artifact-manager.js';
|
|
34
|
+
import { recordIteration, checkCumulativeBudget } from '../lib/budget-ledger.js';
|
|
35
|
+
|
|
36
|
+
import { getTargetRoot, resolveProjectPath, loadProjectConfig } from './project-root.js';
|
|
37
|
+
|
|
38
|
+
const PROJECT_ROOT = getTargetRoot();
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Attach vision CRUD and plan/parse REST routes to an Express app.
|
|
42
|
+
*
|
|
43
|
+
* @param {object} app — Express app
|
|
44
|
+
* @param {{ store: object, scheduleBroadcast: function, broadcastMessage: function, projectRoot: string }} deps
|
|
45
|
+
*/
|
|
46
|
+
export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMessage, projectRoot = PROJECT_ROOT, settingsStore }) {
|
|
47
|
+
// GET /api/vision/items — full state (optional ?group= filter)
|
|
48
|
+
app.get('/api/vision/items', (req, res) => {
|
|
49
|
+
let state = store.getState();
|
|
50
|
+
if (req.query.group) {
|
|
51
|
+
state = { ...state, items: state.items.filter(i => i.group === req.query.group) };
|
|
52
|
+
}
|
|
53
|
+
res.json(state);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// POST /api/vision/items — create item
|
|
57
|
+
app.post('/api/vision/items', (req, res) => {
|
|
58
|
+
try {
|
|
59
|
+
const item = store.createItem(req.body);
|
|
60
|
+
scheduleBroadcast();
|
|
61
|
+
res.status(201).json(item);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
res.status(400).json({ error: err.message });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// PATCH /api/vision/items/:id — update item
|
|
68
|
+
app.patch('/api/vision/items/:id', (req, res) => {
|
|
69
|
+
try {
|
|
70
|
+
const item = store.updateItem(req.params.id, req.body);
|
|
71
|
+
scheduleBroadcast();
|
|
72
|
+
res.json(item);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
const status = err.message.includes('not found') ? 404 : 400;
|
|
75
|
+
res.status(status).json({ error: err.message });
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// DELETE /api/vision/items/:id — delete item + connections
|
|
80
|
+
app.delete('/api/vision/items/:id', (req, res) => {
|
|
81
|
+
try {
|
|
82
|
+
store.deleteItem(req.params.id);
|
|
83
|
+
scheduleBroadcast();
|
|
84
|
+
res.json({ ok: true });
|
|
85
|
+
} catch (err) {
|
|
86
|
+
res.status(404).json({ error: err.message });
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// POST /api/vision/connections — create connection
|
|
91
|
+
app.post('/api/vision/connections', (req, res) => {
|
|
92
|
+
try {
|
|
93
|
+
const conn = store.createConnection(req.body);
|
|
94
|
+
scheduleBroadcast();
|
|
95
|
+
res.status(201).json(conn);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
res.status(400).json({ error: err.message });
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// DELETE /api/vision/connections/:id — delete connection
|
|
102
|
+
app.delete('/api/vision/connections/:id', (req, res) => {
|
|
103
|
+
try {
|
|
104
|
+
store.deleteConnection(req.params.id);
|
|
105
|
+
scheduleBroadcast();
|
|
106
|
+
res.json({ ok: true });
|
|
107
|
+
} catch (err) {
|
|
108
|
+
res.status(404).json({ error: err.message });
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// GET /api/vision/items/:id — get single item by ID
|
|
113
|
+
app.get('/api/vision/items/:id', (req, res) => {
|
|
114
|
+
const items = store.getState().items;
|
|
115
|
+
const item = items.find(i => i.id === req.params.id);
|
|
116
|
+
if (!item) {
|
|
117
|
+
return res.status(404).json({ error: `Item not found: ${req.params.id}` });
|
|
118
|
+
}
|
|
119
|
+
const connections = store.getState().connections.filter(
|
|
120
|
+
c => c.fromId === req.params.id || c.toId === req.params.id
|
|
121
|
+
);
|
|
122
|
+
res.json({ ...item, connections });
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// ── Lifecycle endpoints (simplified — no state machine) ──────────────
|
|
126
|
+
const featuresPath = projectRoot !== PROJECT_ROOT
|
|
127
|
+
? path.join(projectRoot, loadProjectConfig().paths?.features || 'docs/features')
|
|
128
|
+
: resolveProjectPath('features');
|
|
129
|
+
|
|
130
|
+
const TRANSITIONS = {
|
|
131
|
+
explore_design: ['prd', 'architecture', 'blueprint'],
|
|
132
|
+
prd: ['architecture', 'blueprint'],
|
|
133
|
+
architecture: ['blueprint'],
|
|
134
|
+
blueprint: ['verification'],
|
|
135
|
+
verification: ['plan', 'blueprint'],
|
|
136
|
+
plan: ['execute'],
|
|
137
|
+
execute: ['report', 'docs'],
|
|
138
|
+
report: ['docs'],
|
|
139
|
+
docs: ['ship'],
|
|
140
|
+
ship: [],
|
|
141
|
+
};
|
|
142
|
+
const SKIPPABLE = new Set(['prd', 'architecture', 'report']);
|
|
143
|
+
const ITERATION_TYPES = new Set(['review', 'coverage']);
|
|
144
|
+
const TERMINAL = new Set(['complete', 'killed']);
|
|
145
|
+
|
|
146
|
+
app.get('/api/vision/items/:id/lifecycle', (req, res) => {
|
|
147
|
+
try {
|
|
148
|
+
const item = store.items.get(req.params.id);
|
|
149
|
+
if (!item) return res.status(404).json({ error: `Item not found: ${req.params.id}` });
|
|
150
|
+
if (!item.lifecycle) return res.status(404).json({ error: 'No lifecycle on this item' });
|
|
151
|
+
res.json(item.lifecycle);
|
|
152
|
+
} catch (err) {
|
|
153
|
+
res.status(500).json({ error: err.message });
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
app.post('/api/vision/items/:id/lifecycle/start', (req, res) => {
|
|
158
|
+
try {
|
|
159
|
+
const { featureCode } = req.body;
|
|
160
|
+
if (!featureCode) return res.status(400).json({ error: 'featureCode is required' });
|
|
161
|
+
const item = store.items.get(req.params.id);
|
|
162
|
+
if (!item) return res.status(404).json({ error: `Item not found: ${req.params.id}` });
|
|
163
|
+
if (item.lifecycle) return res.status(400).json({ error: `Item ${req.params.id} already has a lifecycle` });
|
|
164
|
+
|
|
165
|
+
const now = new Date().toISOString();
|
|
166
|
+
const lifecycle = {
|
|
167
|
+
currentPhase: 'explore_design',
|
|
168
|
+
featureCode,
|
|
169
|
+
startedAt: now,
|
|
170
|
+
completedAt: null,
|
|
171
|
+
killedAt: null,
|
|
172
|
+
killReason: null,
|
|
173
|
+
};
|
|
174
|
+
store.updateLifecycle(req.params.id, lifecycle);
|
|
175
|
+
scheduleBroadcast();
|
|
176
|
+
broadcastMessage({ type: 'lifecycleStarted', itemId: req.params.id, phase: 'explore_design', featureCode, timestamp: now });
|
|
177
|
+
res.json(lifecycle);
|
|
178
|
+
} catch (err) {
|
|
179
|
+
const status = err.message.includes('not found') ? 404 : 400;
|
|
180
|
+
res.status(status).json({ error: err.message });
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
app.post('/api/vision/items/:id/lifecycle/advance', (req, res) => {
|
|
185
|
+
try {
|
|
186
|
+
const { targetPhase, outcome } = req.body;
|
|
187
|
+
const item = store.items.get(req.params.id);
|
|
188
|
+
if (!item?.lifecycle) return res.status(404).json({ error: 'No lifecycle on this item' });
|
|
189
|
+
const from = item.lifecycle.currentPhase;
|
|
190
|
+
if (TERMINAL.has(from)) return res.status(400).json({ error: `Cannot advance from terminal state: ${from}` });
|
|
191
|
+
const valid = TRANSITIONS[from];
|
|
192
|
+
if (!valid?.includes(targetPhase)) return res.status(400).json({ error: `Invalid transition: ${from} → ${targetPhase}` });
|
|
193
|
+
|
|
194
|
+
item.lifecycle.currentPhase = targetPhase;
|
|
195
|
+
store.updateLifecycle(req.params.id, item.lifecycle);
|
|
196
|
+
const now = new Date().toISOString();
|
|
197
|
+
scheduleBroadcast();
|
|
198
|
+
broadcastMessage({ type: 'lifecycleTransition', itemId: req.params.id, from, to: targetPhase, outcome, timestamp: now });
|
|
199
|
+
res.json({ from, to: targetPhase, outcome });
|
|
200
|
+
} catch (err) {
|
|
201
|
+
const status = err.message.includes('not found') ? 404 : 400;
|
|
202
|
+
res.status(status).json({ error: err.message });
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
app.post('/api/vision/items/:id/lifecycle/skip', (req, res) => {
|
|
207
|
+
try {
|
|
208
|
+
const { targetPhase, reason } = req.body;
|
|
209
|
+
const item = store.items.get(req.params.id);
|
|
210
|
+
if (!item?.lifecycle) return res.status(404).json({ error: 'No lifecycle on this item' });
|
|
211
|
+
const from = item.lifecycle.currentPhase;
|
|
212
|
+
if (TERMINAL.has(from)) return res.status(400).json({ error: `Cannot skip from terminal state: ${from}` });
|
|
213
|
+
if (!SKIPPABLE.has(from)) return res.status(400).json({ error: `Phase ${from} is not skippable` });
|
|
214
|
+
const valid = TRANSITIONS[from];
|
|
215
|
+
if (!valid?.includes(targetPhase)) return res.status(400).json({ error: `Invalid transition: ${from} → ${targetPhase}` });
|
|
216
|
+
|
|
217
|
+
item.lifecycle.currentPhase = targetPhase;
|
|
218
|
+
store.updateLifecycle(req.params.id, item.lifecycle);
|
|
219
|
+
const now = new Date().toISOString();
|
|
220
|
+
scheduleBroadcast();
|
|
221
|
+
broadcastMessage({ type: 'lifecycleTransition', itemId: req.params.id, from, to: targetPhase, outcome: 'skipped', timestamp: now });
|
|
222
|
+
res.json({ from, to: targetPhase, outcome: 'skipped', reason });
|
|
223
|
+
} catch (err) {
|
|
224
|
+
const status = err.message.includes('not found') ? 404 : 400;
|
|
225
|
+
res.status(status).json({ error: err.message });
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
app.post('/api/vision/items/:id/lifecycle/kill', (req, res) => {
|
|
230
|
+
try {
|
|
231
|
+
const { reason } = req.body;
|
|
232
|
+
const item = store.items.get(req.params.id);
|
|
233
|
+
if (!item?.lifecycle) return res.status(404).json({ error: 'No lifecycle on this item' });
|
|
234
|
+
const from = item.lifecycle.currentPhase;
|
|
235
|
+
if (TERMINAL.has(from)) return res.status(400).json({ error: `Cannot kill from terminal state: ${from}` });
|
|
236
|
+
|
|
237
|
+
const now = new Date().toISOString();
|
|
238
|
+
item.lifecycle.currentPhase = 'killed';
|
|
239
|
+
item.lifecycle.killedAt = now;
|
|
240
|
+
item.lifecycle.killReason = reason;
|
|
241
|
+
store.updateLifecycle(req.params.id, item.lifecycle);
|
|
242
|
+
store.updateItem(req.params.id, { status: 'killed' });
|
|
243
|
+
scheduleBroadcast();
|
|
244
|
+
broadcastMessage({ type: 'lifecycleTransition', itemId: req.params.id, from, to: 'killed', outcome: 'killed', timestamp: now });
|
|
245
|
+
res.json({ phase: from, reason });
|
|
246
|
+
} catch (err) {
|
|
247
|
+
const status = err.message.includes('not found') ? 404 : 400;
|
|
248
|
+
res.status(status).json({ error: err.message });
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
app.post('/api/vision/items/:id/lifecycle/complete', (req, res) => {
|
|
253
|
+
try {
|
|
254
|
+
const item = store.items.get(req.params.id);
|
|
255
|
+
if (!item?.lifecycle) return res.status(404).json({ error: 'No lifecycle on this item' });
|
|
256
|
+
if (item.lifecycle.currentPhase !== 'ship') {
|
|
257
|
+
return res.status(400).json({ error: `Can only complete from ship phase, currently in: ${item.lifecycle.currentPhase}` });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const now = new Date().toISOString();
|
|
261
|
+
item.lifecycle.currentPhase = 'complete';
|
|
262
|
+
item.lifecycle.completedAt = now;
|
|
263
|
+
store.updateLifecycle(req.params.id, item.lifecycle);
|
|
264
|
+
store.updateItem(req.params.id, { status: 'complete' });
|
|
265
|
+
scheduleBroadcast();
|
|
266
|
+
broadcastMessage({ type: 'lifecycleTransition', itemId: req.params.id, from: 'ship', to: 'complete', outcome: 'approved', timestamp: now });
|
|
267
|
+
res.json({ completedAt: now });
|
|
268
|
+
} catch (err) {
|
|
269
|
+
const status = err.message.includes('not found') ? 404 : 400;
|
|
270
|
+
res.status(status).json({ error: err.message });
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// ── Iteration loop endpoints ──────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
app.post('/api/vision/items/:id/lifecycle/iteration/start', (req, res) => {
|
|
277
|
+
try {
|
|
278
|
+
const item = store.items.get(req.params.id);
|
|
279
|
+
if (!item?.lifecycle) return res.status(404).json({ error: 'No lifecycle on this item' });
|
|
280
|
+
const { loopType, maxIterations, wallClockTimeout, maxActions } = req.body;
|
|
281
|
+
if (!ITERATION_TYPES.has(loopType)) {
|
|
282
|
+
return res.status(400).json({ error: `Invalid loopType: ${loopType}. Must be one of: ${[...ITERATION_TYPES].join(', ')}` });
|
|
283
|
+
}
|
|
284
|
+
if (item.lifecycle.iterationState?.status === 'running') {
|
|
285
|
+
return res.status(409).json({ error: 'An iteration loop is already running' });
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// COMP-BUDGET: check cumulative budget before starting a new loop
|
|
289
|
+
const composeDir = path.join(projectRoot, '.compose');
|
|
290
|
+
const featureCode = item.lifecycle.featureCode;
|
|
291
|
+
if (featureCode) {
|
|
292
|
+
const settings = settingsStore?.get();
|
|
293
|
+
const maxTotal = settings?.iterations?.[loopType]?.maxTotal;
|
|
294
|
+
if (maxTotal != null) {
|
|
295
|
+
const check = checkCumulativeBudget(composeDir, featureCode, { maxTotalIterations: maxTotal });
|
|
296
|
+
if (check.exceeded) {
|
|
297
|
+
return res.status(429).json({ error: `Cumulative iteration budget exceeded for ${featureCode}: ${check.reason}`, usage: check.usage });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const settingsMax = settingsStore?.get()?.iterations?.[loopType]?.maxIterations;
|
|
303
|
+
const settingsTimeout = settingsStore?.get()?.iterations?.[loopType]?.timeout;
|
|
304
|
+
const max = maxIterations ?? settingsMax ?? 10;
|
|
305
|
+
// wallClockTimeout from body takes precedence; fall back to settings timeout (minutes), then no timeout
|
|
306
|
+
const timeoutMinutes = wallClockTimeout !== undefined ? wallClockTimeout : (settingsTimeout ?? null);
|
|
307
|
+
const now = new Date().toISOString();
|
|
308
|
+
item.lifecycle.iterationState = {
|
|
309
|
+
loopType, loopId: `iter-${Date.now()}`, status: 'running',
|
|
310
|
+
count: 0, maxIterations: max, startedAt: now,
|
|
311
|
+
completedAt: null, outcome: null, iterations: [],
|
|
312
|
+
wallClockTimeout: timeoutMinutes,
|
|
313
|
+
maxActions: maxActions ?? null,
|
|
314
|
+
totalActions: 0,
|
|
315
|
+
};
|
|
316
|
+
store.updateLifecycle(req.params.id, item.lifecycle);
|
|
317
|
+
scheduleBroadcast();
|
|
318
|
+
broadcastMessage({ type: 'iterationStarted', itemId: req.params.id, loopId: item.lifecycle.iterationState.loopId, loopType, maxIterations: max, timestamp: now, startedAt: now, wallClockTimeout: timeoutMinutes, maxActions: maxActions ?? null });
|
|
319
|
+
res.json(item.lifecycle.iterationState);
|
|
320
|
+
} catch (err) {
|
|
321
|
+
res.status(400).json({ error: err.message });
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
app.post('/api/vision/items/:id/lifecycle/iteration/report', (req, res) => {
|
|
326
|
+
try {
|
|
327
|
+
const item = store.items.get(req.params.id);
|
|
328
|
+
if (!item?.lifecycle?.iterationState) return res.status(404).json({ error: 'No iteration loop active' });
|
|
329
|
+
const iter = item.lifecycle.iterationState;
|
|
330
|
+
if (iter.status !== 'running') return res.status(409).json({ error: `Loop is ${iter.status}, not running` });
|
|
331
|
+
const { result } = req.body;
|
|
332
|
+
if (!result || typeof result !== 'object') return res.status(400).json({ error: 'result object required' });
|
|
333
|
+
const now = new Date().toISOString();
|
|
334
|
+
iter.count++;
|
|
335
|
+
iter.iterations.push({ n: iter.count, startedAt: now, result });
|
|
336
|
+
|
|
337
|
+
// COMP-BUDGET: accumulate action count
|
|
338
|
+
iter.totalActions = (iter.totalActions ?? 0) + (result.actionCount ?? 0);
|
|
339
|
+
|
|
340
|
+
const exitMet = iter.loopType === 'review' ? result.clean === true
|
|
341
|
+
: iter.loopType === 'coverage' ? result.passing === true
|
|
342
|
+
: false;
|
|
343
|
+
let shouldContinue = true;
|
|
344
|
+
if (exitMet) {
|
|
345
|
+
iter.status = 'complete'; iter.outcome = 'clean'; iter.completedAt = now; shouldContinue = false;
|
|
346
|
+
} else if (iter.count >= iter.maxIterations) {
|
|
347
|
+
iter.status = 'complete'; iter.outcome = 'max_reached'; iter.completedAt = now; shouldContinue = false;
|
|
348
|
+
} else if (iter.maxActions != null && iter.totalActions >= iter.maxActions) {
|
|
349
|
+
// COMP-BUDGET: action count ceiling
|
|
350
|
+
iter.status = 'complete'; iter.outcome = 'action_limit'; iter.completedAt = now; shouldContinue = false;
|
|
351
|
+
} else if (iter.wallClockTimeout != null) {
|
|
352
|
+
// COMP-BUDGET: wall-clock timeout
|
|
353
|
+
const elapsed = Date.now() - new Date(iter.startedAt).getTime();
|
|
354
|
+
const timeoutMs = iter.wallClockTimeout * 60 * 1000;
|
|
355
|
+
if (elapsed > timeoutMs) {
|
|
356
|
+
const elapsedMinutes = Math.round(elapsed / 60000 * 10) / 10;
|
|
357
|
+
iter.status = 'complete'; iter.outcome = 'timeout'; iter.completedAt = now; iter.elapsedMinutes = elapsedMinutes; shouldContinue = false;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// COMP-BUDGET: record iteration in ledger when loop completes
|
|
362
|
+
if (iter.status === 'complete') {
|
|
363
|
+
const composeDir = path.join(projectRoot, '.compose');
|
|
364
|
+
const featureCode = item.lifecycle.featureCode;
|
|
365
|
+
if (featureCode) {
|
|
366
|
+
const startMs = new Date(iter.startedAt).getTime();
|
|
367
|
+
const timeMs = Date.now() - startMs;
|
|
368
|
+
recordIteration(composeDir, featureCode, { iterations: iter.count, actions: iter.totalActions ?? 0, timeMs });
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
store.updateLifecycle(req.params.id, item.lifecycle);
|
|
373
|
+
scheduleBroadcast();
|
|
374
|
+
if (iter.status === 'complete') {
|
|
375
|
+
const completeMsg = { type: 'iterationComplete', itemId: req.params.id, loopId: iter.loopId, loopType: iter.loopType, outcome: iter.outcome, finalCount: iter.count, timestamp: now };
|
|
376
|
+
if (iter.elapsedMinutes != null) completeMsg.elapsedMinutes = iter.elapsedMinutes;
|
|
377
|
+
broadcastMessage(completeMsg);
|
|
378
|
+
} else {
|
|
379
|
+
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 });
|
|
380
|
+
}
|
|
381
|
+
res.json({ continue: shouldContinue, count: iter.count, maxIterations: iter.maxIterations, outcome: iter.outcome });
|
|
382
|
+
} catch (err) {
|
|
383
|
+
res.status(400).json({ error: err.message });
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
app.post('/api/vision/items/:id/lifecycle/iteration/abort', (req, res) => {
|
|
388
|
+
try {
|
|
389
|
+
const item = store.items.get(req.params.id);
|
|
390
|
+
if (!item?.lifecycle?.iterationState) return res.status(404).json({ error: 'No iteration loop active' });
|
|
391
|
+
const iter = item.lifecycle.iterationState;
|
|
392
|
+
if (iter.status !== 'running') return res.status(409).json({ error: `Loop already ${iter.status}` });
|
|
393
|
+
const now = new Date().toISOString();
|
|
394
|
+
iter.status = 'complete'; iter.outcome = 'aborted'; iter.completedAt = now;
|
|
395
|
+
|
|
396
|
+
// COMP-BUDGET: record iteration in ledger on abort
|
|
397
|
+
const composeDir = path.join(projectRoot, '.compose');
|
|
398
|
+
const featureCode = item.lifecycle.featureCode;
|
|
399
|
+
if (featureCode) {
|
|
400
|
+
const startMs = new Date(iter.startedAt).getTime();
|
|
401
|
+
const timeMs = Date.now() - startMs;
|
|
402
|
+
recordIteration(composeDir, featureCode, { iterations: iter.count, actions: iter.totalActions ?? 0, timeMs });
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
store.updateLifecycle(req.params.id, item.lifecycle);
|
|
406
|
+
scheduleBroadcast();
|
|
407
|
+
broadcastMessage({ type: 'iterationComplete', itemId: req.params.id, loopId: iter.loopId, loopType: iter.loopType, outcome: 'aborted', finalCount: iter.count, timestamp: now });
|
|
408
|
+
res.json({ aborted: true });
|
|
409
|
+
} catch (err) {
|
|
410
|
+
res.status(400).json({ error: err.message });
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// ── Artifact endpoints ───────────────────────────────────────────────
|
|
415
|
+
const artifactManager = new ArtifactManager(featuresPath);
|
|
416
|
+
|
|
417
|
+
app.get('/api/vision/items/:id/artifacts', (req, res) => {
|
|
418
|
+
try {
|
|
419
|
+
const item = store.items.get(req.params.id);
|
|
420
|
+
if (!item) return res.status(404).json({ error: `Item not found: ${req.params.id}` });
|
|
421
|
+
if (!item.lifecycle?.featureCode) {
|
|
422
|
+
return res.status(400).json({ error: 'Item has no lifecycle featureCode' });
|
|
423
|
+
}
|
|
424
|
+
const assessment = artifactManager.assess(item.lifecycle.featureCode);
|
|
425
|
+
res.json(assessment);
|
|
426
|
+
} catch (err) {
|
|
427
|
+
res.status(500).json({ error: err.message });
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
app.post('/api/vision/items/:id/artifacts/scaffold', (req, res) => {
|
|
432
|
+
try {
|
|
433
|
+
const item = store.items.get(req.params.id);
|
|
434
|
+
if (!item) return res.status(404).json({ error: `Item not found: ${req.params.id}` });
|
|
435
|
+
if (!item.lifecycle?.featureCode) {
|
|
436
|
+
return res.status(400).json({ error: 'Item has no lifecycle featureCode' });
|
|
437
|
+
}
|
|
438
|
+
const result = artifactManager.scaffold(item.lifecycle.featureCode, req.body);
|
|
439
|
+
res.json(result);
|
|
440
|
+
} catch (err) {
|
|
441
|
+
res.status(400).json({ error: err.message });
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// ── Gate endpoints ─────────────────────────────────────────────────
|
|
446
|
+
|
|
447
|
+
app.get('/api/vision/gates', (req, res) => {
|
|
448
|
+
try {
|
|
449
|
+
const statusFilter = req.query.status;
|
|
450
|
+
if (statusFilter === 'all') {
|
|
451
|
+
res.json({ gates: store.getAllGates() });
|
|
452
|
+
} else if (statusFilter === 'resolved') {
|
|
453
|
+
res.json({ gates: store.getAllGates().filter(g => g.status === 'resolved') });
|
|
454
|
+
} else {
|
|
455
|
+
const itemId = req.query.itemId || undefined;
|
|
456
|
+
const gates = store.getPendingGates(itemId);
|
|
457
|
+
res.json({ gates });
|
|
458
|
+
}
|
|
459
|
+
} catch (err) {
|
|
460
|
+
res.status(500).json({ error: err.message });
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
// POST /api/vision/gates — create a gate (used by CLI dual-dispatch)
|
|
465
|
+
app.post('/api/vision/gates', (req, res) => {
|
|
466
|
+
try {
|
|
467
|
+
const { flowId, stepId, itemId, artifact, options, fromPhase, toPhase, summary, comment, policyMode } = req.body;
|
|
468
|
+
const round = req.body.round ?? 1;
|
|
469
|
+
let artifactSnapshot = null;
|
|
470
|
+
if (artifact) {
|
|
471
|
+
try {
|
|
472
|
+
const docsRoot = path.resolve(projectRoot, 'docs');
|
|
473
|
+
const fullPath = path.resolve(projectRoot, artifact);
|
|
474
|
+
if (fullPath.startsWith(docsRoot + path.sep) && fullPath.endsWith('.md') && fs.existsSync(fullPath)) {
|
|
475
|
+
artifactSnapshot = fs.readFileSync(fullPath, 'utf-8');
|
|
476
|
+
}
|
|
477
|
+
} catch (e) { /* snapshot is best-effort */ }
|
|
478
|
+
}
|
|
479
|
+
if (!flowId || !stepId) {
|
|
480
|
+
return res.status(400).json({ error: 'flowId and stepId are required' });
|
|
481
|
+
}
|
|
482
|
+
const id = `${flowId}:${stepId}:${round}`;
|
|
483
|
+
const existing = store.getGateById(id);
|
|
484
|
+
if (existing) {
|
|
485
|
+
return res.status(200).json(existing); // idempotent
|
|
486
|
+
}
|
|
487
|
+
// Dedup: if a pending gate already exists for the same item+step, reuse it
|
|
488
|
+
if (itemId) {
|
|
489
|
+
const dupGate = store.findPendingGate(itemId, stepId);
|
|
490
|
+
if (dupGate) {
|
|
491
|
+
return res.status(200).json(dupGate);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
const gate = {
|
|
495
|
+
id,
|
|
496
|
+
flowId,
|
|
497
|
+
stepId,
|
|
498
|
+
round,
|
|
499
|
+
itemId: itemId || null,
|
|
500
|
+
artifact: artifact || null,
|
|
501
|
+
options: options || null,
|
|
502
|
+
fromPhase: fromPhase || null,
|
|
503
|
+
policyMode: policyMode ?? 'gate',
|
|
504
|
+
toPhase: toPhase || null,
|
|
505
|
+
summary: summary || null,
|
|
506
|
+
comment: comment || null,
|
|
507
|
+
artifactSnapshot: artifactSnapshot || null,
|
|
508
|
+
status: 'pending',
|
|
509
|
+
createdAt: new Date().toISOString(),
|
|
510
|
+
};
|
|
511
|
+
store.createGate(gate);
|
|
512
|
+
scheduleBroadcast();
|
|
513
|
+
broadcastMessage({ type: 'gateCreated', gateId: id, itemId: itemId || null, timestamp: gate.createdAt });
|
|
514
|
+
res.status(201).json(gate);
|
|
515
|
+
} catch (err) {
|
|
516
|
+
res.status(400).json({ error: err.message });
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
app.get('/api/vision/gates/:id', (req, res) => {
|
|
521
|
+
try {
|
|
522
|
+
const gate = store.gates.get(req.params.id);
|
|
523
|
+
if (!gate) return res.status(404).json({ error: `Gate not found: ${req.params.id}` });
|
|
524
|
+
// Lazy gate expiry
|
|
525
|
+
const gateTimeout = Number(process.env.COMPOSE_GATE_TIMEOUT) || 30 * 60 * 1000;
|
|
526
|
+
if (gate.status === 'pending' && (Date.now() - new Date(gate.createdAt).getTime()) > gateTimeout) {
|
|
527
|
+
gate.status = 'expired';
|
|
528
|
+
store._save();
|
|
529
|
+
}
|
|
530
|
+
res.json(gate);
|
|
531
|
+
} catch (err) {
|
|
532
|
+
res.status(500).json({ error: err.message });
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
app.post('/api/vision/gates/:id/resolve', (req, res) => {
|
|
537
|
+
try {
|
|
538
|
+
const { outcome: rawOutcome, comment, resolvedBy } = req.body;
|
|
539
|
+
if (!rawOutcome) return res.status(400).json({ error: 'outcome is required' });
|
|
540
|
+
// Normalize legacy outcome values
|
|
541
|
+
const outcomeMap = { approved: 'approve', killed: 'kill', revised: 'revise' };
|
|
542
|
+
const outcome = outcomeMap[rawOutcome] || rawOutcome;
|
|
543
|
+
const gate = store.gates.get(req.params.id);
|
|
544
|
+
if (!gate) return res.status(404).json({ error: `Gate not found: ${req.params.id}` });
|
|
545
|
+
// Idempotent: already-resolved gates return 200
|
|
546
|
+
if (gate.status !== 'pending') {
|
|
547
|
+
return res.status(200).json({ gateId: req.params.id, gateOutcome: gate.outcome });
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// AD-4: Server only updates gate state. CLI owns lifecycle transitions.
|
|
551
|
+
store.resolveGate(req.params.id, { outcome, comment, resolvedBy });
|
|
552
|
+
|
|
553
|
+
scheduleBroadcast();
|
|
554
|
+
broadcastMessage({ type: 'gateResolved', gateId: req.params.id, itemId: gate.itemId, outcome, timestamp: new Date().toISOString() });
|
|
555
|
+
res.json({ gateId: req.params.id, gateOutcome: outcome });
|
|
556
|
+
} catch (err) {
|
|
557
|
+
const status = err.message.includes('not found') ? 404 : 400;
|
|
558
|
+
res.status(status).json({ error: err.message });
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
// GET /api/vision/summary — structured board summary
|
|
563
|
+
app.get('/api/vision/summary', (_req, res) => {
|
|
564
|
+
const { items, connections } = store.getState();
|
|
565
|
+
const byPhase = {};
|
|
566
|
+
const byStatus = {};
|
|
567
|
+
const byType = {};
|
|
568
|
+
let totalConfidence = 0;
|
|
569
|
+
let confidenceCount = 0;
|
|
570
|
+
let openQuestions = 0;
|
|
571
|
+
let blockedItems = 0;
|
|
572
|
+
|
|
573
|
+
for (const item of items) {
|
|
574
|
+
const phase = item.phase || 'unassigned';
|
|
575
|
+
byPhase[phase] = (byPhase[phase] || 0) + 1;
|
|
576
|
+
|
|
577
|
+
const status = item.status || 'planned';
|
|
578
|
+
byStatus[status] = (byStatus[status] || 0) + 1;
|
|
579
|
+
|
|
580
|
+
const type = item.type || 'artifact';
|
|
581
|
+
byType[type] = (byType[type] || 0) + 1;
|
|
582
|
+
|
|
583
|
+
if (typeof item.confidence === 'number') {
|
|
584
|
+
totalConfidence += item.confidence;
|
|
585
|
+
confidenceCount++;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (item.type === 'question' && item.status !== 'complete' && item.status !== 'killed') {
|
|
589
|
+
openQuestions++;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (item.status === 'blocked') {
|
|
593
|
+
blockedItems++;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
res.json({
|
|
598
|
+
totalItems: items.length,
|
|
599
|
+
totalConnections: connections.length,
|
|
600
|
+
byPhase,
|
|
601
|
+
byStatus,
|
|
602
|
+
byType,
|
|
603
|
+
openQuestions,
|
|
604
|
+
blockedItems,
|
|
605
|
+
avgConfidence: confidenceCount > 0 ? Math.round((totalConfidence / confidenceCount) * 100) / 100 : 0,
|
|
606
|
+
});
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
// GET /api/vision/blocked — items blocked by non-complete items
|
|
610
|
+
app.get('/api/vision/blocked', (_req, res) => {
|
|
611
|
+
const { items, connections } = store.getState();
|
|
612
|
+
const itemMap = new Map(items.map(i => [i.id, i]));
|
|
613
|
+
|
|
614
|
+
const blocked = [];
|
|
615
|
+
for (const conn of connections) {
|
|
616
|
+
if (conn.type === 'blocks') {
|
|
617
|
+
const blocker = itemMap.get(conn.fromId);
|
|
618
|
+
const target = itemMap.get(conn.toId);
|
|
619
|
+
if (blocker && target && blocker.status !== 'complete' && blocker.status !== 'killed') {
|
|
620
|
+
blocked.push({
|
|
621
|
+
item: target,
|
|
622
|
+
blockedBy: blocker,
|
|
623
|
+
connectionId: conn.id,
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
res.json({ blocked, count: blocked.length });
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
// POST /api/vision/ui — push UI commands (lens, layout, phase)
|
|
633
|
+
app.post('/api/vision/ui', (req, res) => {
|
|
634
|
+
broadcastMessage({ type: 'visionUI', ...req.body });
|
|
635
|
+
res.json({ ok: true });
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
// POST /api/plan/parse — extract file paths from plan/spec markdown
|
|
639
|
+
app.post('/api/plan/parse', (req, res) => {
|
|
640
|
+
const { filePath, itemId } = req.body || {};
|
|
641
|
+
if (!filePath) return res.status(400).json({ error: 'filePath required' });
|
|
642
|
+
|
|
643
|
+
const fullPath = path.resolve(projectRoot, filePath);
|
|
644
|
+
if (!fullPath.startsWith(projectRoot)) {
|
|
645
|
+
return res.status(400).json({ error: 'Path must be within project' });
|
|
646
|
+
}
|
|
647
|
+
let content;
|
|
648
|
+
try {
|
|
649
|
+
content = fs.readFileSync(fullPath, 'utf-8');
|
|
650
|
+
} catch {
|
|
651
|
+
return res.status(404).json({ error: `File not found: ${filePath}` });
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const extracted = extractFilePaths(content);
|
|
655
|
+
|
|
656
|
+
if (itemId) {
|
|
657
|
+
const item = store.items.get(itemId);
|
|
658
|
+
if (item) {
|
|
659
|
+
const existing = item.files || [];
|
|
660
|
+
const merged = [...new Set([...existing, ...extracted])];
|
|
661
|
+
store.updateItem(itemId, { files: merged });
|
|
662
|
+
scheduleBroadcast();
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
res.json({ files: extracted, itemId: itemId || null });
|
|
667
|
+
});
|
|
668
|
+
}
|