@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.
Files changed (92) hide show
  1. package/README.md +1 -1
  2. package/RELEASE_NOTES.md +170 -14
  3. package/app/api/agents/route.ts +17 -0
  4. package/app/api/delivery/[id]/route.ts +62 -0
  5. package/app/api/delivery/route.ts +40 -0
  6. package/app/api/mobile-chat/route.ts +13 -7
  7. package/app/api/monitor/route.ts +10 -6
  8. package/app/api/pipelines/[id]/route.ts +16 -3
  9. package/app/api/tasks/route.ts +2 -1
  10. package/app/api/workspace/[id]/agents/route.ts +35 -0
  11. package/app/api/workspace/[id]/memory/route.ts +23 -0
  12. package/app/api/workspace/[id]/smith/route.ts +22 -0
  13. package/app/api/workspace/[id]/stream/route.ts +28 -0
  14. package/app/api/workspace/route.ts +100 -0
  15. package/app/global-error.tsx +10 -4
  16. package/app/icon.ico +0 -0
  17. package/app/layout.tsx +2 -2
  18. package/app/login/LoginForm.tsx +96 -0
  19. package/app/login/page.tsx +7 -98
  20. package/app/page.tsx +2 -2
  21. package/bin/forge-server.mjs +13 -1
  22. package/check-forge-status.sh +9 -0
  23. package/components/ConversationEditor.tsx +411 -0
  24. package/components/ConversationGraphView.tsx +347 -0
  25. package/components/ConversationTerminalView.tsx +303 -0
  26. package/components/Dashboard.tsx +36 -39
  27. package/components/DashboardWrapper.tsx +9 -0
  28. package/components/DeliveryFlowEditor.tsx +491 -0
  29. package/components/DeliveryList.tsx +230 -0
  30. package/components/DeliveryWorkspace.tsx +589 -0
  31. package/components/DocTerminal.tsx +10 -2
  32. package/components/DocsViewer.tsx +10 -2
  33. package/components/HelpTerminal.tsx +11 -6
  34. package/components/InlinePipelineView.tsx +111 -0
  35. package/components/MobileView.tsx +20 -0
  36. package/components/MonitorPanel.tsx +9 -4
  37. package/components/NewTaskModal.tsx +32 -0
  38. package/components/PipelineEditor.tsx +49 -6
  39. package/components/PipelineView.tsx +482 -64
  40. package/components/ProjectDetail.tsx +314 -56
  41. package/components/ProjectManager.tsx +49 -4
  42. package/components/SessionView.tsx +27 -13
  43. package/components/SettingsModal.tsx +790 -124
  44. package/components/SkillsPanel.tsx +31 -8
  45. package/components/TaskBoard.tsx +3 -0
  46. package/components/WebTerminal.tsx +257 -43
  47. package/components/WorkspaceTree.tsx +221 -0
  48. package/components/WorkspaceView.tsx +2224 -0
  49. package/install.sh +2 -2
  50. package/lib/agents/claude-adapter.ts +104 -0
  51. package/lib/agents/generic-adapter.ts +64 -0
  52. package/lib/agents/index.ts +242 -0
  53. package/lib/agents/types.ts +70 -0
  54. package/lib/artifacts.ts +106 -0
  55. package/lib/delivery.ts +787 -0
  56. package/lib/forge-skills/forge-inbox.md +37 -0
  57. package/lib/forge-skills/forge-send.md +40 -0
  58. package/lib/forge-skills/forge-status.md +32 -0
  59. package/lib/forge-skills/forge-workspace-sync.md +37 -0
  60. package/lib/help-docs/00-overview.md +7 -1
  61. package/lib/help-docs/01-settings.md +159 -2
  62. package/lib/help-docs/05-pipelines.md +89 -0
  63. package/lib/help-docs/07-projects.md +35 -1
  64. package/lib/help-docs/11-workspace.md +204 -0
  65. package/lib/help-docs/CLAUDE.md +5 -2
  66. package/lib/init.ts +60 -10
  67. package/lib/pipeline.ts +537 -1
  68. package/lib/settings.ts +115 -22
  69. package/lib/skills.ts +249 -372
  70. package/lib/task-manager.ts +113 -33
  71. package/lib/telegram-bot.ts +33 -1
  72. package/lib/workspace/__tests__/state-machine.test.ts +388 -0
  73. package/lib/workspace/__tests__/workspace.test.ts +311 -0
  74. package/lib/workspace/agent-bus.ts +416 -0
  75. package/lib/workspace/agent-worker.ts +667 -0
  76. package/lib/workspace/backends/api-backend.ts +262 -0
  77. package/lib/workspace/backends/cli-backend.ts +479 -0
  78. package/lib/workspace/index.ts +82 -0
  79. package/lib/workspace/manager.ts +136 -0
  80. package/lib/workspace/orchestrator.ts +1804 -0
  81. package/lib/workspace/persistence.ts +310 -0
  82. package/lib/workspace/presets.ts +170 -0
  83. package/lib/workspace/skill-installer.ts +188 -0
  84. package/lib/workspace/smith-memory.ts +498 -0
  85. package/lib/workspace/types.ts +231 -0
  86. package/lib/workspace/watch-manager.ts +288 -0
  87. package/lib/workspace-standalone.ts +790 -0
  88. package/middleware.ts +1 -0
  89. package/package.json +4 -1
  90. package/src/config/index.ts +12 -1
  91. package/src/core/db/database.ts +1 -0
  92. package/start.sh +7 -0
