@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,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Server (Tier 1 — immortal, port 3003 during migration, then 3002)
|
|
3
|
+
*
|
|
4
|
+
* Replaces terminal-server.js. Uses @anthropic-ai/claude-agent-sdk instead of
|
|
5
|
+
* node-pty + tmux. Streams typed SDK messages via SSE to the browser.
|
|
6
|
+
* User input arrives via POST — SSE is the right transport for a unidirectional
|
|
7
|
+
* async iterator.
|
|
8
|
+
*
|
|
9
|
+
* Process isolation: this server only talks to api-server (3001) via SDK hooks.
|
|
10
|
+
* SessionManager and VisionServer require zero changes.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import http from 'node:http';
|
|
14
|
+
import { readFileSync } from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import express from 'express';
|
|
17
|
+
import cors from 'cors';
|
|
18
|
+
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
19
|
+
import { requireSensitiveToken } from './security.js';
|
|
20
|
+
import { HOOK_OPTIONS } from './agent-hooks.js';
|
|
21
|
+
import { getTargetRoot, getDataDir } from './project-root.js';
|
|
22
|
+
import { BuildStreamBridge } from './build-stream-bridge.js';
|
|
23
|
+
import { CoalescingBuffer } from './coalescing-buffer.js';
|
|
24
|
+
|
|
25
|
+
const _agentBuffer = new CoalescingBuffer((flushed) => {
|
|
26
|
+
if (flushed.agentMessage) {
|
|
27
|
+
for (const msg of flushed.agentMessage) {
|
|
28
|
+
_trackMessage(msg);
|
|
29
|
+
broadcast(msg);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}, { intervalMs: 16 });
|
|
33
|
+
_agentBuffer.register('agentMessage', 'append');
|
|
34
|
+
|
|
35
|
+
const _recentMessages = [];
|
|
36
|
+
const HYDRATE_LIMIT = 50;
|
|
37
|
+
|
|
38
|
+
function _trackMessage(msg) {
|
|
39
|
+
_recentMessages.push(msg);
|
|
40
|
+
if (_recentMessages.length > HYDRATE_LIMIT) _recentMessages.shift();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getAgentSnapshot() {
|
|
44
|
+
return _recentMessages.length > 0 ? [..._recentMessages] : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const PORT = process.env.AGENT_PORT || 4002;
|
|
48
|
+
const SETTINGS_FILE = path.join(getDataDir(), 'settings.json');
|
|
49
|
+
|
|
50
|
+
function _readModelSetting() {
|
|
51
|
+
try {
|
|
52
|
+
const raw = readFileSync(SETTINGS_FILE, 'utf-8');
|
|
53
|
+
return JSON.parse(raw)?.models?.interactive || null;
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const app = express();
|
|
60
|
+
app.use(cors({ origin: /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/ }));
|
|
61
|
+
app.use(express.json());
|
|
62
|
+
|
|
63
|
+
app.get('/api/health', (_req, res) => res.json({ ok: true }));
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Session state — one active SDK session at a time
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Active session state:
|
|
71
|
+
* id: string|null — SDK session_id captured from init message
|
|
72
|
+
* queryIter: Query|null — SDK async iterator, has .interrupt() method
|
|
73
|
+
*/
|
|
74
|
+
let _session = { id: null, queryIter: null };
|
|
75
|
+
|
|
76
|
+
/** SSE clients waiting for messages */
|
|
77
|
+
const _sseClients = new Set();
|
|
78
|
+
|
|
79
|
+
function broadcast(msg) {
|
|
80
|
+
const line = `data: ${JSON.stringify(msg)}\n\n`;
|
|
81
|
+
for (const client of _sseClients) {
|
|
82
|
+
try {
|
|
83
|
+
client.write(line);
|
|
84
|
+
} catch {
|
|
85
|
+
_sseClients.delete(client);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// GET /api/agent/stream — SSE subscription
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
app.get('/api/agent/stream', (req, res) => {
|
|
95
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
96
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
97
|
+
res.setHeader('Connection', 'keep-alive');
|
|
98
|
+
res.setHeader('X-Accel-Buffering', 'no'); // disable nginx buffering if proxied
|
|
99
|
+
res.flushHeaders();
|
|
100
|
+
|
|
101
|
+
_sseClients.add(res);
|
|
102
|
+
|
|
103
|
+
const snapshot = getAgentSnapshot();
|
|
104
|
+
if (snapshot) {
|
|
105
|
+
res.write(`event: hydrate\ndata: ${JSON.stringify(snapshot)}\n\n`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Immediately tell the new client whether a session exists
|
|
109
|
+
if (_session.id) {
|
|
110
|
+
res.write(`data: ${JSON.stringify({
|
|
111
|
+
type: 'system', subtype: 'connected', sessionId: _session.id,
|
|
112
|
+
})}\n\n`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
req.on('close', () => _sseClients.delete(res));
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// POST /api/agent/session — create a fresh session
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
app.post('/api/agent/session', requireSensitiveToken, (req, res) => {
|
|
123
|
+
const { prompt = '' } = req.body || {};
|
|
124
|
+
if (!prompt.trim()) return res.status(400).json({ error: 'prompt is required' });
|
|
125
|
+
|
|
126
|
+
_killCurrentSession();
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const q = _startQuery(prompt, null);
|
|
130
|
+
_session = { id: null, queryIter: q };
|
|
131
|
+
_consumeStream(q);
|
|
132
|
+
res.json({ ok: true });
|
|
133
|
+
} catch (err) {
|
|
134
|
+
res.status(500).json({ error: err.message });
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
// POST /api/agent/message — send a follow-up message (resumes session)
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
app.post('/api/agent/message', requireSensitiveToken, (req, res) => {
|
|
143
|
+
const { prompt } = req.body || {};
|
|
144
|
+
if (!prompt?.trim()) return res.status(400).json({ error: 'prompt is required' });
|
|
145
|
+
|
|
146
|
+
const resumeId = _session.id;
|
|
147
|
+
_killCurrentSession();
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
const q = _startQuery(prompt, resumeId);
|
|
151
|
+
_session = { id: resumeId, queryIter: q };
|
|
152
|
+
_consumeStream(q);
|
|
153
|
+
res.json({ ok: true, resumeSessionId: resumeId });
|
|
154
|
+
} catch (err) {
|
|
155
|
+
res.status(500).json({ error: err.message });
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// POST /api/agent/interrupt — interrupt current query
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
app.post('/api/agent/interrupt', requireSensitiveToken, (req, res) => {
|
|
164
|
+
if (!_session.queryIter) {
|
|
165
|
+
return res.status(404).json({ error: 'No active query' });
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
_session.queryIter.interrupt();
|
|
169
|
+
|
|
170
|
+
// COMP-AGT-1: Escalation — if not resolved within 5s, force-kill the session
|
|
171
|
+
const capturedIter = _session.queryIter;
|
|
172
|
+
setTimeout(() => {
|
|
173
|
+
if (_session.queryIter === capturedIter) {
|
|
174
|
+
console.log('[agent-server] Interrupt escalation: killing session after 5s timeout');
|
|
175
|
+
_killCurrentSession();
|
|
176
|
+
}
|
|
177
|
+
}, 5000);
|
|
178
|
+
|
|
179
|
+
res.json({ ok: true });
|
|
180
|
+
} catch (err) {
|
|
181
|
+
res.status(500).json({ error: err.message });
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// GET /api/agent/session/status
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
app.get('/api/agent/session/status', (_req, res) => {
|
|
190
|
+
res.json({
|
|
191
|
+
active: !!_session.queryIter,
|
|
192
|
+
sessionId: _session.id || null,
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
// Internals
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
function _buildOptions(prompt, resumeId) {
|
|
201
|
+
return {
|
|
202
|
+
cwd: getTargetRoot(),
|
|
203
|
+
model: _readModelSetting() || 'claude-sonnet-4-6',
|
|
204
|
+
permissionMode: 'acceptEdits',
|
|
205
|
+
settingSources: ['project'],
|
|
206
|
+
tools: { type: 'preset', preset: 'claude_code' },
|
|
207
|
+
hooks: HOOK_OPTIONS,
|
|
208
|
+
...(resumeId ? { resume: resumeId } : {}),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function _startQuery(prompt, resumeId) {
|
|
213
|
+
return query({ prompt, options: _buildOptions(prompt, resumeId) });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function _killCurrentSession() {
|
|
217
|
+
if (_session.queryIter) {
|
|
218
|
+
try { _session.queryIter.return(); } catch { /* ignore */ }
|
|
219
|
+
}
|
|
220
|
+
_session = { id: null, queryIter: null };
|
|
221
|
+
_recentMessages.length = 0;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function _consumeStream(q) {
|
|
225
|
+
try {
|
|
226
|
+
for await (const msg of q) {
|
|
227
|
+
// Capture session_id from the init message so we can resume
|
|
228
|
+
if (msg.type === 'system' && msg.subtype === 'init') {
|
|
229
|
+
_session.id = msg.session_id;
|
|
230
|
+
}
|
|
231
|
+
_agentBuffer.put('agentMessage', msg);
|
|
232
|
+
}
|
|
233
|
+
} catch (err) {
|
|
234
|
+
if (err?.name !== 'AbortError') {
|
|
235
|
+
broadcast({ type: 'error', message: err.message || String(err) });
|
|
236
|
+
}
|
|
237
|
+
} finally {
|
|
238
|
+
// Signal completion to clients regardless of success/error
|
|
239
|
+
if (_session.queryIter === q) {
|
|
240
|
+
_session.queryIter = null;
|
|
241
|
+
}
|
|
242
|
+
// Keep _recentMessages populated after natural session end so clients
|
|
243
|
+
// reloading the tab post-turn can still hydrate recent history.
|
|
244
|
+
// Ring is cleared only on force-kill / new-session start in _killCurrentSession.
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
// Server startup
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
let serverListening = false;
|
|
253
|
+
const server = http.createServer(app);
|
|
254
|
+
|
|
255
|
+
// Build stream bridge — tails JSONL from CLI build, rebroadcasts via SSE
|
|
256
|
+
const _bridge = new BuildStreamBridge(
|
|
257
|
+
path.join(getTargetRoot(), '.compose'),
|
|
258
|
+
broadcast
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
server.listen(PORT, '127.0.0.1', () => {
|
|
262
|
+
serverListening = true;
|
|
263
|
+
_bridge.start();
|
|
264
|
+
console.log(`Agent server running on http://127.0.0.1:${PORT}`);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
function shutdown(sig) {
|
|
268
|
+
console.log(`[agent-server] ${sig}, shutting down`);
|
|
269
|
+
_agentBuffer.stop();
|
|
270
|
+
_bridge.stop();
|
|
271
|
+
_killCurrentSession();
|
|
272
|
+
server.close();
|
|
273
|
+
setTimeout(() => process.exit(0), 1000);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
277
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
278
|
+
|
|
279
|
+
process.on('uncaughtException', (err) => {
|
|
280
|
+
if (!serverListening && err.code === 'EADDRINUSE') {
|
|
281
|
+
console.error(`[agent-server] Port ${PORT} in use, exiting for supervisor retry`);
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
console.error('[agent-server] Uncaught exception (kept alive):', err.message);
|
|
285
|
+
console.error(err.stack);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
process.on('unhandledRejection', (reason) => {
|
|
289
|
+
console.error('[agent-server] Unhandled rejection (kept alive):', reason);
|
|
290
|
+
});
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-spawn.js — Hidden Claude subagent spawn/poll routes.
|
|
3
|
+
*
|
|
4
|
+
* Routes: POST /api/agent/spawn, GET /api/agent/:id, GET /api/agents
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
|
|
10
|
+
import { getTargetRoot } from './project-root.js';
|
|
11
|
+
import { gracefulKill } from './agent-health.js';
|
|
12
|
+
|
|
13
|
+
const PROJECT_ROOT = getTargetRoot();
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Route registration
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Attach agent spawn/poll routes to an Express app.
|
|
21
|
+
* Each call creates an isolated agent registry — safe for multiple instances.
|
|
22
|
+
*
|
|
23
|
+
* @param {object} app — Express app
|
|
24
|
+
* @param {{ projectRoot: string, broadcastMessage: function, requireSensitiveToken: function }} deps
|
|
25
|
+
*/
|
|
26
|
+
function deriveAgentType(prompt) {
|
|
27
|
+
const lower = (prompt ?? '').toLowerCase();
|
|
28
|
+
if (lower.includes('explore') || lower.includes('find features') || lower.includes('map the architecture'))
|
|
29
|
+
return 'compose-explorer';
|
|
30
|
+
if (lower.includes('architect') || lower.includes('competing') || lower.includes('proposal'))
|
|
31
|
+
return 'compose-architect';
|
|
32
|
+
if (lower.includes('review') || lower.includes('codex'))
|
|
33
|
+
return 'codex';
|
|
34
|
+
return 'claude';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function attachAgentSpawnRoutes(app, { projectRoot = PROJECT_ROOT, broadcastMessage, requireSensitiveToken, registry, sessionManager, healthMonitor, worktreeGC }) {
|
|
38
|
+
const _agents = new Map();
|
|
39
|
+
// POST /api/agent/spawn — spawn a hidden Claude subagent
|
|
40
|
+
app.post('/api/agent/spawn', requireSensitiveToken, (req, res) => {
|
|
41
|
+
const { prompt, id } = req.body || {};
|
|
42
|
+
if (!prompt || typeof prompt !== 'string') return res.status(400).json({ error: 'prompt is required and must be a string' });
|
|
43
|
+
|
|
44
|
+
const agentId = id || `agent-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
45
|
+
|
|
46
|
+
if (_agents.has(agentId)) {
|
|
47
|
+
return res.status(409).json({ error: `Agent ${agentId} already running` });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const cleanEnv = { ...process.env, NO_COLOR: '1' };
|
|
51
|
+
delete cleanEnv.CLAUDECODE;
|
|
52
|
+
|
|
53
|
+
const proc = spawn('claude', [
|
|
54
|
+
'-p', prompt,
|
|
55
|
+
'--dangerously-skip-permissions',
|
|
56
|
+
], {
|
|
57
|
+
cwd: projectRoot,
|
|
58
|
+
env: cleanEnv,
|
|
59
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const agent = {
|
|
63
|
+
process: proc,
|
|
64
|
+
output: '',
|
|
65
|
+
stderr: '',
|
|
66
|
+
status: 'running',
|
|
67
|
+
prompt,
|
|
68
|
+
startedAt: new Date().toISOString(),
|
|
69
|
+
};
|
|
70
|
+
_agents.set(agentId, agent);
|
|
71
|
+
|
|
72
|
+
// Register with persistent registry + broadcast spawn event
|
|
73
|
+
const agentType = deriveAgentType(prompt);
|
|
74
|
+
const parentSessionId = sessionManager?.currentSession?.id ?? null;
|
|
75
|
+
if (registry) {
|
|
76
|
+
registry.register(agentId, { parentSessionId, agentType, prompt, pid: proc.pid });
|
|
77
|
+
}
|
|
78
|
+
broadcastMessage({
|
|
79
|
+
type: 'agentSpawned',
|
|
80
|
+
agentId,
|
|
81
|
+
parentSessionId,
|
|
82
|
+
agentType,
|
|
83
|
+
prompt: prompt.slice(0, 200),
|
|
84
|
+
startedAt: agent.startedAt,
|
|
85
|
+
});
|
|
86
|
+
broadcastMessage({
|
|
87
|
+
type: 'agentRelay',
|
|
88
|
+
fromAgentId: parentSessionId || 'session',
|
|
89
|
+
toAgentId: agentId,
|
|
90
|
+
direction: 'dispatch',
|
|
91
|
+
messagePreview: (prompt || '').slice(0, 80),
|
|
92
|
+
timestamp: new Date().toISOString(),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
proc.stdout.on('data', (chunk) => {
|
|
96
|
+
agent.output += chunk.toString();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
proc.stderr.on('data', (chunk) => {
|
|
100
|
+
agent.stderr += chunk.toString();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// COMP-AGT-1: Wire health monitor after stdout/stderr listeners
|
|
104
|
+
if (healthMonitor) healthMonitor.track(agentId, proc);
|
|
105
|
+
|
|
106
|
+
proc.on('close', (code) => {
|
|
107
|
+
// COMP-AGT-1: Untrack from health monitor, preserve terminal reason
|
|
108
|
+
if (healthMonitor) {
|
|
109
|
+
const terminalReason = healthMonitor.getTerminalReason(agentId);
|
|
110
|
+
healthMonitor.untrack(agentId);
|
|
111
|
+
if (terminalReason) {
|
|
112
|
+
agent.status = 'killed';
|
|
113
|
+
agent.terminalReason = terminalReason;
|
|
114
|
+
agent.exitCode = code;
|
|
115
|
+
if (registry) {
|
|
116
|
+
registry.updateStatus(agentId, 'killed', terminalReason);
|
|
117
|
+
}
|
|
118
|
+
broadcastMessage({
|
|
119
|
+
type: 'agentComplete',
|
|
120
|
+
agentId,
|
|
121
|
+
agentType,
|
|
122
|
+
status: 'killed',
|
|
123
|
+
terminalReason,
|
|
124
|
+
output: agent.output,
|
|
125
|
+
});
|
|
126
|
+
broadcastMessage({
|
|
127
|
+
type: 'agentRelay',
|
|
128
|
+
fromAgentId: agentId,
|
|
129
|
+
toAgentId: parentSessionId || 'session',
|
|
130
|
+
direction: 'result',
|
|
131
|
+
messagePreview: `Killed: ${terminalReason}`,
|
|
132
|
+
timestamp: new Date().toISOString(),
|
|
133
|
+
});
|
|
134
|
+
return; // skip normal close handling
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
agent.status = code === 0 ? 'complete' : 'failed';
|
|
138
|
+
agent.exitCode = code;
|
|
139
|
+
if (registry) {
|
|
140
|
+
registry.complete(agentId, { status: agent.status, exitCode: code });
|
|
141
|
+
}
|
|
142
|
+
broadcastMessage({
|
|
143
|
+
type: 'agentComplete',
|
|
144
|
+
agentId,
|
|
145
|
+
agentType,
|
|
146
|
+
status: agent.status,
|
|
147
|
+
output: agent.output,
|
|
148
|
+
});
|
|
149
|
+
broadcastMessage({
|
|
150
|
+
type: 'agentRelay',
|
|
151
|
+
fromAgentId: agentId,
|
|
152
|
+
toAgentId: parentSessionId || 'session',
|
|
153
|
+
direction: 'result',
|
|
154
|
+
messagePreview: (agent.output || '').slice(0, 80),
|
|
155
|
+
timestamp: new Date().toISOString(),
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
proc.on('error', (err) => {
|
|
160
|
+
agent.status = 'failed';
|
|
161
|
+
agent.stderr += err.message;
|
|
162
|
+
console.error(`[vision] Agent ${agentId} spawn error:`, err.message);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
console.log(`[vision] Agent ${agentId} spawned (PID ${proc.pid})`);
|
|
166
|
+
res.status(201).json({ agentId, pid: proc.pid, status: 'running' });
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// GET /api/agent/:id — poll agent status + output
|
|
170
|
+
app.get('/api/agent/:id', (req, res) => {
|
|
171
|
+
const agent = _agents.get(req.params.id);
|
|
172
|
+
if (!agent) return res.status(404).json({ error: 'Agent not found' });
|
|
173
|
+
res.json({
|
|
174
|
+
agentId: req.params.id,
|
|
175
|
+
status: agent.status,
|
|
176
|
+
output: agent.output,
|
|
177
|
+
stderr: agent.stderr,
|
|
178
|
+
exitCode: agent.exitCode,
|
|
179
|
+
startedAt: agent.startedAt,
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// GET /api/agents — list all agents
|
|
184
|
+
app.get('/api/agents', (_req, res) => {
|
|
185
|
+
const agents = [];
|
|
186
|
+
for (const [id, agent] of _agents) {
|
|
187
|
+
agents.push({
|
|
188
|
+
agentId: id,
|
|
189
|
+
status: agent.status,
|
|
190
|
+
startedAt: agent.startedAt,
|
|
191
|
+
outputLength: agent.output.length,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
res.json({ agents });
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// GET /api/agents/tree — agent hierarchy for current session
|
|
198
|
+
app.get('/api/agents/tree', (_req, res) => {
|
|
199
|
+
if (!registry) return res.json({ agents: [] });
|
|
200
|
+
const parentId = sessionManager?.currentSession?.id ?? null;
|
|
201
|
+
const agents = parentId ? registry.getChildren(parentId) : registry.getAll();
|
|
202
|
+
res.json({ sessionId: parentId, agents });
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// POST /api/agent/:id/stop — COMP-AGT-1: graceful stop (SIGTERM → 5s → SIGKILL)
|
|
206
|
+
app.post('/api/agent/:id/stop', requireSensitiveToken, (req, res) => {
|
|
207
|
+
const agentId = req.params.id;
|
|
208
|
+
const agent = _agents.get(agentId);
|
|
209
|
+
|
|
210
|
+
// For SDK sessions, proxy to agent-server's interrupt endpoint
|
|
211
|
+
const currentSessionId = sessionManager?.currentSession?.id ?? null;
|
|
212
|
+
if (agentId === currentSessionId) {
|
|
213
|
+
const agentPort = process.env.AGENT_PORT || 4002;
|
|
214
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
215
|
+
if (process.env.COMPOSE_API_TOKEN) headers['x-compose-token'] = process.env.COMPOSE_API_TOKEN;
|
|
216
|
+
fetch(`http://127.0.0.1:${agentPort}/api/agent/interrupt`, {
|
|
217
|
+
method: 'POST',
|
|
218
|
+
headers,
|
|
219
|
+
}).then(r => {
|
|
220
|
+
if (!r.ok) {
|
|
221
|
+
return res.status(r.status).json({ ok: false, error: `Interrupt proxy failed: ${r.status}` });
|
|
222
|
+
}
|
|
223
|
+
return r.json().then(data => res.json({ ok: true, proxied: true, ...data }));
|
|
224
|
+
})
|
|
225
|
+
.catch(err => res.status(502).json({ error: `Interrupt proxy failed: ${err.message}` }));
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (!agent) return res.status(404).json({ error: 'Agent not found' });
|
|
230
|
+
if (agent.status !== 'running') return res.json({ ok: true, status: agent.status, note: 'already stopped' });
|
|
231
|
+
|
|
232
|
+
// Set terminal reason before killing
|
|
233
|
+
if (healthMonitor) healthMonitor.setTerminalReason(agentId, 'manual_stop');
|
|
234
|
+
|
|
235
|
+
gracefulKill(agent.process);
|
|
236
|
+
|
|
237
|
+
res.json({ ok: true, agentId, action: 'SIGTERM sent, SIGKILL in 5s if needed' });
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// POST /api/agent/gc — COMP-AGT-1: trigger worktree garbage collection
|
|
241
|
+
app.post('/api/agent/gc', requireSensitiveToken, async (_req, res) => {
|
|
242
|
+
if (!worktreeGC) return res.status(503).json({ error: 'WorktreeGC not available' });
|
|
243
|
+
try {
|
|
244
|
+
const removed = await worktreeGC.runNow();
|
|
245
|
+
broadcastMessage({ type: 'agentGC', removed, timestamp: new Date().toISOString() });
|
|
246
|
+
res.json({ ok: true, removed });
|
|
247
|
+
} catch (err) {
|
|
248
|
+
res.status(500).json({ error: err.message });
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-templates.js — Agent capability profiles for COMP-AGENT-CAPS.
|
|
3
|
+
*
|
|
4
|
+
* Defines which tools each agent role is allowed or denied.
|
|
5
|
+
* Violations are informational in v1 — logged but not blocking.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @type {Map<string, {allowedTools: string[]|null, disallowedTools: string[]|null, description: string}>}
|
|
10
|
+
*/
|
|
11
|
+
export const AGENT_TEMPLATES = new Map([
|
|
12
|
+
['read-only-reviewer', {
|
|
13
|
+
allowedTools: ['Read', 'Grep', 'Glob', 'Agent'],
|
|
14
|
+
disallowedTools: ['Edit', 'Write', 'Bash'],
|
|
15
|
+
description: 'Read-only review agent',
|
|
16
|
+
}],
|
|
17
|
+
['read-only-researcher', {
|
|
18
|
+
allowedTools: ['Read', 'Grep', 'Glob', 'Agent', 'WebSearch', 'WebFetch'],
|
|
19
|
+
disallowedTools: ['Edit', 'Write', 'Bash'],
|
|
20
|
+
description: 'Read-only research agent with web access',
|
|
21
|
+
}],
|
|
22
|
+
['implementer', {
|
|
23
|
+
allowedTools: null,
|
|
24
|
+
disallowedTools: null,
|
|
25
|
+
description: 'Full access implementation agent',
|
|
26
|
+
}],
|
|
27
|
+
['orchestrator', {
|
|
28
|
+
allowedTools: ['Read', 'Grep', 'Glob', 'Agent', 'Bash'],
|
|
29
|
+
disallowedTools: ['Edit', 'Write'],
|
|
30
|
+
description: 'Meta-config orchestrator',
|
|
31
|
+
}],
|
|
32
|
+
['security-auditor', {
|
|
33
|
+
allowedTools: ['Read', 'Grep', 'Glob', 'Bash'],
|
|
34
|
+
disallowedTools: ['Edit', 'Write'],
|
|
35
|
+
description: 'Security audit agent',
|
|
36
|
+
}],
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve a template by name.
|
|
41
|
+
* @param {string|null|undefined} templateName
|
|
42
|
+
* @returns {{ allowedTools: string[]|null, disallowedTools: string[]|null, description: string }|null}
|
|
43
|
+
* Returns null if templateName is unknown — callers fall back to no restrictions.
|
|
44
|
+
*/
|
|
45
|
+
export function resolveTemplate(templateName) {
|
|
46
|
+
if (!templateName) return null;
|
|
47
|
+
return AGENT_TEMPLATES.get(templateName) ?? null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Check whether a given tool is allowed under a template.
|
|
52
|
+
* @param {{ allowedTools: string[]|null, disallowedTools: string[]|null }|null} template
|
|
53
|
+
* @param {string} toolName
|
|
54
|
+
* @returns {{ allowed: boolean, reason: string }}
|
|
55
|
+
*/
|
|
56
|
+
export function validateCapabilities(template, toolName) {
|
|
57
|
+
if (!template) {
|
|
58
|
+
return { allowed: true, reason: 'No template — all tools permitted' };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const { allowedTools, disallowedTools } = template;
|
|
62
|
+
|
|
63
|
+
// Explicit deny list takes priority
|
|
64
|
+
if (disallowedTools && disallowedTools.includes(toolName)) {
|
|
65
|
+
return { allowed: false, reason: `Tool "${toolName}" is in disallowedTools` };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// If allowedTools is set, tool must appear in it
|
|
69
|
+
if (allowedTools !== null) {
|
|
70
|
+
if (allowedTools.includes(toolName)) {
|
|
71
|
+
return { allowed: true, reason: `Tool "${toolName}" is in allowedTools` };
|
|
72
|
+
}
|
|
73
|
+
return { allowed: false, reason: `Tool "${toolName}" is not in allowedTools` };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { allowed: true, reason: 'No restrictions defined' };
|
|
77
|
+
}
|