@smartmemory/compose 0.1.1-beta → 0.1.2-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/.claude/skills/bug-fix/SKILL.md +143 -0
  2. package/.claude/skills/compose/SKILL.md +604 -0
  3. package/.compose-deps.json +89 -0
  4. package/README.md +14 -3
  5. package/bin/compose.js +473 -0
  6. package/contracts/comp-obs-contract.schema.json +362 -0
  7. package/contracts/cross-model-review-result.json +78 -0
  8. package/contracts/review-result.json +126 -0
  9. package/dist/assets/{_baseUniq-CQwX6VLz.js → _baseUniq-D-avYfn5.js} +1 -1
  10. package/dist/assets/{arc-SxJ2J1sh.js → arc-BC4dfQ-X.js} +1 -1
  11. package/dist/assets/{architectureDiagram-Q4EWVU46-BykunY1F.js → architectureDiagram-Q4EWVU46-BZmFXnGI.js} +1 -1
  12. package/dist/assets/{blockDiagram-DXYQGD6D-ohAKBOUw.js → blockDiagram-DXYQGD6D-DlfWSuux.js} +1 -1
  13. package/dist/assets/{c4Diagram-AHTNJAMY-DBDC3ENB.js → c4Diagram-AHTNJAMY-Y__uJrRx.js} +1 -1
  14. package/dist/assets/channel-LRG9kHqJ.js +1 -0
  15. package/dist/assets/{chunk-4BX2VUAB-Cv93Z7uM.js → chunk-4BX2VUAB-BfMePfTp.js} +1 -1
  16. package/dist/assets/{chunk-4TB4RGXK-DE0WBDkj.js → chunk-4TB4RGXK-BdlMSdEA.js} +1 -1
  17. package/dist/assets/{chunk-55IACEB6-CE1EXenG.js → chunk-55IACEB6-vrQHZTdv.js} +1 -1
  18. package/dist/assets/{chunk-EDXVE4YY-DA7Ana6H.js → chunk-EDXVE4YY-B8wioVlW.js} +1 -1
  19. package/dist/assets/{chunk-FMBD7UC4-CTDIPA3p.js → chunk-FMBD7UC4-Cd6Hrux2.js} +1 -1
  20. package/dist/assets/{chunk-OYMX7WX6-uGBaPaTX.js → chunk-OYMX7WX6-CfrhdQXY.js} +1 -1
  21. package/dist/assets/{chunk-QZHKN3VN-CYlnXuUO.js → chunk-QZHKN3VN-B9JQerOU.js} +1 -1
  22. package/dist/assets/{chunk-YZCP3GAM-ojGkzcZK.js → chunk-YZCP3GAM-DFN9X99H.js} +1 -1
  23. package/dist/assets/classDiagram-6PBFFD2Q-BC9a6pDE.js +1 -0
  24. package/dist/assets/classDiagram-v2-HSJHXN6E-BC9a6pDE.js +1 -0
  25. package/dist/assets/clone-dRxgFrBv.js +1 -0
  26. package/dist/assets/{cose-bilkent-S5V4N54A-Bktn9hL-.js → cose-bilkent-S5V4N54A-BAn0ap_E.js} +1 -1
  27. package/dist/assets/{dagre-KV5264BT-DFaSzuRF.js → dagre-KV5264BT-DyxnVq1g.js} +1 -1
  28. package/dist/assets/{diagram-5BDNPKRD-DnfmDzEm.js → diagram-5BDNPKRD-XCrzqski.js} +1 -1
  29. package/dist/assets/{diagram-G4DWMVQ6-Bm8W9YnG.js → diagram-G4DWMVQ6-MBCAXft_.js} +1 -1
  30. package/dist/assets/{diagram-MMDJMWI5-B5-TSKvp.js → diagram-MMDJMWI5-DbtB2yS6.js} +1 -1
  31. package/dist/assets/{diagram-TYMM5635-ls4rqlky.js → diagram-TYMM5635-Bb5NzX61.js} +1 -1
  32. package/dist/assets/{erDiagram-SMLLAGMA-giG6WO-r.js → erDiagram-SMLLAGMA-CpIeCOh2.js} +1 -1
  33. package/dist/assets/{flowDiagram-DWJPFMVM-XvlUuz-7.js → flowDiagram-DWJPFMVM-CHyoKnhW.js} +1 -1
  34. package/dist/assets/{ganttDiagram-T4ZO3ILL-hLBV57oV.js → ganttDiagram-T4ZO3ILL-DErKteO_.js} +1 -1
  35. package/dist/assets/{gitGraphDiagram-UUTBAWPF-BHu3s_Gn.js → gitGraphDiagram-UUTBAWPF-KFVAtj2F.js} +1 -1
  36. package/dist/assets/{graph-D0Cfv00Y.js → graph-CRnO_ifT.js} +1 -1
  37. package/dist/assets/index-DKBsEUJ-.css +1 -0
  38. package/dist/assets/index-DkRKLuNr.js +1144 -0
  39. package/dist/assets/{infoDiagram-42DDH7IO-DbqRsOo3.js → infoDiagram-42DDH7IO-BZFnuSp5.js} +1 -1
  40. package/dist/assets/{ishikawaDiagram-UXIWVN3A-DnCdx7zb.js → ishikawaDiagram-UXIWVN3A-4Xe2Szde.js} +1 -1
  41. package/dist/assets/{journeyDiagram-VCZTEJTY-CfD7eNcP.js → journeyDiagram-VCZTEJTY-CZRByfS-.js} +1 -1
  42. package/dist/assets/{kanban-definition-6JOO6SKY-BYaO9-mK.js → kanban-definition-6JOO6SKY-B95sk6Fk.js} +1 -1
  43. package/dist/assets/{layout-Bj72wOEB.js → layout-BqNQzxWT.js} +1 -1
  44. package/dist/assets/{linear-BRFo114D.js → linear-CUh7qb64.js} +1 -1
  45. package/dist/assets/{min-GCHnKlJS.js → min-wXgOS3ig.js} +1 -1
  46. package/dist/assets/{mindmap-definition-QFDTVHPH-n0PMebY4.js → mindmap-definition-QFDTVHPH-DB6iaAbO.js} +1 -1
  47. package/dist/assets/{pieDiagram-DEJITSTG-pN4CljHF.js → pieDiagram-DEJITSTG-CHkZHrTW.js} +1 -1
  48. package/dist/assets/{quadrantDiagram-34T5L4WZ-DNoAy8-D.js → quadrantDiagram-34T5L4WZ-DoTEO8e3.js} +1 -1
  49. package/dist/assets/{requirementDiagram-MS252O5E-BhtY05PT.js → requirementDiagram-MS252O5E-Dn8peXYp.js} +1 -1
  50. package/dist/assets/{sankeyDiagram-XADWPNL6-B6AD-16A.js → sankeyDiagram-XADWPNL6-DRXs6Ipb.js} +1 -1
  51. package/dist/assets/{sequenceDiagram-FGHM5R23-DShHM-uk.js → sequenceDiagram-FGHM5R23-wBBYZ0aq.js} +1 -1
  52. package/dist/assets/{stateDiagram-FHFEXIEX-DMxn7HTo.js → stateDiagram-FHFEXIEX-DPlBNGmf.js} +1 -1
  53. package/dist/assets/stateDiagram-v2-QKLJ7IA2-BW0ezXb4.js +1 -0
  54. package/dist/assets/{timeline-definition-GMOUNBTQ-Cdu6uq52.js → timeline-definition-GMOUNBTQ-CbbyTlHk.js} +1 -1
  55. package/dist/assets/{vennDiagram-DHZGUBPP-CpK29iRe.js → vennDiagram-DHZGUBPP-Bj4GaFfj.js} +1 -1
  56. package/dist/assets/{wardley-RL74JXVD-BQgSkdcO.js → wardley-RL74JXVD-RtNzq8KU.js} +55 -55
  57. package/dist/assets/{wardleyDiagram-NUSXRM2D-DJHYev6O.js → wardleyDiagram-NUSXRM2D-CDfE3zSj.js} +1 -1
  58. package/dist/assets/{xychartDiagram-5P7HB3ND-1d75pbaO.js → xychartDiagram-5P7HB3ND-CZXHHYD5.js} +1 -1
  59. package/dist/index.html +2 -2
  60. package/lib/budget-ledger.js +45 -0
  61. package/lib/bug-bisect.js +292 -0
  62. package/lib/bug-checkpoint.js +191 -0
  63. package/lib/bug-escalation.js +306 -0
  64. package/lib/bug-index-gen.js +136 -0
  65. package/lib/bug-ledger.js +126 -0
  66. package/lib/build-stream-schema.js +176 -0
  67. package/lib/build-stream-writer.js +3 -1
  68. package/lib/build.js +854 -284
  69. package/lib/connector-factory-shim.js +167 -0
  70. package/lib/constants.js +18 -0
  71. package/lib/debug-discipline.js +176 -27
  72. package/lib/deps.js +205 -0
  73. package/lib/health-score.js +4 -4
  74. package/lib/import.js +26 -13
  75. package/lib/inject-schema.js +21 -0
  76. package/lib/new.js +27 -53
  77. package/lib/result-normalizer.js +160 -144
  78. package/lib/review-lenses.js +5 -5
  79. package/lib/review-normalize.js +413 -0
  80. package/lib/review-prompt.js +163 -0
  81. package/lib/sections.js +325 -0
  82. package/lib/step-prompt.js +21 -1
  83. package/lib/step-validator.js +5 -3
  84. package/lib/stratum-mcp-client.js +172 -7
  85. package/package.json +14 -3
  86. package/pipelines/bug-fix.stratum.yaml +39 -1
  87. package/pipelines/build.stratum.yaml +28 -45
  88. package/pipelines/review-fix.stratum.yaml +1 -1
  89. package/presets/team-review.stratum.yaml +21 -14
  90. package/server/build-stream-bridge.js +28 -0
  91. package/server/cc-session-feature-resolver.js +111 -0
  92. package/server/cc-session-reader.js +327 -0
  93. package/server/cc-session-watcher.js +318 -0
  94. package/server/compose-mcp-tools.js +0 -125
  95. package/server/compose-mcp.js +2 -4
  96. package/server/contract-diff.js +192 -0
  97. package/server/decision-event-emit.js +175 -0
  98. package/server/decision-event-id.js +64 -0
  99. package/server/decision-events-snapshot.js +166 -0
  100. package/server/design-routes.js +92 -49
  101. package/server/drift-axes.js +365 -0
  102. package/server/drift-emit.js +121 -0
  103. package/server/gate-log-store.js +102 -0
  104. package/server/lifecycle-phase-history.js +44 -0
  105. package/server/open-loops-store.js +102 -0
  106. package/server/schema-validator.js +49 -0
  107. package/server/status-emit.js +27 -0
  108. package/server/status-snapshot.js +218 -0
  109. package/server/vision-routes.js +332 -4
  110. package/server/vision-server.js +104 -12
  111. package/server/vision-store.js +21 -0
  112. package/dist/assets/channel-DGElom1e.js +0 -1
  113. package/dist/assets/classDiagram-6PBFFD2Q-KqWP9wWZ.js +0 -1
  114. package/dist/assets/classDiagram-v2-HSJHXN6E-KqWP9wWZ.js +0 -1
  115. package/dist/assets/clone-DUJKJXd7.js +0 -1
  116. package/dist/assets/index-CUd6pFGF.css +0 -1
  117. package/dist/assets/index-DReRlzZI.js +0 -1144
  118. package/dist/assets/stateDiagram-v2-QKLJ7IA2-o6PnCs4e.js +0 -1
  119. package/server/connectors/agent-connector.js +0 -78
  120. package/server/connectors/claude-sdk-connector.js +0 -198
  121. package/server/connectors/codex-connector.js +0 -240
  122. package/server/connectors/connector-discovery.js +0 -18
  123. package/server/connectors/connector-runtime.js +0 -13
  124. package/server/connectors/opencode-connector.js +0 -200