@@ -0,0 +1,388 @@
1
+ /**
2
+ * State Machine Tests — verify smith/task status transitions and message flow.
3
+ *
4
+ * Usage: npx tsx lib/workspace/__tests__/state-machine.test.ts
5
+ */
6
+
7
+ import assert from 'node:assert';
8
+ import { WorkspaceOrchestrator } from '../orchestrator';
9
+ import { AgentBus } from '../agent-bus';
10
+ import type { WorkspaceAgentConfig, AgentState, BusMessage, WorkerEvent } from '../types';
11
+
12
+ const TEST_WS = 'test-sm-' + Date.now();
13
+ const TEST_PATH = '/tmp/test-sm';
14
+
15
+ // ─── Helpers ─────────────────────────────────────────────
16
+
17
+ function createOrch(): WorkspaceOrchestrator {
18
+ return new WorkspaceOrchestrator(TEST_WS, TEST_PATH, 'test');
19
+ }
20
+
21
+ function addInput(orch: WorkspaceOrchestrator, id = 'input-1'): void {
22
+ orch.addAgent({
23
+ id, label: 'Requirements', icon: '📝', type: 'input',
24
+ content: '', entries: [], role: '', backend: 'cli',
25
+ dependsOn: [], outputs: [], steps: [],
26
+ });
27
+ }
28
+
29
+ function addAgent(orch: WorkspaceOrchestrator, id: string, label: string, dependsOn: string[]): void {
30
+ orch.addAgent({
31
+ id, label, icon: '🤖', role: 'test', backend: 'cli',
32
+ dependsOn, outputs: [], workDir: id, // unique workDir to avoid conflicts
33
+ steps: [{ id: 's1', label: 'Step 1', prompt: 'do something' }],
34
+ });
35
+ }
36
+
37
+ function getState(orch: WorkspaceOrchestrator, id: string): AgentState {
38
+ return orch.getAllAgentStates()[id];
39
+ }
40
+
41
+ function collectEvents(orch: WorkspaceOrchestrator): any[] {
42
+ const events: any[] = [];
43
+ orch.on('event', (e: any) => events.push(e));
44
+ return events;
45
+ }
46
+
47
+ let passed = 0;
48
+ let failed = 0;
49
+ let testNum = 0;
50
+
51
+ function ok(condition: boolean, msg: string) {
52
+ if (condition) {
53
+ console.log(` ✅ ${msg}`);
54
+ passed++;
55
+ } else {
56
+ console.log(` ❌ ${msg}`);
57
+ failed++;
58
+ }
59
+ }
60
+
61
+ function test(name: string, fn: () => void | Promise<void>) {
62
+ testNum++;
63
+ console.log(`\n📋 Test ${testNum}: ${name}`);
64
+ try {
65
+ const result = fn();
66
+ if (result instanceof Promise) {
67
+ return result.catch(e => {
68
+ console.log(` 💥 Crashed: ${e.message}`);
69
+ failed++;
70
+ });
71
+ }
72
+ } catch (e: any) {
73
+ console.log(` 💥 Crashed: ${e.message}`);
74
+ failed++;
75
+ }
76
+ }
77
+
78
+ // ─── Tests ───────────────────────────────────────────────
79
+
80
+ async function runAll() {
81
+ console.log('🧪 State Machine Tests\n');
82
+
83
+ // Test 1: Initial state
84
+ await test('Initial agent state', () => {
85
+ const orch = createOrch();
86
+ addAgent(orch, 'a1', 'Agent1', []);
87
+ const s = getState(orch, 'a1');
88
+ ok(s.smithStatus === 'down', `smithStatus = down (got ${s.smithStatus})`);
89
+ ok(s.taskStatus === 'idle', `taskStatus = idle (got ${s.taskStatus})`);
90
+ ok(s.mode === 'auto', `mode = auto (got ${s.mode})`);
91
+ });
92
+
93
+ // Test 2: startDaemon sets all smiths to active
94
+ await test('startDaemon sets smithStatus=active', async () => {
95
+ const orch = createOrch();
96
+ addInput(orch);
97
+ addAgent(orch, 'pm', 'PM', ['input-1']);
98
+ addAgent(orch, 'eng', 'Engineer', ['pm']);
99
+
100
+ ok(getState(orch, 'pm').smithStatus === 'down', 'PM starts down');
101
+ ok(getState(orch, 'eng').smithStatus === 'down', 'Engineer starts down');
102
+
103
+ await orch.startDaemon();
104
+
105
+ ok(getState(orch, 'pm').smithStatus === 'active', 'PM active after startDaemon');
106
+ ok(getState(orch, 'eng').smithStatus === 'active', 'Engineer active after startDaemon');
107
+ ok(orch.isDaemonActive(), 'daemonActive = true');
108
+
109
+ orch.stopDaemon();
110
+ ok(getState(orch, 'pm').smithStatus === 'down', 'PM down after stopDaemon');
111
+ ok(getState(orch, 'eng').smithStatus === 'down', 'Engineer down after stopDaemon');
112
+ ok(!orch.isDaemonActive(), 'daemonActive = false');
113
+ });
114
+
115
+ // Test 3: stopDaemon preserves taskStatus
116
+ await test('stopDaemon preserves taskStatus', async () => {
117
+ const orch = createOrch();
118
+ addAgent(orch, 'a1', 'Agent1', []);
119
+
120
+ // Manually set to done to simulate completed agent
121
+ const states = orch.getAllAgentStates();
122
+ (states['a1'] as any).taskStatus = 'done';
123
+ // Hack: directly modify internal state for testing
124
+ (orch as any).agents.get('a1').state.taskStatus = 'done';
125
+
126
+ await orch.startDaemon();
127
+ ok(getState(orch, 'a1').taskStatus === 'done', 'taskStatus stays done after startDaemon');
128
+
129
+ orch.stopDaemon();
130
+ ok(getState(orch, 'a1').taskStatus === 'done', 'taskStatus stays done after stopDaemon');
131
+ });
132
+
133
+ // Test 4: loadSnapshot resets smith to down and pending messages to failed
134
+ await test('loadSnapshot resets state correctly', () => {
135
+ const orch = createOrch();
136
+
137
+ const busLog: BusMessage[] = [
138
+ { id: 'm1', from: 'a', to: 'b', type: 'notify', payload: { action: 'test' }, timestamp: Date.now(), status: 'pending' },
139
+ { id: 'm2', from: 'a', to: 'b', type: 'notify', payload: { action: 'test2' }, timestamp: Date.now(), status: 'done' },
140
+ { id: 'm3', from: 'a', to: 'b', type: 'notify', payload: { action: 'test3' }, timestamp: Date.now(), status: 'pending' },
141
+ ];
142
+
143
+ orch.loadSnapshot({
144
+ agents: [{
145
+ id: 'a1', label: 'Agent1', icon: '🤖', role: '', backend: 'cli',
146
+ dependsOn: [], outputs: [], workDir: 'a1',
147
+ steps: [{ id: 's1', label: 'Step1', prompt: 'test' }],
148
+ }],
149
+ agentStates: {
150
+ 'a1': {
151
+ smithStatus: 'active', mode: 'auto', taskStatus: 'running',
152
+ history: [], artifacts: [],
153
+ } as AgentState,
154
+ },
155
+ busLog,
156
+ });
157
+
158
+ const s = getState(orch, 'a1');
159
+ ok(s.smithStatus === 'down', 'smithStatus reset to down after load');
160
+ ok(s.taskStatus === 'failed', 'running taskStatus becomes failed after load');
161
+
162
+ // Check bus messages
163
+ const log = orch.getBusLog();
164
+ const m1 = log.find(m => m.id === 'm1');
165
+ const m2 = log.find(m => m.id === 'm2');
166
+ const m3 = log.find(m => m.id === 'm3');
167
+ ok(m1?.status === 'failed', 'pending message m1 marked failed');
168
+ ok(m2?.status === 'done', 'acked message m2 unchanged');
169
+ ok(m3?.status === 'failed', 'pending message m3 marked failed');
170
+ });
171
+
172
+ // Test 5: completeInput sends input_updated messages (no daemon to avoid CLI spawn)
173
+ await test('completeInput sends bus messages to downstream', () => {
174
+ const orch = createOrch();
175
+ addInput(orch);
176
+ addAgent(orch, 'pm', 'PM', ['input-1']);
177
+
178
+ // Don't start daemon — just test that completeInput sends bus messages
179
+ const busLog = orch.getBusLog();
180
+ const before = busLog.length;
181
+
182
+ orch.completeInput('input-1', 'Build a todo app');
183
+
184
+ const after = busLog.length;
185
+ ok(after > before, `New bus messages sent (${before} → ${after})`);
186
+
187
+ const inputMsgs = busLog.filter(m => m.payload.action === 'input_updated');
188
+ ok(inputMsgs.length > 0, 'input_updated message found');
189
+ ok(inputMsgs[0].from === 'input-1', 'from = input-1');
190
+ ok(inputMsgs[0].to === 'pm', 'to = pm');
191
+ // Smith is down so message stays pending (not acked)
192
+ ok(inputMsgs[0].status === 'pending', `message pending (smith down) — got ${inputMsgs[0].status}`);
193
+ });
194
+
195
+ // Test 6: broadcastCompletion sends upstream_complete (no daemon)
196
+ await test('broadcastCompletion sends upstream_complete', () => {
197
+ const orch = createOrch();
198
+ addInput(orch);
199
+ addAgent(orch, 'pm', 'PM', ['input-1']);
200
+ addAgent(orch, 'eng', 'Engineer', ['pm']);
201
+
202
+ // Set PM to done with artifacts
203
+ (orch as any).agents.get('pm').state.taskStatus = 'done';
204
+ (orch as any).agents.get('pm').state.artifacts = [{ type: 'file', path: 'docs/prd.md' }];
205
+ (orch as any).agents.get('pm').state.history = [
206
+ { type: 'result', subtype: 'final_summary', content: 'PRD completed', timestamp: new Date().toISOString() }
207
+ ];
208
+
209
+ (orch as any).broadcastCompletion('pm');
210
+
211
+ const busLog = orch.getBusLog();
212
+ const upstreamMsgs = busLog.filter(m => m.payload.action === 'upstream_complete' && m.from === 'pm');
213
+ ok(upstreamMsgs.length > 0, 'upstream_complete message sent');
214
+ ok(upstreamMsgs[0].to === 'eng', 'sent to Engineer');
215
+ ok(upstreamMsgs[0].payload.files?.includes('docs/prd.md') === true, 'includes file path');
216
+ });
217
+
218
+ // Test 7: Bus message ACK flow
219
+ await test('Bus message ACK: pending → acked on success, pending → failed on error', () => {
220
+ const bus = new AgentBus();
221
+ bus.setAgentStatus('a', 'alive');
222
+ bus.setAgentStatus('b', 'alive');
223
+
224
+ const msg = bus.send('a', 'b', 'notify', { action: 'test', content: 'hello' });
225
+ ok(msg.status === 'pending', 'starts as pending');
226
+
227
+ // Simulate ack
228
+ msg.status = 'done';
229
+ ok(msg.status === 'done', 'acked after processing');
230
+
231
+ // Test failed path
232
+ const msg2 = bus.send('a', 'b', 'notify', { action: 'test2', content: 'world' });
233
+ msg2.status = 'failed';
234
+ ok(msg2.status === 'failed', 'failed on error');
235
+
236
+ // Test retry
237
+ const retried = bus.retryMessage(msg2.id);
238
+ ok(retried !== null, 'retryMessage returns message');
239
+ ok(retried!.status === 'pending', 'retried message back to pending');
240
+ });
241
+
242
+ // Test 8: markAllPendingAsFailed
243
+ await test('markAllPendingAsFailed on restart', () => {
244
+ const bus = new AgentBus();
245
+ bus.setAgentStatus('a', 'alive');
246
+ bus.setAgentStatus('b', 'alive');
247
+
248
+ const m1 = bus.send('a', 'b', 'notify', { action: 'test1' });
249
+ const m2 = bus.send('a', 'b', 'notify', { action: 'test2' });
250
+ m1.status = 'done'; // already processed
251
+ // m2 stays pending
252
+
253
+ bus.markAllPendingAsFailed();
254
+
255
+ ok(m1.status === 'done', 'acked message unchanged');
256
+ ok(m2.status === 'failed', 'pending message marked failed');
257
+ });
258
+
259
+ // Test 9: Manual run (force) dep check logic — test validateCanRun vs force dep check
260
+ await test('Force run checks smith active, normal run checks taskStatus done', () => {
261
+ const orch = createOrch();
262
+ addInput(orch);
263
+ addAgent(orch, 'pm', 'PM', ['input-1']);
264
+ addAgent(orch, 'eng', 'Engineer', ['pm']);
265
+
266
+ // Normal validateCanRun: PM idle → should fail
267
+ let error1 = '';
268
+ try { orch.validateCanRun('eng'); } catch (e: any) { error1 = e.message; }
269
+ ok(error1.includes('not completed'), `Normal: rejects when PM idle`);
270
+
271
+ // PM done → should pass
272
+ (orch as any).agents.get('pm').state.taskStatus = 'done';
273
+ let error2 = '';
274
+ try { orch.validateCanRun('eng'); } catch (e: any) { error2 = e.message; }
275
+ ok(!error2, `Normal: passes when PM done`);
276
+
277
+ // Force dep check: PM smith down → should fail
278
+ (orch as any).agents.get('pm').state.taskStatus = 'idle';
279
+ (orch as any).agents.get('pm').state.smithStatus = 'down';
280
+ // Simulate the force dep check from runAgentDaemon
281
+ const config = (orch as any).agents.get('eng').config;
282
+ let forceError = '';
283
+ for (const depId of config.dependsOn) {
284
+ const dep = (orch as any).agents.get(depId);
285
+ if (dep && dep.config.type !== 'input' && dep.state.smithStatus !== 'active') {
286
+ forceError = `${dep.config.label} smith not active`;
287
+ }
288
+ }
289
+ ok(forceError.includes('not active'), `Force: rejects when PM smith down`);
290
+
291
+ // PM smith active → should pass
292
+ (orch as any).agents.get('pm').state.smithStatus = 'active';
293
+ forceError = '';
294
+ for (const depId of config.dependsOn) {
295
+ const dep = (orch as any).agents.get(depId);
296
+ if (dep && dep.config.type !== 'input' && dep.state.smithStatus !== 'active') {
297
+ forceError = `${dep.config.label} smith not active`;
298
+ }
299
+ }
300
+ ok(!forceError, `Force: passes when PM smith active`);
301
+ });
302
+
303
+ // Test 10: Auto trigger requires taskStatus=done (check via validateCanRun)
304
+ await test('Auto trigger requires dep taskStatus=done', () => {
305
+ const orch = createOrch();
306
+ addInput(orch);
307
+ addAgent(orch, 'pm', 'PM', ['input-1']);
308
+ addAgent(orch, 'eng', 'Engineer', ['pm']);
309
+
310
+ // PM is idle — validateCanRun for Engineer should fail
311
+ let error = '';
312
+ try {
313
+ orch.validateCanRun('eng');
314
+ } catch (e: any) {
315
+ error = e.message;
316
+ }
317
+ ok(error.includes('not completed'), `validateCanRun rejects when PM idle: ${error}`);
318
+
319
+ // Set PM to done — should pass
320
+ (orch as any).agents.get('pm').state.taskStatus = 'done';
321
+ let error2 = '';
322
+ try {
323
+ orch.validateCanRun('eng');
324
+ } catch (e: any) {
325
+ error2 = e.message;
326
+ }
327
+ ok(!error2, `validateCanRun passes when PM done (got: ${error2 || 'no error'})`);
328
+ });
329
+
330
+ // Test 11: taskStatus transitions - never goes from done to idle
331
+ await test('taskStatus never goes from done→idle', async () => {
332
+ const orch = createOrch();
333
+ addAgent(orch, 'a1', 'Agent1', []);
334
+
335
+ // Set to done
336
+ (orch as any).agents.get('a1').state.taskStatus = 'done';
337
+ (orch as any).agents.get('a1').state.smithStatus = 'active';
338
+
339
+ // Stop daemon should NOT change taskStatus
340
+ await orch.startDaemon();
341
+ orch.stopDaemon();
342
+
343
+ ok(getState(orch, 'a1').taskStatus === 'done',
344
+ `taskStatus stays done after stop (got ${getState(orch, 'a1').taskStatus})`);
345
+ });
346
+
347
+ // Test 12: startDaemon doesn't re-send input messages
348
+ await test('startDaemon does NOT broadcast existing input', async () => {
349
+ const orch = createOrch();
350
+ addInput(orch);
351
+ addAgent(orch, 'pm', 'PM', ['input-1']);
352
+
353
+ // Complete input first
354
+ orch.completeInput('input-1', 'test content');
355
+
356
+ const busLogBefore = orch.getBusLog().length;
357
+
358
+ await orch.startDaemon();
359
+
360
+ const busLogAfter = orch.getBusLog().length;
361
+ const newMsgs = orch.getBusLog().slice(busLogBefore);
362
+ const inputMsgs = newMsgs.filter(m => m.payload.action === 'input_updated');
363
+
364
+ ok(inputMsgs.length === 0, `No input_updated sent by startDaemon (got ${inputMsgs.length})`);
365
+
366
+ orch.stopDaemon();
367
+ });
368
+
369
+ // ─── Summary ──────────────────────────────────────────
370
+
371
+ console.log('\n' + '═'.repeat(40));
372
+ console.log(`Results: ${passed} passed, ${failed} failed`);
373
+ console.log('═'.repeat(40));
374
+
375
+ console.log('');
376
+ if (failed > 0) {
377
+ process.exit(1);
378
+ } else {
379
+ process.exit(0);
380
+ }
381
+ }
382
+
383
+ // Force exit after 8s in case of dangling timers/workers
384
+ setTimeout(() => {
385
+ console.log('\n(Force exit due to dangling timers)');
386
+ process.exit(passed > 0 && failed === 0 ? 0 : 1);
387
+ }, 8000);
388
+ runAll();