@smartmemory/compose 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1014 -0
  3. package/bin/compose.js +1515 -0
  4. package/dist/assets/_baseUniq-CQwX6VLz.js +1 -0
  5. package/dist/assets/arc-SxJ2J1sh.js +1 -0
  6. package/dist/assets/architectureDiagram-Q4EWVU46-BykunY1F.js +36 -0
  7. package/dist/assets/blockDiagram-DXYQGD6D-ohAKBOUw.js +132 -0
  8. package/dist/assets/c4Diagram-AHTNJAMY-DBDC3ENB.js +10 -0
  9. package/dist/assets/channel-DGElom1e.js +1 -0
  10. package/dist/assets/chunk-4BX2VUAB-Cv93Z7uM.js +1 -0
  11. package/dist/assets/chunk-4TB4RGXK-DE0WBDkj.js +206 -0
  12. package/dist/assets/chunk-55IACEB6-CE1EXenG.js +1 -0
  13. package/dist/assets/chunk-EDXVE4YY-DA7Ana6H.js +1 -0
  14. package/dist/assets/chunk-FMBD7UC4-CTDIPA3p.js +15 -0
  15. package/dist/assets/chunk-OYMX7WX6-uGBaPaTX.js +231 -0
  16. package/dist/assets/chunk-QZHKN3VN-CYlnXuUO.js +1 -0
  17. package/dist/assets/chunk-YZCP3GAM-ojGkzcZK.js +1 -0
  18. package/dist/assets/classDiagram-6PBFFD2Q-KqWP9wWZ.js +1 -0
  19. package/dist/assets/classDiagram-v2-HSJHXN6E-KqWP9wWZ.js +1 -0
  20. package/dist/assets/clone-DUJKJXd7.js +1 -0
  21. package/dist/assets/cose-bilkent-S5V4N54A-Bktn9hL-.js +1 -0
  22. package/dist/assets/dagre-KV5264BT-DFaSzuRF.js +4 -0
  23. package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  24. package/dist/assets/diagram-5BDNPKRD-DnfmDzEm.js +10 -0
  25. package/dist/assets/diagram-G4DWMVQ6-Bm8W9YnG.js +24 -0
  26. package/dist/assets/diagram-MMDJMWI5-B5-TSKvp.js +43 -0
  27. package/dist/assets/diagram-TYMM5635-ls4rqlky.js +24 -0
  28. package/dist/assets/erDiagram-SMLLAGMA-giG6WO-r.js +85 -0
  29. package/dist/assets/flowDiagram-DWJPFMVM-XvlUuz-7.js +162 -0
  30. package/dist/assets/ganttDiagram-T4ZO3ILL-hLBV57oV.js +292 -0
  31. package/dist/assets/gitGraphDiagram-UUTBAWPF-BHu3s_Gn.js +106 -0
  32. package/dist/assets/graph-D0Cfv00Y.js +1 -0
  33. package/dist/assets/index-CUd6pFGF.css +1 -0
  34. package/dist/assets/index-DReRlzZI.js +1144 -0
  35. package/dist/assets/infoDiagram-42DDH7IO-DbqRsOo3.js +2 -0
  36. package/dist/assets/init-Gi6I4Gst.js +1 -0
  37. package/dist/assets/ishikawaDiagram-UXIWVN3A-DnCdx7zb.js +70 -0
  38. package/dist/assets/journeyDiagram-VCZTEJTY-CfD7eNcP.js +139 -0
  39. package/dist/assets/kanban-definition-6JOO6SKY-BYaO9-mK.js +89 -0
  40. package/dist/assets/katex-DkKDou_j.js +257 -0
  41. package/dist/assets/layout-Bj72wOEB.js +1 -0
  42. package/dist/assets/linear-BRFo114D.js +1 -0
  43. package/dist/assets/min-GCHnKlJS.js +1 -0
  44. package/dist/assets/mindmap-definition-QFDTVHPH-n0PMebY4.js +96 -0
  45. package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  46. package/dist/assets/pieDiagram-DEJITSTG-pN4CljHF.js +30 -0
  47. package/dist/assets/quadrantDiagram-34T5L4WZ-DNoAy8-D.js +7 -0
  48. package/dist/assets/requirementDiagram-MS252O5E-BhtY05PT.js +84 -0
  49. package/dist/assets/sankeyDiagram-XADWPNL6-B6AD-16A.js +10 -0
  50. package/dist/assets/sequenceDiagram-FGHM5R23-DShHM-uk.js +157 -0
  51. package/dist/assets/stateDiagram-FHFEXIEX-DMxn7HTo.js +1 -0
  52. package/dist/assets/stateDiagram-v2-QKLJ7IA2-o6PnCs4e.js +1 -0
  53. package/dist/assets/timeline-definition-GMOUNBTQ-Cdu6uq52.js +120 -0
  54. package/dist/assets/vennDiagram-DHZGUBPP-CpK29iRe.js +34 -0
  55. package/dist/assets/wardley-RL74JXVD-BQgSkdcO.js +162 -0
  56. package/dist/assets/wardleyDiagram-NUSXRM2D-DJHYev6O.js +20 -0
  57. package/dist/assets/xychartDiagram-5P7HB3ND-1d75pbaO.js +7 -0
  58. package/dist/index.html +30 -0
  59. package/lib/agent-chains.js +65 -0
  60. package/lib/agent-string.js +86 -0
  61. package/lib/budget-ledger.js +86 -0
  62. package/lib/build-all.js +162 -0
  63. package/lib/build-dag.js +120 -0
  64. package/lib/build-stream-writer.js +190 -0
  65. package/lib/build.js +2997 -0
  66. package/lib/capability-checker.js +53 -0
  67. package/lib/cert-inject.js +38 -0
  68. package/lib/cli-progress.js +483 -0
  69. package/lib/constants.js +69 -0
  70. package/lib/cross-layer-audit.js +84 -0
  71. package/lib/debug-discipline.js +173 -0
  72. package/lib/feature-json.js +106 -0
  73. package/lib/gate-prompt.js +291 -0
  74. package/lib/gate-tiers.js +194 -0
  75. package/lib/health-history.js +119 -0
  76. package/lib/health-score.js +227 -0
  77. package/lib/ideabox.js +570 -0
  78. package/lib/import.js +244 -0
  79. package/lib/migrate-roadmap.js +94 -0
  80. package/lib/model-pricing.js +67 -0
  81. package/lib/new.js +413 -0
  82. package/lib/pipeline-cli.js +489 -0
  83. package/lib/plan-parser.js +103 -0
  84. package/lib/qa-scoping.js +474 -0
  85. package/lib/questionnaire.js +200 -0
  86. package/lib/resolve-port.js +7 -0
  87. package/lib/result-normalizer.js +349 -0
  88. package/lib/review-lenses.js +166 -0
  89. package/lib/roadmap-gen.js +210 -0
  90. package/lib/roadmap-parser.js +176 -0
  91. package/lib/server-probe.js +23 -0
  92. package/lib/staleness.js +87 -0
  93. package/lib/step-prompt.js +260 -0
  94. package/lib/step-validator.js +49 -0
  95. package/lib/stratum-mcp-client.js +365 -0
  96. package/lib/team-flag.js +46 -0
  97. package/lib/test-bootstrap.js +401 -0
  98. package/lib/triage.js +274 -0
  99. package/lib/vision-writer.js +391 -0
  100. package/package.json +111 -0
  101. package/pipelines/bug-fix.stratum.yaml +230 -0
  102. package/pipelines/build.stratum.yaml +498 -0
  103. package/pipelines/content.stratum.yaml +112 -0
  104. package/pipelines/coverage-sweep.stratum.yaml +52 -0
  105. package/pipelines/refactor.stratum.yaml +169 -0
  106. package/pipelines/research.stratum.yaml +88 -0
  107. package/pipelines/review-fix.stratum.yaml +109 -0
  108. package/presets/team-feature.stratum.yaml +105 -0
  109. package/presets/team-research.stratum.yaml +108 -0
  110. package/presets/team-review.stratum.yaml +106 -0
  111. package/scripts/agent-activity-hook.sh +31 -0
  112. package/scripts/agent-error-hook.sh +28 -0
  113. package/scripts/analyze-orphans.mjs +50 -0
  114. package/scripts/find-orphans.mjs +26 -0
  115. package/scripts/fix-phases.mjs +49 -0
  116. package/scripts/generate-stratum-spec.mjs +137 -0
  117. package/scripts/import-roadmap.mjs +116 -0
  118. package/scripts/phase-audit.mjs +33 -0
  119. package/scripts/run-pipeline.mjs +314 -0
  120. package/scripts/session-end-hook.sh +18 -0
  121. package/scripts/session-start-hook.sh +38 -0
  122. package/scripts/vision-hook.sh +104 -0
  123. package/scripts/vision-track.mjs +554 -0
  124. package/scripts/wire-all-orphans.mjs +108 -0
  125. package/scripts/wire-orphans.mjs +164 -0
  126. package/server/activity-routes.js +123 -0
  127. package/server/agent-health.js +197 -0
  128. package/server/agent-hooks.js +102 -0
  129. package/server/agent-mcp.js +10 -0
  130. package/server/agent-registry.js +95 -0
  131. package/server/agent-server.js +290 -0
  132. package/server/agent-spawn.js +251 -0
  133. package/server/agent-templates.js +77 -0
  134. package/server/artifact-manager.js +247 -0
  135. package/server/artifact-templates/architecture.md +28 -0
  136. package/server/artifact-templates/blueprint.md +21 -0
  137. package/server/artifact-templates/design.md +36 -0
  138. package/server/artifact-templates/plan.md +25 -0
  139. package/server/artifact-templates/prd.md +43 -0
  140. package/server/artifact-templates/report.md +40 -0
  141. package/server/block-tracker.js +90 -0
  142. package/server/build-stream-bridge.js +502 -0
  143. package/server/coalescing-buffer.js +46 -0
  144. package/server/compose-mcp-tools.js +479 -0
  145. package/server/compose-mcp.js +324 -0
  146. package/server/connectors/agent-connector.js +78 -0
  147. package/server/connectors/claude-sdk-connector.js +198 -0
  148. package/server/connectors/codex-connector.js +240 -0
  149. package/server/connectors/connector-discovery.js +18 -0
  150. package/server/connectors/connector-runtime.js +13 -0
  151. package/server/connectors/opencode-connector.js +200 -0
  152. package/server/design-routes.js +540 -0
  153. package/server/design-session.js +161 -0
  154. package/server/feature-scan.js +593 -0
  155. package/server/file-watcher.js +284 -0
  156. package/server/find-root.js +29 -0
  157. package/server/graph-export.js +343 -0
  158. package/server/ideabox-cache.js +77 -0
  159. package/server/ideabox-routes.js +294 -0
  160. package/server/index.js +156 -0
  161. package/server/model-tiers.js +49 -0
  162. package/server/pipeline-routes.js +288 -0
  163. package/server/policy-evaluator.js +36 -0
  164. package/server/project-root.js +122 -0
  165. package/server/security.js +23 -0
  166. package/server/session-manager.js +403 -0
  167. package/server/session-routes.js +190 -0
  168. package/server/session-store.js +107 -0
  169. package/server/settings-routes.js +35 -0
  170. package/server/settings-store.js +234 -0
  171. package/server/stratum-api.js +102 -0
  172. package/server/stratum-client.js +192 -0
  173. package/server/stratum-sync.js +193 -0
  174. package/server/summarizer.js +139 -0
  175. package/server/supervisor.js +196 -0
  176. package/server/vision-routes.js +668 -0
  177. package/server/vision-server.js +393 -0
  178. package/server/vision-store.js +360 -0
  179. package/server/vision-utils.js +179 -0
  180. package/server/worktree-gc.js +137 -0
  181. package/templates/ROADMAP.md +46 -0
