@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
@@ -107,8 +107,59 @@ function buildRecommendation(gateDispatch, gateExtras) {
107
107
  };
108
108
  }
109
109
 
110
- function ask(rl, question) {
111
- return new Promise(resolve => rl.question(question, resolve));
110
+ // COMP-PLAN-GATE-LOOP (secondary): default deadline for a gate prompt that is
111
+ // reading the real stdin in a non-interactive context. Long enough for a piped
112
+ // producer to answer, short enough that a backgrounded/no-input runner fails
113
+ // fast instead of blocking forever.
114
+ export const GATE_NONINTERACTIVE_TIMEOUT_MS = 120_000;
115
+
116
+ const GATE_NONINTERACTIVE_HELP =
117
+ 'stdin is not a TTY and the Compose server is unreachable, so the gate cannot ' +
118
+ 'be answered. Start the :4001 server, run the gate interactively, or pass --all ' +
119
+ 'to auto-approve. The flow state is preserved — re-run with --resume.';
120
+
121
+ export class GateInputUnavailableError extends Error {
122
+ constructor(reason) {
123
+ super(`Gate decision required but ${reason}. ${GATE_NONINTERACTIVE_HELP}`);
124
+ this.name = 'GateInputUnavailableError';
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Read one line from the readline interface.
130
+ *
131
+ * When `armGuard` is set (real stdin, no TTY — see promptGate) the read is
132
+ * bounded: it rejects with GateInputUnavailableError if the input stream ends
133
+ * (EOF, e.g. stdin is /dev/null) or no line arrives within `deadlineMs`, so a
134
+ * non-interactive runner fails fast instead of blocking forever. Piped input
135
+ * still resolves normally; an unguarded read keeps the original behavior.
136
+ *
137
+ * @param {import('node:readline').Interface} rl
138
+ * @param {string} question
139
+ * @param {{ armGuard?: boolean, deadlineMs?: number }} [opts]
140
+ * @returns {Promise<string>}
141
+ */
142
+ export function ask(rl, question, { armGuard = false, deadlineMs = GATE_NONINTERACTIVE_TIMEOUT_MS } = {}) {
143
+ if (!armGuard) {
144
+ return new Promise(resolve => rl.question(question, resolve));
145
+ }
146
+ return new Promise((resolve, reject) => {
147
+ let settled = false;
148
+ const onEnd = () => finish(() => reject(new GateInputUnavailableError('stdin was closed before a decision')));
149
+ const timer = setTimeout(
150
+ () => finish(() => reject(new GateInputUnavailableError(`no input arrived within ${Math.round(deadlineMs / 1000)}s`))),
151
+ deadlineMs,
152
+ );
153
+ function finish(act) {
154
+ if (settled) return;
155
+ settled = true;
156
+ clearTimeout(timer);
157
+ rl.input.removeListener('end', onEnd);
158
+ act();
159
+ }
160
+ rl.input.once('end', onEnd);
161
+ rl.question(question, answer => finish(() => resolve(answer)));
162
+ });
112
163
  }
113
164
 
114
165
  /**
@@ -179,7 +230,7 @@ function drawGatePanel(out, gateDispatch, { artifact, gateExtras } = {}) {
179
230
  * @param {object} [options.gateExtras] - { fromPhase, toPhase } for panel display
180
231
  * @returns {Promise<{ outcome: string, rationale: string }>}
181
232
  */
182
- export async function promptGate(gateDispatch, { input, output, artifact, askAgent, gateExtras, nonInteractive } = {}) {
233
+ export async function promptGate(gateDispatch, { input, output, artifact, askAgent, gateExtras, nonInteractive, timeoutMs } = {}) {
183
234
  if (nonInteractive) {
184
235
  return { outcome: 'approve', rationale: 'auto-approved (--all mode)' };
185
236
  }
@@ -189,6 +240,15 @@ export async function promptGate(gateDispatch, { input, output, artifact, askAge
189
240
  output: output ?? process.stdout,
190
241
  });
191
242
 
243
+ // COMP-PLAN-GATE-LOOP (secondary): only bound the read when there is no
244
+ // interactive terminal AND we're on the real stdin. A real TTY (a human who
245
+ // may take their time) and an injected `input` (tests, piped/programmatic
246
+ // callers) keep the original block-until-answered behavior.
247
+ const askGuard = {
248
+ armGuard: input === undefined && !process.stdin.isTTY,
249
+ deadlineMs: timeoutMs ?? GATE_NONINTERACTIVE_TIMEOUT_MS,
250
+ };
251
+
192
252
  try {
193
253
  const { step_id, on_approve, on_revise, on_kill } = gateDispatch;
194
254
 
@@ -219,7 +279,7 @@ export async function promptGate(gateDispatch, { input, output, artifact, askAge
219
279
  let outcome;
220
280
 
221
281
  while (!outcome) {
222
- const raw = await ask(rl, '\n> ');
282
+ const raw = await ask(rl, '\n> ', askGuard);
223
283
  const trimmed = raw.trim();
224
284
 
225
285
  // COMP-UX-3b: Enter alone → use recommended default action (only if recommendation was shown)
@@ -266,7 +326,7 @@ export async function promptGate(gateDispatch, { input, output, artifact, askAge
266
326
  let rationale;
267
327
  if (notes.length > 0) {
268
328
  rationale = notes.join('\n');
269
- const addMore = await ask(rl, ' Additional rationale (or Enter to use notes): ');
329
+ const addMore = await ask(rl, ' Additional rationale (or Enter to use notes): ', askGuard);
270
330
  if (addMore.trim()) {
271
331
  rationale += '\n' + addMore.trim();
272
332
  }
@@ -274,7 +334,7 @@ export async function promptGate(gateDispatch, { input, output, artifact, askAge
274
334
  rationale = 'approved';
275
335
  } else {
276
336
  while (!rationale) {
277
- const raw = await ask(rl, 'Rationale: ');
337
+ const raw = await ask(rl, 'Rationale: ', askGuard);
278
338
  const trimmed = raw.trim();
279
339
  if (trimmed) {
280
340
  rationale = trimmed;
@@ -0,0 +1,213 @@
1
+ // lib/lifecycle-modes.js
2
+ //
3
+ // COMP-ROADMAP-MODES — the mode-keyed lifecycle registry (keystone).
4
+ //
5
+ // Compose's lifecycle data (phase graph, skippable/terminal sets, the
6
+ // completion edge, per-phase artifacts, edge evidence, phase ordering, and the
7
+ // runner's per-mode behavioral switches) was hard-coded to the BUILD graph and
8
+ // scattered across server/lifecycle-guard.js, server/artifact-manager.js, and
9
+ // lib/build.js. This module lifts that data into a single source of truth keyed
10
+ // by lifecycle mode (`build | fix | plan`), so a third lifecycle (`plan`) is a
11
+ // data add, not a code fork, and so integration ports can later be pluggable.
12
+ //
13
+ // INVARIANT: the `build` entry reproduces the legacy hard-coded data VERBATIM —
14
+ // `build` behavior must stay byte-identical. test/lifecycle-modes.test.js pins
15
+ // the build entry against the legacy exports.
16
+ //
17
+ // Pure data + accessors. No imports from server/ (lib is the lower layer; the
18
+ // runner and server consume THIS, never the reverse). Loaders/functions stay in
19
+ // their owning modules, keyed here by a string the consumer interprets.
20
+ //
21
+ // See docs/features/COMP-ROADMAP-MODES/{design,blueprint,plan}.md.
22
+
23
+ /**
24
+ * @typedef {Object} ModeRunnerConfig
25
+ * @property {'features'|'docs/bugs'|'docs/plans'} artifactRoot token the runner maps to a dir
26
+ * @property {boolean} runsTriage whether the runner runs feature triage
27
+ * @property {boolean} tracksFeatureJson whether the runner writes feature.json lifecycle status
28
+ * @property {'feature'|'bug'|'plan'} descriptionLoader which description loader the runner uses
29
+ * @property {'feature'|'bug'|'plan'} planInputs which Stratum plan-input shape to build
30
+ * @property {string} defaultTemplate default Stratum template name for the mode
31
+ */
32
+
33
+ /** The mode registry. Keys are canonical modes; `build` data is verbatim-legacy. */
34
+ export const LIFECYCLE_MODES = {
35
+ // -------------------------------------------------------------------------
36
+ // BUILD — verbatim from server/lifecycle-guard.js (BASE_TRANSITIONS/SKIPPABLE/
37
+ // TERMINAL) + server/artifact-manager.js (PHASE_ARTIFACTS) + the runner's
38
+ // current feature behavior. MUST stay byte-identical.
39
+ // -------------------------------------------------------------------------
40
+ build: {
41
+ transitions: {
42
+ explore_design: ['prd', 'architecture', 'blueprint'],
43
+ prd: ['architecture', 'blueprint'],
44
+ architecture: ['blueprint'],
45
+ blueprint: ['verification'],
46
+ verification: ['plan', 'blueprint'],
47
+ plan: ['execute'],
48
+ execute: ['report', 'docs'],
49
+ report: ['docs'],
50
+ docs: ['ship'],
51
+ ship: [],
52
+ },
53
+ skippable: ['prd', 'architecture', 'report'],
54
+ terminal: ['complete', 'killed'],
55
+ genesis: 'explore_design',
56
+ completablePhase: 'ship', // the phase that → complete (was hard-coded by name)
57
+ phaseArtifacts: ['design.md', 'prd.md', 'architecture.md', 'blueprint.md', 'plan.md', 'report.md'],
58
+ edgeEvidence: {
59
+ 'explore_design->blueprint': 'design.md',
60
+ 'blueprint->verification': 'blueprint.md',
61
+ 'plan->execute': 'plan.md',
62
+ },
63
+ phaseOrder: ['explore_design', 'prd', 'architecture', 'blueprint', 'verification', 'plan', 'execute', 'report', 'docs', 'ship'],
64
+ runner: {
65
+ artifactRoot: 'features',
66
+ runsTriage: true,
67
+ tracksFeatureJson: true,
68
+ descriptionLoader: 'feature',
69
+ planInputs: 'feature',
70
+ defaultTemplate: 'build',
71
+ },
72
+ },
73
+
74
+ // -------------------------------------------------------------------------
75
+ // FIX — describes today's bug-fix lifecycle. Data parity only: the bug flow
76
+ // registers NO guard today and MODES does not change that, so the graph here
77
+ // is informational (exercised by registry tests, not wired into a live guard).
78
+ // The `runner` block IS load-bearing: it reproduces today's bug runner
79
+ // behavior byte-identically (docs/bugs root, no triage, no feature.json).
80
+ // phaseArtifacts is [] → assess() falls back to the global default set, which
81
+ // is exactly how bug items are assessed today (no change).
82
+ // -------------------------------------------------------------------------
83
+ fix: {
84
+ transitions: {
85
+ reproduce: ['diagnose'],
86
+ diagnose: ['scope_check', 'fix'],
87
+ scope_check: ['fix'],
88
+ fix: ['test'],
89
+ test: ['verify', 'fix'],
90
+ verify: ['retro_check', 'ship'],
91
+ retro_check: ['ship'],
92
+ ship: [],
93
+ },
94
+ skippable: [],
95
+ terminal: ['complete', 'killed'],
96
+ genesis: 'reproduce',
97
+ completablePhase: 'ship',
98
+ phaseArtifacts: [], // empty → assess uses the global default (preserves today's bug behavior)
99
+ edgeEvidence: {},
100
+ phaseOrder: ['reproduce', 'diagnose', 'scope_check', 'fix', 'test', 'verify', 'retro_check', 'ship'],
101
+ runner: {
102
+ artifactRoot: 'docs/bugs',
103
+ runsTriage: false,
104
+ tracksFeatureJson: false,
105
+ descriptionLoader: 'bug',
106
+ planInputs: 'bug',
107
+ defaultTemplate: 'bug-fix',
108
+ },
109
+ },
110
+
111
+ // -------------------------------------------------------------------------
112
+ // PLAN — minimal SEED graph. COMP-ROADMAP-PLAN owns the real plan lifecycle
113
+ // (framing → research → ideation → convergence → build-handshake) and the
114
+ // `compose plan` command. MODES ships only the mechanism: a coherent seed so
115
+ // a third mode is proven to be a data add. Not run by any CLI command yet.
116
+ // -------------------------------------------------------------------------
117
+ plan: {
118
+ transitions: {
119
+ explore_design: ['plan'],
120
+ plan: ['ship'],
121
+ ship: [],
122
+ },
123
+ skippable: [],
124
+ terminal: ['complete', 'killed'],
125
+ genesis: 'explore_design',
126
+ completablePhase: 'ship',
127
+ phaseArtifacts: ['design.md', 'plan.md'],
128
+ edgeEvidence: { 'explore_design->plan': 'design.md' },
129
+ phaseOrder: ['explore_design', 'plan', 'ship'],
130
+ runner: {
131
+ artifactRoot: 'docs/plans',
132
+ runsTriage: false,
133
+ tracksFeatureJson: false,
134
+ descriptionLoader: 'plan',
135
+ planInputs: 'plan',
136
+ defaultTemplate: 'plan',
137
+ },
138
+ },
139
+ };
140
+
141
+ /**
142
+ * Map any runtime or canonical mode token to a canonical registry key. Any key
143
+ * present in LIFECYCLE_MODES resolves to itself — so adding a new mode is a
144
+ * DATA-ONLY change (no edit here). The legacy runtime aliases (feature→build,
145
+ * bug→fix) are honored for back-compat; everything else falls back to build.
146
+ */
147
+ export function resolveMode(raw) {
148
+ if (raw && Object.prototype.hasOwnProperty.call(LIFECYCLE_MODES, raw)) return raw;
149
+ if (raw === 'bug') return 'fix';
150
+ if (raw === 'feature') return 'build';
151
+ return 'build'; // undefined / null / unknown → build (legacy default)
152
+ }
153
+
154
+ /** Get the mode entry, normalizing the input. Unknown → build. */
155
+ export function getMode(mode) {
156
+ return LIFECYCLE_MODES[resolveMode(mode)];
157
+ }
158
+
159
+ export function genesisOf(mode) {
160
+ return getMode(mode).genesis;
161
+ }
162
+
163
+ export function completablePhaseOf(mode) {
164
+ return getMode(mode).completablePhase;
165
+ }
166
+
167
+ export function transitionsOf(mode) {
168
+ return getMode(mode).transitions;
169
+ }
170
+
171
+ export function skippableOf(mode) {
172
+ return getMode(mode).skippable;
173
+ }
174
+
175
+ export function terminalOf(mode) {
176
+ return getMode(mode).terminal;
177
+ }
178
+
179
+ export function phaseOrderOf(mode) {
180
+ return getMode(mode).phaseOrder;
181
+ }
182
+
183
+ /** Per-edge ("from->to" → artifact filename) evidence map for the mode. */
184
+ export function edgeEvidenceOf(mode) {
185
+ return getMode(mode).edgeEvidence;
186
+ }
187
+
188
+ /**
189
+ * The union of every mode's phase vocabulary — each mode's `phaseOrder` plus its
190
+ * terminal phases. Read-side recognizers (e.g. status-snapshot's KNOWN_PHASES)
191
+ * derive from this so a non-build-mode phase isn't flagged "unrecognized".
192
+ * MUST include the terminal phases (complete/killed): the snapshot's
193
+ * unknown-phase guard runs BEFORE its terminal branches.
194
+ */
195
+ export function allKnownPhases() {
196
+ const set = new Set();
197
+ for (const key of Object.keys(LIFECYCLE_MODES)) {
198
+ const m = LIFECYCLE_MODES[key];
199
+ for (const p of m.phaseOrder) set.add(p);
200
+ for (const t of m.terminal) set.add(t);
201
+ }
202
+ return [...set];
203
+ }
204
+
205
+ /**
206
+ * The artifact filenames a mode declares. An EMPTY list means "no narrowing" —
207
+ * the artifact manager falls back to the global ARTIFACT_SCHEMAS key set (which
208
+ * is exactly today's behavior for build and bug items). A non-empty list scopes
209
+ * assessment to that subset.
210
+ */
211
+ export function artifactsOf(mode) {
212
+ return getMode(mode).phaseArtifacts;
213
+ }
package/lib/new.js CHANGED
@@ -16,6 +16,7 @@ import { installFactoryShim } from './connector-factory-shim.js';
16
16
  import { buildStepPrompt, buildRetryPrompt, buildGateContext } from './step-prompt.js';
17
17
  import { promptGate } from './gate-prompt.js';
18
18
  import { VisionWriter } from './vision-writer.js';
19
+ import { readFlowRound } from './flow-state.js';
19
20
 
20
21
  import { validateStep } from './step-validator.js';
21
22
 
@@ -197,7 +198,9 @@ export async function runNew(intent, opts = {}) {
197
198
  }
198
199
  }
199
200
 
200
- const gateId = await visionWriter.createGate(flowId, stepId, itemId);
201
+ // COMP-PLAN-GATE-LOOP: round-aware gate id so a revise re-entry mints a
202
+ // fresh gate instead of reusing the prior resolved one.
203
+ const gateId = await visionWriter.createGate(flowId, stepId, itemId, { round: readFlowRound(flowId) });
201
204
 
202
205
  // Resolve artifact path for this gate
203
206
  const gateArtifact = priorValConfig?.artifact
@@ -340,7 +343,8 @@ async function executeChildFlow(
340
343
 
341
344
  } else if (resp.status === 'await_gate') {
342
345
  console.log(` [${childFlowName}] Gate: ${resp.step_id}`);
343
- const gateId = await visionWriter.createGate(childFlowId, resp.step_id, itemId);
346
+ // COMP-PLAN-GATE-LOOP: round-aware gate id (child flow has its own round).
347
+ const gateId = await visionWriter.createGate(childFlowId, resp.step_id, itemId, { round: readFlowRound(childFlowId) });
344
348
  const { outcome, rationale } = await promptGate(resp, gateOpts);
345
349
  await visionWriter.resolveGate(gateId, outcome);
346
350
  resp = await stratum.gateResolve(childFlowId, resp.step_id, outcome, rationale, 'human');
@@ -1,45 +1,47 @@
1
1
  /**
2
- * index.js — public API for the roadmap dependency graph generator
3
- * (COMP-ROADMAP-GRAPH-1).
2
+ * index.js — shared render/write/check helpers for the roadmap dependency
3
+ * graph (COMP-ROADMAP-GRAPH-1 renderer; COMP-ROADMAP-GRAPH-2 made it
4
+ * source-agnostic). The collection of inputs lives with the caller — today the
5
+ * sole collector is the vision projection in server/roadmap-graph-vision.js.
4
6
  *
5
- * generateRoadmapGraph(cwd, opts) — render + atomic-write the HTML
6
- * checkRoadmapGraph(cwd, opts) render in-memory, diff vs on-disk
7
+ * buildArtifactFromInputs(cwd, inputs) — buildGraph + render (no write)
8
+ * writeArtifact(cwd, built, opts) atomic-write the HTML
9
+ * checkArtifact(cwd, built, opts) — diff vs on-disk
7
10
  *
8
- * Both throw DanglingEdgeError (code DANGLING_EDGE) when an edge points at an
9
- * unknown feature — the Cytoscape-crash bug class this feature kills.
11
+ * buildGraph throws DanglingEdgeError (code DANGLING_EDGE) when an edge points
12
+ * at an unknown feature — the Cytoscape-crash bug class this feature kills.
10
13
  */
11
14
  import { existsSync, readFileSync, writeFileSync, renameSync, unlinkSync, mkdirSync } from 'node:fs';
12
15
  import { join, isAbsolute, dirname } from 'node:path';
13
- import { resolveFeaturesPath } from '../project-paths.js';
14
- import { collectGraphInputs } from './collect.js';
15
16
  import { buildGraph } from './model.js';
16
17
  import { renderGraphHtml } from './render.js';
17
18
  import { loadGraphConfig } from './config.js';
18
19
 
19
20
  /**
20
- * Build the HTML + graph stats for a project without writing to disk.
21
+ * Build the HTML + stats from already-collected graph inputs. The inputs may
22
+ * come from any collector (the static feature.json/deps.yaml path, or the
23
+ * vision-store canonical projection) — this is the single render seam.
21
24
  * @param {string} cwd
22
- * @returns {{ html: string, nodes: object[], edges: object[], dropped: string[], warnings: string[], config: object }}
25
+ * @param {{ nodes: object[], rawEdges: object[], knownCodes: Set<string>, warnings?: string[] }} inputs
23
26
  */
24
- export function buildRoadmapGraph(cwd) {
25
- const featuresDir = resolveFeaturesPath(cwd);
26
- const inputs = collectGraphInputs(cwd, featuresDir);
27
+ export function buildArtifactFromInputs(cwd, inputs) {
27
28
  const graph = buildGraph(inputs); // throws DanglingEdgeError
28
29
  const config = loadGraphConfig(cwd);
29
30
  const html = renderGraphHtml({ nodes: graph.nodes, edges: graph.edges, config });
30
- return { html, nodes: graph.nodes, edges: graph.edges, dropped: graph.dropped, warnings: inputs.warnings, config };
31
+ return { html, nodes: graph.nodes, edges: graph.edges, dropped: graph.dropped, warnings: inputs.warnings || [], config };
32
+ }
33
+
34
+ function resolveOutPath(cwd, built, opts) {
35
+ const outRel = opts.out || built.config.out;
36
+ return isAbsolute(outRel) ? outRel : join(cwd, outRel);
31
37
  }
32
38
 
33
39
  /**
34
- * Generate and atomically write roadmap-graph.html.
35
- * @param {string} cwd project root
36
- * @param {{ out?: string }} [opts] out path (relative to cwd unless absolute); defaults to config.out
40
+ * Atomically write a built artifact's HTML and return the summary.
37
41
  * @returns {{ path: string, nodeCount: number, edgeCount: number, droppedCount: number, warnings: string[] }}
38
42
  */
39
- export function generateRoadmapGraph(cwd, opts = {}) {
40
- const built = buildRoadmapGraph(cwd);
41
- const outRel = opts.out || built.config.out;
42
- const outPath = isAbsolute(outRel) ? outRel : join(cwd, outRel);
43
+ export function writeArtifact(cwd, built, opts = {}) {
44
+ const outPath = resolveOutPath(cwd, built, opts);
43
45
  atomicWrite(outPath, built.html);
44
46
  return {
45
47
  path: outPath,
@@ -51,15 +53,11 @@ export function generateRoadmapGraph(cwd, opts = {}) {
51
53
  }
52
54
 
53
55
  /**
54
- * Render in-memory and compare to the on-disk file.
55
- * @param {string} cwd
56
- * @param {{ out?: string }} [opts]
56
+ * Compare a built artifact's HTML to the on-disk file.
57
57
  * @returns {{ matches: boolean, path: string, exists: boolean, diffSummary: string, nodeCount: number, edgeCount: number, warnings: string[] }}
58
58
  */
59
- export function checkRoadmapGraph(cwd, opts = {}) {
60
- const built = buildRoadmapGraph(cwd);
61
- const outRel = opts.out || built.config.out;
62
- const outPath = isAbsolute(outRel) ? outRel : join(cwd, outRel);
59
+ export function checkArtifact(cwd, built, opts = {}) {
60
+ const outPath = resolveOutPath(cwd, built, opts);
63
61
  const exists = existsSync(outPath);
64
62
  const onDisk = exists ? readFileSync(outPath, 'utf-8') : null;
65
63
  const matches = exists && onDisk === built.html;
@@ -0,0 +1,126 @@
1
+ /**
2
+ * vision-adapter.js — COMP-ROADMAP-GRAPH-2.
3
+ *
4
+ * Converts a VisionStore's items + connections into the
5
+ * { nodes, rawEdges, knownCodes, warnings } shape that buildGraph() consumes
6
+ * (the same shape collect.js produces from feature.json/deps.yaml). This is the
7
+ * single seam that lets the cockpit/vision model drive the one canonical
8
+ * renderer (buildGraph -> renderGraphHtml).
9
+ *
10
+ * The adapter is source-agnostic: it is fed the LIVE in-memory store (cockpit
11
+ * export routes) or a freshly-seeded throwaway store (canonical headless
12
+ * generation). Both go through this one function.
13
+ */
14
+
15
+ // Vision connection vocab -> graph edge vocab. Mirrors the dependency/parallel
16
+ // split the static path encodes in deps.yaml (depends_on/blocks -> dep,
17
+ // concurrent_with -> concurrent).
18
+ const VISION_EDGE_TYPE_MAP = {
19
+ blocks: 'dep',
20
+ informs: 'dep',
21
+ implements: 'dep',
22
+ supports: 'concurrent',
23
+ contradicts: 'concurrent',
24
+ };
25
+
26
+ // Vision status (lowercase, 9 values) -> buildGraph input status (UPPERCASE).
27
+ // COMPLETE/KILLED/SUPERSEDED are DROP_STATUSES in model.js and get dropped there.
28
+ const VISION_STATUS_MAP = {
29
+ planned: 'PLANNED',
30
+ ready: 'PLANNED',
31
+ in_progress: 'IN_PROGRESS',
32
+ review: 'IN_PROGRESS',
33
+ complete: 'COMPLETE',
34
+ blocked: 'BLOCKED',
35
+ parked: 'PARKED',
36
+ killed: 'KILLED',
37
+ superseded: 'SUPERSEDED',
38
+ };
39
+
40
+ function toUpperStatus(status) {
41
+ return VISION_STATUS_MAP[status] || String(status || '').toUpperCase() || 'PLANNED';
42
+ }
43
+
44
+ /**
45
+ * Remove `Track:` / `Priority:` annotation lines from a description so the
46
+ * graph `desc` is clean prose. The static path reads these from structured
47
+ * metadata; the cockpit historically encoded them inline.
48
+ */
49
+ function stripTrackPriority(description) {
50
+ if (!description) return '';
51
+ return String(description)
52
+ .split('\n')
53
+ .filter((line) => !/^\s*(track|priority)\s*:/i.test(line))
54
+ .join('\n')
55
+ .trim();
56
+ }
57
+
58
+ /**
59
+ * @param {Array} items - vision items (e.g. [...store.items.values()])
60
+ * @param {Array} connections - vision connections (e.g. [...store.connections.values()])
61
+ * @param {{externalPrefixes?: string[], includeProseEdges?: boolean}} [opts]
62
+ * includeProseEdges (default true): the LIVE projection renders every
63
+ * relationship, including prose-derived `informs` edges (shipped cockpit
64
+ * behavior). The CANONICAL projection passes false so only structural
65
+ * (deps.yaml-derived blocks/supports/...) edges remain, matching the static
66
+ * collector which only ever drew deps.yaml edges.
67
+ * @returns {{nodes: object[], rawEdges: object[], knownCodes: Set<string>, warnings: string[]}}
68
+ */
69
+ export function visionToGraphInputs(items, connections, { externalPrefixes = [], includeProseEdges = true } = {}) {
70
+ const isExternal = (code) => externalPrefixes.some((p) => code && code.startsWith(p));
71
+ const warnings = [];
72
+ const nodes = [];
73
+ const knownCodes = new Set();
74
+ const idToCode = new Map();
75
+
76
+ for (const it of items || []) {
77
+ if (it.type !== 'feature') continue;
78
+ // The live store carries the canonical code in lifecycle.featureCode
79
+ // (set via updateLifecycle); the top-level featureCode and title are
80
+ // fallbacks. Mirror the precedence the export route used historically.
81
+ const code = it.lifecycle?.featureCode || it.featureCode || it.title;
82
+ if (!code) continue;
83
+ idToCode.set(it.id, code);
84
+ knownCodes.add(code);
85
+ // External-prefixed codes are known (so edges to them don't dangle) but are
86
+ // not rendered as nodes — buildGraph silently drops their edges.
87
+ if (isExternal(code)) continue;
88
+ nodes.push({
89
+ id: code,
90
+ status: toUpperStatus(it.status),
91
+ name: it.title || code,
92
+ priority: it.priority || 'medium',
93
+ track: it.group || 'standalone',
94
+ desc: stripTrackPriority(it.description),
95
+ });
96
+ }
97
+
98
+ const rawEdges = [];
99
+ for (const c of connections || []) {
100
+ const fromCode = idToCode.get(c.fromId);
101
+ const toCode = idToCode.get(c.toId);
102
+ if (!fromCode || !toCode) {
103
+ const bad = !fromCode ? c.fromId : c.toId;
104
+ warnings.push(`connection ${c.id}: endpoint ${bad} is not a feature node (unresolved) — dropped`);
105
+ continue;
106
+ }
107
+ // `informs` is the prose-derived relationship type. The canonical
108
+ // projection excludes it so it matches the deps.yaml-only static collector.
109
+ if (!includeProseEdges && c.type === 'informs') continue;
110
+ const type = VISION_EDGE_TYPE_MAP[c.type];
111
+ if (!type) {
112
+ warnings.push(`connection ${c.id}: unmapped connection type "${c.type}" — dropped`);
113
+ continue;
114
+ }
115
+ rawEdges.push({ from: fromCode, to: toCode, type });
116
+ }
117
+
118
+ // Deterministic node order (independent of readdirSync / store insertion order)
119
+ // so the rendered HTML is byte-stable for --check. buildGraph preserves this
120
+ // order; edges are sorted inside buildGraph.
121
+ nodes.sort((a, b) => a.id.localeCompare(b.id));
122
+
123
+ return { nodes, rawEdges, knownCodes, warnings };
124
+ }
125
+
126
+ export { VISION_EDGE_TYPE_MAP, VISION_STATUS_MAP };