@smartmemory/compose 0.2.52-beta → 0.2.54-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/.claude/skills/compose/SKILL.md +0 -8
  2. package/README.md +39 -0
  3. package/bin/compose.js +378 -3
  4. package/dist/assets/App-D_tvTs_n.js +896 -0
  5. package/dist/assets/abnfDiagram-VRR7QNED-Dhb7O5rp.js +1 -0
  6. package/dist/assets/arc-C4oVdnk2.js +1 -0
  7. package/dist/assets/architectureDiagram-ZJ3FMSHR-DqHrb5_F.js +36 -0
  8. package/dist/assets/blockDiagram-677ZJIJ3-DUyBayYK.js +132 -0
  9. package/dist/assets/{browser-CnKiSnlr.js → browser-Cr0recrN.js} +6 -6
  10. package/dist/assets/{c4Diagram-AHTNJAMY-DtDSNlOX.js → c4Diagram-LMCZKHZV-CxlkWMRK.js} +1 -1
  11. package/dist/assets/channel-xb4mQfuL.js +1 -0
  12. package/dist/assets/{chunk-QZHKN3VN-BMWFLZ8t.js → chunk-2Q5K7J3B-C6QJsyOQ.js} +1 -1
  13. package/dist/assets/{chunk-YZCP3GAM-CqjNXooj.js → chunk-32BRIVSS-D9NgYu9W.js} +1 -1
  14. package/dist/assets/{chunk-FMBD7UC4-BAa7Edp6.js → chunk-5VM5RSS4-DbD86rhK.js} +1 -1
  15. package/dist/assets/chunk-EX3LRPZG-E4m4xNMw.js +231 -0
  16. package/dist/assets/{chunk-4BX2VUAB-oXU2npoL.js → chunk-JWPE2WC7-DHGlRWbw.js} +1 -1
  17. package/dist/assets/chunk-MOJQB5TN-BblYnFUq.js +88 -0
  18. package/dist/assets/chunk-RYQCIY6F-CGT-88vL.js +1 -0
  19. package/dist/assets/chunk-V7JOEXUC-BJJmX0mc.js +206 -0
  20. package/dist/assets/{chunk-EDXVE4YY-Bse2kdRh.js → chunk-VR4S4FIN-B4ygduCD.js} +1 -1
  21. package/dist/assets/{chunk-55IACEB6-BEEk2FdH.js → chunk-XXDRQBXY-7APXuI2s.js} +1 -1
  22. package/dist/assets/classDiagram-OUVF2IWQ-BIvgPTgh.js +1 -0
  23. package/dist/assets/classDiagram-v2-EOCWNBFH-BIvgPTgh.js +1 -0
  24. package/dist/assets/{cose-bilkent-S5V4N54A-NYnH3mVW.js → cose-bilkent-JH36ORCC-DnVcT6R6.js} +1 -1
  25. package/dist/assets/cynefin-VYW2F7L2-C9qiUSfX.js +178 -0
  26. package/dist/assets/cynefinDiagram-TSTJHNR4-BowkQhEu.js +62 -0
  27. package/dist/assets/dagre-VKFMJZFB-Y5V-7U3p.js +4 -0
  28. package/dist/assets/diagram-FQU43EPY-I7wDE-Xp.js +3 -0
  29. package/dist/assets/diagram-G47NLZAW-DapGa1HI.js +24 -0
  30. package/dist/assets/diagram-NH7WQ7WH-CHO1JntO.js +24 -0
  31. package/dist/assets/diagram-OA4YK3LP-mBWP2D1G.js +30 -0
  32. package/dist/assets/diagram-WEI45ONY-C8KN8YnF.js +41 -0
  33. package/dist/assets/ebnfDiagram-CCIWWBDH-ZkJG9NOL.js +1 -0
  34. package/dist/assets/erDiagram-Q63AITRT-DGnL3kW1.js +85 -0
  35. package/dist/assets/flowDiagram-23GEKE2U-DHYupfOu.js +156 -0
  36. package/dist/assets/ganttDiagram-NO4QXBWP-B1B2zpwR.js +292 -0
  37. package/dist/assets/gitGraphDiagram-IHSO6WYX-B9_OP4Fs.js +106 -0
  38. package/dist/assets/graph-C9eacEi8.js +1 -0
  39. package/dist/assets/graph-xkel59g2.js +331 -0
  40. package/dist/assets/index-0Hq9436Q.js +119 -0
  41. package/dist/assets/index-LIwREYgH.css +1 -0
  42. package/dist/assets/infoDiagram-FWYZ7A6U-D-a34TqE.js +2 -0
  43. package/dist/assets/{ishikawaDiagram-UXIWVN3A-CK2IFFAP.js → ishikawaDiagram-FXEZZL3T-h0bUvXGV.js} +5 -5
  44. package/dist/assets/{journeyDiagram-VCZTEJTY-DAH5Stkf.js → journeyDiagram-5HDEW3XC-B4lm0RBa.js} +1 -1
  45. package/dist/assets/{kanban-definition-6JOO6SKY-Bi0aCqkv.js → kanban-definition-HUTT4EX6-msvfouRX.js} +7 -7
  46. package/dist/assets/katex-C5jXJg4s.js +257 -0
  47. package/dist/assets/layout-DEXfKzaS.js +1 -0
  48. package/dist/assets/{linear-ClGEGlS2.js → linear-B0trv2xN.js} +1 -1
  49. package/dist/assets/map-Czzmt4hB.js +1 -0
  50. package/dist/assets/{mindmap-definition-QFDTVHPH-Wjr-PgC6.js → mindmap-definition-LN4V7U3C-CvswbRgd.js} +7 -7
  51. package/dist/assets/{mobile-Chw8RWyH.js → mobile-CNLMhdFP.js} +2 -2
  52. package/dist/assets/pegDiagram-2B236MQR-DZEmIYC7.js +1 -0
  53. package/dist/assets/pieDiagram-ENE6RG2P-1ZfqxHkL.js +39 -0
  54. package/dist/assets/quadrantDiagram-ABIIQ3AL-Owx49uiJ.js +7 -0
  55. package/dist/assets/railroadDiagram-RFXS5EU6-B7RiaIGf.js +1 -0
  56. package/dist/assets/{requirementDiagram-MS252O5E-CU9Rlyq5.js → requirementDiagram-TGXJPOKE-fqmHs_5o.js} +3 -3
  57. package/dist/assets/sankeyDiagram-HTMAVEWB-D0Q3FeKV.js +40 -0
  58. package/dist/assets/sequenceDiagram-DBY2YBRQ-CnPuB3fy.js +162 -0
  59. package/dist/assets/sizeCapture-X5ZJPWSS-CsHoR37V.js +1 -0
  60. package/dist/assets/stateDiagram-2N3HPSRC-GVwpflU5.js +1 -0
  61. package/dist/assets/stateDiagram-v2-6OUMAXLB-DCGIHFu9.js +1 -0
  62. package/dist/assets/swimlanes-5IMT3BWC-DRR41dsQ.js +2 -0
  63. package/dist/assets/swimlanesDiagram-G3AALYLV-C4RhLkZo.js +8 -0
  64. package/dist/assets/{timeline-definition-GMOUNBTQ-C8SZmaLp.js → timeline-definition-FHXFAJF6-ePaEemL7.js} +3 -3
  65. package/dist/assets/vennDiagram-L72KCM5P-BAYP8dNF.js +34 -0
  66. package/dist/assets/wardleyDiagram-EHGQE667-jNVH5Adm.js +78 -0
  67. package/dist/assets/xychartDiagram-FW5EYKEG-BYDWGXbZ.js +7 -0
  68. package/dist/index.html +3 -3
  69. package/lib/agent-string.js +34 -0
  70. package/lib/build.js +542 -148
  71. package/lib/experiment-judge.js +145 -0
  72. package/lib/experiment-metrics.js +265 -0
  73. package/lib/experiment-pricing.js +60 -0
  74. package/lib/experiment-report.js +306 -0
  75. package/lib/experiment-sandbox.js +147 -0
  76. package/lib/experiment.js +539 -0
  77. package/lib/feature-events.js +10 -0
  78. package/lib/feature-json.js +1 -1
  79. package/lib/feature-writer.js +30 -0
  80. package/lib/flow-state.js +36 -0
  81. package/lib/gate-prompt.js +66 -6
  82. package/lib/lifecycle-modes.js +213 -0
  83. package/lib/new.js +6 -2
  84. package/lib/roadmap-graph/index.js +26 -28
  85. package/lib/roadmap-graph/vision-adapter.js +126 -0
  86. package/lib/smartmemory-client.js +139 -0
  87. package/lib/smartmemory-config.js +50 -0
  88. package/lib/smartmemory-ingest.js +115 -0
  89. package/lib/smartmemory-sync.js +275 -0
  90. package/lib/stratum-mcp-client.js +9 -0
  91. package/lib/triage.js +7 -1
  92. package/lib/vision-writer.js +30 -6
  93. package/package.json +1 -1
  94. package/pipelines/plan.stratum.yaml +161 -0
  95. package/server/artifact-manager.js +30 -3
  96. package/server/compose-mcp-tools.js +11 -9
  97. package/server/compose-mcp.js +4 -0
  98. package/server/feature-scan.js +87 -173
  99. package/server/gate-log-store.js +11 -1
  100. package/server/graph-export.js +0 -0
  101. package/server/index.js +3 -5
  102. package/server/lifecycle-guard.js +62 -25
  103. package/server/roadmap-graph-vision.js +138 -0
  104. package/server/smartmemory-routes.js +113 -0
  105. package/server/status-snapshot.js +8 -7
  106. package/server/vision-routes.js +64 -33
  107. package/server/vision-server.js +6 -0
  108. package/server/vision-store.js +13 -4
  109. package/.claude/skills/compose/references/hermes-tools.md +0 -80
  110. package/dist/assets/App-Ddhfx18X.js +0 -889
  111. package/dist/assets/_baseUniq-CoEVVYyC.js +0 -1
  112. package/dist/assets/arc-DFnGxuCB.js +0 -1
  113. package/dist/assets/architectureDiagram-Q4EWVU46-66rx01Tn.js +0 -36
  114. package/dist/assets/blockDiagram-DXYQGD6D-AE5PKSak.js +0 -132
  115. package/dist/assets/channel-CScyWprq.js +0 -1
  116. package/dist/assets/chunk-4TB4RGXK-5w2lZAjW.js +0 -206
  117. package/dist/assets/chunk-OYMX7WX6-CGr_YiAc.js +0 -231
  118. package/dist/assets/classDiagram-6PBFFD2Q-DibVYYdr.js +0 -1
  119. package/dist/assets/classDiagram-v2-HSJHXN6E-DibVYYdr.js +0 -1
  120. package/dist/assets/clone-ZMALIFmw.js +0 -1
  121. package/dist/assets/dagre-KV5264BT--mrfaTHR.js +0 -4
  122. package/dist/assets/diagram-5BDNPKRD-DjXvyFzh.js +0 -10
  123. package/dist/assets/diagram-G4DWMVQ6-BrrexEvm.js +0 -24
  124. package/dist/assets/diagram-MMDJMWI5-D3Q0OT2M.js +0 -43
  125. package/dist/assets/diagram-TYMM5635-DKTntmxE.js +0 -24
  126. package/dist/assets/erDiagram-SMLLAGMA-BWbR0dPj.js +0 -85
  127. package/dist/assets/flowDiagram-DWJPFMVM-Dj5M5XaY.js +0 -162
  128. package/dist/assets/ganttDiagram-T4ZO3ILL-DKppFh_5.js +0 -292
  129. package/dist/assets/gitGraphDiagram-UUTBAWPF-9xQ9O25V.js +0 -106
  130. package/dist/assets/graph-Cgmhvu1T.js +0 -1
  131. package/dist/assets/graph-DZe55uk8.js +0 -331
  132. package/dist/assets/index-BxRamj_i.css +0 -1
  133. package/dist/assets/index-D8uDfn8y.js +0 -123
  134. package/dist/assets/infoDiagram-42DDH7IO-DQYWVzVN.js +0 -2
  135. package/dist/assets/katex-DkKDou_j.js +0 -257
  136. package/dist/assets/layout-DN13RkA1.js +0 -1
  137. package/dist/assets/min-CXr-fUFS.js +0 -1
  138. package/dist/assets/pieDiagram-DEJITSTG-CVipbIlr.js +0 -30
  139. package/dist/assets/quadrantDiagram-34T5L4WZ-CHJFyhm-.js +0 -7
  140. package/dist/assets/sankeyDiagram-XADWPNL6-BSULeqST.js +0 -10
  141. package/dist/assets/sequenceDiagram-FGHM5R23-CCOKXvjj.js +0 -157
  142. package/dist/assets/stateDiagram-FHFEXIEX-B91gKA7Q.js +0 -1
  143. package/dist/assets/stateDiagram-v2-QKLJ7IA2-Dc8ur9ta.js +0 -1
  144. package/dist/assets/vennDiagram-DHZGUBPP-YvEPDlsM.js +0 -34
  145. package/dist/assets/wardley-RL74JXVD-TMBUq_w1.js +0 -162
  146. package/dist/assets/wardleyDiagram-NUSXRM2D-DzEkPrri.js +0 -20
  147. package/dist/assets/xychartDiagram-5P7HB3ND-BIiCeiYk.js +0 -7
  148. package/lib/roadmap-graph/collect.js +0 -178
