@aion0/forge 0.4.16 → 0.5.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/README.md +1 -1
- package/RELEASE_NOTES.md +170 -14
- package/app/api/agents/route.ts +17 -0
- package/app/api/delivery/[id]/route.ts +62 -0
- package/app/api/delivery/route.ts +40 -0
- package/app/api/mobile-chat/route.ts +13 -7
- package/app/api/monitor/route.ts +10 -6
- package/app/api/pipelines/[id]/route.ts +16 -3
- package/app/api/tasks/route.ts +2 -1
- package/app/api/workspace/[id]/agents/route.ts +35 -0
- package/app/api/workspace/[id]/memory/route.ts +23 -0
- package/app/api/workspace/[id]/smith/route.ts +22 -0
- package/app/api/workspace/[id]/stream/route.ts +28 -0
- package/app/api/workspace/route.ts +100 -0
- package/app/global-error.tsx +10 -4
- package/app/icon.ico +0 -0
- package/app/layout.tsx +2 -2
- package/app/login/LoginForm.tsx +96 -0
- package/app/login/page.tsx +7 -98
- package/app/page.tsx +2 -2
- package/bin/forge-server.mjs +13 -1
- package/check-forge-status.sh +9 -0
- package/components/ConversationEditor.tsx +411 -0
- package/components/ConversationGraphView.tsx +347 -0
- package/components/ConversationTerminalView.tsx +303 -0
- package/components/Dashboard.tsx +36 -39
- package/components/DashboardWrapper.tsx +9 -0
- package/components/DeliveryFlowEditor.tsx +491 -0
- package/components/DeliveryList.tsx +230 -0
- package/components/DeliveryWorkspace.tsx +589 -0
- package/components/DocTerminal.tsx +10 -2
- package/components/DocsViewer.tsx +10 -2
- package/components/HelpTerminal.tsx +11 -6
- package/components/InlinePipelineView.tsx +111 -0
- package/components/MobileView.tsx +20 -0
- package/components/MonitorPanel.tsx +9 -4
- package/components/NewTaskModal.tsx +32 -0
- package/components/PipelineEditor.tsx +49 -6
- package/components/PipelineView.tsx +482 -64
- package/components/ProjectDetail.tsx +314 -56
- package/components/ProjectManager.tsx +49 -4
- package/components/SessionView.tsx +27 -13
- package/components/SettingsModal.tsx +790 -124
- package/components/SkillsPanel.tsx +31 -8
- package/components/TaskBoard.tsx +3 -0
- package/components/WebTerminal.tsx +257 -43
- package/components/WorkspaceTree.tsx +221 -0
- package/components/WorkspaceView.tsx +2224 -0
- package/install.sh +2 -2
- package/lib/agents/claude-adapter.ts +104 -0
- package/lib/agents/generic-adapter.ts +64 -0
- package/lib/agents/index.ts +242 -0
- package/lib/agents/types.ts +70 -0
- package/lib/artifacts.ts +106 -0
- package/lib/delivery.ts +787 -0
- package/lib/forge-skills/forge-inbox.md +37 -0
- package/lib/forge-skills/forge-send.md +40 -0
- package/lib/forge-skills/forge-status.md +32 -0
- package/lib/forge-skills/forge-workspace-sync.md +37 -0
- package/lib/help-docs/00-overview.md +7 -1
- package/lib/help-docs/01-settings.md +159 -2
- package/lib/help-docs/05-pipelines.md +89 -0
- package/lib/help-docs/07-projects.md +35 -1
- package/lib/help-docs/11-workspace.md +204 -0
- package/lib/help-docs/CLAUDE.md +5 -2
- package/lib/init.ts +60 -10
- package/lib/pipeline.ts +537 -1
- package/lib/settings.ts +115 -22
- package/lib/skills.ts +249 -372
- package/lib/task-manager.ts +113 -33
- package/lib/telegram-bot.ts +33 -1
- package/lib/workspace/__tests__/state-machine.test.ts +388 -0
- package/lib/workspace/__tests__/workspace.test.ts +311 -0
- package/lib/workspace/agent-bus.ts +416 -0
- package/lib/workspace/agent-worker.ts +667 -0
- package/lib/workspace/backends/api-backend.ts +262 -0
- package/lib/workspace/backends/cli-backend.ts +479 -0
- package/lib/workspace/index.ts +82 -0
- package/lib/workspace/manager.ts +136 -0
- package/lib/workspace/orchestrator.ts +1804 -0
- package/lib/workspace/persistence.ts +310 -0
- package/lib/workspace/presets.ts +170 -0
- package/lib/workspace/skill-installer.ts +188 -0
- package/lib/workspace/smith-memory.ts +498 -0
- package/lib/workspace/types.ts +231 -0
- package/lib/workspace/watch-manager.ts +288 -0
- package/lib/workspace-standalone.ts +790 -0
- package/middleware.ts +1 -0
- package/package.json +4 -1
- package/src/config/index.ts +12 -1
- package/src/core/db/database.ts +1 -0
- package/start.sh +7 -0
package/lib/task-manager.ts
CHANGED
|
@@ -60,9 +60,11 @@ export function createTask(opts: {
|
|
|
60
60
|
conversationId?: string; // Explicit override; otherwise auto-inherits from project
|
|
61
61
|
scheduledAt?: string; // ISO timestamp — task won't run until this time
|
|
62
62
|
watchConfig?: WatchConfig;
|
|
63
|
+
agent?: string; // Agent ID (default: from settings)
|
|
63
64
|
}): Task {
|
|
64
65
|
const id = randomUUID().slice(0, 8);
|
|
65
66
|
const mode = opts.mode || 'prompt';
|
|
67
|
+
const agent = opts.agent || '';
|
|
66
68
|
|
|
67
69
|
// For prompt mode: auto-inherit conversation_id
|
|
68
70
|
// For monitor mode: conversationId is required (the session to watch)
|
|
@@ -71,12 +73,13 @@ export function createTask(opts: {
|
|
|
71
73
|
: (opts.conversationId || (mode === 'prompt' ? getProjectConversationId(opts.projectName) : null));
|
|
72
74
|
|
|
73
75
|
db().prepare(`
|
|
74
|
-
INSERT INTO tasks (id, project_name, project_path, prompt, mode, status, priority, conversation_id, log, scheduled_at, watch_config)
|
|
75
|
-
VALUES (?, ?, ?, ?, ?, 'queued', ?, ?, '[]', ?, ?)
|
|
76
|
+
INSERT INTO tasks (id, project_name, project_path, prompt, mode, status, priority, conversation_id, log, scheduled_at, watch_config, agent)
|
|
77
|
+
VALUES (?, ?, ?, ?, ?, 'queued', ?, ?, '[]', ?, ?, ?)
|
|
76
78
|
`).run(
|
|
77
79
|
id, opts.projectName, opts.projectPath, opts.prompt, mode,
|
|
78
80
|
opts.priority || 0, convId || null, opts.scheduledAt || null,
|
|
79
81
|
opts.watchConfig ? JSON.stringify(opts.watchConfig) : null,
|
|
82
|
+
agent || null,
|
|
80
83
|
);
|
|
81
84
|
|
|
82
85
|
// Kick the runner
|
|
@@ -180,12 +183,13 @@ export function retryTask(id: string): Task | null {
|
|
|
180
183
|
if (!task) return null;
|
|
181
184
|
if (task.status !== 'failed' && task.status !== 'cancelled') return null;
|
|
182
185
|
|
|
183
|
-
// Create a new task with same params
|
|
186
|
+
// Create a new task with same params (including agent)
|
|
184
187
|
return createTask({
|
|
185
188
|
projectName: task.projectName,
|
|
186
189
|
projectPath: task.projectPath,
|
|
187
190
|
prompt: task.prompt,
|
|
188
191
|
priority: task.priority,
|
|
192
|
+
agent: (task as any).agent || undefined,
|
|
189
193
|
});
|
|
190
194
|
}
|
|
191
195
|
|
|
@@ -284,38 +288,103 @@ function executeTask(task: Task): Promise<void> {
|
|
|
284
288
|
|
|
285
289
|
return new Promise((resolve, reject) => {
|
|
286
290
|
const settings = loadSettings();
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
//
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
291
|
+
const { getAgent } = require('./agents');
|
|
292
|
+
const agentId = (task as any).agent || settings.defaultAgent || 'claude';
|
|
293
|
+
const adapter = getAgent(agentId);
|
|
294
|
+
|
|
295
|
+
// Model: per-task override > agent scene model > global fallback
|
|
296
|
+
const agentCfg = settings.agents?.[agentId];
|
|
297
|
+
const isPipeline = (() => { try { const { pipelineTaskIds: p } = require('./pipeline'); return p.has(task.id); } catch { return false; } })();
|
|
298
|
+
const model = taskModelOverrides.get(task.id) || (isPipeline ? agentCfg?.models?.task : agentCfg?.models?.task) || agentCfg?.models?.task || settings.taskModel;
|
|
299
|
+
const supportsModel = adapter.config.capabilities?.supportsModel;
|
|
300
|
+
const spawnOpts = adapter.buildTaskSpawn({
|
|
301
|
+
projectPath: task.projectPath,
|
|
302
|
+
prompt: task.prompt,
|
|
303
|
+
model: supportsModel && model && model !== 'default' ? model : undefined,
|
|
304
|
+
conversationId: task.conversationId || undefined,
|
|
305
|
+
skipPermissions: true,
|
|
306
|
+
outputFormat: adapter.config.capabilities?.supportsStreamJson ? 'stream-json' : undefined,
|
|
307
|
+
});
|
|
303
308
|
|
|
304
|
-
const env = { ...process.env };
|
|
309
|
+
const env = { ...process.env, ...(spawnOpts.env || {}) };
|
|
305
310
|
delete env.CLAUDECODE;
|
|
306
311
|
|
|
307
312
|
updateTaskStatus(task.id, 'running');
|
|
308
313
|
db().prepare('UPDATE tasks SET started_at = datetime(\'now\') WHERE id = ?').run(task.id);
|
|
309
314
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
315
|
+
const agentName = adapter.config.name || agentId;
|
|
316
|
+
console.log(`[task] ${task.projectName} [${agentName}${supportsModel && model ? '/' + model : ''}]: "${task.prompt.slice(0, 60)}..."`);
|
|
317
|
+
|
|
318
|
+
// Log agent info as first entry
|
|
319
|
+
appendLog(task.id, { type: 'system', subtype: 'init', content: `Agent: ${agentName}${supportsModel && model && model !== 'default' ? ` | Model: ${model}` : ''}`, timestamp: new Date().toISOString() });
|
|
320
|
+
|
|
321
|
+
const needsTTY = adapter.config.capabilities?.requiresTTY;
|
|
322
|
+
let child: any;
|
|
323
|
+
let ptyProcess: any = null;
|
|
324
|
+
|
|
325
|
+
if (needsTTY) {
|
|
326
|
+
// Use node-pty for agents that require a terminal environment
|
|
327
|
+
const pty = require('node-pty');
|
|
328
|
+
ptyProcess = pty.spawn(spawnOpts.cmd, spawnOpts.args, {
|
|
329
|
+
name: 'xterm-256color',
|
|
330
|
+
cols: 120,
|
|
331
|
+
rows: 40,
|
|
332
|
+
cwd: task.projectPath,
|
|
333
|
+
env,
|
|
334
|
+
});
|
|
335
|
+
// Strip terminal control codes from PTY output for clean logging
|
|
336
|
+
const stripAnsi = (s: string) => s
|
|
337
|
+
.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, '') // CSI sequences
|
|
338
|
+
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '') // OSC sequences
|
|
339
|
+
.replace(/\x1b[()][0-9A-B]/g, '') // charset
|
|
340
|
+
.replace(/\x1b[=>]/g, '') // keypad
|
|
341
|
+
.replace(/\r/g, '') // carriage return
|
|
342
|
+
.replace(/\x07/g, ''); // bell
|
|
343
|
+
|
|
344
|
+
// Auto-kill PTY after idle (interactive agents don't exit on their own)
|
|
345
|
+
let ptyBytes = 0;
|
|
346
|
+
let ptyIdleTimer: any = null;
|
|
347
|
+
const PTY_IDLE_MS = 15000; // 15s idle = done
|
|
348
|
+
|
|
349
|
+
// Create a child-like interface for pty
|
|
350
|
+
let exitCb: Function | null = null;
|
|
351
|
+
|
|
352
|
+
ptyProcess.onData((data: string) => {
|
|
353
|
+
const clean = stripAnsi(data);
|
|
354
|
+
ptyBytes += clean.length;
|
|
355
|
+
if (dataCallback) dataCallback(Buffer.from(clean));
|
|
356
|
+
// Reset idle timer
|
|
357
|
+
if (ptyIdleTimer) clearTimeout(ptyIdleTimer);
|
|
358
|
+
if (ptyBytes > 500) {
|
|
359
|
+
ptyIdleTimer = setTimeout(() => {
|
|
360
|
+
console.log(`[task] PTY idle timeout — killing process (${ptyBytes} bytes received)`);
|
|
361
|
+
try { ptyProcess.kill(); } catch {}
|
|
362
|
+
}, PTY_IDLE_MS);
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
ptyProcess.onExit(({ exitCode }: any) => {
|
|
367
|
+
if (ptyIdleTimer) clearTimeout(ptyIdleTimer);
|
|
368
|
+
if (exitCb) exitCb(exitCode, null);
|
|
369
|
+
});
|
|
313
370
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
371
|
+
let dataCallback: Function | null = null;
|
|
372
|
+
child = {
|
|
373
|
+
stdout: { on: (evt: string, cb: Function) => { if (evt === 'data') dataCallback = cb; } },
|
|
374
|
+
stderr: { on: (_evt: string, _cb: Function) => {} },
|
|
375
|
+
on: (evt: string, cb: Function) => { if (evt === 'exit') exitCb = cb; if (evt === 'error') {} },
|
|
376
|
+
kill: (sig: string) => { if (ptyIdleTimer) clearTimeout(ptyIdleTimer); try { ptyProcess.kill(sig); } catch {} },
|
|
377
|
+
stdin: null,
|
|
378
|
+
pid: ptyProcess.pid,
|
|
379
|
+
};
|
|
380
|
+
} else {
|
|
381
|
+
child = spawn(spawnOpts.cmd, spawnOpts.args, {
|
|
382
|
+
cwd: task.projectPath,
|
|
383
|
+
env,
|
|
384
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
385
|
+
});
|
|
386
|
+
child.stdin?.end();
|
|
387
|
+
}
|
|
319
388
|
|
|
320
389
|
let buffer = '';
|
|
321
390
|
let resultText = '';
|
|
@@ -325,7 +394,7 @@ function executeTask(task: Task): Promise<void> {
|
|
|
325
394
|
let totalInputTokens = 0;
|
|
326
395
|
let totalOutputTokens = 0;
|
|
327
396
|
|
|
328
|
-
child.on('error', (err) => {
|
|
397
|
+
child.on('error', (err: any) => {
|
|
329
398
|
console.error(`[task-runner] Spawn error:`, err.message);
|
|
330
399
|
updateTaskStatus(task.id, 'failed', err.message);
|
|
331
400
|
reject(err);
|
|
@@ -346,10 +415,14 @@ function executeTask(task: Task): Promise<void> {
|
|
|
346
415
|
|
|
347
416
|
for (const line of lines) {
|
|
348
417
|
if (!line.trim()) continue;
|
|
418
|
+
let jsonParsed = false;
|
|
349
419
|
try {
|
|
350
420
|
const parsed = JSON.parse(line);
|
|
421
|
+
jsonParsed = true;
|
|
351
422
|
const entries = parseStreamJson(parsed);
|
|
352
423
|
for (const entry of entries) {
|
|
424
|
+
// Skip Claude's Model init line for non-claude agents (we already logged our own)
|
|
425
|
+
if (entry.subtype === 'init' && agentId !== 'claude' && entry.content?.startsWith('Model:')) continue;
|
|
353
426
|
appendLog(task.id, entry);
|
|
354
427
|
}
|
|
355
428
|
|
|
@@ -368,7 +441,13 @@ function executeTask(task: Task): Promise<void> {
|
|
|
368
441
|
if (parsed.total_input_tokens) totalInputTokens = parsed.total_input_tokens;
|
|
369
442
|
if (parsed.total_output_tokens) totalOutputTokens = parsed.total_output_tokens;
|
|
370
443
|
}
|
|
371
|
-
} catch {
|
|
444
|
+
} catch {
|
|
445
|
+
// Non-JSON output (generic agents) — log as raw text
|
|
446
|
+
if (!jsonParsed) {
|
|
447
|
+
resultText += (resultText ? '\n' : '') + line;
|
|
448
|
+
appendLog(task.id, { type: 'system', subtype: 'text', content: line, timestamp: new Date().toISOString() });
|
|
449
|
+
}
|
|
450
|
+
}
|
|
372
451
|
}
|
|
373
452
|
});
|
|
374
453
|
|
|
@@ -380,7 +459,7 @@ function executeTask(task: Task): Promise<void> {
|
|
|
380
459
|
}
|
|
381
460
|
});
|
|
382
461
|
|
|
383
|
-
child.on('exit', (code, signal) => {
|
|
462
|
+
child.on('exit', (code: any, signal: any) => {
|
|
384
463
|
// Process exit handled below
|
|
385
464
|
// Process remaining buffer
|
|
386
465
|
if (buffer.trim()) {
|
|
@@ -453,7 +532,7 @@ function executeTask(task: Task): Promise<void> {
|
|
|
453
532
|
}
|
|
454
533
|
});
|
|
455
534
|
|
|
456
|
-
child.on('error', (err) => {
|
|
535
|
+
child.on('error', (err: any) => {
|
|
457
536
|
updateTaskStatus(task.id, 'failed', err.message);
|
|
458
537
|
reject(err);
|
|
459
538
|
});
|
|
@@ -637,7 +716,8 @@ function rowToTask(row: any): Task {
|
|
|
637
716
|
startedAt: toIsoUTC(row.started_at) ?? undefined,
|
|
638
717
|
completedAt: toIsoUTC(row.completed_at) ?? undefined,
|
|
639
718
|
scheduledAt: toIsoUTC(row.scheduled_at) ?? undefined,
|
|
640
|
-
|
|
719
|
+
agent: row.agent || undefined,
|
|
720
|
+
} as Task;
|
|
641
721
|
}
|
|
642
722
|
|
|
643
723
|
// ─── Monitor task execution ──────────────────────────────────
|
package/lib/telegram-bot.ts
CHANGED
|
@@ -223,6 +223,9 @@ async function handleMessage(msg: any) {
|
|
|
223
223
|
case '/p':
|
|
224
224
|
await sendProjectList(chatId);
|
|
225
225
|
break;
|
|
226
|
+
case '/agents':
|
|
227
|
+
await sendAgentList(chatId);
|
|
228
|
+
break;
|
|
226
229
|
case '/watch':
|
|
227
230
|
case '/w':
|
|
228
231
|
if (args.length > 0) {
|
|
@@ -309,10 +312,30 @@ async function sendHelp(chatId: number) {
|
|
|
309
312
|
`🌐 /tunnel — status\n` +
|
|
310
313
|
`/tunnel_start / /tunnel_stop\n` +
|
|
311
314
|
`/tunnel_code <admin_pw> — get session code\n\n` +
|
|
315
|
+
`🤖 /agents — list available agents\n` +
|
|
316
|
+
`Use @agent in /task to select (e.g. /task app @codex: review)\n\n` +
|
|
312
317
|
`Reply number to select`
|
|
313
318
|
);
|
|
314
319
|
}
|
|
315
320
|
|
|
321
|
+
async function sendAgentList(chatId: number) {
|
|
322
|
+
try {
|
|
323
|
+
const { listAgents, getDefaultAgentId } = require('./agents');
|
|
324
|
+
const agents = listAgents();
|
|
325
|
+
const defaultId = getDefaultAgentId();
|
|
326
|
+
if (agents.length === 0) {
|
|
327
|
+
await send(chatId, 'No agents detected.');
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const lines = agents.map((a: any) =>
|
|
331
|
+
`${a.id === defaultId ? '⭐' : ' '} ${a.name} (${a.id})${a.detected === false ? ' ⚠️ not installed' : ''}`
|
|
332
|
+
);
|
|
333
|
+
await send(chatId, `🤖 Agents:\n\n${lines.join('\n')}\n\nUse @agent in /task command`);
|
|
334
|
+
} catch {
|
|
335
|
+
await send(chatId, 'Failed to list agents.');
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
316
339
|
async function sendNumberedTaskList(chatId: number, statusFilter?: string) {
|
|
317
340
|
// Get running/queued first, then recent done/failed
|
|
318
341
|
const allTasks = listTasks(statusFilter as any || undefined);
|
|
@@ -671,10 +694,11 @@ async function handleNewTask(chatId: number, input: string) {
|
|
|
671
694
|
await send(chatId,
|
|
672
695
|
'Usage:\nproject: instructions\n\n' +
|
|
673
696
|
'Options:\n' +
|
|
697
|
+
' @agent — use specific agent (e.g. @codex @aider)\n' +
|
|
674
698
|
' -s <sessionId> — resume specific session\n' +
|
|
675
699
|
' -in 30m — delay (e.g. 10m, 2h, 1d)\n' +
|
|
676
700
|
' -at 18:00 — schedule at time\n\n' +
|
|
677
|
-
'Example:\nmy-app: Fix the login bug\nmy-app -s abc123 -in 1h: continue work'
|
|
701
|
+
'Example:\nmy-app: Fix the login bug\nmy-app @codex: review code\nmy-app -s abc123 -in 1h: continue work'
|
|
678
702
|
);
|
|
679
703
|
return;
|
|
680
704
|
}
|
|
@@ -709,6 +733,7 @@ async function handleNewTask(chatId: number, input: string) {
|
|
|
709
733
|
// Parse flags
|
|
710
734
|
let sessionId: string | undefined;
|
|
711
735
|
let scheduledAt: string | undefined;
|
|
736
|
+
let agentId: string | undefined;
|
|
712
737
|
let tokens = restPart.split(/\s+/);
|
|
713
738
|
const promptTokens: string[] = [];
|
|
714
739
|
|
|
@@ -719,6 +744,8 @@ async function handleNewTask(chatId: number, input: string) {
|
|
|
719
744
|
scheduledAt = parseDelay(tokens[++i]);
|
|
720
745
|
} else if (tokens[i] === '-at' && i + 1 < tokens.length) {
|
|
721
746
|
scheduledAt = parseTimeAt(tokens[++i]);
|
|
747
|
+
} else if (tokens[i].startsWith('@')) {
|
|
748
|
+
agentId = tokens[i].slice(1); // @codex → codex
|
|
722
749
|
} else {
|
|
723
750
|
promptTokens.push(tokens[i]);
|
|
724
751
|
}
|
|
@@ -730,12 +757,17 @@ async function handleNewTask(chatId: number, input: string) {
|
|
|
730
757
|
return;
|
|
731
758
|
}
|
|
732
759
|
|
|
760
|
+
// Use @agent if specified, else telegram default, else global default
|
|
761
|
+
const settings = loadSettings();
|
|
762
|
+
const resolvedAgent = agentId || settings.telegramAgent || undefined;
|
|
763
|
+
|
|
733
764
|
const task = createTask({
|
|
734
765
|
projectName: project.name,
|
|
735
766
|
projectPath: project.path,
|
|
736
767
|
prompt,
|
|
737
768
|
conversationId: sessionId,
|
|
738
769
|
scheduledAt,
|
|
770
|
+
agent: resolvedAgent,
|
|
739
771
|
});
|
|
740
772
|
|
|
741
773
|
let statusLine = 'Status: queued';
|