@aion0/forge 0.5.7 → 0.5.8

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.
@@ -53,36 +53,11 @@ export function listClaudeSessions(projectName: string): ClaudeSessionInfo[] {
53
53
  const dir = getClaudeDirForProject(projectName);
54
54
  if (!dir) return [];
55
55
 
56
- // Try reading sessions-index.json first
57
- const indexPath = join(dir, 'sessions-index.json');
58
- if (existsSync(indexPath)) {
59
- try {
60
- const index = JSON.parse(readFileSync(indexPath, 'utf-8'));
61
- const sessions: ClaudeSessionInfo[] = (index.entries || []).map((e: any) => ({
62
- sessionId: e.sessionId,
63
- summary: e.summary,
64
- firstPrompt: e.firstPrompt,
65
- messageCount: e.messageCount,
66
- created: e.created,
67
- modified: e.modified,
68
- gitBranch: e.gitBranch,
69
- fileSize: 0,
70
- }));
71
-
72
- // Enrich with file size
73
- for (const s of sessions) {
74
- const fp = join(dir, `${s.sessionId}.jsonl`);
75
- try { s.fileSize = statSync(fp).size; } catch {}
76
- }
77
-
78
- return sessions.sort((a, b) => (b.modified || '').localeCompare(a.modified || ''));
79
- } catch {}
80
- }
81
-
82
- // Fallback: scan for .jsonl files
56
+ // Scan .jsonl files on disk (authoritative source)
57
+ let diskSessions: ClaudeSessionInfo[] = [];
83
58
  try {
84
59
  const files = readdirSync(dir).filter(f => f.endsWith('.jsonl'));
85
- return files.map(f => {
60
+ diskSessions = files.map(f => {
86
61
  const sessionId = f.replace('.jsonl', '');
87
62
  const fp = join(dir, f);
88
63
  const stat = statSync(fp);
@@ -96,6 +71,29 @@ export function listClaudeSessions(projectName: string): ClaudeSessionInfo[] {
96
71
  } catch {
97
72
  return [];
98
73
  }
74
+
75
+ // Enrich with metadata from sessions-index.json if available
76
+ const indexPath = join(dir, 'sessions-index.json');
77
+ if (existsSync(indexPath)) {
78
+ try {
79
+ const index = JSON.parse(readFileSync(indexPath, 'utf-8'));
80
+ const indexMap = new Map<string, any>();
81
+ for (const e of (index.entries || [])) {
82
+ if (e.sessionId) indexMap.set(e.sessionId, e);
83
+ }
84
+ for (const s of diskSessions) {
85
+ const meta = indexMap.get(s.sessionId);
86
+ if (meta) {
87
+ s.summary = meta.summary;
88
+ s.firstPrompt = meta.firstPrompt;
89
+ s.messageCount = meta.messageCount;
90
+ s.gitBranch = meta.gitBranch;
91
+ }
92
+ }
93
+ } catch {}
94
+ }
95
+
96
+ return diskSessions;
99
97
  }
100
98
 
101
99
  /**
@@ -0,0 +1,389 @@
1
+ /**
2
+ * Forge MCP Server — agent communication bus via Model Context Protocol.
3
+ *
4
+ * Each Claude Code session connects with context baked into the SSE URL:
5
+ * http://localhost:8406/sse?workspaceId=xxx&agentId=yyy
6
+ *
7
+ * The agent doesn't need to know IDs. It just calls:
8
+ * send_message(to: "Reviewer", content: "fixed the bug")
9
+ * get_inbox()
10
+ * get_status()
11
+ *
12
+ * Forge resolves everything from the connection context.
13
+ */
14
+
15
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
16
+ import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
17
+ import { z } from 'zod';
18
+ import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
19
+
20
+ // Lazy imports to avoid circular deps (workspace modules)
21
+ let _getOrchestrator: ((workspaceId: string) => any) | null = null;
22
+
23
+ export function setOrchestratorResolver(fn: (id: string) => any): void {
24
+ _getOrchestrator = fn;
25
+ }
26
+
27
+ function getOrch(workspaceId: string): any {
28
+ if (!_getOrchestrator) throw new Error('Orchestrator resolver not set');
29
+ return _getOrchestrator(workspaceId);
30
+ }
31
+
32
+ // Per-session context (resolved from SSE URL + orchestrator topo)
33
+ interface SessionContext {
34
+ workspaceId: string;
35
+ agentId: string; // resolved dynamically, may be empty for non-agent terminals
36
+ }
37
+ const sessionContexts = new Map<string, SessionContext>();
38
+
39
+ /** Resolve agentId from orchestrator's agent-tmux mapping */
40
+ function resolveAgentFromOrch(workspaceId: string): string {
41
+ // For now, default to primary agent. Future: resolve from tmux session → agent map
42
+ try {
43
+ const orch = getOrch(workspaceId);
44
+ const primary = orch.getPrimaryAgent();
45
+ return primary?.config?.id || '';
46
+ } catch { return ''; }
47
+ }
48
+
49
+ // ─── MCP Server Definition ──────────────────────────────
50
+
51
+ function createForgeMcpServer(sessionId: string): McpServer {
52
+ const server = new McpServer({
53
+ name: 'forge',
54
+ version: '1.0.0',
55
+ });
56
+
57
+ // Helper: get context for this session
58
+ const ctx = () => sessionContexts.get(sessionId) || { workspaceId: '', agentId: '' };
59
+
60
+ // ── send_message ──────────────────────────
61
+ server.tool(
62
+ 'send_message',
63
+ 'Send a message to another agent in the workspace',
64
+ {
65
+ to: z.string().describe('Target agent — name like "Reviewer", or description like "the one who does testing"'),
66
+ content: z.string().describe('Message content'),
67
+ action: z.string().optional().describe('Message type: fix_request, update_notify, question, review, info_request'),
68
+ },
69
+ async (params) => {
70
+ const { to, content, action = 'update_notify' } = params;
71
+ const { workspaceId, agentId } = ctx();
72
+ if (!workspaceId) return { content: [{ type: 'text', text: 'Error: No workspace context.' }] };
73
+
74
+ try {
75
+ const orch = getOrch(workspaceId);
76
+ const snapshot = orch.getSnapshot();
77
+ const candidates = snapshot.agents.filter((a: any) => a.type !== 'input' && a.id !== agentId);
78
+
79
+ // Match: exact label > label contains > role contains
80
+ const toLower = to.toLowerCase();
81
+ let target = candidates.find((a: any) => a.label.toLowerCase() === toLower);
82
+ if (!target) target = candidates.find((a: any) => a.label.toLowerCase().includes(toLower));
83
+ if (!target) target = candidates.find((a: any) => (a.role || '').toLowerCase().includes(toLower));
84
+
85
+ if (!target) {
86
+ const available = candidates.map((a: any) => `${a.label} (${(a.role || '').slice(0, 50)})`).join(', ');
87
+ return { content: [{ type: 'text', text: `No agent matches "${to}". Available: ${available}` }] };
88
+ }
89
+
90
+ orch.getBus().send(agentId, target.id, 'notify', { action, content });
91
+ return { content: [{ type: 'text', text: `Message sent to ${target.label}` }] };
92
+ } catch (err: any) {
93
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
94
+ }
95
+ }
96
+ );
97
+
98
+ // ── get_inbox ─────────────────────────────
99
+ server.tool(
100
+ 'get_inbox',
101
+ 'Check inbox messages from other agents',
102
+ {},
103
+ async () => {
104
+ const { workspaceId, agentId } = ctx();
105
+ if (!workspaceId) return { content: [{ type: 'text', text: 'Error: No workspace context' }] };
106
+
107
+ try {
108
+ const orch = getOrch(workspaceId);
109
+ const messages = orch.getBus().getLog()
110
+ .filter((m: any) => m.to === agentId && m.type !== 'ack')
111
+ .slice(-20);
112
+
113
+ if (messages.length === 0) {
114
+ return { content: [{ type: 'text', text: 'No messages in inbox.' }] };
115
+ }
116
+
117
+ const snapshot = orch.getSnapshot();
118
+ const getLabel = (id: string) => snapshot.agents.find((a: any) => a.id === id)?.label || id;
119
+
120
+ const formatted = messages.map((m: any) =>
121
+ `[${m.status}] From ${getLabel(m.from)}: ${m.payload?.content || m.payload?.action || '(no content)'} (${m.id.slice(0, 8)})`
122
+ ).join('\n');
123
+
124
+ return { content: [{ type: 'text', text: formatted }] };
125
+ } catch (err: any) {
126
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
127
+ }
128
+ }
129
+ );
130
+
131
+ // ── mark_message_done ─────────────────────
132
+ server.tool(
133
+ 'mark_message_done',
134
+ 'Mark an inbox message as done after handling it',
135
+ {
136
+ message_id: z.string().describe('Message ID (first 8 chars or full UUID)'),
137
+ },
138
+ async (params) => {
139
+ const { workspaceId, agentId } = ctx();
140
+ if (!workspaceId) return { content: [{ type: 'text', text: 'Error: No workspace context' }] };
141
+
142
+ try {
143
+ const orch = getOrch(workspaceId);
144
+ const msg = orch.getBus().getLog().find((m: any) =>
145
+ (m.id === params.message_id || m.id.startsWith(params.message_id)) && m.to === agentId
146
+ );
147
+ if (!msg) return { content: [{ type: 'text', text: 'Message not found' }] };
148
+
149
+ msg.status = 'done';
150
+ return { content: [{ type: 'text', text: `Message ${params.message_id} marked as done` }] };
151
+ } catch (err: any) {
152
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
153
+ }
154
+ }
155
+ );
156
+
157
+ // ── get_status ────────────────────────────
158
+ server.tool(
159
+ 'get_status',
160
+ 'Get status of all agents in the workspace',
161
+ {},
162
+ async () => {
163
+ const { workspaceId } = ctx();
164
+ if (!workspaceId) return { content: [{ type: 'text', text: 'Error: No workspace context' }] };
165
+
166
+ try {
167
+ const orch = getOrch(workspaceId);
168
+ const snapshot = orch.getSnapshot();
169
+ const states = orch.getAllAgentStates();
170
+
171
+ const lines = snapshot.agents
172
+ .filter((a: any) => a.type !== 'input')
173
+ .map((a: any) => {
174
+ const s = states[a.id];
175
+ const smith = s?.smithStatus || 'down';
176
+ const task = s?.taskStatus || 'idle';
177
+ const icon = smith === 'active' ? (task === 'running' ? '🔵' : task === 'done' ? '✅' : task === 'failed' ? '🔴' : '🟢') : '⬚';
178
+ return `${icon} ${a.label}: smith=${smith} task=${task}${s?.error ? ` error=${s.error}` : ''}`;
179
+ });
180
+
181
+ return { content: [{ type: 'text', text: lines.join('\n') || 'No agents configured.' }] };
182
+ } catch (err: any) {
183
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
184
+ }
185
+ }
186
+ );
187
+
188
+ // ── get_agents ────────────────────────────
189
+ server.tool(
190
+ 'get_agents',
191
+ 'Get all agents in the workspace with their roles and relationships. Use this to understand who does what before sending messages.',
192
+ {},
193
+ async () => {
194
+ const { workspaceId, agentId } = ctx();
195
+ if (!workspaceId) return { content: [{ type: 'text', text: 'Error: No workspace context' }] };
196
+
197
+ try {
198
+ const orch = getOrch(workspaceId);
199
+ const snapshot = orch.getSnapshot();
200
+
201
+ const agents = snapshot.agents
202
+ .filter((a: any) => a.type !== 'input')
203
+ .map((a: any) => {
204
+ const deps = a.dependsOn
205
+ .map((depId: string) => snapshot.agents.find((d: any) => d.id === depId)?.label || depId)
206
+ .join(', ');
207
+ const isMe = a.id === agentId;
208
+ return [
209
+ `${a.icon} ${a.label}${isMe ? ' (you)' : ''}${a.primary ? ' [PRIMARY]' : ''}`,
210
+ ` Role: ${a.role || '(no role defined)'}`,
211
+ deps ? ` Depends on: ${deps}` : null,
212
+ a.workDir && a.workDir !== './' ? ` Work dir: ${a.workDir}` : null,
213
+ ].filter(Boolean).join('\n');
214
+ });
215
+
216
+ return { content: [{ type: 'text', text: agents.join('\n\n') || 'No agents configured.' }] };
217
+ } catch (err: any) {
218
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
219
+ }
220
+ }
221
+ );
222
+
223
+ // ── sync_progress ─────────────────────────
224
+ server.tool(
225
+ 'sync_progress',
226
+ 'Report your work progress to the workspace (what you did, files changed)',
227
+ {
228
+ summary: z.string().describe('Brief summary of what you accomplished'),
229
+ files: z.array(z.string()).optional().describe('List of files changed'),
230
+ },
231
+ async (params) => {
232
+ const { workspaceId, agentId } = ctx();
233
+ if (!workspaceId) return { content: [{ type: 'text', text: 'Error: No workspace context' }] };
234
+
235
+ try {
236
+ const orch = getOrch(workspaceId);
237
+ const entry = orch.getSnapshot().agents.find((a: any) => a.id === agentId);
238
+ if (!entry) return { content: [{ type: 'text', text: 'Agent not found in workspace' }] };
239
+
240
+ orch.completeManualAgent(agentId, params.files || []);
241
+
242
+ return { content: [{ type: 'text', text: `Progress synced: ${params.summary}` }] };
243
+ } catch (err: any) {
244
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
245
+ }
246
+ }
247
+ );
248
+
249
+ // ── check_outbox ──────────────────────────
250
+ server.tool(
251
+ 'check_outbox',
252
+ 'Check status of messages you sent to other agents. See if they replied or completed.',
253
+ {},
254
+ async () => {
255
+ const { workspaceId, agentId } = ctx();
256
+ if (!workspaceId) return { content: [{ type: 'text', text: 'Error: No workspace context' }] };
257
+
258
+ try {
259
+ const orch = getOrch(workspaceId);
260
+ const snapshot = orch.getSnapshot();
261
+ const getLabel = (id: string) => snapshot.agents.find((a: any) => a.id === id)?.label || id;
262
+
263
+ // Messages sent BY this agent
264
+ const sent = orch.getBus().getLog()
265
+ .filter((m: any) => m.from === agentId && m.type !== 'ack')
266
+ .slice(-20);
267
+
268
+ if (sent.length === 0) {
269
+ return { content: [{ type: 'text', text: 'No messages sent.' }] };
270
+ }
271
+
272
+ // Check for replies
273
+ const formatted = sent.map((m: any) => {
274
+ const targetLabel = getLabel(m.to);
275
+ const replies = orch.getBus().getLog().filter((r: any) =>
276
+ r.from === m.to && r.to === agentId && r.timestamp > m.timestamp && r.type !== 'ack'
277
+ );
278
+ const replyInfo = replies.length > 0
279
+ ? `replied: ${replies[replies.length - 1].payload?.content?.slice(0, 100) || '(no content)'}`
280
+ : 'no reply yet';
281
+ return `→ ${targetLabel}: [${m.status}] ${(m.payload?.content || '').slice(0, 60)} | ${replyInfo}`;
282
+ }).join('\n');
283
+
284
+ return { content: [{ type: 'text', text: formatted }] };
285
+ } catch (err: any) {
286
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
287
+ }
288
+ }
289
+ );
290
+
291
+ return server;
292
+ }
293
+
294
+ // ─── HTTP Server with SSE Transport ─────────────────────
295
+
296
+ let mcpHttpServer: ReturnType<typeof createServer> | null = null;
297
+ const transports = new Map<string, SSEServerTransport>();
298
+
299
+ export async function startMcpServer(port: number): Promise<void> {
300
+ mcpHttpServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {
301
+ const url = new URL(req.url || '/', `http://localhost:${port}`);
302
+
303
+ // CORS
304
+ res.setHeader('Access-Control-Allow-Origin', '*');
305
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
306
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
307
+ if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
308
+
309
+ // SSE endpoint — each connection gets its own MCP server instance
310
+ if (url.pathname === '/sse' && req.method === 'GET') {
311
+ const transport = new SSEServerTransport('/message', res);
312
+ const sessionId = transport.sessionId;
313
+ transports.set(sessionId, transport);
314
+
315
+ // Extract workspace context from URL params
316
+ const workspaceId = url.searchParams.get('workspaceId') || '';
317
+ const agentId = url.searchParams.get('agentId') || (workspaceId ? resolveAgentFromOrch(workspaceId) : '');
318
+ sessionContexts.set(sessionId, { workspaceId, agentId });
319
+
320
+ transport.onclose = () => {
321
+ transports.delete(sessionId);
322
+ sessionContexts.delete(sessionId);
323
+ };
324
+
325
+ // Each session gets its own MCP server with context
326
+ const server = createForgeMcpServer(sessionId);
327
+ await server.connect(transport);
328
+ const agentLabel = workspaceId ? (getOrch(workspaceId)?.getSnapshot()?.agents?.find((a: any) => a.id === agentId)?.label || agentId) : 'unknown';
329
+ console.log(`[forge-mcp] Client connected: ${agentLabel} (ws=${workspaceId.slice(0, 8)}, session=${sessionId})`);
330
+ return;
331
+ }
332
+
333
+ // Message endpoint — route by sessionId query param
334
+ if (url.pathname === '/message' && req.method === 'POST') {
335
+ const sessionId = url.searchParams.get('sessionId');
336
+ if (!sessionId) {
337
+ res.writeHead(400);
338
+ res.end('Missing sessionId parameter');
339
+ return;
340
+ }
341
+
342
+ const transport = transports.get(sessionId);
343
+ if (!transport) {
344
+ res.writeHead(404);
345
+ res.end('Session not found');
346
+ return;
347
+ }
348
+
349
+ // Read body and pass to transport
350
+ const body: Buffer[] = [];
351
+ req.on('data', (chunk: Buffer) => body.push(chunk));
352
+ req.on('end', async () => {
353
+ try {
354
+ const parsed = JSON.parse(Buffer.concat(body).toString());
355
+ await transport.handlePostMessage(req, res, parsed);
356
+ } catch (err: any) {
357
+ if (!res.headersSent) { res.writeHead(400); res.end('Invalid JSON'); }
358
+ }
359
+ });
360
+ return;
361
+ }
362
+
363
+ // Health check
364
+ if (url.pathname === '/health') {
365
+ res.writeHead(200, { 'Content-Type': 'application/json' });
366
+ res.end(JSON.stringify({ ok: true, sessions: transports.size }));
367
+ return;
368
+ }
369
+
370
+ res.writeHead(404);
371
+ res.end('Not found');
372
+ });
373
+
374
+ mcpHttpServer.listen(port, () => {
375
+ console.log(`[forge-mcp] MCP Server running on http://localhost:${port}`);
376
+ });
377
+ }
378
+
379
+ export function stopMcpServer(): void {
380
+ if (mcpHttpServer) {
381
+ mcpHttpServer.close();
382
+ mcpHttpServer = null;
383
+ transports.clear();
384
+ }
385
+ }
386
+
387
+ export function getMcpPort(): number {
388
+ return Number(process.env.MCP_PORT) || 8406;
389
+ }
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Check and manage messages from other Forge Workspace agents (QA reports, review feedback, PM requests)
2
+ description: "[FALLBACK only use if MCP get_inbox tool is NOT available] Check messages from other Forge Workspace agents via HTTP API"
3
3
  ---
4
4
 
5
5
  # Forge Inbox
@@ -12,26 +12,27 @@ Check for messages from other agents and manage their status.
12
12
 
13
13
  ## How to use
14
14
 
15
- IMPORTANT: Do NOT check environment variables. Just run the commands — they auto-discover the workspace.
15
+ ### Option 1: MCP Tools (preferred)
16
+ If MCP tools are available, use them directly:
17
+ - `get_inbox()` — check messages
18
+ - `mark_message_done(message_id: "ID")` — mark as done
19
+ - `check_outbox()` — check messages you sent
16
20
 
17
- Step 1 Get workspace ID:
18
- ```bash
19
- curl -s "http://localhost:8403/api/workspace?projectPath=$(pwd)" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))"
20
- ```
21
+ ### Option 2: HTTP API (fallback)
21
22
 
22
- Step 2Check inbox (replace WORKSPACE_ID):
23
+ Step 1Get workspace ID (env var first, then API fallback):
23
24
  ```bash
24
- curl -s -X POST "http://localhost:8403/api/workspace/WORKSPACE_ID/smith" -H "Content-Type: application/json" -d '{"action":"inbox","agentId":"'"$FORGE_AGENT_ID"'"}'
25
+ WS_ID="${FORGE_WORKSPACE_ID:-$(curl -s "http://localhost:8405/resolve?projectPath=$(pwd)" | python3 -c "import sys,json; print(json.load(sys.stdin).get('workspaceId',''))" 2>/dev/null)}"
25
26
  ```
26
27
 
27
- ## Mark message as done
28
+ Step 2 Check inbox:
28
29
  ```bash
29
- curl -s -X POST "http://localhost:8403/api/workspace/WORKSPACE_ID/smith" -H "Content-Type: application/json" -d '{"action":"message_done","agentId":"'"$FORGE_AGENT_ID"'","messageId":"MESSAGE_ID"}'
30
+ curl -s -X POST "http://localhost:8403/api/workspace/$WS_ID/smith" -H "Content-Type: application/json" -d '{"action":"inbox","agentId":"'"$FORGE_AGENT_ID"'"}'
30
31
  ```
31
32
 
32
- ## Mark message as failed
33
+ ## Mark message as done
33
34
  ```bash
34
- curl -s -X POST "http://localhost:8403/api/workspace/WORKSPACE_ID/smith" -H "Content-Type: application/json" -d '{"action":"message_failed","agentId":"'"$FORGE_AGENT_ID"'","messageId":"MESSAGE_ID"}'
35
+ curl -s -X POST "http://localhost:8403/api/workspace/$WS_ID/smith" -H "Content-Type: application/json" -d '{"action":"message_done","agentId":"'"$FORGE_AGENT_ID"'","messageId":"MESSAGE_ID"}'
35
36
  ```
36
37
 
37
38
  After handling a message, always mark it as done or failed.
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Send a message to another Forge Workspace agent immediately via API (notify QA of a fix, ask PM a question, etc.)
2
+ description: "[FALLBACK only use if MCP send_message tool is NOT available] Send a message to another Forge Workspace agent via HTTP API"
3
3
  ---
4
4
 
5
5
  # Forge Send
@@ -17,16 +17,23 @@ Send a message to another agent in the Forge Workspace.
17
17
 
18
18
  ## How to send
19
19
 
20
- IMPORTANT: Do NOT check environment variables first. Just run the command below — it auto-discovers everything.
20
+ ### Option 1: MCP Tool (preferred)
21
+ If the `send_message` tool is available, use it directly:
22
+ ```
23
+ send_message(to: "TARGET_LABEL", content: "YOUR MESSAGE", action: "ACTION")
24
+ ```
25
+
26
+ ### Option 2: HTTP API (fallback if MCP not available)
21
27
 
22
- Step 1 — Get workspace ID:
28
+ Step 1 — Get workspace ID (env var first, then API fallback):
23
29
  ```bash
24
- curl -s "http://localhost:8403/api/workspace?projectPath=$(pwd)" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))"
30
+ WS_ID="${FORGE_WORKSPACE_ID:-$(curl -s "http://localhost:8405/resolve?projectPath=$(pwd)" | python3 -c "import sys,json; print(json.load(sys.stdin).get('workspaceId',''))" 2>/dev/null)}"
31
+ echo "$WS_ID"
25
32
  ```
26
33
 
27
- Step 2 — Send message (replace WORKSPACE_ID with result from step 1):
34
+ Step 2 — Send message:
28
35
  ```bash
29
- curl -s -X POST "http://localhost:8403/api/workspace/WORKSPACE_ID/smith" -H "Content-Type: application/json" -d '{"action":"send","agentId":"'"$FORGE_AGENT_ID"'","to":"TARGET_LABEL","msgAction":"ACTION","content":"YOUR MESSAGE"}'
36
+ curl -s -X POST "http://localhost:8403/api/workspace/$WS_ID/smith" -H "Content-Type: application/json" -d '{"action":"send","agentId":"'"$FORGE_AGENT_ID"'","to":"TARGET_LABEL","msgAction":"ACTION","content":"YOUR MESSAGE"}'
30
37
  ```
31
38
 
32
39
  Replace:
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Check the status of all agents in the Forge Workspace (who is running, done, waiting)
2
+ description: "[FALLBACK only use if MCP get_status tool is NOT available] Check agent statuses via HTTP API"
3
3
  ---
4
4
 
5
5
  # Forge Status
@@ -12,21 +12,21 @@ Check the current status of all agents in the Forge Workspace.
12
12
 
13
13
  ## How to check
14
14
 
15
- IMPORTANT: Do NOT check environment variables. Just run the commands.
15
+ ### Option 1: MCP Tools (preferred)
16
+ If MCP tools are available:
17
+ - `get_status()` — all agent statuses
18
+ - `get_agents()` — agent details (roles, dependencies)
16
19
 
17
- Step 1 Get workspace ID:
20
+ ### Option 2: HTTP API (fallback)
21
+
22
+ Step 1 — Get workspace ID (env var first, then API fallback):
18
23
  ```bash
19
- curl -s "http://localhost:8403/api/workspace?projectPath=$(pwd)" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))"
24
+ WS_ID="${FORGE_WORKSPACE_ID:-$(curl -s "http://localhost:8405/resolve?projectPath=$(pwd)" | python3 -c "import sys,json; print(json.load(sys.stdin).get('workspaceId',''))" 2>/dev/null)}"
20
25
  ```
21
26
 
22
- Step 2 — Check status (replace WORKSPACE_ID):
27
+ Step 2 — Check status:
23
28
  ```bash
24
- curl -s -X POST "http://localhost:8403/api/workspace/WORKSPACE_ID/smith" -H "Content-Type: application/json" -d '{"action":"status","agentId":"'"$FORGE_AGENT_ID"'"}'
29
+ curl -s -X POST "http://localhost:8403/api/workspace/$WS_ID/smith" -H "Content-Type: application/json" -d '{"action":"status","agentId":"'"$FORGE_AGENT_ID"'"}'
25
30
  ```
26
31
 
27
- Present the results as a clear status overview:
28
- - 🟢 active — smith is online and listening
29
- - 🔵 running — agent is currently executing a task
30
- - ✅ done — agent completed its work
31
- - 🔴 failed — agent encountered an error
32
- - ⬚ down — smith is not started
32
+ Present the results as a clear status overview.
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Project-level fixed session binding.
3
+ * Stores { projectPath: sessionId } in ~/.forge/data/project-sessions.json
4
+ */
5
+
6
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { getDataDir } from '@/src/config';
9
+
10
+ function getFilePath(): string {
11
+ const dir = getDataDir();
12
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
13
+ return join(dir, 'project-sessions.json');
14
+ }
15
+
16
+ function loadAll(): Record<string, string> {
17
+ const fp = getFilePath();
18
+ if (!existsSync(fp)) return {};
19
+ try { return JSON.parse(readFileSync(fp, 'utf-8')); } catch { return {}; }
20
+ }
21
+
22
+ function saveAll(data: Record<string, string>): void {
23
+ writeFileSync(getFilePath(), JSON.stringify(data, null, 2));
24
+ }
25
+
26
+ /** Get the fixed session ID for a project */
27
+ export function getFixedSession(projectPath: string): string | undefined {
28
+ return loadAll()[projectPath] || undefined;
29
+ }
30
+
31
+ /** Set the fixed session ID for a project */
32
+ export function setFixedSession(projectPath: string, sessionId: string): void {
33
+ const data = loadAll();
34
+ data[projectPath] = sessionId;
35
+ saveAll(data);
36
+ }
37
+
38
+ /** Clear the fixed session for a project */
39
+ export function clearFixedSession(projectPath: string): void {
40
+ const data = loadAll();
41
+ delete data[projectPath];
42
+ saveAll(data);
43
+ }
44
+
45
+ /** Get all bindings */
46
+ export function getAllFixedSessions(): Record<string, string> {
47
+ return loadAll();
48
+ }