@@ -0,0 +1,138 @@
1
+ /**
2
+ * roadmap-graph-vision.js — COMP-ROADMAP-GRAPH-2 S4.
3
+ *
4
+ * The CANONICAL projection of the one graph model: build the roadmap graph from
5
+ * a freshly, deterministically seeded vision store (committed source only — no
6
+ * persisted cockpit state), through the same adapter + renderer the live
7
+ * cockpit export uses. This is what the headless CLI / MCP / CI call.
8
+ *
9
+ * Lives in server/ (not lib/) because it depends on the vision store + seed,
10
+ * which are server-side; it reuses the pure render/write/check helpers from
11
+ * lib/roadmap-graph/index.js. This is the seam that lets us retire collect.js.
12
+ */
13
+ import { mkdtempSync, existsSync, readFileSync, rmSync } from 'node:fs';
14
+ import { tmpdir } from 'node:os';
15
+ import { join } from 'node:path';
16
+
17
+ import { VisionStore } from './vision-store.js';
18
+ import { scanFeatures, seedFeatures } from './feature-scan.js';
19
+ import { resolveFeaturesPath, resolveRoadmapPath, loadExternalPrefixes } from '../lib/project-paths.js';
20
+ import { parseRoadmap } from '../lib/roadmap-parser.js';
21
+ import { visionToGraphInputs } from '../lib/roadmap-graph/vision-adapter.js';
22
+ import { depsToEdges, DanglingEdgeError } from '../lib/roadmap-graph/model.js';
23
+ import { buildArtifactFromInputs, writeArtifact, checkArtifact } from '../lib/roadmap-graph/index.js';
24
+
25
+ // ROADMAP/feature UPPERCASE status -> vision status. The vision vocab has no
26
+ // `partial`; PARTIAL collapses to in_progress (documented delta — the adapter
27
+ // maps it back to IN_PROGRESS, so a PARTIAL feature renders as in_progress in
28
+ // the canonical graph rather than the static path's `partial` border).
29
+ const ROADMAP_TO_VISION_STATUS = {
30
+ PLANNED: 'planned',
31
+ IN_PROGRESS: 'in_progress',
32
+ PARTIAL: 'in_progress',
33
+ COMPLETE: 'complete',
34
+ BLOCKED: 'blocked',
35
+ PARKED: 'parked',
36
+ KILLED: 'killed',
37
+ SUPERSEDED: 'superseded',
38
+ };
39
+
40
+ function toVisionStatus(status) {
41
+ return ROADMAP_TO_VISION_STATUS[String(status || 'PLANNED').toUpperCase()] || 'planned';
42
+ }
43
+
44
+ /**
45
+ * Seed ROADMAP.md rows that lack a feature folder as fallback feature items,
46
+ * mirroring collect.js section (b) so the node universe matches.
47
+ */
48
+ function seedRoadmapFallback(store, cwd, externalPrefixes) {
49
+ const roadmapPath = resolveRoadmapPath(cwd);
50
+ if (!existsSync(roadmapPath)) return;
51
+ const isExternal = (code) => externalPrefixes.some((p) => code.startsWith(p));
52
+ const have = new Set(
53
+ [...store.items.values()]
54
+ .filter((i) => i.type === 'feature')
55
+ .map((i) => i.lifecycle?.featureCode || i.title),
56
+ );
57
+ let entries = [];
58
+ try { entries = parseRoadmap(readFileSync(roadmapPath, 'utf-8')); } catch { return; }
59
+ for (const e of entries) {
60
+ if (!e.code || e.code.startsWith('_anon_')) continue;
61
+ if (have.has(e.code) || isExternal(e.code)) continue;
62
+ const item = store.createItem({
63
+ type: 'feature',
64
+ title: e.code,
65
+ description: e.description || '',
66
+ status: toVisionStatus(e.status),
67
+ phase: 'planning',
68
+ confidence: 0,
69
+ group: 'standalone',
70
+ });
71
+ try { store.updateLifecycle(item.id, { featureCode: e.code, currentPhase: 'explore_design' }); } catch { /* optional */ }
72
+ have.add(e.code);
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Build the canonical graph inputs from committed source via a throwaway store.
78
+ * @param {string} cwd
79
+ * @returns {{ nodes, rawEdges, knownCodes, warnings }}
80
+ */
81
+ export function collectVisionInputs(cwd) {
82
+ const dataDir = mkdtempSync(join(tmpdir(), 'rg2-canonical-'));
83
+ try {
84
+ const store = new VisionStore(dataDir);
85
+ const featuresDir = resolveFeaturesPath(cwd);
86
+ const externalPrefixes = loadExternalPrefixes(cwd);
87
+ // Canon: only feature.json-backed folders are managed features (mirrors
88
+ // collect.js's listFeatures universe — doc-only folders are not nodes).
89
+ const features = scanFeatures(featuresDir).filter((f) => f.hasFeatureJson);
90
+ seedFeatures(features, store);
91
+ seedRoadmapFallback(store, cwd, externalPrefixes);
92
+
93
+ // Preserve the dangling-edge refusal (the anti-typo lint kept from the
94
+ // static path). A deps.yaml edge to a code that resolves to no managed
95
+ // feature and isn't an external reference can't become a connection, so it
96
+ // would silently vanish; detect + refuse instead, mirroring collect.js.
97
+ const isExternal = (code) => externalPrefixes.some((p) => code.startsWith(p));
98
+ const known = new Set(
99
+ [...store.items.values()]
100
+ .filter((i) => i.type === 'feature')
101
+ .map((i) => i.lifecycle?.featureCode || i.title),
102
+ );
103
+ const dangling = [];
104
+ for (const f of features) {
105
+ if (!f.deps) continue;
106
+ for (const e of depsToEdges(f.name, f.deps)) {
107
+ const missing = [e.from, e.to].filter((c) => !known.has(c) && !isExternal(c));
108
+ if (missing.length) dangling.push({ from: e.from, to: e.to, kind: e.type, missing });
109
+ }
110
+ }
111
+ if (dangling.length) throw new DanglingEdgeError(dangling);
112
+
113
+ return visionToGraphInputs(
114
+ [...store.items.values()],
115
+ [...store.connections.values()],
116
+ { externalPrefixes, includeProseEdges: false },
117
+ );
118
+ } finally {
119
+ try { rmSync(dataDir, { recursive: true, force: true }); } catch { /* best-effort */ }
120
+ }
121
+ }
122
+
123
+ /** Build HTML + stats from the canonical vision projection (no disk write). */
124
+ export function buildRoadmapGraph(cwd) {
125
+ return buildArtifactFromInputs(cwd, collectVisionInputs(cwd));
126
+ }
127
+
128
+ /** Generate + atomically write roadmap-graph.html from the canonical projection. */
129
+ export function generateRoadmapGraph(cwd, opts = {}) {
130
+ return writeArtifact(cwd, buildRoadmapGraph(cwd), opts);
131
+ }
132
+
133
+ /** Render in-memory and diff vs on-disk, from the canonical projection. */
134
+ export function checkRoadmapGraph(cwd, opts = {}) {
135
+ return checkArtifact(cwd, buildRoadmapGraph(cwd), opts);
136
+ }
137
+
138
+ export { DanglingEdgeError };
@@ -0,0 +1,113 @@
1
+ /**
2
+ * smartmemory-routes.js — COMP-SMARTMEMORY-RECALL S01.
3
+ *
4
+ * Route:
5
+ * GET /api/smartmemory/recall?featureCode=<CODE> — ranked prior-context
6
+ * lookup for the cockpit's Recall tab, backed by INGEST's SmartMemory
7
+ * client + config (lib/smartmemory-client.js, lib/smartmemory-config.js).
8
+ *
9
+ * Degrade-never-fail (the qa-scope-routes.js model): every branch returns a
10
+ * shaped 200, never a throw. Flag OFF (or no workspace root) answers from
11
+ * config alone — zero client construction, zero SmartMemory-bound traffic.
12
+ *
13
+ * Not on the auth allowlist (server/index.js) — auth-gated in remote mode
14
+ * like the vision routes, since results can carry local project context.
15
+ */
16
+ import { getSmartmemoryConfig, resolveProjectTag as defaultResolveProjectTag } from '../lib/smartmemory-config.js';
17
+ import { createSmartmemoryClient } from '../lib/smartmemory-client.js';
18
+ import { readFeature as defaultReadFeature } from '../lib/feature-json.js';
19
+ import { resolveFeaturesPath as defaultResolveFeaturesPath } from '../lib/project-paths.js';
20
+
21
+ function defaultCreateClient(cfg) {
22
+ return createSmartmemoryClient(cfg);
23
+ }
24
+
25
+ function shortReason(e) {
26
+ return e?.message?.slice(0, 200) || 'unreachable';
27
+ }
28
+
29
+ function safeProjectTag(resolveProjectTag, root) {
30
+ try {
31
+ return resolveProjectTag(root);
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ /** Defensive adapter over the SmartMemory hit shape (multiple field-name fallbacks). */
38
+ function mapHit(hit) {
39
+ const content = hit?.content ?? hit?.text ?? '';
40
+ return {
41
+ id: hit?.id ?? hit?.item_id ?? null,
42
+ snippet: content.length > 280 ? content.slice(0, 280) + '…' : content,
43
+ score: hit?.score ?? null,
44
+ memoryType: hit?.memory_type ?? hit?.memoryType ?? null,
45
+ ts: hit?.context?.event?.ts ?? hit?.context?.ts ?? hit?.ts ?? null,
46
+ project: hit?.context?.project ?? null,
47
+ };
48
+ }
49
+
50
+ /**
51
+ * @param {import('express').Express} app
52
+ * @param {object} [deps]
53
+ * @param {Function} [deps.getConfig] — (cwd) => {} | {enabled,baseUrl,apiKeyEnv,timeoutMs}
54
+ * @param {Function} [deps.createClient] — (cfg) => { search(query, opts) }
55
+ * @param {Function} [deps.readFeature] — (cwd, code, featuresDir) => FeatureJson|null
56
+ * @param {Function} [deps.resolveFeaturesPath] — (cwd) => absolute features dir
57
+ * @param {Function} [deps.resolveProjectTag] — (cwd) => canonical project tag
58
+ */
59
+ export function attachSmartmemoryRoutes(app, {
60
+ getConfig = getSmartmemoryConfig,
61
+ createClient = defaultCreateClient,
62
+ readFeature = defaultReadFeature,
63
+ resolveFeaturesPath = defaultResolveFeaturesPath,
64
+ resolveProjectTag = defaultResolveProjectTag,
65
+ } = {}) {
66
+ app.get('/api/smartmemory/recall', async (req, res) => {
67
+ const root = req.workspace?.root;
68
+ if (!root) {
69
+ return res.json({ enabled: false });
70
+ }
71
+
72
+ let cfg;
73
+ try {
74
+ cfg = getConfig(root);
75
+ } catch {
76
+ cfg = {};
77
+ }
78
+ if (cfg?.enabled !== true) {
79
+ return res.json({ enabled: false });
80
+ }
81
+
82
+ const featureCode = (req.query.featureCode || '').toString().trim();
83
+ if (!featureCode) {
84
+ return res.json({ enabled: true, available: true, results: [], invalidFeatureCode: true });
85
+ }
86
+
87
+ let feature;
88
+ try {
89
+ feature = readFeature(root, featureCode, resolveFeaturesPath(root));
90
+ } catch {
91
+ feature = null;
92
+ }
93
+ const desc = feature?.description || '';
94
+ const query = desc ? `${featureCode} ${desc}` : featureCode;
95
+
96
+ let hits;
97
+ try {
98
+ const client = createClient(cfg);
99
+ hits = await client.search(query, { top_k: 10 });
100
+ } catch (e) {
101
+ return res.json({ enabled: true, available: false, error: shortReason(e) });
102
+ }
103
+
104
+ const results = (Array.isArray(hits) ? hits : (hits?.results ?? [])).map(mapHit);
105
+ return res.json({
106
+ enabled: true,
107
+ available: true,
108
+ featureCode,
109
+ project: safeProjectTag(resolveProjectTag, root),
110
+ results,
111
+ });
112
+ });
113
+ }
@@ -21,13 +21,14 @@
21
21
 
22
22
  import { readGateLog } from './gate-log-store.js';
23
23
  import { isStaleLoop } from './open-loops-store.js';
24
-
25
- // Known lifecycle phases — must match LIFECYCLE_PHASE_LABELS in constants.js
26
- const KNOWN_PHASES = new Set([
27
- 'explore_design', 'prd', 'architecture', 'blueprint',
28
- 'verification', 'plan', 'execute', 'report', 'docs', 'ship',
29
- 'complete', 'killed',
30
- ]);
24
+ import { allKnownPhases } from '../lib/lifecycle-modes.js';
25
+
26
+ // Known lifecycle phases — DERIVED from the mode registry (the union of every
27
+ // mode's phaseOrder ∪ terminal). This is a superset of the legacy build-only
28
+ // list, so non-build-mode phases (e.g. a fix lifecycle's `diagnose`) are no
29
+ // longer flagged "unrecognized". It still includes the terminal phases, which
30
+ // is required because the unknown-phase guard runs BEFORE the terminal branches.
31
+ const KNOWN_PHASES = new Set(allKnownPhases());
31
32
 
32
33
  const TERMINAL_PHASES = new Set(['complete', 'killed']);
33
34
  const MAX_SENTENCE = 280;
@@ -50,10 +50,13 @@ import { anchorBoundary } from '../lib/checkpoint/checkpoint-writer.js';
50
50
  import { appendGateLogEntry, readGateLog, mapResolveOutcomeToSchema } from './gate-log-store.js';
51
51
  import { addOpenLoop, resolveOpenLoop, listOpenLoops } from './open-loops-store.js';
52
52
  import {
53
- BASE_TRANSITIONS, SKIPPABLE, TERMINAL,
53
+ TERMINAL,
54
54
  guardedTransition, ensureGuard, projectFeatureStatus,
55
55
  verifyCompletionEvidence, guardTestCommand,
56
56
  } from './lifecycle-guard.js';
57
+ import {
58
+ transitionsOf, skippableOf, completablePhaseOf, getMode, resolveMode,
59
+ } from '../lib/lifecycle-modes.js';
57
60
  import { requireSensitiveOrPaired as requireSensitiveToken } from './security.js';
58
61
 
59
62
  const PROJECT_ROOT = getTargetRoot();
@@ -214,9 +217,13 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
214
217
  ? resolveFeaturesPathFromConfig(projectRoot, loadComposeConfig(projectRoot))
215
218
  : resolveProjectPath('features');
216
219
 
217
- // Phase graph + SKIPPABLE + TERMINAL are owned by lifecycle-guard.js (single
218
- // source of truth shared with the STRAT-GUARD graph) — see COMP-MCP-ENFORCE.
219
- const TRANSITIONS = BASE_TRANSITIONS;
220
+ // Phase graph + skippable + terminal are owned by the mode registry (single
221
+ // source of truth shared with the STRAT-GUARD graph) — see COMP-MCP-ENFORCE /
222
+ // COMP-ROADMAP-MODES. Legality is resolved PER-ITEM from its lifecycle mode
223
+ // (build|fix|plan); legacy items with no mode default to build, so build
224
+ // transitions are byte-identical. typeToMode stamps the mode at creation.
225
+ const modeOf = (item) => item?.lifecycle?.mode ?? 'build';
226
+ const typeToMode = (type) => (type === 'bug' ? 'fix' : 'build');
220
227
  const ITERATION_TYPES = new Set(['review', 'coverage']);
221
228
 
222
229
  app.get('/api/vision/items/:id/lifecycle', (req, res) => {
@@ -251,9 +258,18 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
251
258
  if (!item) return res.status(404).json({ error: `Item not found: ${req.params.id}` });
252
259
  if (item.lifecycle) return res.status(400).json({ error: `Item ${req.params.id} already has a lifecycle` });
253
260
 
261
+ // COMP-ROADMAP-MODES / COMP-ROADMAP-PLAN: stamp the lifecycle mode. Prefer
262
+ // an explicit mode on the start payload (the CLI/VisionWriter forwards
263
+ // `mode` so a plan session is stamped mode='plan'), normalized through
264
+ // resolveMode (feature→build, bug→fix). Fall back to typeToMode(item.type)
265
+ // when the caller does not specify one — byte-identical to pre-PLAN for
266
+ // build AND bug. The genesis phase stays 'explore_design'.
267
+ const mode = req.body.mode != null ? resolveMode(req.body.mode) : typeToMode(item.type);
268
+ const genesis = 'explore_design';
254
269
  const now = new Date().toISOString();
255
270
  const lifecycle = {
256
- currentPhase: 'explore_design',
271
+ currentPhase: genesis,
272
+ mode,
257
273
  featureCode,
258
274
  startedAt: now,
259
275
  completedAt: null,
@@ -261,22 +277,26 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
261
277
  killReason: null,
262
278
  };
263
279
  // COMP-OBS-TIMELINE: populate phaseHistory before storing
264
- appendPhaseHistory({ lifecycle }, { from: null, to: 'explore_design', outcome: null, timestamp: now });
280
+ appendPhaseHistory({ lifecycle }, { from: null, to: genesis, outcome: null, timestamp: now });
265
281
  store.updateLifecycle(req.params.id, lifecycle);
266
282
  // COMP-MCP-ENFORCE: eager guard registration seeded at the genesis phase
267
283
  // (the clean bootstrap path; backfill for pre-rollout items happens lazily
268
284
  // on first transition). Best-effort — a registration hiccup must not block
269
285
  // starting a lifecycle; the next guarded transition re-attempts ensureGuard.
270
286
  if (guardEnabled) {
271
- try { await ensureGuard(featureCode, 'explore_design', projectRoot); }
287
+ try { await ensureGuard(featureCode, genesis, projectRoot, mode); }
272
288
  catch (e) { console.warn(`[lifecycle/start] guard register for ${featureCode} failed: ${e.message}`); }
273
- // Slice 2: starting a lifecycle projects explore_design → IN_PROGRESS so
289
+ // Slice 2: starting a lifecycle projects the genesis phase → IN_PROGRESS so
274
290
  // the first active phase is not left stuck at PLANNED in feature.json.
275
- await projectFeatureStatus({ featureCode, phase: 'explore_design', cwd: projectRoot });
291
+ // COMP-ROADMAP-PLAN: skipped for tracksFeatureJson:false modes (e.g. a
292
+ // plan session has no feature.json to project onto).
293
+ if (getMode(mode).runner.tracksFeatureJson) {
294
+ await projectFeatureStatus({ featureCode, phase: genesis, cwd: projectRoot });
295
+ }
276
296
  }
277
297
  scheduleBroadcast();
278
- broadcastMessage({ type: 'lifecycleStarted', itemId: req.params.id, phase: 'explore_design', featureCode, timestamp: now });
279
- emitDecisionEvent(broadcastMessage, buildPhaseTransitionEvent({ featureCode, from: null, to: 'explore_design', outcome: null, timestamp: now }));
298
+ broadcastMessage({ type: 'lifecycleStarted', itemId: req.params.id, phase: genesis, featureCode, timestamp: now });
299
+ emitDecisionEvent(broadcastMessage, buildPhaseTransitionEvent({ featureCode, from: null, to: genesis, outcome: null, timestamp: now }));
280
300
  // COMP-OBS-DRIFT: emit drift axes before status so STATUS reads fresh drift_axes
281
301
  const startedItem = store.items.get(req.params.id);
282
302
  if (startedItem) emitDriftAxes(broadcastMessage, store, startedItem, projectRoot, now);
@@ -328,12 +348,12 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
328
348
  if (!item?.lifecycle) return res.status(404).json({ error: 'No lifecycle on this item' });
329
349
  const from = item.lifecycle.currentPhase;
330
350
  if (TERMINAL.has(from)) return res.status(400).json({ error: `Cannot advance from terminal state: ${from}` });
331
- const valid = TRANSITIONS[from];
351
+ const valid = transitionsOf(modeOf(item))[from];
332
352
  if (!valid?.includes(targetPhase)) return res.status(400).json({ error: `Invalid transition: ${from} → ${targetPhase}` });
333
353
 
334
354
  // COMP-MCP-ENFORCE: verdict-gate the transition (fail-closed) before mutating.
335
355
  if (guardEnabled) {
336
- const g = await guardedTransition({ featureCode: item.lifecycle.featureCode, from, to: targetPhase, workspaceRoot: projectRoot, resolvedBy: 'agent' });
356
+ const g = await guardedTransition({ featureCode: item.lifecycle.featureCode, from, to: targetPhase, workspaceRoot: projectRoot, resolvedBy: 'agent', mode: modeOf(item) });
337
357
  if (!g.applied) return res.status(422).json({ error: 'transition refused by guard', from, to: targetPhase, verdict: g.verdict, guardError: g.error });
338
358
  }
339
359
 
@@ -344,7 +364,8 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
344
364
  store.updateLifecycle(req.params.id, item.lifecycle);
345
365
  // COMP-MCP-ENFORCE Slice 2 (lifecycle-as-truth): project the new phase onto
346
366
  // feature.json STATUS (best-effort; idempotent — only writes on a real change).
347
- if (guardEnabled) await projectFeatureStatus({ featureCode: item.lifecycle.featureCode, phase: targetPhase, cwd: projectRoot });
367
+ // COMP-ROADMAP-PLAN: skipped for tracksFeatureJson:false modes (no feature.json).
368
+ if (guardEnabled && getMode(modeOf(item)).runner.tracksFeatureJson) await projectFeatureStatus({ featureCode: item.lifecycle.featureCode, phase: targetPhase, cwd: projectRoot });
348
369
  scheduleBroadcast();
349
370
  broadcastMessage({ type: 'lifecycleTransition', itemId: req.params.id, from, to: targetPhase, outcome, timestamp: now });
350
371
  emitDecisionEvent(broadcastMessage, buildPhaseTransitionEvent({ featureCode: item.lifecycle.featureCode, from, to: targetPhase, outcome, timestamp: now }));
@@ -368,13 +389,13 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
368
389
  if (!item?.lifecycle) return res.status(404).json({ error: 'No lifecycle on this item' });
369
390
  const from = item.lifecycle.currentPhase;
370
391
  if (TERMINAL.has(from)) return res.status(400).json({ error: `Cannot skip from terminal state: ${from}` });
371
- if (!SKIPPABLE.has(from)) return res.status(400).json({ error: `Phase ${from} is not skippable` });
372
- const valid = TRANSITIONS[from];
392
+ if (!skippableOf(modeOf(item)).includes(from)) return res.status(400).json({ error: `Phase ${from} is not skippable` });
393
+ const valid = transitionsOf(modeOf(item))[from];
373
394
  if (!valid?.includes(targetPhase)) return res.status(400).json({ error: `Invalid transition: ${from} → ${targetPhase}` });
374
395
 
375
396
  // COMP-MCP-ENFORCE: verdict-gate the skip (fail-closed) before mutating.
376
397
  if (guardEnabled) {
377
- const g = await guardedTransition({ featureCode: item.lifecycle.featureCode, from, to: targetPhase, workspaceRoot: projectRoot, resolvedBy: 'agent' });
398
+ const g = await guardedTransition({ featureCode: item.lifecycle.featureCode, from, to: targetPhase, workspaceRoot: projectRoot, resolvedBy: 'agent', mode: modeOf(item) });
378
399
  if (!g.applied) return res.status(422).json({ error: 'transition refused by guard', from, to: targetPhase, verdict: g.verdict, guardError: g.error });
379
400
  }
380
401
 
@@ -384,7 +405,8 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
384
405
  appendPhaseHistory(item, { from, to: targetPhase, outcome: 'skipped', timestamp: now });
385
406
  store.updateLifecycle(req.params.id, item.lifecycle);
386
407
  // COMP-MCP-ENFORCE Slice 2 (lifecycle-as-truth): project phase → STATUS.
387
- if (guardEnabled) await projectFeatureStatus({ featureCode: item.lifecycle.featureCode, phase: targetPhase, cwd: projectRoot });
408
+ // COMP-ROADMAP-PLAN: skipped for tracksFeatureJson:false modes (no feature.json).
409
+ if (guardEnabled && getMode(modeOf(item)).runner.tracksFeatureJson) await projectFeatureStatus({ featureCode: item.lifecycle.featureCode, phase: targetPhase, cwd: projectRoot });
388
410
  scheduleBroadcast();
389
411
  broadcastMessage({ type: 'lifecycleTransition', itemId: req.params.id, from, to: targetPhase, outcome: 'skipped', timestamp: now });
390
412
  emitDecisionEvent(broadcastMessage, buildPhaseTransitionEvent({ featureCode: item.lifecycle.featureCode, from, to: targetPhase, outcome: 'skipped', timestamp: now }));
@@ -414,7 +436,7 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
414
436
  // is unreachable — consistent with advance/skip/complete. Authorized
415
437
  // bypass remains stratum_guard_override (Slice 3).
416
438
  if (guardEnabled) {
417
- const g = await guardedTransition({ featureCode: item.lifecycle.featureCode, from, to: 'killed', workspaceRoot: projectRoot, resolvedBy: 'agent' });
439
+ const g = await guardedTransition({ featureCode: item.lifecycle.featureCode, from, to: 'killed', workspaceRoot: projectRoot, resolvedBy: 'agent', mode: modeOf(item) });
418
440
  if (!g.applied) return res.status(422).json({ error: 'kill refused by guard', from, to: 'killed', verdict: g.verdict, guardError: g.error });
419
441
  }
420
442
 
@@ -428,7 +450,8 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
428
450
  store.updateItem(req.params.id, { status: 'killed' });
429
451
  // COMP-MCP-ENFORCE Slice 2: project kill → KILLED onto feature.json
430
452
  // (closes the COMP-PARITY-7 gap — kill previously wrote vision-state only).
431
- if (guardEnabled) await projectFeatureStatus({ featureCode: item.lifecycle.featureCode, phase: 'killed', cwd: projectRoot });
453
+ // COMP-ROADMAP-PLAN: skipped for tracksFeatureJson:false modes (no feature.json).
454
+ if (guardEnabled && getMode(modeOf(item)).runner.tracksFeatureJson) await projectFeatureStatus({ featureCode: item.lifecycle.featureCode, phase: 'killed', cwd: projectRoot });
432
455
  scheduleBroadcast();
433
456
  broadcastMessage({ type: 'lifecycleTransition', itemId: req.params.id, from, to: 'killed', outcome: 'killed', timestamp: now });
434
457
  emitDecisionEvent(broadcastMessage, buildPhaseTransitionEvent({ featureCode: item.lifecycle.featureCode, from, to: 'killed', outcome: 'killed', timestamp: now }));
@@ -449,8 +472,11 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
449
472
  try {
450
473
  const item = store.items.get(req.params.id);
451
474
  if (!item?.lifecycle) return res.status(404).json({ error: 'No lifecycle on this item' });
452
- if (item.lifecycle.currentPhase !== 'ship') {
453
- return res.status(400).json({ error: `Can only complete from ship phase, currently in: ${item.lifecycle.currentPhase}` });
475
+ // COMP-ROADMAP-MODES: the phase that completes is the mode's completable
476
+ // phase (build 'ship', byte-identical), not a hard-coded literal.
477
+ const completable = completablePhaseOf(modeOf(item));
478
+ if (item.lifecycle.currentPhase !== completable) {
479
+ return res.status(400).json({ error: `Can only complete from ${completable} phase, currently in: ${item.lifecycle.currentPhase}` });
454
480
  }
455
481
 
456
482
  // COMP-MCP-ENFORCE Slice 3: evidence-bound completion. Under the guard,
@@ -471,15 +497,15 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
471
497
  }
472
498
  verifiedTestsPass = ev.testsAttested ? true : (req.body?.tests_pass === true);
473
499
 
474
- const g = await guardedTransition({ featureCode: item.lifecycle.featureCode, from: 'ship', to: 'complete', workspaceRoot: projectRoot, commitSha: req.body?.commit_sha, resolvedBy: 'agent' });
475
- if (!g.applied) return res.status(422).json({ error: 'completion refused by guard', from: 'ship', to: 'complete', verdict: g.verdict, guardError: g.error });
500
+ const g = await guardedTransition({ featureCode: item.lifecycle.featureCode, from: completable, to: 'complete', workspaceRoot: projectRoot, commitSha: req.body?.commit_sha, resolvedBy: 'agent', mode: modeOf(item) });
501
+ if (!g.applied) return res.status(422).json({ error: 'completion refused by guard', from: completable, to: 'complete', verdict: g.verdict, guardError: g.error });
476
502
  }
477
503
 
478
504
  const now = new Date().toISOString();
479
505
  item.lifecycle.currentPhase = 'complete';
480
506
  item.lifecycle.completedAt = now;
481
507
  // COMP-OBS-TIMELINE: populate phaseHistory + emit phase_transition DecisionEvent
482
- appendPhaseHistory(item, { from: 'ship', to: 'complete', outcome: 'approved', timestamp: now });
508
+ appendPhaseHistory(item, { from: completable, to: 'complete', outcome: 'approved', timestamp: now });
483
509
  store.updateLifecycle(req.params.id, item.lifecycle);
484
510
  store.updateItem(req.params.id, { status: 'complete' });
485
511
  // COMP-MCP-ENFORCE Slice 2: status projection for the no-commit path is
@@ -487,8 +513,8 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
487
513
  // recordCompletion bridge, which is the authority + partial-write reporter
488
514
  // on the commit_sha path.
489
515
  scheduleBroadcast();
490
- broadcastMessage({ type: 'lifecycleTransition', itemId: req.params.id, from: 'ship', to: 'complete', outcome: 'approved', timestamp: now });
491
- emitDecisionEvent(broadcastMessage, buildPhaseTransitionEvent({ featureCode: item.lifecycle.featureCode, from: 'ship', to: 'complete', outcome: 'approved', timestamp: now }));
516
+ broadcastMessage({ type: 'lifecycleTransition', itemId: req.params.id, from: completable, to: 'complete', outcome: 'approved', timestamp: now });
517
+ emitDecisionEvent(broadcastMessage, buildPhaseTransitionEvent({ featureCode: item.lifecycle.featureCode, from: completable, to: 'complete', outcome: 'approved', timestamp: now }));
492
518
  // COMP-OBS-DRIFT: emit drift axes before status so STATUS reads fresh drift_axes
493
519
  emitDriftAxes(broadcastMessage, store, item, projectRoot, now);
494
520
  // COMP-OBS-STATUS: emit status snapshot after lifecycle transition (complete)
@@ -545,7 +571,8 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
545
571
  } catch { /* decision event emit best-effort */ }
546
572
  // No recordCompletion bridge ran — project complete → COMPLETE so
547
573
  // lifecycle-as-truth still reaches feature.json (best-effort).
548
- if (guardEnabled) await projectFeatureStatus({ featureCode, phase: 'complete', cwd: projectRoot });
574
+ // COMP-ROADMAP-PLAN: skipped for tracksFeatureJson:false modes (no feature.json).
575
+ if (guardEnabled && getMode(modeOf(item)).runner.tracksFeatureJson) await projectFeatureStatus({ featureCode, phase: 'complete', cwd: projectRoot });
549
576
  }
550
577
  }
551
578
 
@@ -756,7 +783,9 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
756
783
  });
757
784
 
758
785
  // ── Artifact endpoints ───────────────────────────────────────────────
759
- const artifactManager = new ArtifactManager(featuresPath);
786
+ // ArtifactManager is constructed per-request from the item's lifecycle mode —
787
+ // the artifact metadata it assesses/scaffolds is mode-scoped (the root stays
788
+ // features-rooted; mode-owned root resolution is a deferred follow-up).
760
789
 
761
790
  app.get('/api/vision/items/:id/artifacts', (req, res) => {
762
791
  try {
@@ -765,7 +794,7 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
765
794
  if (!item.lifecycle?.featureCode) {
766
795
  return res.status(400).json({ error: 'Item has no lifecycle featureCode' });
767
796
  }
768
- const assessment = artifactManager.assess(item.lifecycle.featureCode);
797
+ const assessment = new ArtifactManager(featuresPath, item.lifecycle?.mode).assess(item.lifecycle.featureCode);
769
798
  res.json(assessment);
770
799
  } catch (err) {
771
800
  res.status(500).json({ error: err.message });
@@ -779,7 +808,7 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
779
808
  if (!item.lifecycle?.featureCode) {
780
809
  return res.status(400).json({ error: 'Item has no lifecycle featureCode' });
781
810
  }
782
- const result = artifactManager.scaffold(item.lifecycle.featureCode, req.body);
811
+ const result = new ArtifactManager(featuresPath, item.lifecycle?.mode).scaffold(item.lifecycle.featureCode, req.body);
783
812
  res.json(result);
784
813
  } catch (err) {
785
814
  res.status(400).json({ error: err.message });
@@ -828,9 +857,11 @@ export function attachVisionRoutes(app, { store, scheduleBroadcast, broadcastMes
828
857
  if (existing) {
829
858
  return res.status(200).json(existing); // idempotent
830
859
  }
831
- // Dedup: if a pending gate already exists for the same item+step, reuse it
860
+ // Dedup: if a pending gate already exists for the same item+step in THIS
861
+ // flow, reuse it. Scoped to flowId so a fresh run never inherits a stale
862
+ // pending gate from a prior crashed/aborted run for the same feature.
832
863
  if (itemId) {
833
- const dupGate = store.findPendingGate(itemId, stepId);
864
+ const dupGate = store.findPendingGate(itemId, stepId, flowId);
834
865
  if (dupGate) {
835
866
  return res.status(200).json(dupGate);
836
867
  }
@@ -30,6 +30,7 @@ import { attachSettingsRoutes } from './settings-routes.js';
30
30
  import { attachHealthRoutes } from './health-routes.js';
31
31
  import { attachValidateRoutes } from './validate-routes.js';
32
32
  import { attachQaScopeRoutes } from './qa-scope-routes.js';
33
+ import { attachSmartmemoryRoutes } from './smartmemory-routes.js';
33
34
  import { attachDesignRoutes } from './design-routes.js';
34
35
  import { DesignSessionManager } from './design-session.js';
35
36
  import { attachPipelineRoutes } from './pipeline-routes.js';
@@ -102,6 +103,11 @@ export class VisionServer {
102
103
  // Read-only GET /api/qa-scope?featureCode=…; reads off req.workspace.
103
104
  attachQaScopeRoutes(app);
104
105
 
106
+ // ── SmartMemory recall (COMP-SMARTMEMORY-RECALL, opt-in) ───────────────
107
+ // Read-only GET /api/smartmemory/recall?featureCode=…; degrade-never-fail,
108
+ // answers {enabled:false} from config alone when the flag is OFF.
109
+ attachSmartmemoryRoutes(app);
110
+
105
111
  // ── Vision CRUD + lifecycle routes ─────────────────────────────────────
106
112
  attachVisionRoutes(app, {
107
113
  store: this.store,
@@ -200,7 +200,7 @@ export class VisionStore {
200
200
  throw new Error(`Invalid phase: ${updates.phase}`);
201
201
  }
202
202
 
203
- const allowed = ['type', 'title', 'description', 'confidence', 'status', 'phase', 'position', 'parentId', 'summary', 'files', 'featureCode', 'stratumFlowId', 'evidence', 'group'];
203
+ const allowed = ['type', 'title', 'description', 'confidence', 'status', 'phase', 'position', 'parentId', 'summary', 'files', 'featureCode', 'stratumFlowId', 'evidence', 'group', 'priority'];
204
204
  for (const key of allowed) {
205
205
  if (updates[key] !== undefined) {
206
206
  item[key] = updates[key];
@@ -349,10 +349,19 @@ export class VisionStore {
349
349
  return this.gates.get(gateId) || null;
350
350
  }
351
351
 
352
- /** Find an existing pending gate for the same item+step */
353
- findPendingGate(itemId, stepId) {
352
+ /**
353
+ * Find an existing pending gate for the same item+step within the same flow.
354
+ *
355
+ * The flowId guard prevents a fresh run (new flowId) from reusing a stale
356
+ * pending gate left by a prior crashed/aborted run for the same feature — that
357
+ * gate is never resolved, so reusing it would poll until the server TTL
358
+ * expires it and then error. A null flowId falls back to the legacy
359
+ * item+step match (callers that don't track a flow).
360
+ */
361
+ findPendingGate(itemId, stepId, flowId = null) {
354
362
  for (const gate of this.gates.values()) {
355
- if (gate.status === 'pending' && gate.itemId === itemId && gate.stepId === stepId) {
363
+ if (gate.status === 'pending' && gate.itemId === itemId && gate.stepId === stepId
364
+ && (flowId == null || gate.flowId === flowId)) {
356
365
  return gate;
357
366
  }
358
367
  }