@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.
- package/LICENSE +21 -0
- package/README.md +1014 -0
- package/bin/compose.js +1515 -0
- package/dist/assets/_baseUniq-CQwX6VLz.js +1 -0
- package/dist/assets/arc-SxJ2J1sh.js +1 -0
- package/dist/assets/architectureDiagram-Q4EWVU46-BykunY1F.js +36 -0
- package/dist/assets/blockDiagram-DXYQGD6D-ohAKBOUw.js +132 -0
- package/dist/assets/c4Diagram-AHTNJAMY-DBDC3ENB.js +10 -0
- package/dist/assets/channel-DGElom1e.js +1 -0
- package/dist/assets/chunk-4BX2VUAB-Cv93Z7uM.js +1 -0
- package/dist/assets/chunk-4TB4RGXK-DE0WBDkj.js +206 -0
- package/dist/assets/chunk-55IACEB6-CE1EXenG.js +1 -0
- package/dist/assets/chunk-EDXVE4YY-DA7Ana6H.js +1 -0
- package/dist/assets/chunk-FMBD7UC4-CTDIPA3p.js +15 -0
- package/dist/assets/chunk-OYMX7WX6-uGBaPaTX.js +231 -0
- package/dist/assets/chunk-QZHKN3VN-CYlnXuUO.js +1 -0
- package/dist/assets/chunk-YZCP3GAM-ojGkzcZK.js +1 -0
- package/dist/assets/classDiagram-6PBFFD2Q-KqWP9wWZ.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-KqWP9wWZ.js +1 -0
- package/dist/assets/clone-DUJKJXd7.js +1 -0
- package/dist/assets/cose-bilkent-S5V4N54A-Bktn9hL-.js +1 -0
- package/dist/assets/dagre-KV5264BT-DFaSzuRF.js +4 -0
- package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/assets/diagram-5BDNPKRD-DnfmDzEm.js +10 -0
- package/dist/assets/diagram-G4DWMVQ6-Bm8W9YnG.js +24 -0
- package/dist/assets/diagram-MMDJMWI5-B5-TSKvp.js +43 -0
- package/dist/assets/diagram-TYMM5635-ls4rqlky.js +24 -0
- package/dist/assets/erDiagram-SMLLAGMA-giG6WO-r.js +85 -0
- package/dist/assets/flowDiagram-DWJPFMVM-XvlUuz-7.js +162 -0
- package/dist/assets/ganttDiagram-T4ZO3ILL-hLBV57oV.js +292 -0
- package/dist/assets/gitGraphDiagram-UUTBAWPF-BHu3s_Gn.js +106 -0
- package/dist/assets/graph-D0Cfv00Y.js +1 -0
- package/dist/assets/index-CUd6pFGF.css +1 -0
- package/dist/assets/index-DReRlzZI.js +1144 -0
- package/dist/assets/infoDiagram-42DDH7IO-DbqRsOo3.js +2 -0
- package/dist/assets/init-Gi6I4Gst.js +1 -0
- package/dist/assets/ishikawaDiagram-UXIWVN3A-DnCdx7zb.js +70 -0
- package/dist/assets/journeyDiagram-VCZTEJTY-CfD7eNcP.js +139 -0
- package/dist/assets/kanban-definition-6JOO6SKY-BYaO9-mK.js +89 -0
- package/dist/assets/katex-DkKDou_j.js +257 -0
- package/dist/assets/layout-Bj72wOEB.js +1 -0
- package/dist/assets/linear-BRFo114D.js +1 -0
- package/dist/assets/min-GCHnKlJS.js +1 -0
- package/dist/assets/mindmap-definition-QFDTVHPH-n0PMebY4.js +96 -0
- package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/dist/assets/pieDiagram-DEJITSTG-pN4CljHF.js +30 -0
- package/dist/assets/quadrantDiagram-34T5L4WZ-DNoAy8-D.js +7 -0
- package/dist/assets/requirementDiagram-MS252O5E-BhtY05PT.js +84 -0
- package/dist/assets/sankeyDiagram-XADWPNL6-B6AD-16A.js +10 -0
- package/dist/assets/sequenceDiagram-FGHM5R23-DShHM-uk.js +157 -0
- package/dist/assets/stateDiagram-FHFEXIEX-DMxn7HTo.js +1 -0
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-o6PnCs4e.js +1 -0
- package/dist/assets/timeline-definition-GMOUNBTQ-Cdu6uq52.js +120 -0
- package/dist/assets/vennDiagram-DHZGUBPP-CpK29iRe.js +34 -0
- package/dist/assets/wardley-RL74JXVD-BQgSkdcO.js +162 -0
- package/dist/assets/wardleyDiagram-NUSXRM2D-DJHYev6O.js +20 -0
- package/dist/assets/xychartDiagram-5P7HB3ND-1d75pbaO.js +7 -0
- package/dist/index.html +30 -0
- package/lib/agent-chains.js +65 -0
- package/lib/agent-string.js +86 -0
- package/lib/budget-ledger.js +86 -0
- package/lib/build-all.js +162 -0
- package/lib/build-dag.js +120 -0
- package/lib/build-stream-writer.js +190 -0
- package/lib/build.js +2997 -0
- package/lib/capability-checker.js +53 -0
- package/lib/cert-inject.js +38 -0
- package/lib/cli-progress.js +483 -0
- package/lib/constants.js +69 -0
- package/lib/cross-layer-audit.js +84 -0
- package/lib/debug-discipline.js +173 -0
- package/lib/feature-json.js +106 -0
- package/lib/gate-prompt.js +291 -0
- package/lib/gate-tiers.js +194 -0
- package/lib/health-history.js +119 -0
- package/lib/health-score.js +227 -0
- package/lib/ideabox.js +570 -0
- package/lib/import.js +244 -0
- package/lib/migrate-roadmap.js +94 -0
- package/lib/model-pricing.js +67 -0
- package/lib/new.js +413 -0
- package/lib/pipeline-cli.js +489 -0
- package/lib/plan-parser.js +103 -0
- package/lib/qa-scoping.js +474 -0
- package/lib/questionnaire.js +200 -0
- package/lib/resolve-port.js +7 -0
- package/lib/result-normalizer.js +349 -0
- package/lib/review-lenses.js +166 -0
- package/lib/roadmap-gen.js +210 -0
- package/lib/roadmap-parser.js +176 -0
- package/lib/server-probe.js +23 -0
- package/lib/staleness.js +87 -0
- package/lib/step-prompt.js +260 -0
- package/lib/step-validator.js +49 -0
- package/lib/stratum-mcp-client.js +365 -0
- package/lib/team-flag.js +46 -0
- package/lib/test-bootstrap.js +401 -0
- package/lib/triage.js +274 -0
- package/lib/vision-writer.js +391 -0
- package/package.json +111 -0
- package/pipelines/bug-fix.stratum.yaml +230 -0
- package/pipelines/build.stratum.yaml +498 -0
- package/pipelines/content.stratum.yaml +112 -0
- package/pipelines/coverage-sweep.stratum.yaml +52 -0
- package/pipelines/refactor.stratum.yaml +169 -0
- package/pipelines/research.stratum.yaml +88 -0
- package/pipelines/review-fix.stratum.yaml +109 -0
- package/presets/team-feature.stratum.yaml +105 -0
- package/presets/team-research.stratum.yaml +108 -0
- package/presets/team-review.stratum.yaml +106 -0
- package/scripts/agent-activity-hook.sh +31 -0
- package/scripts/agent-error-hook.sh +28 -0
- package/scripts/analyze-orphans.mjs +50 -0
- package/scripts/find-orphans.mjs +26 -0
- package/scripts/fix-phases.mjs +49 -0
- package/scripts/generate-stratum-spec.mjs +137 -0
- package/scripts/import-roadmap.mjs +116 -0
- package/scripts/phase-audit.mjs +33 -0
- package/scripts/run-pipeline.mjs +314 -0
- package/scripts/session-end-hook.sh +18 -0
- package/scripts/session-start-hook.sh +38 -0
- package/scripts/vision-hook.sh +104 -0
- package/scripts/vision-track.mjs +554 -0
- package/scripts/wire-all-orphans.mjs +108 -0
- package/scripts/wire-orphans.mjs +164 -0
- package/server/activity-routes.js +123 -0
- package/server/agent-health.js +197 -0
- package/server/agent-hooks.js +102 -0
- package/server/agent-mcp.js +10 -0
- package/server/agent-registry.js +95 -0
- package/server/agent-server.js +290 -0
- package/server/agent-spawn.js +251 -0
- package/server/agent-templates.js +77 -0
- package/server/artifact-manager.js +247 -0
- package/server/artifact-templates/architecture.md +28 -0
- package/server/artifact-templates/blueprint.md +21 -0
- package/server/artifact-templates/design.md +36 -0
- package/server/artifact-templates/plan.md +25 -0
- package/server/artifact-templates/prd.md +43 -0
- package/server/artifact-templates/report.md +40 -0
- package/server/block-tracker.js +90 -0
- package/server/build-stream-bridge.js +502 -0
- package/server/coalescing-buffer.js +46 -0
- package/server/compose-mcp-tools.js +479 -0
- package/server/compose-mcp.js +324 -0
- package/server/connectors/agent-connector.js +78 -0
- package/server/connectors/claude-sdk-connector.js +198 -0
- package/server/connectors/codex-connector.js +240 -0
- package/server/connectors/connector-discovery.js +18 -0
- package/server/connectors/connector-runtime.js +13 -0
- package/server/connectors/opencode-connector.js +200 -0
- package/server/design-routes.js +540 -0
- package/server/design-session.js +161 -0
- package/server/feature-scan.js +593 -0
- package/server/file-watcher.js +284 -0
- package/server/find-root.js +29 -0
- package/server/graph-export.js +343 -0
- package/server/ideabox-cache.js +77 -0
- package/server/ideabox-routes.js +294 -0
- package/server/index.js +156 -0
- package/server/model-tiers.js +49 -0
- package/server/pipeline-routes.js +288 -0
- package/server/policy-evaluator.js +36 -0
- package/server/project-root.js +122 -0
- package/server/security.js +23 -0
- package/server/session-manager.js +403 -0
- package/server/session-routes.js +190 -0
- package/server/session-store.js +107 -0
- package/server/settings-routes.js +35 -0
- package/server/settings-store.js +234 -0
- package/server/stratum-api.js +102 -0
- package/server/stratum-client.js +192 -0
- package/server/stratum-sync.js +193 -0
- package/server/summarizer.js +139 -0
- package/server/supervisor.js +196 -0
- package/server/vision-routes.js +668 -0
- package/server/vision-server.js +393 -0
- package/server/vision-store.js +360 -0
- package/server/vision-utils.js +179 -0
- package/server/worktree-gc.js +137 -0
- 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
|
+
}
|