@@ -0,0 +1,166 @@
1
+ /**
2
+ * decision-events-snapshot.js — Derive DecisionEvent[] from persisted lifecycle state.
3
+ *
4
+ * COMP-OBS-TIMELINE A6: on WS connect the server re-derives the current
5
+ * feature's DecisionEvents from already-persisted sources so the client can
6
+ * seed its store without waiting for a live replay.
7
+ *
8
+ * Sources:
9
+ * - kind=phase_transition → lifecycle.phaseHistory[] (populated by lifecycle-phase-history.js)
10
+ * - kind=iteration → lifecycle.iterationState (start + complete pairs)
11
+ * - kind=branch → lifecycle.lifecycle_ext.branch_lineage.branches[]
12
+ * - kind=gate → gate-log.jsonl (populated by COMP-OBS-GATELOG)
13
+ * - kind=drift_threshold → lifecycle.lifecycle_ext.drift_axes[] (COMP-OBS-DRIFT)
14
+ *
15
+ * Deterministic ids: re-derive == identity with the live emitters because both
16
+ * use the same id helpers from decision-event-id.js.
17
+ */
18
+
19
+ import {
20
+ phaseTransitionDecisionEventId,
21
+ iterationDecisionEventId,
22
+ branchDecisionEventId,
23
+ } from './decision-event-id.js';
24
+ import { buildPhaseTransitionEvent, buildIterationEvent, buildGateEvent, buildDriftThresholdEvent } from './decision-event-emit.js';
25
+ import { readGateLog } from './gate-log-store.js';
26
+
27
+ /**
28
+ * Derive all DecisionEvents for a given featureCode from persisted lifecycle state.
29
+ *
30
+ * @param {object} state — { items: Map<id, item> } (from VisionStore.getState or internal)
31
+ * @param {string} featureCode — the feature to filter for
32
+ * @returns {DecisionEvent[]} array sorted by timestamp ascending
33
+ */
34
+ export function deriveDecisionEvents(state, featureCode) {
35
+ const events = [];
36
+
37
+ // state may expose items as Map (internal) or Array (getState). Handle both.
38
+ const itemsIterable = state.items instanceof Map
39
+ ? state.items.values()
40
+ : (Array.isArray(state.items) ? state.items : []);
41
+
42
+ for (const item of itemsIterable) {
43
+ const lc = item?.lifecycle;
44
+ if (!lc) continue;
45
+ if (lc.featureCode !== featureCode) continue;
46
+
47
+ // ── phase_transition events ─────────────────────────────────────────────
48
+ for (const entry of lc.phaseHistory || []) {
49
+ events.push(buildPhaseTransitionEvent({
50
+ featureCode,
51
+ from: entry.from,
52
+ to: entry.to,
53
+ outcome: entry.outcome,
54
+ agent_id: entry.agent_id || null,
55
+ timestamp: entry.timestamp,
56
+ }));
57
+ }
58
+
59
+ // ── iteration events ────────────────────────────────────────────────────
60
+ const iter = lc.iterationState;
61
+ if (iter?.loopId) {
62
+ // Always emit start event
63
+ events.push(buildIterationEvent({
64
+ featureCode,
65
+ loopId: iter.loopId,
66
+ loopType: iter.loopType,
67
+ stage: 'start',
68
+ attempt: null,
69
+ outcome: 'retry',
70
+ timestamp: iter.startedAt,
71
+ }));
72
+
73
+ // Emit complete event only when loop has actually completed
74
+ if (iter.status === 'complete' && iter.completedAt) {
75
+ events.push(buildIterationEvent({
76
+ featureCode,
77
+ loopId: iter.loopId,
78
+ loopType: iter.loopType,
79
+ stage: 'complete',
80
+ attempt: iter.count,
81
+ outcome: iter.outcome,
82
+ timestamp: iter.completedAt,
83
+ }));
84
+ }
85
+ }
86
+
87
+ // ── branch events ───────────────────────────────────────────────────────
88
+ // Production lineage is persisted at `item.lifecycle.lifecycle_ext.branch_lineage`
89
+ // by `vision-store.updateLifecycleExt`. Earlier drafts read `item.lifecycle_ext`
90
+ // (top-level), which is why test fixtures occasionally produced the wrong shape.
91
+ const lineage = lc.lifecycle_ext?.branch_lineage;
92
+ if (lineage?.branches) {
93
+ // Compute sibling_branch_ids per branch — all branches sharing the same
94
+ // non-null fork_uuid (the convention BRANCH's live emitter uses).
95
+ const siblingsByFork = new Map();
96
+ for (const b of lineage.branches) {
97
+ if (!b.fork_uuid) continue;
98
+ if (!siblingsByFork.has(b.fork_uuid)) siblingsByFork.set(b.fork_uuid, []);
99
+ siblingsByFork.get(b.fork_uuid).push(b.branch_id);
100
+ }
101
+ for (const branch of lineage.branches) {
102
+ const eventId = branchDecisionEventId(featureCode, branch.branch_id);
103
+ const siblingIds = branch.fork_uuid ? siblingsByFork.get(branch.fork_uuid) ?? [] : [];
104
+ events.push({
105
+ id: eventId,
106
+ feature_code: featureCode,
107
+ timestamp: branch.started_at,
108
+ kind: 'branch',
109
+ title: `New branch ${branch.branch_id.slice(0, 8)}…`,
110
+ metadata: {
111
+ branch_id: branch.branch_id,
112
+ fork_uuid: branch.fork_uuid || null,
113
+ sibling_branch_ids: siblingIds,
114
+ },
115
+ roles: [],
116
+ });
117
+ }
118
+ }
119
+
120
+ // ── drift_threshold events (kind=drift_threshold) — COMP-OBS-DRIFT ────────
121
+ // Rehydrate from persisted DriftAxis.breach_event_id + breach_started_at.
122
+ // Using persisted fields guarantees byte-for-byte identity with the live emit
123
+ // — we do NOT recompute from current computed_at, which would produce a
124
+ // different id on every reconnect.
125
+ const driftAxes = lc.lifecycle_ext?.drift_axes ?? [];
126
+ for (const axis of driftAxes) {
127
+ if (axis.breached === true && axis.breach_event_id && axis.breach_started_at) {
128
+ events.push(buildDriftThresholdEvent({
129
+ featureCode,
130
+ axisId: axis.axis_id,
131
+ ratio: axis.ratio,
132
+ threshold: axis.threshold,
133
+ breachStartedAt: axis.breach_started_at,
134
+ breachEventId: axis.breach_event_id,
135
+ }));
136
+ }
137
+ }
138
+ }
139
+
140
+ // ── gate events (kind=gate) — rehydrate from project gate-log.jsonl ──────
141
+ // Without this, live gate cards on the timeline disappear after WS reconnect.
142
+ // The gate log is project-scoped (NOT app-global), so cross-feature filter is safe.
143
+ try {
144
+ const entries = readGateLog({ featureCode });
145
+ for (const entry of entries) {
146
+ // Translate route-vocab decision (approve/revise/kill) into schema vocab
147
+ // before composing the event. Entry already stores schema vocab, but
148
+ // buildGateEvent re-maps for safety.
149
+ const event = buildGateEvent({
150
+ featureCode,
151
+ gateLogEntryId: entry.id,
152
+ gateId: entry.gate_id,
153
+ decision: entry.decision, // already schema vocab — buildGateEvent passes through
154
+ timestamp: entry.timestamp,
155
+ });
156
+ events.push(event);
157
+ }
158
+ } catch (err) {
159
+ // Gate log read is best-effort; missing/unreadable files yield no gate events.
160
+ // Existing rehydration of phase/iteration/branch events still proceeds.
161
+ }
162
+
163
+ // Sort by timestamp ascending (oldest first — strip renders newest-right)
164
+ events.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
165
+ return events;
166
+ }
@@ -12,7 +12,34 @@
12
12
 
