@aion0/forge 0.5.23 → 0.5.25
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/RELEASE_NOTES.md +5 -6
- package/app/api/smith-templates/route.ts +81 -0
- package/components/WorkspaceView.tsx +841 -83
- package/docs/Forge_Memory_Layer_Design.docx +0 -0
- package/docs/Forge_Strategy_Research_2026.docx +0 -0
- package/lib/claude-sessions.ts +2 -1
- package/lib/forge-mcp-server.ts +247 -33
- package/lib/help-docs/11-workspace.md +722 -166
- package/lib/project-sessions.ts +1 -1
- package/lib/telegram-bot.ts +1 -1
- package/lib/workspace/orchestrator.ts +263 -76
- package/lib/workspace/presets.ts +535 -58
- package/lib/workspace/requests.ts +287 -0
- package/lib/workspace/session-monitor.ts +4 -3
- package/lib/workspace/types.ts +1 -0
- package/lib/workspace/watch-manager.ts +1 -1
- package/lib/workspace-standalone.ts +1 -1
- package/next-env.d.ts +1 -1
- package/package.json +1 -1
- package/pnpm-workspace.yaml +1 -0
- package/scripts/bench/README.md +66 -0
- package/scripts/bench/results/.gitignore +2 -0
- package/scripts/bench/run.ts +635 -0
- package/scripts/bench/tasks/01-text-utils/task.md +26 -0
- package/scripts/bench/tasks/01-text-utils/validator.sh +46 -0
- package/scripts/bench/tasks/02-pagination/setup.sh +19 -0
- package/scripts/bench/tasks/02-pagination/task.md +48 -0
- package/scripts/bench/tasks/02-pagination/validator.sh +69 -0
- package/scripts/bench/tasks/03-bug-fix/setup.sh +82 -0
- package/scripts/bench/tasks/03-bug-fix/task.md +30 -0
- package/scripts/bench/tasks/03-bug-fix/validator.sh +29 -0
- package/templates/smith-lead.json +45 -0
|
Binary file
|
|
Binary file
|
package/lib/claude-sessions.ts
CHANGED
|
@@ -32,7 +32,8 @@ export interface SessionEntry {
|
|
|
32
32
|
* Claude uses: ~/.claude/projects/<path-with-slashes-replaced-by-dashes>/
|
|
33
33
|
*/
|
|
34
34
|
export function projectPathToClaudeDir(projectPath: string): string {
|
|
35
|
-
|
|
35
|
+
// Claude Code encodes paths by replacing all non-alphanumeric chars with '-'
|
|
36
|
+
const hash = projectPath.replace(/[^a-zA-Z0-9]/g, '-');
|
|
36
37
|
return join(getClaudeDir(), 'projects', hash);
|
|
37
38
|
}
|
|
38
39
|
|
package/lib/forge-mcp-server.ts
CHANGED
|
@@ -117,9 +117,10 @@ function createForgeMcpServer(sessionId: string): McpServer {
|
|
|
117
117
|
const snapshot = orch.getSnapshot();
|
|
118
118
|
const getLabel = (id: string) => snapshot.agents.find((a: any) => a.id === id)?.label || id;
|
|
119
119
|
|
|
120
|
-
const formatted = messages.map((m: any) =>
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
const formatted = messages.map((m: any) => {
|
|
121
|
+
const refInfo = m.payload?.ref ? ` [ref: ${m.payload.ref}]` : '';
|
|
122
|
+
return `[${m.status}] From ${getLabel(m.from)}: ${m.payload?.content || m.payload?.action || '(no content)'}${refInfo} (${m.id.slice(0, 8)})`;
|
|
123
|
+
}).join('\n');
|
|
123
124
|
|
|
124
125
|
return { content: [{ type: 'text', text: formatted }] };
|
|
125
126
|
} catch (err: any) {
|
|
@@ -157,7 +158,7 @@ function createForgeMcpServer(sessionId: string): McpServer {
|
|
|
157
158
|
// ── get_status ────────────────────────────
|
|
158
159
|
server.tool(
|
|
159
160
|
'get_status',
|
|
160
|
-
'Get status of all agents in the workspace',
|
|
161
|
+
'Get live status of all agents in the workspace (from topology cache)',
|
|
161
162
|
{},
|
|
162
163
|
async () => {
|
|
163
164
|
const { workspaceId } = ctx();
|
|
@@ -165,18 +166,15 @@ function createForgeMcpServer(sessionId: string): McpServer {
|
|
|
165
166
|
|
|
166
167
|
try {
|
|
167
168
|
const orch = getOrch(workspaceId);
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
});
|
|
169
|
+
const topo = orch.getWorkspaceTopo();
|
|
170
|
+
|
|
171
|
+
const lines = topo.agents.map((a: any) => {
|
|
172
|
+
const icon = a.smithStatus === 'active'
|
|
173
|
+
? (a.taskStatus === 'running' ? '🔵' : a.taskStatus === 'done' ? '✅' : a.taskStatus === 'failed' ? '🔴' : '🟢')
|
|
174
|
+
: '⬚';
|
|
175
|
+
return `${icon} ${a.label}: smith=${a.smithStatus} task=${a.taskStatus}`;
|
|
176
|
+
});
|
|
177
|
+
lines.unshift(`Flow: ${topo.flow}\n`);
|
|
180
178
|
|
|
181
179
|
return { content: [{ type: 'text', text: lines.join('\n') || 'No agents configured.' }] };
|
|
182
180
|
} catch (err: any) {
|
|
@@ -188,7 +186,7 @@ function createForgeMcpServer(sessionId: string): McpServer {
|
|
|
188
186
|
// ── get_agents ────────────────────────────
|
|
189
187
|
server.tool(
|
|
190
188
|
'get_agents',
|
|
191
|
-
'Get all agents
|
|
189
|
+
'Get workspace topology — all agents, their roles, relationships, current status, and execution flow. Cached and auto-refreshed on any agent change. Call this to understand the full team composition before planning work.',
|
|
192
190
|
{},
|
|
193
191
|
async () => {
|
|
194
192
|
const { workspaceId, agentId } = ctx();
|
|
@@ -196,24 +194,35 @@ function createForgeMcpServer(sessionId: string): McpServer {
|
|
|
196
194
|
|
|
197
195
|
try {
|
|
198
196
|
const orch = getOrch(workspaceId);
|
|
199
|
-
const
|
|
197
|
+
const topo = orch.getWorkspaceTopo();
|
|
198
|
+
|
|
199
|
+
const lines: string[] = [];
|
|
200
|
+
lines.push(`## Workspace Topology (${topo.agents.length} agents)`);
|
|
201
|
+
lines.push(`Flow: ${topo.flow}\n`);
|
|
202
|
+
|
|
203
|
+
// Identify present and missing standard roles
|
|
204
|
+
const labels = new Set(topo.agents.map((a: any) => a.label.toLowerCase()));
|
|
205
|
+
const standardRoles = ['architect', 'engineer', 'qa', 'reviewer', 'pm', 'lead'];
|
|
206
|
+
const present = standardRoles.filter(r => labels.has(r));
|
|
207
|
+
const missing = standardRoles.filter(r => !labels.has(r));
|
|
208
|
+
if (missing.length > 0) {
|
|
209
|
+
lines.push(`Present roles: ${present.join(', ') || 'none'}`);
|
|
210
|
+
lines.push(`Missing roles: ${missing.join(', ')} — these responsibilities must be covered by existing agents\n`);
|
|
211
|
+
}
|
|
200
212
|
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
.
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
a.workDir && a.workDir !== './' ? ` Work dir: ${a.workDir}` : null,
|
|
213
|
-
].filter(Boolean).join('\n');
|
|
214
|
-
});
|
|
213
|
+
for (const a of topo.agents as any[]) {
|
|
214
|
+
const isMe = a.id === agentId;
|
|
215
|
+
lines.push(`### ${a.icon} ${a.label}${isMe ? ' ← YOU' : ''}${a.primary ? ' [PRIMARY]' : ''}`);
|
|
216
|
+
lines.push(`Status: smith=${a.smithStatus} task=${a.taskStatus}`);
|
|
217
|
+
lines.push(`Role: ${a.roleSummary}`);
|
|
218
|
+
if (a.dependsOn.length > 0) lines.push(`Depends on: ${a.dependsOn.join(', ')}`);
|
|
219
|
+
if (a.workDir !== './') lines.push(`Work dir: ${a.workDir}`);
|
|
220
|
+
if (a.outputs.length > 0) lines.push(`Outputs: ${a.outputs.join(', ')}`);
|
|
221
|
+
if (a.steps.length > 0) lines.push(`Steps: ${a.steps.join(' → ')}`);
|
|
222
|
+
lines.push('');
|
|
223
|
+
}
|
|
215
224
|
|
|
216
|
-
return { content: [{ type: 'text', text:
|
|
225
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
217
226
|
} catch (err: any) {
|
|
218
227
|
return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
219
228
|
}
|
|
@@ -401,6 +410,211 @@ function createForgeMcpServer(sessionId: string): McpServer {
|
|
|
401
410
|
}
|
|
402
411
|
);
|
|
403
412
|
|
|
413
|
+
// ─── Request/Response Document Tools ─────────────────────
|
|
414
|
+
|
|
415
|
+
server.tool(
|
|
416
|
+
'create_request',
|
|
417
|
+
'Create a new request document for implementation. Auto-notifies downstream agents via DAG.',
|
|
418
|
+
{
|
|
419
|
+
title: z.string().describe('Short title for the request'),
|
|
420
|
+
description: z.string().describe('Detailed description of what to implement'),
|
|
421
|
+
type: z.enum(['feature', 'bugfix', 'refactor', 'task']).optional().describe('Request type (default: feature)'),
|
|
422
|
+
modules: z.array(z.object({
|
|
423
|
+
name: z.string(),
|
|
424
|
+
description: z.string(),
|
|
425
|
+
acceptance_criteria: z.array(z.string()),
|
|
426
|
+
})).describe('Feature modules with acceptance criteria'),
|
|
427
|
+
batch: z.string().optional().describe('Batch name to group related requests (default: auto-generated from date)'),
|
|
428
|
+
priority: z.enum(['high', 'medium', 'low']).optional().describe('Priority level (default: medium)'),
|
|
429
|
+
},
|
|
430
|
+
async (params) => {
|
|
431
|
+
const { workspaceId, agentId } = ctx();
|
|
432
|
+
if (!workspaceId) return { content: [{ type: 'text', text: 'Error: No workspace context.' }] };
|
|
433
|
+
try {
|
|
434
|
+
const orch = getOrch(workspaceId);
|
|
435
|
+
const { createRequest } = await import('./workspace/requests') as any;
|
|
436
|
+
const batch = params.batch || `delivery-${new Date().toISOString().slice(0, 10)}`;
|
|
437
|
+
const agentLabel = orch.getSnapshot().agents.find((a: any) => a.id === agentId)?.label || agentId;
|
|
438
|
+
|
|
439
|
+
const ref = createRequest(orch.projectPath, {
|
|
440
|
+
title: params.title,
|
|
441
|
+
description: params.description,
|
|
442
|
+
type: params.type || 'feature',
|
|
443
|
+
modules: params.modules,
|
|
444
|
+
batch,
|
|
445
|
+
priority: params.priority || 'medium',
|
|
446
|
+
status: 'open',
|
|
447
|
+
assigned_to: '',
|
|
448
|
+
created_by: agentLabel,
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// Auto-notify downstream agents via DAG
|
|
452
|
+
const snapshot = orch.getSnapshot();
|
|
453
|
+
const notified: string[] = [];
|
|
454
|
+
for (const agent of snapshot.agents) {
|
|
455
|
+
if (agent.type === 'input') continue;
|
|
456
|
+
if (!agent.dependsOn?.includes(agentId)) continue;
|
|
457
|
+
orch.getBus().send(agentId, agent.id, 'notify', {
|
|
458
|
+
action: 'new_request',
|
|
459
|
+
content: `New request: ${params.title} [${params.priority || 'medium'}] — ${params.modules.length} module(s). Use list_requests and claim_request to pick it up.`,
|
|
460
|
+
ref,
|
|
461
|
+
});
|
|
462
|
+
notified.push(agent.label);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return { content: [{ type: 'text', text: `Created request: ${ref}\nBatch: ${batch}\nModules: ${params.modules.length}\nNotified: ${notified.length > 0 ? notified.join(', ') : '(no downstream agents)'}` }] };
|
|
466
|
+
} catch (err: any) {
|
|
467
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
server.tool(
|
|
473
|
+
'claim_request',
|
|
474
|
+
'Claim an open request for implementation. Prevents other agents from working on the same request.',
|
|
475
|
+
{
|
|
476
|
+
request_id: z.string().describe('Request ID to claim (e.g., REQ-20260403-001)'),
|
|
477
|
+
},
|
|
478
|
+
async (params) => {
|
|
479
|
+
const { workspaceId, agentId } = ctx();
|
|
480
|
+
if (!workspaceId) return { content: [{ type: 'text', text: 'Error: No workspace context.' }] };
|
|
481
|
+
try {
|
|
482
|
+
const orch = getOrch(workspaceId);
|
|
483
|
+
const { claimRequest } = await import('./workspace/requests') as any;
|
|
484
|
+
const agentLabel = orch.getSnapshot().agents.find((a: any) => a.id === agentId)?.label || agentId;
|
|
485
|
+
|
|
486
|
+
const result = claimRequest(orch.projectPath, params.request_id, agentLabel);
|
|
487
|
+
if (!result.ok) {
|
|
488
|
+
return { content: [{ type: 'text', text: `Cannot claim ${params.request_id}: already claimed by ${result.claimedBy}. Use list_requests(status: "open") to find available requests.` }] };
|
|
489
|
+
}
|
|
490
|
+
return { content: [{ type: 'text', text: `Claimed ${params.request_id}. Status: in_progress. You can now implement it and use update_response when done.` }] };
|
|
491
|
+
} catch (err: any) {
|
|
492
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
server.tool(
|
|
498
|
+
'update_response',
|
|
499
|
+
'Update a response document with your work results. Auto-advances status and notifies downstream agents via DAG.',
|
|
500
|
+
{
|
|
501
|
+
request_id: z.string().describe('Request ID (e.g., REQ-20260403-001)'),
|
|
502
|
+
section: z.enum(['engineer', 'review', 'qa']).describe('Which section to update'),
|
|
503
|
+
data: z.object({
|
|
504
|
+
files_changed: z.array(z.string()).optional().describe('Files modified (engineer)'),
|
|
505
|
+
notes: z.string().optional().describe('Implementation notes (engineer)'),
|
|
506
|
+
result: z.string().optional().describe('Result: approved/changes_requested/rejected (review) or passed/failed (qa)'),
|
|
507
|
+
findings: z.array(z.object({
|
|
508
|
+
severity: z.string(),
|
|
509
|
+
description: z.string(),
|
|
510
|
+
})).optional().describe('Issues found (review/qa)'),
|
|
511
|
+
test_files: z.array(z.string()).optional().describe('Test files run (qa)'),
|
|
512
|
+
}).describe('Response data for your section'),
|
|
513
|
+
},
|
|
514
|
+
async (params) => {
|
|
515
|
+
const { workspaceId, agentId } = ctx();
|
|
516
|
+
if (!workspaceId) return { content: [{ type: 'text', text: 'Error: No workspace context.' }] };
|
|
517
|
+
try {
|
|
518
|
+
const orch = getOrch(workspaceId);
|
|
519
|
+
const { updateResponse, getRequest } = await import('./workspace/requests') as any;
|
|
520
|
+
|
|
521
|
+
const ref = updateResponse(orch.projectPath, params.request_id, params.section, params.data);
|
|
522
|
+
const updated = getRequest(orch.projectPath, params.request_id);
|
|
523
|
+
const newStatus = updated?.request?.status || 'unknown';
|
|
524
|
+
|
|
525
|
+
// Auto-notify downstream agents via DAG
|
|
526
|
+
const snapshot = orch.getSnapshot();
|
|
527
|
+
const notified: string[] = [];
|
|
528
|
+
for (const agent of snapshot.agents) {
|
|
529
|
+
if (agent.type === 'input') continue;
|
|
530
|
+
if (!agent.dependsOn?.includes(agentId)) continue;
|
|
531
|
+
orch.getBus().send(agentId, agent.id, 'notify', {
|
|
532
|
+
action: 'response_updated',
|
|
533
|
+
content: `${params.section} completed for ${params.request_id} → status: ${newStatus}. Use get_request to review details.`,
|
|
534
|
+
ref,
|
|
535
|
+
});
|
|
536
|
+
notified.push(agent.label);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return { content: [{ type: 'text', text: `Updated ${params.request_id} [${params.section}] → status: ${newStatus}\nNotified: ${notified.length > 0 ? notified.join(', ') : '(no downstream agents)'}` }] };
|
|
540
|
+
} catch (err: any) {
|
|
541
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
server.tool(
|
|
547
|
+
'list_requests',
|
|
548
|
+
'List all request documents in the project, optionally filtered by batch or status.',
|
|
549
|
+
{
|
|
550
|
+
batch: z.string().optional().describe('Filter by batch/delivery name'),
|
|
551
|
+
status: z.enum(['open', 'in_progress', 'review', 'qa', 'done', 'rejected']).optional().describe('Filter by status'),
|
|
552
|
+
},
|
|
553
|
+
async (params) => {
|
|
554
|
+
const { workspaceId } = ctx();
|
|
555
|
+
if (!workspaceId) return { content: [{ type: 'text', text: 'Error: No workspace context.' }] };
|
|
556
|
+
try {
|
|
557
|
+
const orch = getOrch(workspaceId);
|
|
558
|
+
const { listRequests, getBatchStatus } = await import('./workspace/requests') as any;
|
|
559
|
+
|
|
560
|
+
const requests = listRequests(orch.projectPath, { batch: params.batch, status: params.status as any });
|
|
561
|
+
if (requests.length === 0) {
|
|
562
|
+
return { content: [{ type: 'text', text: params.batch || params.status ? 'No requests match the filter.' : 'No requests found. Use create_request to create one.' }] };
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const lines = requests.map((r: any) =>
|
|
566
|
+
`[${r.status}] ${r.id}: ${r.title} (${r.priority}) — ${r.modules?.length || 0} module(s)${r.assigned_to ? ` → ${r.assigned_to}` : ''}`
|
|
567
|
+
);
|
|
568
|
+
|
|
569
|
+
// Add batch summary if filtering by batch
|
|
570
|
+
if (params.batch) {
|
|
571
|
+
const bs = getBatchStatus(orch.projectPath, params.batch);
|
|
572
|
+
lines.push(`\nBatch "${params.batch}": ${bs.done}/${bs.total} done${bs.allDone ? ' ✓ ALL COMPLETE' : ''}`);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
576
|
+
} catch (err: any) {
|
|
577
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
);
|
|
581
|
+
|
|
582
|
+
server.tool(
|
|
583
|
+
'get_request',
|
|
584
|
+
'Get full details of a request document and its response.',
|
|
585
|
+
{
|
|
586
|
+
request_id: z.string().describe('Request ID (e.g., REQ-20260403-001)'),
|
|
587
|
+
},
|
|
588
|
+
async (params) => {
|
|
589
|
+
const { workspaceId } = ctx();
|
|
590
|
+
if (!workspaceId) return { content: [{ type: 'text', text: 'Error: No workspace context.' }] };
|
|
591
|
+
try {
|
|
592
|
+
const orch = getOrch(workspaceId);
|
|
593
|
+
const { getRequest } = await import('./workspace/requests') as any;
|
|
594
|
+
|
|
595
|
+
const result = getRequest(orch.projectPath, params.request_id);
|
|
596
|
+
if (!result) return { content: [{ type: 'text', text: `Request "${params.request_id}" not found.` }] };
|
|
597
|
+
|
|
598
|
+
const YAML = (await import('yaml')).default;
|
|
599
|
+
let text = `# Request: ${result.request.title}\n\n`;
|
|
600
|
+
text += YAML.stringify(result.request);
|
|
601
|
+
if (result.response) {
|
|
602
|
+
text += `\n---\n# Response\n\n`;
|
|
603
|
+
text += YAML.stringify(result.response);
|
|
604
|
+
} else {
|
|
605
|
+
text += `\n---\nNo response yet.`;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
return { content: [{ type: 'text', text }] };
|
|
609
|
+
} catch (err: any) {
|
|
610
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
);
|
|
614
|
+
|
|
615
|
+
// Update get_inbox to show ref field
|
|
616
|
+
// (Already handled — ref is part of payload, shown via content)
|
|
617
|
+
|
|
404
618
|
return server;
|
|
405
619
|
}
|
|
406
620
|
|