@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,193 @@
1
+ /**
2
+ * stratum-sync.js — Stratum flow poller + route registration.
3
+ *
4
+ * Polls stratum flow state via stratum-client (not direct file reads) and syncs
5
+ * into the vision store every 15 seconds.
6
+ *
7
+ * Routes: POST /api/stratum/bind, POST /api/stratum/audit/:itemId
8
+ * (Flow/gate query routes are now in stratum-api.js)
9
+ */
10
+
11
+ import { queryFlows } from './stratum-client.js';
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // StratumSync class
15
+ // ---------------------------------------------------------------------------
16
+
17
+ export class StratumSync {
18
+ #store;
19
+ #scheduleBroadcast;
20
+ #pollTimer = null;
21
+
22
+ /**
23
+ * @param {object} store — VisionStore instance
24
+ * @param {function} scheduleBroadcast
25
+ */
26
+ constructor(store, scheduleBroadcast) {
27
+ this.#store = store;
28
+ this.#scheduleBroadcast = scheduleBroadcast;
29
+ }
30
+
31
+ /** Start the 15s polling interval. */
32
+ start() {
33
+ this.#pollTimer = setInterval(() => {
34
+ this.#syncFlows().catch(err => {
35
+ console.error('[vision] Stratum poll error:', err.message);
36
+ });
37
+ }, 15_000);
38
+ }
39
+
40
+ /** Stop the polling interval. */
41
+ stop() {
42
+ if (this.#pollTimer) {
43
+ clearInterval(this.#pollTimer);
44
+ this.#pollTimer = null;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Fetch flow summaries via stratum-client (stable contract, not direct file reads).
50
+ * Maps stratum's query output to the shape the sync logic expects.
51
+ * @returns {Promise<Array>}
52
+ */
53
+ async readFlows() {
54
+ const raw = await queryFlows();
55
+ if (!Array.isArray(raw)) return [];
56
+
57
+ return raw.map(f => {
58
+ // Map stratum canonical status → legacy sync status labels
59
+ const status = f.status === 'running' || f.status === 'awaiting_gate' ? 'running'
60
+ : f.status === 'complete' ? 'complete'
61
+ : f.status === 'killed' ? 'blocked'
62
+ : 'paused';
63
+
64
+ // STRAT-PAR-3: completed_steps and active_steps are now string[] in v0.3.
65
+ // Fall back to [] when stratum returns the old integer format (backward compat).
66
+ const completedSet = Array.isArray(f.completed_steps) ? f.completed_steps : [];
67
+ const activeSet = Array.isArray(f.active_steps) ? f.active_steps : [];
68
+
69
+ return {
70
+ flowId: f.flow_id,
71
+ flowName: f.flow_name,
72
+ status,
73
+ stepsCompleted: completedSet, // string[] — set of completed step IDs
74
+ stepsActive: activeSet, // string[] — set of active (in-progress) step IDs
75
+ stepCount: f.step_count ?? null,
76
+ steps: [],
77
+ // currentIdx removed — consumers use stepsCompleted.length
78
+ };
79
+ });
80
+ }
81
+
82
+ /** Sync stratum flow states → vision item statuses + violation evidence. */
83
+ async #syncFlows() {
84
+ const flows = await this.readFlows();
85
+ if (flows.length === 0) return;
86
+
87
+ const flowMap = new Map(flows.map(f => [f.flowId, f]));
88
+ let changed = false;
89
+
90
+ for (const item of this.#store.items.values()) {
91
+ if (!item.stratumFlowId) continue;
92
+ const flow = flowMap.get(item.stratumFlowId);
93
+ if (!flow) continue;
94
+
95
+ const targetStatus = flow.status === 'running' ? 'in_progress'
96
+ : flow.status === 'blocked' ? 'blocked'
97
+ : null;
98
+
99
+ if (targetStatus && targetStatus !== item.status) {
100
+ try {
101
+ this.#store.updateItem(item.id, { status: targetStatus });
102
+ changed = true;
103
+ } catch { /* ignore */ }
104
+ }
105
+
106
+ const existing = item.evidence || {};
107
+ if (flow.status === 'blocked') {
108
+ // Flow was killed via gate reject — record factual evidence, not retry semantics.
109
+ const killNote = `Flow '${flow.flowName}' was killed via gate reject`;
110
+ if (existing.stratumKillNote !== killNote) {
111
+ try {
112
+ // Also clear any stale retry-exhaustion evidence from old sync format.
113
+ const { stratumViolations: _v, violatedAt: _va, ...rest } = existing;
114
+ this.#store.updateItem(item.id, {
115
+ evidence: { ...rest, stratumKillNote: killNote, killedAt: new Date().toISOString() },
116
+ });
117
+ changed = true;
118
+ } catch { /* ignore */ }
119
+ }
120
+ } else if (existing.stratumKillNote || existing.stratumViolations) {
121
+ // Flow is no longer blocked — clear kill evidence.
122
+ try {
123
+ const { stratumKillNote: _k, killedAt: _ka, stratumViolations: _v, violatedAt: _va, ...rest } = existing;
124
+ this.#store.updateItem(item.id, { evidence: rest });
125
+ changed = true;
126
+ } catch { /* ignore */ }
127
+ }
128
+ }
129
+
130
+ if (changed) this.#scheduleBroadcast();
131
+ }
132
+ }
133
+
134
+ // ---------------------------------------------------------------------------
135
+ // Route registration
136
+ // ---------------------------------------------------------------------------
137
+
138
+ /**
139
+ * Attach stratum REST routes to an Express app.
140
+ *
141
+ * @param {object} app — Express app
142
+ * @param {{ store: object, scheduleBroadcast: function, broadcastMessage: function, sync: StratumSync }} deps
143
+ */
144
+ export function attachStratumRoutes(app, { store, scheduleBroadcast, broadcastMessage, sync }) {
145
+ // POST /api/stratum/bind — link a stratum flow_id to a vision item
146
+ app.post('/api/stratum/bind', (req, res) => {
147
+ const { flowId, itemId } = req.body || {};
148
+ if (!flowId || !itemId) return res.status(400).json({ error: 'flowId and itemId required' });
149
+ try {
150
+ const item = store.updateItem(itemId, { stratumFlowId: flowId });
151
+ scheduleBroadcast();
152
+ res.json({ ok: true, itemId, flowId, item });
153
+ } catch (err) {
154
+ const status = err.message.includes('not found') ? 404 : 400;
155
+ res.status(status).json({ error: err.message });
156
+ }
157
+ });
158
+
159
+ // POST /api/stratum/audit/:itemId — store audit trace in item evidence + emit session log event
160
+ app.post('/api/stratum/audit/:itemId', (req, res) => {
161
+ const { trace } = req.body || {};
162
+ if (!trace) return res.status(400).json({ error: 'trace required' });
163
+ try {
164
+ const item = store.items.get(req.params.itemId);
165
+ if (!item) return res.status(404).json({ error: `Item not found: ${req.params.itemId}` });
166
+ // eslint-disable-next-line no-unused-vars
167
+ const { stratumViolations: _v, violatedAt: _va, ...existingEvidence } = item.evidence || {};
168
+ const evidence = { ...existingEvidence, stratumTrace: trace, tracedAt: new Date().toISOString() };
169
+ const updates = { evidence };
170
+ if (trace.status === 'complete' && item.status !== 'complete') {
171
+ updates.status = 'complete';
172
+ }
173
+ const updatedItem = store.updateItem(req.params.itemId, updates);
174
+ scheduleBroadcast();
175
+
176
+ const stepsCompleted = Array.isArray(trace.trace) ? trace.trace.length : 0;
177
+ const totalMs = trace.total_duration_ms || null;
178
+ broadcastMessage({
179
+ type: 'agentActivity',
180
+ tool: 'stratum_audit',
181
+ category: 'delegating',
182
+ detail: `${trace.flow_name || trace.flow_id}: ${stepsCompleted} steps${totalMs != null ? `, ${(totalMs / 1000).toFixed(1)}s` : ''}`,
183
+ error: null,
184
+ items: [{ id: updatedItem.id, title: updatedItem.title, status: updatedItem.status }],
185
+ timestamp: new Date().toISOString(),
186
+ });
187
+
188
+ res.json({ ok: true, itemId: req.params.itemId });
189
+ } catch (err) {
190
+ res.status(500).json({ error: err.message });
191
+ }
192
+ });
193
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * summarizer.js — Spawn a Claude CLI subprocess to summarize batch events as JSON.
3
+ *
4
+ * Model-agnostic: defaults to haiku for cost efficiency but accepts any model.
5
+ * Extracted from SessionManager for independent reuse and testing.
6
+ */
7
+
8
+ import { spawn } from 'node:child_process';
9
+ import path from 'node:path';
10
+
11
+ import { getTargetRoot } from './project-root.js';
12
+
13
+ const PROJECT_ROOT = getTargetRoot();
14
+
15
+ const DEFAULT_MODEL = process.env.SUMMARIZER_MODEL || 'haiku';
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Prompt builder
19
+ // ---------------------------------------------------------------------------
20
+
21
+ /**
22
+ * Format batch events into a prompt asking the model for structured JSON.
23
+ *
24
+ * @param {Array} batch — array of buffered tool-use events
25
+ * @param {string} [projectRoot]
26
+ * @returns {string}
27
+ */
28
+ export function buildSummaryPrompt(batch, projectRoot = PROJECT_ROOT) {
29
+ const eventLines = batch.map(evt => {
30
+ const itemLabel = evt.itemTitles.length > 0
31
+ ? ` [${evt.itemTitles.join(', ')}]`
32
+ : '';
33
+ const fileLabel = evt.filePath
34
+ ? ` on ${path.relative(projectRoot, evt.filePath) || evt.filePath}`
35
+ : ' on (no file)';
36
+ return `- ${evt.tool}${fileLabel}${itemLabel}: ${evt.input}`;
37
+ }).join('\n');
38
+
39
+ return `Summarize these developer tool actions as a JSON object. Return ONLY valid JSON, no markdown.
40
+
41
+ Events:
42
+ ${eventLines}
43
+
44
+ JSON schema:
45
+ {
46
+ "summary": "one sentence describing what these actions accomplish together",
47
+ "intent": "feature|bugfix|refactor|test|docs|config|debug",
48
+ "component": "which part of the system (derived from file paths)",
49
+ "complexity": "trivial|low|medium|high",
50
+ "signals": ["string tags like new_file, error_handling, api_change, test_added"],
51
+ "status_hint": "review_ready|needs_test|blocked|null"
52
+ }`;
53
+ }
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Summarize caller
57
+ // ---------------------------------------------------------------------------
58
+
59
+ /**
60
+ * Spawn `claude -p <prompt> --model <model> --max-turns 1` with CLAUDECODE unset.
61
+ * Parse JSON from output. Returns parsed object or null on failure.
62
+ *
63
+ * @param {string} prompt
64
+ * @param {object} [opts]
65
+ * @param {string} [opts.model] — model to use (default: haiku or SUMMARIZER_MODEL env)
66
+ * @param {string} [opts.projectRoot]
67
+ * @returns {Promise<object|null>}
68
+ */
69
+ export function summarize(prompt, { model = DEFAULT_MODEL, projectRoot = PROJECT_ROOT } = {}) {
70
+ return new Promise((resolve) => {
71
+ const cleanEnv = { ...process.env, NO_COLOR: '1' };
72
+ delete cleanEnv.CLAUDECODE;
73
+
74
+ const proc = spawn('claude', [
75
+ '-p', prompt,
76
+ '--model', model,
77
+ '--max-turns', '1',
78
+ ], {
79
+ cwd: projectRoot,
80
+ env: cleanEnv,
81
+ stdio: ['ignore', 'pipe', 'pipe'],
82
+ });
83
+
84
+ let stdout = '';
85
+ let stderr = '';
86
+
87
+ proc.stdout.on('data', (chunk) => { stdout += chunk.toString(); });
88
+ proc.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
89
+
90
+ proc.on('error', (err) => {
91
+ console.error('[session] Summarizer spawn error:', err.message);
92
+ resolve(null);
93
+ });
94
+
95
+ proc.on('close', (code) => {
96
+ if (code !== 0) {
97
+ console.error(`[session] Summarizer exited with code ${code}:`, stderr.slice(0, 200));
98
+ resolve(null);
99
+ return;
100
+ }
101
+
102
+ try {
103
+ const json = extractJSON(stdout);
104
+ resolve(json);
105
+ } catch (err) {
106
+ console.error('[session] Summarizer JSON parse failed:', err.message, 'raw:', stdout.slice(0, 300));
107
+ resolve(null);
108
+ }
109
+ });
110
+ });
111
+ }
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // JSON extractor
115
+ // ---------------------------------------------------------------------------
116
+
117
+ /**
118
+ * Extract JSON from model output, handling possible markdown fences.
119
+ *
120
+ * @param {string} text — raw stdout
121
+ * @returns {object}
122
+ * @throws {Error} if no JSON found
123
+ */
124
+ export function extractJSON(text) {
125
+ const trimmed = text.trim();
126
+ try {
127
+ return JSON.parse(trimmed);
128
+ } catch {
129
+ const fenceMatch = trimmed.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/);
130
+ if (fenceMatch) {
131
+ return JSON.parse(fenceMatch[1].trim());
132
+ }
133
+ const braceMatch = trimmed.match(/\{[\s\S]*\}/);
134
+ if (braceMatch) {
135
+ return JSON.parse(braceMatch[0]);
136
+ }
137
+ throw new Error('No JSON found in output');
138
+ }
139
+ }
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Process supervisor for Compose.
3
+ * Manages three independent processes:
4
+ * 1. API server (port 3001) — Express + file-watcher + vision
5
+ * 2. Agent server (port 3002) — SDK streaming, structured messages (Tier 1, immortal)
6
+ * 3. Vite dev server (port 5173) — Frontend HMR
7
+ *
8
+ * Each process gets independent restart with exponential backoff.
9
+ * If a process keeps crashing for > 1 minute, the supervisor gives up on it.
10
+ *
11
+ * Singleton enforcement: Uses a PID file to ensure only one supervisor runs.
12
+ * Starting a new supervisor kills the old one and all its children first.
13
+ */
14
+
15
+ import { fork, spawn, execFileSync } from 'node:child_process';
16
+ import crypto from 'node:crypto';
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+ import { fileURLToPath } from 'node:url';
20
+ import { COMPOSE_HOME, getTargetRoot, ensureDataDir } from './project-root.js';
21
+
22
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
+ console.log('[supervisor] Target project:', getTargetRoot());
24
+ const PID_FILE = path.join(COMPOSE_HOME, '.compose-supervisor.pid');
25
+
26
+ const PROCESSES = [
27
+ {
28
+ name: 'api-server',
29
+ path: path.join(__dirname, 'index.js'),
30
+ port: process.env.PORT || 4001,
31
+ type: 'fork',
32
+ },
33
+ {
34
+ name: 'agent-server',
35
+ path: path.join(__dirname, 'agent-server.js'),
36
+ port: process.env.AGENT_PORT || 4002,
37
+ type: 'fork',
38
+ },
39
+ {
40
+ name: 'vite',
41
+ command: path.join(COMPOSE_HOME, 'node_modules', '.bin', 'vite'),
42
+ port: process.env.VITE_PORT || 5195,
43
+ type: 'spawn',
44
+ },
45
+ ];
46
+
47
+ const MIN_BACKOFF = 500;
48
+ const MAX_BACKOFF = 10_000;
49
+ const HEALTHY_THRESHOLD = 5_000;
50
+ const GIVE_UP_AFTER = 60_000; // stop retrying after 1 min of continuous failures
51
+
52
+ let stopping = false;
53
+
54
+ function ensureComposeApiToken() {
55
+ if (!process.env.COMPOSE_API_TOKEN) {
56
+ process.env.COMPOSE_API_TOKEN = crypto.randomBytes(24).toString('hex');
57
+ console.log('[supervisor] Generated COMPOSE_API_TOKEN for this session');
58
+ }
59
+ // Expose the same token to Vite client code.
60
+ process.env.VITE_COMPOSE_API_TOKEN = process.env.COMPOSE_API_TOKEN;
61
+ // Expose AGENT_PORT so AgentStream.jsx can reach the right port
62
+ process.env.VITE_AGENT_PORT = process.env.AGENT_PORT || '4002';
63
+ }
64
+
65
+ // --- Singleton enforcement ---
66
+
67
+ function killExistingSupervisor() {
68
+ try {
69
+ const oldPid = parseInt(fs.readFileSync(PID_FILE, 'utf8').trim(), 10);
70
+ if (oldPid && oldPid !== process.pid) {
71
+ try {
72
+ // Check if process exists
73
+ process.kill(oldPid, 0);
74
+ console.log(`[supervisor] Killing previous supervisor (PID ${oldPid})...`);
75
+ process.kill(oldPid, 'SIGTERM');
76
+ // Give it time to clean up children
77
+ execFileSync('sleep', ['2']);
78
+ } catch {
79
+ // Process doesn't exist — stale PID file
80
+ }
81
+ }
82
+ } catch {
83
+ // No PID file — first run
84
+ }
85
+ }
86
+
87
+ function writePidFile() {
88
+ fs.writeFileSync(PID_FILE, String(process.pid));
89
+ }
90
+
91
+ function removePidFile() {
92
+ try { fs.unlinkSync(PID_FILE); } catch {}
93
+ }
94
+
95
+ // Kill old supervisor before anything else
96
+ killExistingSupervisor();
97
+ ensureComposeApiToken();
98
+ ensureDataDir();
99
+
100
+ // Kill anything listening on our ports (stale children from old supervisor)
101
+ function freePort(port, childPid) {
102
+ try {
103
+ const output = execFileSync('lsof', ['-ti', `:${port}`, '-sTCP:LISTEN'], {
104
+ encoding: 'utf8',
105
+ timeout: 3000,
106
+ }).trim();
107
+ if (!output) return;
108
+
109
+ const myPid = process.pid;
110
+ const pids = output.split('\n').map(p => parseInt(p, 10)).filter(Boolean);
111
+ const stale = pids.filter(pid => pid !== myPid && pid !== childPid);
112
+
113
+ if (stale.length > 0) {
114
+ console.log(`[supervisor] Killing stale listener(s) on port ${port}: ${stale.join(', ')}`);
115
+ for (const pid of stale) {
116
+ try { process.kill(pid, 'SIGKILL'); } catch {}
117
+ }
118
+ execFileSync('sleep', ['1']);
119
+ }
120
+ } catch {
121
+ // lsof returns non-zero if no matches — port is free
122
+ }
123
+ }
124
+
125
+ // Free all ports before starting (clean slate)
126
+ for (const proc of PROCESSES) {
127
+ if (proc.port) freePort(proc.port, null);
128
+ }
129
+
130
+ // Write our PID file
131
+ writePidFile();
132
+
133
+ // --- Process management ---
134
+
135
+ function startProcess(proc) {
136
+ if (stopping) return;
137
+ if (proc.port) freePort(proc.port, proc.child ? proc.child.pid : null);
138
+
139
+ const startTime = Date.now();
140
+ console.log(`[supervisor] Starting ${proc.name}...`);
141
+
142
+ if (proc.type === 'fork') {
143
+ proc.child = fork(proc.path, { stdio: 'inherit' });
144
+ } else {
145
+ proc.child = spawn(proc.command, [], {
146
+ stdio: 'inherit',
147
+ cwd: COMPOSE_HOME,
148
+ env: process.env,
149
+ });
150
+ }
151
+
152
+ proc.child.on('exit', (code, signal) => {
153
+ if (stopping) return;
154
+
155
+ const uptime = Date.now() - startTime;
156
+ console.error(`[supervisor] ${proc.name} exited (code: ${code}, signal: ${signal}, uptime: ${uptime}ms)`);
157
+ proc.child = null;
158
+
159
+ if (uptime > HEALTHY_THRESHOLD) {
160
+ proc.backoff = MIN_BACKOFF;
161
+ proc.firstFailTime = null;
162
+ } else {
163
+ proc.backoff = Math.min((proc.backoff || MIN_BACKOFF) * 2, MAX_BACKOFF);
164
+ if (!proc.firstFailTime) proc.firstFailTime = Date.now();
165
+
166
+ if (Date.now() - proc.firstFailTime > GIVE_UP_AFTER) {
167
+ console.error(`[supervisor] ${proc.name} has been failing for >1 min — giving up`);
168
+ return;
169
+ }
170
+ }
171
+
172
+ console.log(`[supervisor] Restarting ${proc.name} in ${proc.backoff}ms...`);
173
+ setTimeout(() => startProcess(proc), proc.backoff);
174
+ });
175
+ }
176
+
177
+ // Start all processes
178
+ for (const proc of PROCESSES) {
179
+ proc.backoff = MIN_BACKOFF;
180
+ proc.child = null;
181
+ proc.firstFailTime = null;
182
+ startProcess(proc);
183
+ }
184
+
185
+ // Forward termination signals to all children, then exit cleanly
186
+ for (const sig of ['SIGINT', 'SIGTERM']) {
187
+ process.on(sig, () => {
188
+ stopping = true;
189
+ console.log(`[supervisor] ${sig} received, stopping all processes...`);
190
+ for (const proc of PROCESSES) {
191
+ if (proc.child) proc.child.kill(sig);
192
+ }
193
+ removePidFile();
194
+ setTimeout(() => process.exit(0), 2000);
195
+ });
196
+ }