13
13
  import fs from 'node:fs';
14
14
  import path from 'node:path';
15
+ import { randomUUID } from 'node:crypto';
15
16
  import { parseDecisionBlocks } from '../src/components/vision/designSessionState.js';
17
+ import { StratumMcpClient } from '../lib/stratum-mcp-client.js';
18
+
19
+ // Lazy singleton — design conversations share one stratum-mcp connection
20
+ // across the server process lifetime. Concurrent runs are correlation-id scoped.
21
+ let _stratumClient = null;
22
+ let _stratumConnectPromise = null;
23
+ async function _getStratum() {
24
+ if (_stratumClient) return _stratumClient;
25
+ if (!_stratumConnectPromise) {
26
+ const c = new StratumMcpClient();
27
+ _stratumConnectPromise = c.connect()
28
+ .then(() => { _stratumClient = c; return c; })
29
+ .catch((err) => { _stratumConnectPromise = null; throw err; });
30
+ }
31
+ return _stratumConnectPromise;
32
+ }
33
+
34
+ /** Tear down the cached stratum-mcp connection. Tests should call this in after(). */
35
+ export async function closeDesignStratum() {
36
+ const c = _stratumClient;
37
+ _stratumClient = null;
38
+ _stratumConnectPromise = null;
39
+ if (c) {
40
+ try { await c.close(); } catch { /* ignore */ }
41
+ }
42
+ }
16
43
 