@@ -0,0 +1,35 @@
1
+ /**
2
+ * settings-routes.js — Settings REST API.
3
+ *
4
+ * Routes:
5
+ * GET /api/settings — current merged settings
6
+ * PATCH /api/settings — partial update
7
+ * POST /api/settings/reset — reset all or a section
8
+ */
9
+
10
+ /**
11
+ * @param {object} app — Express app
12
+ * @param {{ settingsStore: object, broadcastMessage: function }} deps
13
+ */
14
+ export function attachSettingsRoutes(app, { settingsStore, broadcastMessage }) {
15
+ app.get('/api/settings', (_req, res) => {
16
+ res.json(settingsStore.get());
17
+ });
18
+
19
+ app.patch('/api/settings', (req, res) => {
20
+ try {
21
+ const updated = settingsStore.update(req.body);
22
+ broadcastMessage({ type: 'settingsUpdated', settings: updated });
23
+ res.json(updated);
24
+ } catch (err) {
25
+ res.status(400).json({ error: err.message });
26
+ }
27
+ });
28
+
29
+ app.post('/api/settings/reset', (req, res) => {
30
+ const section = req.body?.section || undefined;
31
+ const updated = settingsStore.reset(section);
32
+ broadcastMessage({ type: 'settingsUpdated', settings: updated });
33
+ res.json(updated);
34
+ });
35
+ }
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Settings Store — JSON-file-backed storage for user preferences.
3
+ * Loads from disk on startup, saves after every mutation.
4
+ * Returns merged view: contract defaults + user overrides.
5
+ */
6
+
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import { getDataDir as getDefaultDataDir } from './project-root.js';
10
+
11
+ const VALID_VIEWS = ['graph', 'tree', 'pipeline', 'gates', 'docs', 'design', 'sessions', 'settings'];
12
+ const VALID_THEMES = ['light', 'dark', 'system'];
13
+ const VALID_THINKING_MODES = ['adaptive', 'off'];
14
+ const VALID_EFFORT_LEVELS = ['low', 'medium', 'high', 'xhigh', 'max'];
15
+
16
+ export class SettingsStore {
17
+ constructor(dataDir, contract) {
18
+ const dir = dataDir || getDefaultDataDir();
19
+ this._file = path.join(dir, 'settings.json');
20
+ this._contract = contract;
21
+ this._userSettings = {};
22
+ this._load();
23
+ }
24
+
25
+ _load() {
26
+ try {
27
+ const raw = fs.readFileSync(this._file, 'utf-8');
28
+ this._userSettings = JSON.parse(raw);
29
+ } catch (err) {
30
+ if (err.code !== 'ENOENT') {
31
+ console.error('[settings] Failed to load settings, using defaults:', err.message);
32
+ }
33
+ this._userSettings = {};
34
+ }
35
+ }
36
+
37
+ _save() {
38
+ try {
39
+ fs.mkdirSync(path.dirname(this._file), { recursive: true });
40
+ fs.writeFileSync(this._file, JSON.stringify(this._userSettings, null, 2), 'utf-8');
41
+ } catch (err) {
42
+ console.error('[settings] Failed to save settings:', err.message);
43
+ }
44
+ }
45
+
46
+ _defaults() {
47
+ return {
48
+ policies: Object.fromEntries(
49
+ this._contract.phases.map(p => [p.id, p.defaultPolicy])
50
+ ),
51
+ iterations: { ...this._contract.iterationDefaults },
52
+ models: {
53
+ interactive: 'claude-sonnet-4-6',
54
+ agentRun: process.env.CLAUDE_MODEL || 'claude-sonnet-4-6',
55
+ summarizer: process.env.SUMMARIZER_MODEL || 'haiku',
56
+ },
57
+ ui: { theme: 'system', defaultView: 'graph' },
58
+ // COMP-CAPS-ENFORCE: runtime capability enforcement policy
59
+ capabilities: { enforcement: 'log' },
60
+ // COMP-HEALTH: quantified quality score settings
61
+ health: {
62
+ enabled: true,
63
+ gate_threshold: null, // null = off, number = min_score required
64
+ weights: {}, // dimension weight overrides (must sum to 1.0 ± 0.01)
65
+ },
66
+ // Claude thinking/effort controls. mode='tier' inherits the tier default
67
+ // (critical → adaptive+xhigh, standard → adaptive+high, fast → off).
68
+ // effort=null likewise inherits the tier default.
69
+ thinking: {
70
+ mode: 'tier', // 'tier' | 'adaptive' | 'off'
71
+ effort: null, // null (tier default) | 'low' | 'medium' | 'high' | 'xhigh' | 'max'
72
+ },
73
+ };
74
+ }
75
+
76
+ /** Returns merged defaults + user overrides. */
77
+ get() {
78
+ const defaults = this._defaults();
79
+ const user = this._userSettings;
80
+ return {
81
+ policies: { ...defaults.policies, ...user.policies },
82
+ iterations: {
83
+ review: { ...defaults.iterations.review, ...user.iterations?.review },
84
+ coverage: { ...defaults.iterations.coverage, ...user.iterations?.coverage },
85
+ },
86
+ models: { ...defaults.models, ...user.models },
87
+ ui: { ...defaults.ui, ...user.ui },
88
+ capabilities: { ...defaults.capabilities, ...user.capabilities },
89
+ // COMP-HEALTH: merge health settings
90
+ health: {
91
+ ...defaults.health,
92
+ ...user.health,
93
+ weights: { ...defaults.health.weights, ...user.health?.weights },
94
+ },
95
+ thinking: { ...defaults.thinking, ...user.thinking },
96
+ };
97
+ }
98
+
99
+ /** Validate and apply a partial settings update. */
100
+ update(patch) {
101
+ this._validate(patch);
102
+ // Deep merge into user settings
103
+ for (const section of ['policies', 'iterations', 'models', 'ui', 'capabilities', 'health', 'thinking']) {
104
+ if (patch[section]) {
105
+ if (!this._userSettings[section]) this._userSettings[section] = {};
106
+ if (section === 'iterations') {
107
+ for (const [key, val] of Object.entries(patch.iterations)) {
108
+ this._userSettings.iterations[key] = { ...this._userSettings.iterations[key], ...val };
109
+ }
110
+ } else {
111
+ Object.assign(this._userSettings[section], patch[section]);
112
+ }
113
+ }
114
+ }
115
+ this._save();
116
+ return this.get();
117
+ }
118
+
119
+ /** Reset user overrides. If section given, reset only that section. */
120
+ reset(section) {
121
+ if (section) {
122
+ delete this._userSettings[section];
123
+ } else {
124
+ this._userSettings = {};
125
+ }
126
+ this._save();
127
+ return this.get();
128
+ }
129
+
130
+ _validate(patch) {
131
+ const validModes = new Set(this._contract.policyModes);
132
+
133
+ // Reject unknown top-level keys
134
+ for (const key of Object.keys(patch)) {
135
+ if (!['policies', 'iterations', 'models', 'ui', 'capabilities', 'health', 'thinking'].includes(key)) {
136
+ throw new Error(`Unknown settings section: ${key}`);
137
+ }
138
+ }
139
+
140
+ if (patch.policies) {
141
+ for (const [phase, mode] of Object.entries(patch.policies)) {
142
+ if (mode !== null && !validModes.has(mode)) {
143
+ throw new Error(`Invalid policy mode for ${phase}: ${mode}`);
144
+ }
145
+ }
146
+ }
147
+
148
+ if (patch.iterations) {
149
+ for (const [type, config] of Object.entries(patch.iterations)) {
150
+ if (config.maxIterations !== undefined) {
151
+ const n = config.maxIterations;
152
+ if (!Number.isInteger(n) || n < 1 || n > 100) {
153
+ throw new Error(`Invalid maxIterations for ${type}: must be integer 1-100`);
154
+ }
155
+ }
156
+ if (config.timeout !== undefined) {
157
+ const t = config.timeout;
158
+ if (!Number.isInteger(t) || t < 1 || t > 120) {
159
+ throw new Error(`Invalid timeout for ${type}: must be integer 1-120 (minutes)`);
160
+ }
161
+ }
162
+ if (config.maxTotal !== undefined) {
163
+ const m = config.maxTotal;
164
+ if (!Number.isInteger(m) || m < 1 || m > 200) {
165
+ throw new Error(`Invalid maxTotal for ${type}: must be integer 1-200`);
166
+ }
167
+ }
168
+ }
169
+ }
170
+
171
+ if (patch.models) {
172
+ for (const [key, val] of Object.entries(patch.models)) {
173
+ if (typeof val !== 'string' || val.length === 0) {
174
+ throw new Error(`Invalid model for ${key}: must be non-empty string`);
175
+ }
176
+ }
177
+ }
178
+
179
+ if (patch.ui) {
180
+ if (patch.ui.theme !== undefined && !VALID_THEMES.includes(patch.ui.theme)) {
181
+ throw new Error(`Invalid theme: ${patch.ui.theme} (must be ${VALID_THEMES.join(', ')})`);
182
+ }
183
+ if (patch.ui.defaultView !== undefined && !VALID_VIEWS.includes(patch.ui.defaultView)) {
184
+ throw new Error(`Invalid defaultView: ${patch.ui.defaultView} (must be ${VALID_VIEWS.join(', ')})`);
185
+ }
186
+ }
187
+
188
+ if (patch.capabilities) {
189
+ if (patch.capabilities.enforcement !== undefined) {
190
+ const VALID_ENFORCEMENT = ['log', 'block'];
191
+ if (!VALID_ENFORCEMENT.includes(patch.capabilities.enforcement)) {
192
+ throw new Error(`Invalid enforcement: ${patch.capabilities.enforcement} (must be ${VALID_ENFORCEMENT.join(', ')})`);
193
+ }
194
+ }
195
+ }
196
+
197
+ if (patch.thinking) {
198
+ if (patch.thinking.mode !== undefined) {
199
+ const m = patch.thinking.mode;
200
+ if (m !== 'tier' && !VALID_THINKING_MODES.includes(m)) {
201
+ throw new Error(`Invalid thinking.mode: ${m} (must be tier, ${VALID_THINKING_MODES.join(', ')})`);
202
+ }
203
+ }
204
+ if (patch.thinking.effort !== undefined && patch.thinking.effort !== null) {
205
+ if (!VALID_EFFORT_LEVELS.includes(patch.thinking.effort)) {
206
+ throw new Error(`Invalid thinking.effort: ${patch.thinking.effort} (must be null or ${VALID_EFFORT_LEVELS.join(', ')})`);
207
+ }
208
+ }
209
+ }
210
+
211
+ if (patch.health) {
212
+ // Validate gate_threshold: must be null or 0-100
213
+ if (patch.health.gate_threshold !== undefined && patch.health.gate_threshold !== null) {
214
+ const t = patch.health.gate_threshold;
215
+ if (typeof t !== 'number' || t < 0 || t > 100) {
216
+ throw new Error(`Invalid health.gate_threshold: must be null or a number 0-100`);
217
+ }
218
+ }
219
+ // Validate weights: each value must be a number; sum must be 1.0 ± 0.01
220
+ if (patch.health.weights !== undefined) {
221
+ const vals = Object.values(patch.health.weights);
222
+ if (vals.some(v => typeof v !== 'number' || v < 0)) {
223
+ throw new Error('Invalid health.weights: all values must be non-negative numbers');
224
+ }
225
+ if (vals.length > 0) {
226
+ const sum = vals.reduce((s, v) => s + v, 0);
227
+ if (Math.abs(sum - 1.0) > 0.01) {
228
+ throw new Error(`Invalid health.weights: values must sum to 1.0 ± 0.01 (got ${sum.toFixed(4)})`);
229
+ }
230
+ }
231
+ }
232
+ }
233
+ }
234
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * stratum-api.js — Express router for Stratum pipeline monitor + gate UI.
3
+ *
4
+ * Pure transport adapter: calls stratum-client, maps results to HTTP.
5
+ * Zero domain logic — all gate semantics live in stratum.
6
+ *
7
+ * Routes:
8
+ * GET /api/stratum/flows
9
+ * GET /api/stratum/flows/:flowId
10
+ * GET /api/stratum/gates
11
+ * POST /api/stratum/gates/:flowId/:stepId/approve
12
+ * POST /api/stratum/gates/:flowId/:stepId/reject
13
+ * POST /api/stratum/gates/:flowId/:stepId/revise
14
+ */
15
+
16
+ import { Router } from 'express';
17
+ import * as _defaultClient from './stratum-client.js';
18
+
19
+ /** Wrap an async route handler so rejections call next(err) instead of going unhandled. */
20
+ function ar(fn) {
21
+ return (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
22
+ }
23
+
24
+ /**
25
+ * @param {object} [client] — override stratum-client (for tests only)
26
+ */
27
+ export function createStratumRouter(client) {
28
+ const stratum = client ?? _defaultClient;
29
+ const router = Router();
30
+
31
+ // -- Read routes -----------------------------------------------------------
32
+
33
+ router.get('/flows', ar(async (_req, res) => {
34
+ const result = await stratum.queryFlows();
35
+ if (result?.error) return res.status(_errorStatus(result.error.code)).json(result);
36
+ res.json(result);
37
+ }));
38
+
39
+ router.get('/flows/:flowId', ar(async (req, res) => {
40
+ const result = await stratum.queryFlow(req.params.flowId);
41
+ if (result?.error) return res.status(_errorStatus(result.error.code)).json(result);
42
+ res.json(result);
43
+ }));
44
+
45
+ router.get('/gates', ar(async (_req, res) => {
46
+ const result = await stratum.queryGates();
47
+ if (result?.error) return res.status(_errorStatus(result.error.code)).json(result);
48
+ res.json(result);
49
+ }));
50
+
51
+ // -- Gate mutation routes --------------------------------------------------
52
+
53
+ router.post('/gates/:flowId/:stepId/approve', ar(async (req, res) => {
54
+ const { flowId, stepId } = req.params;
55
+ const { note, resolvedBy } = req.body || {};
56
+ const result = await stratum.gateApprove(flowId, stepId, note, resolvedBy);
57
+ res.status(_mutationStatus(result)).json(result);
58
+ }));
59
+
60
+ router.post('/gates/:flowId/:stepId/reject', ar(async (req, res) => {
61
+ const { flowId, stepId } = req.params;
62
+ const { note, resolvedBy } = req.body || {};
63
+ const result = await stratum.gateReject(flowId, stepId, note, resolvedBy);
64
+ res.status(_mutationStatus(result)).json(result);
65
+ }));
66
+
67
+ router.post('/gates/:flowId/:stepId/revise', ar(async (req, res) => {
68
+ const { flowId, stepId } = req.params;
69
+ const { note, resolvedBy } = req.body || {};
70
+ const result = await stratum.gateRevise(flowId, stepId, note, resolvedBy);
71
+ res.status(_mutationStatus(result)).json(result);
72
+ }));
73
+
74
+ // -- Error middleware (catches ENOENT, unexpected throws, etc.) -------------
75
+ // eslint-disable-next-line no-unused-vars
76
+ router.use((err, _req, res, _next) => {
77
+ console.error('[stratum-api] unhandled error:', err.message);
78
+ res.status(503).json({ error: { code: 'UNAVAILABLE', message: err.message, detail: '' } });
79
+ });
80
+
81
+ return router;
82
+ }
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // Status code mapping — error codes → HTTP status
86
+ // ---------------------------------------------------------------------------
87
+
88
+ function _errorStatus(code) {
89
+ switch (code) {
90
+ case 'NOT_FOUND': return 404;
91
+ case 'TIMEOUT': return 504;
92
+ case 'INVALID': return 400;
93
+ default: return 500;
94
+ }
95
+ }
96
+
97
+ function _mutationStatus(result) {
98
+ if (result?.conflict) return 409;
99
+ if (result?.error) return _errorStatus(result.error.code);
100
+ if (result?.ok === true) return 200;
101
+ return 500;
102
+ }
@@ -0,0 +1,192 @@
1
+ /**
2
+ * stratum-client.js — Single adapter for all stratum-mcp subprocess calls.
3
+ *
4
+ * This is the ONLY module in compose that spawns stratum-mcp processes.
5
+ * All query and mutation calls go through the exported functions below.
6
+ * No other file may call execFile/spawn with 'stratum-mcp' as the command.
7
+ *
8
+ * Contract:
9
+ * - Query calls: 5s timeout, 1 retry on timeout, no retry on error
10
+ * - Mutation calls: 10s timeout, no retry (mutations are not idempotent to retry)
11
+ * - Exit 0 → parse stdout as JSON, return result
12
+ * - Exit 2 → conflict (idempotency), return { conflict: true, ... }
13
+ * - Non-zero → log stderr internally, return { error: { code, message, detail } }
14
+ * - stderr is NEVER forwarded to callers
15
+ */
16
+
17
+ import { execFile as _execFileDefault } from 'node:child_process';
18
+
19
+ const STRATUM_BIN = 'stratum-mcp';
20
+
21
+ // Injected executor — replaced by tests only. Production code never calls this setter.
22
+ let _execFile = _execFileDefault;
23
+ export function _testOnly_setExecFile(fn) { _execFile = fn; }
24
+ const QUERY_TIMEOUT_MS = 5_000;
25
+ const MUTATION_TIMEOUT_MS = 10_000;
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Core subprocess runner
29
+ // ---------------------------------------------------------------------------
30
+
31
+ /**
32
+ * Spawn stratum-mcp with args. Returns a Promise resolving to { stdout, code }.
33
+ * Rejects only on spawn failure (binary not found).
34
+ *
35
+ * @param {string[]} args
36
+ * @param {number} timeoutMs
37
+ * @returns {Promise<{ stdout: string, stderr: string, code: number }>}
38
+ */
39
+ function spawnStratum(args, timeoutMs) {
40
+ return new Promise((resolve, reject) => {
41
+ let stdout = '';
42
+ let stderr = '';
43
+
44
+ const proc = _execFile(STRATUM_BIN, args, { timeout: timeoutMs }, (err, out, err2) => {
45
+ stdout = out || '';
46
+ stderr = err2 || '';
47
+ const code = err?.code === 'ETIMEDOUT' ? -1
48
+ : (typeof err?.code === 'number' ? err.code : 0);
49
+ resolve({ stdout, stderr, code });
50
+ });
51
+
52
+ proc.on('error', (err) => {
53
+ if (err.code === 'ENOENT') {
54
+ reject(new Error(`stratum-mcp not found. Install with: pip install stratum-mcp`));
55
+ } else {
56
+ reject(err);
57
+ }
58
+ });
59
+ });
60
+ }
61
+
62
+ /**
63
+ * Run a query command (read-only). Retries once on timeout.
64
+ *
65
+ * @returns {Promise<any>} parsed JSON result, or throws StratumError
66
+ */
67
+ async function runQuery(args) {
68
+ let result = await spawnStratum(args, QUERY_TIMEOUT_MS);
69
+
70
+ if (result.code === -1) {
71
+ // Retry once on timeout
72
+ result = await spawnStratum(args, QUERY_TIMEOUT_MS);
73
+ if (result.code === -1) {
74
+ return { error: { code: 'TIMEOUT', message: 'stratum-mcp query timed out', detail: '' } };
75
+ }
76
+ }
77
+
78
+ if (result.code !== 0) {
79
+ console.error('[stratum-client] query error stderr:', result.stderr);
80
+ try {
81
+ return JSON.parse(result.stdout);
82
+ } catch {
83
+ return { error: { code: 'UNKNOWN', message: 'stratum-mcp query failed', detail: '' } };
84
+ }
85
+ }
86
+
87
+ try {
88
+ return JSON.parse(result.stdout);
89
+ } catch {
90
+ return { error: { code: 'PARSE_ERROR', message: 'stratum-mcp returned invalid JSON', detail: '' } };
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Run a mutation command (gate approve/reject/revise). No retry.
96
+ *
97
+ * @returns {Promise<any>} parsed JSON result, or { conflict }, or { error }
98
+ */
99
+ async function runMutation(args) {
100
+ const result = await spawnStratum(args, MUTATION_TIMEOUT_MS);
101
+
102
+ if (result.code === -1) {
103
+ return { error: { code: 'TIMEOUT', message: 'stratum-mcp gate timed out', detail: '' } };
104
+ }
105
+
106
+ if (result.code === 2) {
107
+ try {
108
+ return JSON.parse(result.stdout); // { conflict: true, ... }
109
+ } catch {
110
+ return { conflict: true, detail: '' };
111
+ }
112
+ }
113
+
114
+ if (result.code !== 0) {
115
+ console.error('[stratum-client] mutation error stderr:', result.stderr);
116
+ try {
117
+ return JSON.parse(result.stdout);
118
+ } catch {
119
+ return { error: { code: 'UNKNOWN', message: 'stratum-mcp gate failed', detail: '' } };
120
+ }
121
+ }
122
+
123
+ try {
124
+ return JSON.parse(result.stdout);
125
+ } catch {
126
+ return { error: { code: 'PARSE_ERROR', message: 'stratum-mcp returned invalid JSON', detail: '' } };
127
+ }
128
+ }
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // Public API
132
+ // ---------------------------------------------------------------------------
133
+
134
+ /** List all persisted flows. @returns {Promise<FlowSummary[]|ErrorResult>} */
135
+ export async function queryFlows() {
136
+ return runQuery(['query', 'flows']);
137
+ }
138
+
139
+ /** Full state for a single flow. @returns {Promise<FlowState|ErrorResult>} */
140
+ export async function queryFlow(flowId) {
141
+ return runQuery(['query', 'flow', flowId]);
142
+ }
143
+
144
+ /** List all pending gate steps. @returns {Promise<PendingGate[]|ErrorResult>} */
145
+ export async function queryGates() {
146
+ return runQuery(['query', 'gates']);
147
+ }
148
+
149
+ /**
150
+ * Approve a gate step. Stratum is the mutation authority.
151
+ * @param {string} flowId
152
+ * @param {string} stepId
153
+ * @param {string} [note]
154
+ * @param {'human'|'agent'|'system'} [resolvedBy]
155
+ * @returns {Promise<GateMutationResult|ConflictResult|ErrorResult>}
156
+ */
157
+ export async function gateApprove(flowId, stepId, note = '', resolvedBy = 'human') {
158
+ const args = ['gate', 'approve', flowId, stepId];
159
+ if (note) args.push('--note', note);
160
+ if (resolvedBy !== 'human') args.push('--resolved-by', resolvedBy);
161
+ return runMutation(args);
162
+ }
163
+
164
+ /**
165
+ * Reject (kill) a gate step.
166
+ * @param {string} flowId
167
+ * @param {string} stepId
168
+ * @param {string} [note]
169
+ * @param {'human'|'agent'|'system'} [resolvedBy]
170
+ * @returns {Promise<GateMutationResult|ConflictResult|ErrorResult>}
171
+ */
172
+ export async function gateReject(flowId, stepId, note = '', resolvedBy = 'human') {
173
+ const args = ['gate', 'reject', flowId, stepId];
174
+ if (note) args.push('--note', note);
175
+ if (resolvedBy !== 'human') args.push('--resolved-by', resolvedBy);
176
+ return runMutation(args);
177
+ }
178
+
179
+ /**
180
+ * Send a gate step back for revision.
181
+ * @param {string} flowId
182
+ * @param {string} stepId
183
+ * @param {string} [note]
184
+ * @param {'human'|'agent'|'system'} [resolvedBy]
185
+ * @returns {Promise<GateMutationResult|ConflictResult|ErrorResult>}
186
+ */
187
+ export async function gateRevise(flowId, stepId, note = '', resolvedBy = 'human') {
188
+ const args = ['gate', 'revise', flowId, stepId];
189
+ if (note) args.push('--note', note);
190
+ if (resolvedBy !== 'human') args.push('--resolved-by', resolvedBy);
191
+ return runMutation(args);
192
+ }