17
44
  /** @type {Map<string, Set<import('node:http').ServerResponse>>} — key is `${scope}:${featureCode || ''}` */
18
45
  export const designListeners = new Map();
@@ -64,6 +91,9 @@ export function broadcastDesignEvent(key, type, data) {
64
91
  async function dispatchDesignAgent(sessionManager, projectRoot, scope, featureCode) {
65
92
  const key = sessionKey(scope, featureCode, projectRoot);
66
93
  if (_inFlight.has(key)) return; // already running
94
+ // Tests assert on the HTTP response only — skip the LLM round-trip to avoid
95
+ // keeping a stratum-mcp subprocess alive past the test lifetime.
96
+ if (process.env.NODE_ENV === 'test' || process.env.COMPOSE_DESIGN_DISPATCH === '0') return;
67
97
  _inFlight.add(key);
68
98
  // Snapshot message count so we can detect new messages arriving during the run
69
99
  let promptMessageCount = 0;
@@ -107,44 +137,60 @@ Conversation history:
107
137
  ${formattedMessages}`;
108
138
 
109
139
  let fullContent = '';
110
- let lastToolName = null;
111
140
  let toolUseCounter = 0;
112
- let lastToolUseId = null;
113
-
114
- const { ClaudeSDKConnector } = await import('./connectors/claude-sdk-connector.js');
115
- const connector = new ClaudeSDKConnector({ cwd: projectRoot });
116
-
117
- for await (const event of connector.run(systemPrompt)) {
118
- if (event.type === 'assistant' && event.content) {
119
- fullContent += event.content;
120
- broadcastDesignEvent(key, 'text', { content: event.content });
121
- } else if (event.type === 'result' && event.content) {
122
- // Final aggregated text use it if we haven't accumulated anything
123
- if (!fullContent) {
124
- fullContent = event.content;
125
- broadcastDesignEvent(key, 'text', { content: event.content });
141
+
142
+ const stratum = await _getStratum();
143
+ const correlationId = randomUUID();
144
+ const subStepId = '_agent_run';
145
+
146
+ const unsub = stratum.onEvent(correlationId, subStepId, (env) => {
147
+ if (!env || env.schema_version !== '0.2.5') return;
148
+ const m = env.metadata ?? {};
149
+ switch (env.kind) {
150
+ case 'agent_relay':
151
+ if (m.role === 'assistant' && typeof m.text === 'string' && m.text.length > 0) {
152
+ fullContent += m.text;
153
+ broadcastDesignEvent(key, 'text', { content: m.text });
154
+ }
155
+ break;
156
+ case 'tool_use_summary': {
157
+ const toolUseId = `tu-${++toolUseCounter}`;
158
+ if (m.tool) {
159
+ broadcastDesignEvent(key, 'research', {
160
+ id: toolUseId,
161
+ tool: m.tool,
162
+ input: m.input ?? {},
163
+ timestamp: new Date().toISOString(),
164
+ });
165
+ }
166
+ if (m.summary) {
167
+ broadcastDesignEvent(key, 'research_result', {
168
+ id: toolUseId,
169
+ tool: m.tool ?? null,
170
+ summary: String(m.summary).slice(0, 200),
171
+ timestamp: new Date().toISOString(),
172
+ });
173
+ }
174
+ break;
126
175
  }
127
- } else if (event.type === 'error') {
128
- broadcastDesignEvent(key, 'error', { message: event.message });
129
- return;
130
- } else if (event.type === 'tool_use') {
131
- lastToolName = event.tool;
132
- lastToolUseId = `tu-${++toolUseCounter}`;
133
- broadcastDesignEvent(key, 'research', {
134
- id: lastToolUseId,
135
- tool: event.tool,
136
- input: event.input,
137
- timestamp: new Date().toISOString(),
138
- });
139
- } else if (event.type === 'tool_use_summary') {
140
- broadcastDesignEvent(key, 'research_result', {
141
- id: lastToolUseId,
142
- tool: lastToolName,
143
- summary: (event.summary || '').slice(0, 200),
144
- timestamp: new Date().toISOString(),
145
- });
176
+ default:
177
+ break;
146
178
  }
147
- // Ignore other system init/complete events
179
+ });
180
+
181
+ let runResult;
182
+ try {
183
+ runResult = await stratum.agentRun('claude', systemPrompt, {
184
+ cwd: projectRoot,
185
+ correlationId,
186
+ });
187
+ } finally {
188
+ unsub();
189
+ }
190
+ // Fall back to result.text if no agent_relay events arrived (older producers).
191
+ if (!fullContent && runResult?.text) {
192
+ fullContent = runResult.text;
193
+ broadcastDesignEvent(key, 'text', { content: fullContent });
148
194
  }
149
195
 
150
196
  // Parse for decision blocks and broadcast them
@@ -190,9 +236,9 @@ ${formattedMessages}`;
190
236
  * surviving project switches via /api/project/switch.
191
237
  *
192
238
  * @param {object} app — Express app
193
- * @param {{ getSessionManager: () => import('./design-session.js').DesignSessionManager, getConnector: () => import('./connectors/claude-sdk-connector.js').ClaudeSDKConnector|null, getProjectRoot: () => string }} deps
239
+ * @param {{ getSessionManager: () => import('./design-session.js').DesignSessionManager, getProjectRoot: () => string }} deps
194
240
  */
195
- export function attachDesignRoutes(app, { getSessionManager, getConnector, getProjectRoot }) {
241
+ export function attachDesignRoutes(app, { getSessionManager, getProjectRoot }) {
196
242
  // POST /api/design/start
197
243
  app.post('/api/design/start', (req, res) => {
198
244
  const { scope, featureCode } = req.body || {};
@@ -328,8 +374,10 @@ export function attachDesignRoutes(app, { getSessionManager, getConnector, getPr
328
374
  return res.status(404).json({ error: `No session found for ${scope}${featureCode ? `:${featureCode}` : ''}` });
329
375
  }
330
376
 
331
- // If no connector available or no projectRoot, mark complete and skip doc generation
332
- if (!getConnector() || !projectRoot) {
377
+ // No projectRoot, or test/dispatch-disabled mode mark complete and skip doc generation.
378
+ const dispatchDisabled =
379
+ process.env.NODE_ENV === 'test' || process.env.COMPOSE_DESIGN_DISPATCH === '0';
380
+ if (!projectRoot || dispatchDisabled) {
333
381
  const completedSession = sessionManager.completeSession(scope, featureCode);
334
382
  res.json({ session: completedSession });
335
383
  return;
@@ -379,18 +427,13 @@ Write the design document in Markdown format. Include:
379
427
 
380
428
  Output ONLY the Markdown content, no code fences.`;
381
429
 
382
- // Generate the design doc via a fresh connector
383
- const { ClaudeSDKConnector } = await import('./connectors/claude-sdk-connector.js');
384
- const connector = new ClaudeSDKConnector({ cwd: projectRoot });
430
+ // Generate the design doc via the persistent stratum-mcp client.
431
+ // No streaming needed for the one-shot doc generation.
385
432
  let docContent = '';
386
433
  try {
387
- for await (const event of connector.run(docPrompt)) {
388
- if (event.type === 'assistant' && event.content) {
389
- docContent += event.content;
390
- } else if (event.type === 'result' && event.content && !docContent) {
391
- docContent = event.content;
392
- }
393
- }
434
+ const stratum = await _getStratum();
435
+ const result = await stratum.runAgentText('claude', docPrompt, { cwd: projectRoot });
436
+ docContent = result || '';
394
437
  } catch (err) {
395
438
  // If generation fails, don't complete — let user retry
396
439
  console.error('[design] Doc generation failed:', err.message);