@aion0/forge 0.5.0 โ†’ 0.5.1

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 CHANGED
@@ -49,11 +49,36 @@ Forge turns Claude Code into a remote-accessible coding platform. Run it on your
49
49
 
50
50
  No API keys required. Uses your existing Claude Code subscription. Code never leaves your machine.
51
51
 
52
+ ## Multi-Agent Workspace <sup>v0.5.0</sup>
53
+
54
+ Define agent teams with roles, dependencies, and steps. Agents run as daemons and communicate via a structured message bus.
55
+
56
+ ```mermaid
57
+ graph LR
58
+ Input["๐Ÿ“‹ Requirements"] --> PM["๐ŸŽฏ PM"]
59
+ PM --> Eng["๐Ÿ”จ Engineer"]
60
+ Eng --> QA["๐Ÿงช QA"]
61
+ Eng --> Rev["๐Ÿ‘ Reviewer"]
62
+ QA --> Rev
63
+
64
+ style Input fill:#f0883e,stroke:#f0883e,color:#fff
65
+ style PM fill:#a371f7,stroke:#a371f7,color:#fff
66
+ style Eng fill:#58a6ff,stroke:#58a6ff,color:#fff
67
+ style QA fill:#3fb950,stroke:#3fb950,color:#fff
68
+ style Rev fill:#f778ba,stroke:#f778ba,color:#fff
69
+ ```
70
+
71
+ - **Agent profiles** โ€” Reusable configs with env vars, model overrides, custom API endpoints (Claude, Codex, Aider)
72
+ - **Message system** โ€” Notifications (follow DAG) + Tickets (any direction, retry limits) with causedBy tracing
73
+ - **Manual mode** โ€” Open a terminal on any agent, work interactively, return to auto
74
+ - **Watch manager** โ€” Monitor files, git, or commands. Auto-analyze or require approval on changes
75
+
52
76
  ## Features
53
77
 
54
78
  | | |
55
79
  |---|---|
56
- | **Vibe Coding** | Browser tmux terminal, multi-tab, split panes, persistent sessions |
80
+ | **Multi-Agent Workspace** | Agent teams with DAG dependencies, message bus, watch monitoring, manual/auto mode |
81
+ | **Vibe Coding** | Browser tmux terminal, multi-tab, split panes, WebGL, Ctrl+F search |
57
82
  | **AI Tasks** | Background Claude Code execution with live streaming output |
58
83
  | **Pipelines** | YAML DAG workflows with parallel execution & visual editor |
59
84
  | **Remote Access** | Cloudflare Tunnel with 2FA (password + session code) |
package/RELEASE_NOTES.md CHANGED
@@ -1,182 +1,33 @@
1
- # Forge v0.5.0
1
+ # Forge v0.5.1
2
2
 
3
3
  Released: 2026-03-28
4
4
 
5
- ## Changes since v0.4.16
5
+ ## Changes since v0.5.0
6
6
 
7
7
  ### Features
8
- - feat: watch actions (log/analyze/approve) + config UI
9
- - feat: agent watch โ€” autonomous periodic monitoring
10
- - feat: Workspace โ€” multi-agent terminal view per project
11
- - feat: requires-driven scheduling for delivery engine
12
- - feat: standardized envelope format + request/response audit trail
13
- - feat: visible data contracts on flow editor โ€” artifact names on edges and nodes
14
- - feat: ReactFlow-based delivery role editor โ€” drag & connect agent topology
15
- - feat: customizable delivery roles โ€” users compose agent phases from presets
16
- - feat: show all 4 agent panels in 2x2 grid with SVG flow arrows
17
- - feat: Delivery Workspace โ€” multi-agent orchestrated software delivery
18
- - feat: conversation terminal view with inline input and data flow
19
- - feat: Conversation Mode โ€” multi-agent dialogue with graph view and live logs
20
- - feat: Pipeline editor node edit modal has Agent + Mode selectors
21
- - feat: Pipeline UI shows agent per node
22
- - feat: per-doc-root agent config (Settings + Docs toolbar)
23
- - feat: Telegram default agent + Docs agent config
24
- - feat: TTY support for agents that need terminal (e.g. Codex)
25
- - feat: per-agent skip permissions flag with presets
26
- - feat: Telegram agent support โ€” @agent syntax + /agents command
27
- - feat: agent selection for Pipeline, Mobile, Help
28
- - feat: per-scene model config for each agent
29
- - feat: model config moved from global to per-agent
30
- - feat: task system uses agent adapter + agent selector in NewTaskModal
31
- - feat: terminal tab agent selection
32
- - feat: Agents management UI in Settings
33
- - feat: multi-agent foundation โ€” registry, adapters, API
8
+ - feat: daemon health check + SSE for all message actions
9
+ - feat: pending_approval status for watch approve + requiresApproval
34
10
 
35
11
  ### Bug Fixes
36
- - fix: tolerate Next.js 16 _global-error prerender bug in build
37
- - fix: Log panel reads from persistent logs.jsonl + clear logs button
38
- - fix: restart watch loop when agent config is updated
39
- - fix: watch directory picker uses correct tree type and flattens nested dirs
40
- - fix: send block only when message is still running, not after done
41
- - fix: block forge-send reply to message sender + skill prompt hint
42
- - fix: hasRunning check uses worker's current message, not all bus log
43
- - fix: no extra reply message when processing downstream request
44
- - fix: smith send API returns messageId for outbox tracking
45
- - fix: inject FORGE_AGENT_ID/WORKSPACE_ID/PORT into manual terminal env
46
- - fix: forge skills use $FORGE_AGENT_ID instead of hardcoded 'unknown'
47
- - fix: deduplicate bus_message SSE events by message ID
48
- - fix: emit done before markMessageDone so causedBy can read messageId
49
- - fix: remove notifyDownstreamForRevalidation + prevent multiple running messages
50
- - fix: abort_message no longer errors on already-aborted messages
51
- - fix: getAllAgentStates returns worker state with entry mode override
52
- - fix: manual stays in mode field, displayed as purple 'manual' on node
53
- - fix: show 'manual' task status when agent is in manual mode
54
- - fix: close terminal uses close_terminal action instead of reset
55
- - fix: restartAgentDaemon recreates worker after resetAgent kills it
56
- - fix: parseBusMarkers re-scanning entire history causes message loops
57
- - fix: restartAgentDaemon aligned with simplified setManualMode
58
- - fix: manual mode shows down because worker async cleanup overrides state
59
- - fix: buffered wake prevents lost messages in daemon loop
60
- - fix: simplify retry โ€” reset original message to pending, no emit
61
- - fix: retry creates new message, preserves original for history
62
- - fix: message retry causing duplicate execution
63
- - fix: PTY spawn in ESM โ€” use createRequire for node-pty
64
- - fix: TTY detection for codex profiles + clear agent cache on terminal open
65
- - fix: project terminal dialog loads sessions from claude-sessions API
66
- - fix: bypass GitHub CDN cache on skills sync
67
- - fix: merge tags from info.json during v2 registry sync
68
- - fix: enable allowProposedApi for search decorations
69
- - fix: profile env/model propagation across all terminal launch paths
70
- - fix: saveAgentConfig preserves profile fields (base/env/model/type)
71
- - fix: sessions API uses orch.projectPath, ESM imports, non-claude compat
72
- - fix: stricter workDir validation โ€” block .. and sibling dir escape
73
- - fix: workDir with leading / treated as relative to project, not absolute
74
- - fix: workDir normalize strips ./ prefix, default to smith label
75
- - fix: only use claude -c (resume) if existing session exists
76
- - fix: handle unknown agentId in smith send API
77
- - fix: whitelist /api/workspace in middleware for forge skill auto-discover
78
- - fix: install skills as directories with SKILL.md (Claude Code format)
79
- - fix: use imported resolve instead of require('node:path') in ESM context
80
- - fix: orchestrator actively manages smith lifecycle in start/stop daemon
81
- - fix: startDaemon error handling + stopDaemon cleanup
82
- - fix: close terminal should enter listening, not execute steps
83
- - fix: workspace terminal uses correct message types from terminal server
84
- - fix: workspace terminal input + keep alive when switching tabs
85
- - fix: rewrite WorkspaceView โ€” each agent is a real interactive terminal
86
- - fix: move ssr:false dynamic import to client wrapper component
87
- - fix: disable SSR for Dashboard to eliminate hydration mismatch
88
- - fix: default phases missing _outputArtifactName/_label/_icon metadata
89
- - fix: data flow arrows based on requires/produces, not sequential order
90
- - fix: suppress hydration warning from locale/extension mismatch
91
- - fix: pipeline node shows 'default' instead of 'claude' when no agent set
92
- - fix: PTY onExit/onData registered once, fixes stuck tasks after cancel
93
- - fix: auto-kill PTY agents after 15s idle
94
- - fix: strip ANSI/terminal control codes from PTY agent output
95
- - fix: retryTask preserves original agent selection
96
- - fix: use pipe stdin for task spawn, close immediately after
97
- - fix: non-claude agents no longer fallback to claude or show claude model
98
- - fix: settings agent config debounced save + unsaved warning on close
99
- - fix: generic agents use taskFlags from settings, log raw text output
100
- - fix: only pass model flag to agents that support it, show agent in task
101
- - fix: settings agent colors match terminal โ€” use API detected status
102
- - fix: show all configured agents, not just detected ones
103
-
104
- ### Performance
105
- - perf: watch heartbeat only logs to console, not to files/history
106
- - perf: watch uses timestamp comparison instead of full snapshot
107
-
108
- ### Refactoring
109
- - refactor: remove Delivery tab, keep only Workspace
12
+ - fix: stop old worker before creating new one in enterDaemonListening
13
+ - fix: prevent multiple running messages + clean stale running
14
+ - fix: requiresApproval set at message arrival, not in message loop
15
+ - fix: approved messages not re-converted to pending_approval
16
+ - fix: message loop never stops + auto-recreate dead workers
17
+ - fix: approve/reject emit SSE events + reject marks as failed
18
+ - fix: emit bus_message_status after watch approve sets pending_approval
19
+ - fix: pending_approval edge cases
20
+ - fix: system messages (_watch, _system, user) bypass causedBy rules
110
21
 
111
22
  ### Documentation
112
- - docs: update workspace help with message system design
113
- - docs: add workspace help, update settings/projects/overview docs
23
+ - docs: add workspace section + agent flow diagram to README
24
+ - docs: README with Mermaid diagrams for v0.5.0
25
+ - docs: update README for v0.5.0 โ€” multi-agent workspace
26
+ - docs: update workspace help with watch, logs, forge skills, send protection
114
27
 
115
28
  ### Other
116
- - watch: push heartbeat + alert logs to agent history for Log panel
117
- - watch: log heartbeat on each check cycle
118
- - ui: structured watch target builder with directory picker
119
- - debug: log deleteMessage results for diagnosing message reappearance
120
- - persist currentMessageId as task trigger identifier
121
- - inbox: abort all pending button for batch operations
122
- - inbox/outbox: batch select + delete for completed messages
123
- - Phase 3: ticket UI, retry limits, ticket API actions
124
- - Phase 2: causedBy chain + ticket messages + receive rules
125
- - Phase 1: anti-loop โ€” DAG cycle detection, directional broadcast, disable SEND markers
126
- - ui: show mode (auto/manual) as separate line on agent node
127
- - simplify: setManualMode only changes mode, message loop skips manual
128
- - debug: log when agent message loop skips due to not-listening state
129
- - skills: auto-loop sync with progress indicator
130
- - skills: incremental sync โ€” registry fast, info.json in batches
131
- - terminal: add Ctrl/Cmd+F search in terminal buffer
132
- - terminal: add WebGL rendering + Unicode 11 support
133
- - unified terminal launch: resolveTerminalLaunch for both VibeCoding + Workspace
134
- - settings: agent has profile selector dropdown
135
- - settings: profiles are global/shared, not per-agent
136
- - settings: add cliType selector to agent config panel
137
- - settings: profiles nested inside each agent, not standalone section
138
- - open_terminal: return cliType + cliCmd from agent registry
139
- - settings: env var templates per CLI type for profiles
140
- - agent config: add cliType field (claude-code/codex/aider/generic)
141
- - vibecoding: profile selector + session picker + env injection for terminal
142
- - terminal profile: env var injection via export, not settings.json
143
- - terminal: session picker with recent sessions list
144
- - terminal: styled launch dialog โ€” New Session / Resume Latest
145
- - terminal: simple prompt dialog for new/resume before opening
146
- - terminal: prompt user to choose new session or resume
147
- - cleanup: simplify resume flag check in FloatingTerminal
148
- - UI: show resolved workDir path hint below input
149
- - forge skills: explicit 2-step commands, no env var checks
150
- - forge skills: inline auto-discover, no separate setup step
151
- - workDir validation: unique per smith, must be within project, no nesting
152
- - profile settings: write to smith workDir, not project root
153
- - profile terminal: apply env/model to .claude/settings.json on open_terminal
154
- - forge skills: install/update on every forge startup
155
- - forge skills: auto-discover workspace context with fallback defaults
156
- - skills: install once in startDaemon, remove per-smith install
157
- - skills: install to ~/.claude/skills/ globally + fix project deny rules
158
- - skill installer: auto-fix .claude/settings.json curl permissions
159
- - forge skills: env var injection + auto-install on smith startup
160
- - agent profiles: editable profile rows with expand/collapse
161
- - agent profiles: env vars support for custom CLI configs (e.g., FortiAI)
162
- - agent profiles UI: settings management + workspace profile selector
163
- - agent profiles + provider config data layer
164
- - smith message-driven architecture: independent message loop, inbox management, status simplification
165
- - multiple agent implementation
166
- - fix issue
167
- - fix issue for workspace
168
- - optmized projects
169
- - refactoring workspace
170
- - implement workspace and fix issue
171
- - implement multiple agents
172
- - ui: show agent badge on ReactFlow node blocks in pipeline editor
173
- - simplify: single docs agent instead of per-root
174
- - ui: remove Docs page agent selector, keep Settings-only config
175
- - ui: remove leftover model/permissions migration notes from Settings
176
- - ui: consistent agent colors across terminal and settings
177
- - ui: agent buttons green=installed, gray=not installed
178
- - ui: agent buttons โ‰ค3 inline, >3 overflow with dropdown
179
- - ui: agent buttons inline with project name in new tab modal
29
+ - ui: add requiresApproval toggle in agent config modal
30
+ - debug: log watch analyze skip reasons
180
31
 
181
32
 
182
- **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.4.16...v0.5.0
33
+ **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.5.0...v0.5.1
@@ -279,6 +279,7 @@ function AgentConfigModal({ initial, mode, existingAgents, projectPath, onConfir
279
279
  const [stepsText, setStepsText] = useState(
280
280
  (initial.steps || []).map(s => `${s.label}: ${s.prompt}`).join('\n') || ''
281
281
  );
282
+ const [requiresApproval, setRequiresApproval] = useState(initial.requiresApproval || false);
282
283
  const [watchEnabled, setWatchEnabled] = useState(initial.watch?.enabled || false);
283
284
  const [watchInterval, setWatchInterval] = useState(String(initial.watch?.interval || 60));
284
285
  const [watchAction, setWatchAction] = useState<'log' | 'analyze' | 'approve'>(initial.watch?.action || 'log');
@@ -465,6 +466,13 @@ function AgentConfigModal({ initial, mode, existingAgents, projectPath, onConfir
465
466
  </div>
466
467
  </div>
467
468
 
469
+ {/* Requires Approval */}
470
+ <div className="flex items-center gap-2">
471
+ <input type="checkbox" id="requiresApproval" checked={requiresApproval} onChange={e => setRequiresApproval(e.target.checked)}
472
+ className="accent-[#58a6ff]" />
473
+ <label htmlFor="requiresApproval" className="text-[9px] text-gray-400">Require approval before processing inbox messages</label>
474
+ </div>
475
+
468
476
  {/* Steps */}
469
477
  <div className="flex flex-col gap-1">
470
478
  <label className="text-[9px] text-gray-500 uppercase">Steps (one per line โ€” Label: Prompt)</label>
@@ -570,6 +578,7 @@ function AgentConfigModal({ initial, mode, existingAgents, projectPath, onConfir
570
578
  workDir: workDirVal.trim() || label.trim().toLowerCase().replace(/\s+/g, '-') + '/',
571
579
  outputs: outputs.split(',').map(s => s.trim()).filter(Boolean),
572
580
  steps: parseSteps(),
581
+ requiresApproval: requiresApproval || undefined,
573
582
  watch: watchEnabled && watchTargets.length > 0 ? {
574
583
  enabled: true,
575
584
  interval: Math.max(10, parseInt(watchInterval) || 60),
@@ -1001,7 +1010,7 @@ function InboxPanel({ agentId, agentLabel, busLog, agents, workspaceId, onClose
1001
1010
  }`}>{msg.ticketStatus}</span>
1002
1011
  )}
1003
1012
  {/* Message delivery status */}
1004
- <span className={`text-[7px] ${msg.status === 'done' ? 'text-green-500' : msg.status === 'running' ? 'text-blue-400' : msg.status === 'failed' ? 'text-red-500' : 'text-yellow-500'}`}>
1013
+ <span className={`text-[7px] ${msg.status === 'done' ? 'text-green-500' : msg.status === 'running' ? 'text-blue-400' : msg.status === 'failed' ? 'text-red-500' : msg.status === 'pending_approval' ? 'text-orange-400' : 'text-yellow-500'}`}>
1005
1014
  {msg.status || 'pending'}
1006
1015
  </span>
1007
1016
  {/* Retry count for tickets */}
@@ -1015,6 +1024,18 @@ function InboxPanel({ agentId, agentLabel, busLog, agents, workspaceId, onClose
1015
1024
  </span>
1016
1025
  )}
1017
1026
  {/* Actions */}
1027
+ {msg.status === 'pending_approval' && (
1028
+ <div className="flex gap-1 ml-auto">
1029
+ <button onClick={() => wsApi(workspaceId, 'approve_message', { messageId: msg.id })}
1030
+ className="text-[7px] px-1.5 py-0.5 rounded bg-green-600/20 text-green-400 hover:bg-green-600/30">
1031
+ โœ“ Approve
1032
+ </button>
1033
+ <button onClick={() => wsApi(workspaceId, 'reject_message', { messageId: msg.id })}
1034
+ className="text-[7px] px-1.5 py-0.5 rounded bg-red-600/20 text-red-400 hover:bg-red-600/30">
1035
+ โœ• Reject
1036
+ </button>
1037
+ </div>
1038
+ )}
1018
1039
  {msg.status === 'pending' && msg.type !== 'ack' && (
1019
1040
  <button onClick={() => wsApi(workspaceId, 'abort_message', { messageId: msg.id })}
1020
1041
  className="text-[7px] px-1.5 py-0.5 rounded bg-red-600/20 text-red-400 hover:bg-red-600/30 ml-auto">
@@ -1780,7 +1801,7 @@ function WorkspaceViewInner({ projectPath, projectName, onClose }: {
1780
1801
  onShowLog: () => setLogTarget({ id: agent.id, label: agent.label }),
1781
1802
  onShowMemory: () => setMemoryTarget({ id: agent.id, label: agent.label }),
1782
1803
  onShowInbox: () => setInboxTarget({ id: agent.id, label: agent.label }),
1783
- inboxPending: busLog.filter(m => m.to === agent.id && m.status === 'pending' && m.type !== 'ack').length,
1804
+ inboxPending: busLog.filter(m => m.to === agent.id && (m.status === 'pending' || m.status === 'pending_approval') && m.type !== 'ack').length,
1784
1805
  inboxFailed: busLog.filter(m => m.to === agent.id && m.status === 'failed' && m.type !== 'ack').length,
1785
1806
  onOpenTerminal: async () => {
1786
1807
  if (!workspaceId) return;
@@ -184,9 +184,58 @@ curl -X POST http://localhost:8403/api/workspace/<id>/smith \
184
184
  curl http://localhost:8403/api/workspace/<id>/stream
185
185
  ```
186
186
 
187
+ ## Watch (Autonomous Monitoring)
188
+
189
+ Agents can autonomously monitor file changes, git commits, or custom commands without relying on messages.
190
+
191
+ ### Configuration
192
+
193
+ In the agent config modal, enable Watch and configure:
194
+ - **Interval**: Check frequency in seconds (min 10, default 60)
195
+ - **Targets**: What to monitor
196
+ - `Directory` โ€” select from project folders, detect file mtime changes
197
+ - `Git` โ€” detect new commits via HEAD hash comparison
198
+ - `Agent Output` โ€” monitor another agent's declared output paths
199
+ - `Command` โ€” run a shell command, detect output changes
200
+ - **On Change**: Action when changes detected
201
+ - `Log` โ€” write to agent log only (default, no token cost)
202
+ - `Analyze` โ€” auto-wake agent to analyze changes (costs tokens)
203
+ - `Approve` โ€” create pending approval, user decides whether to trigger
204
+
205
+ ### Watch Behavior
206
+
207
+ - First check builds a baseline (no alert)
208
+ - Subsequent checks compare timestamps โ€” only files modified since last check are reported
209
+ - No-change heartbeats log to console only (not to files)
210
+ - Change alerts write to `logs.jsonl` and appear in Log panel
211
+ - Watch never sends bus messages โ€” report only, no auto-triggering other agents
212
+
213
+ ## Agent Logs
214
+
215
+ Each agent has a persistent log file (`logs.jsonl`) that survives daemon restarts and agent re-execution.
216
+
217
+ - **Log panel**: Click the log button on any agent node to view
218
+ - **Persistent**: Logs are append-only, not cleared on reset or re-run
219
+ - **Clear**: Use the "Clear" button in the Log panel header to manually wipe logs
220
+ - **Content**: Execution output, watch alerts, bus message receipts, system events
221
+
222
+ ## Forge Skills (Terminal Communication)
223
+
224
+ When in manual mode, agents have forge env vars injected (`FORGE_AGENT_ID`, `FORGE_WORKSPACE_ID`, `FORGE_PORT`) and can use:
225
+
226
+ | Skill | Description |
227
+ |-------|-------------|
228
+ | `/forge-send` | Send a message to another smith |
229
+ | `/forge-inbox` | Check incoming messages |
230
+ | `/forge-status` | Check all smiths' status |
231
+ | `/forge-workspace-sync` | Sync progress back to workspace |
232
+
233
+ **Send protection**: If an agent is currently processing a message from another agent, `/forge-send` to that agent is blocked (returns `skipped: true`). Results are delivered automatically via the message system. Only use `/forge-send` for new issues to other agents.
234
+
187
235
  ## Persistence
188
236
 
189
237
  - Workspace state: `~/.forge/workspaces/<id>/state.json`
238
+ - Agent logs: `~/.forge/workspaces/<id>/agents/<agentId>/logs.jsonl`
190
239
  - Auto-saved every 10 seconds
191
240
  - Atomic writes (temp file โ†’ rename) for crash safety
192
241
  - Synchronous save on daemon shutdown
@@ -200,5 +249,6 @@ curl http://localhost:8403/api/workspace/<id>/stream
200
249
  4. **Notifications flow downstream** โ€” upstream agents won't receive downstream broadcasts
201
250
  5. **Use tickets for bugs** โ€” tickets ignore DAG direction, have retry limits
202
251
  6. **Open Terminal** for manual intervention โ€” mode switches to manual, inbox pauses
203
- 7. **Check Inbox** when a smith is stuck โ€” it may have unprocessed messages
204
- 8. **Batch delete** completed messages to keep inbox clean
252
+ 7. **Use Watch for monitoring** โ€” detect file changes without message overhead (set action to `log` to avoid token costs)
253
+ 8. **Check Log panel** for execution history and watch alerts โ€” logs persist across restarts
254
+ 9. **Batch operations** โ€” select all completed messages for bulk delete, or abort all pending at once
@@ -40,5 +40,7 @@ Your job is to answer user questions about Forge features, configuration, and tr
40
40
  - Skill/marketplace โ†’ `06-skills.md`
41
41
  - Project/favorite/terminal โ†’ `07-projects.md`
42
42
  - Rules/CLAUDE.md/template โ†’ `08-rules.md`
43
- - Workspace/smith/daemon/multi-agent/bus/message โ†’ `11-workspace.md`
43
+ - Workspace/smith/daemon/multi-agent/bus/message/ticket โ†’ `11-workspace.md`
44
+ - Watch/monitor/detect/file changes/autonomous โ†’ `11-workspace.md`
44
45
  - Agent profile/env/model/cliType โ†’ `01-settings.md` + `11-workspace.md`
46
+ - Agent log/logs/history/clear logs โ†’ `11-workspace.md`
@@ -265,7 +265,7 @@ export class AgentBus extends EventEmitter {
265
265
  /** Retry/re-run a message โ€” set back to pending and re-deliver */
266
266
  retryMessage(messageId: string): BusMessage | null {
267
267
  const msg = this.log.find(m => m.id === messageId);
268
- if (!msg || msg.status === 'pending' || msg.status === 'running') return null;
268
+ if (!msg || msg.status === 'pending' || msg.status === 'pending_approval' || msg.status === 'running') return null;
269
269
  msg.status = 'pending';
270
270
  msg.retries = 0;
271
271
  this.unsee(messageId);
@@ -61,6 +61,7 @@ export class WorkspaceOrchestrator extends EventEmitter {
61
61
  private approvalQueue = new Set<string>();
62
62
  private daemonActive = false;
63
63
  private createdAt = Date.now();
64
+ private healthCheckTimer: NodeJS.Timeout | null = null;
64
65
 
65
66
  constructor(workspaceId: string, projectPath: string, projectName: string) {
66
67
  super();
@@ -816,6 +817,9 @@ export class WorkspaceOrchestrator extends EventEmitter {
816
817
  // Start watch loops for agents with watch config
817
818
  this.watchManager.start();
818
819
 
820
+ // Start health check โ€” monitor all agents every 10s, auto-heal
821
+ this.startHealthCheck();
822
+
819
823
  console.log(`[workspace] Daemon started: ${started} smiths active, ${failed} failed`);
820
824
  this.emitAgentsChanged();
821
825
  }
@@ -852,10 +856,14 @@ export class WorkspaceOrchestrator extends EventEmitter {
852
856
  const entry = this.agents.get(agentId);
853
857
  if (!entry) return;
854
858
 
855
- const { config } = entry;
859
+ // Stop existing worker first to prevent duplicate execution
860
+ if (entry.worker) {
861
+ entry.worker.removeAllListeners();
862
+ entry.worker.stop();
863
+ entry.worker = null;
864
+ }
856
865
 
857
- // TODO: per-smith install hook for future use (commands, custom skills, etc.)
858
- // Skills are installed globally in startDaemon, not per-smith
866
+ const { config } = entry;
859
867
 
860
868
  const backend = this.createBackend(config, agentId);
861
869
  const peerAgentIds = Array.from(this.agents.keys()).filter(id => id !== agentId);
@@ -960,9 +968,81 @@ export class WorkspaceOrchestrator extends EventEmitter {
960
968
  this.bus.markAllRunningAsFailed();
961
969
  this.emitAgentsChanged();
962
970
  this.watchManager.stop();
971
+ this.stopHealthCheck();
963
972
  console.log('[workspace] Daemon stopped');
964
973
  }
965
974
 
975
+ // โ”€โ”€โ”€ Health Check โ€” auto-heal agents โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
976
+
977
+ private startHealthCheck(): void {
978
+ if (this.healthCheckTimer) return;
979
+ this.healthCheckTimer = setInterval(() => this.runHealthCheck(), 10_000);
980
+ this.healthCheckTimer.unref();
981
+ }
982
+
983
+ private stopHealthCheck(): void {
984
+ if (this.healthCheckTimer) {
985
+ clearInterval(this.healthCheckTimer);
986
+ this.healthCheckTimer = null;
987
+ }
988
+ }
989
+
990
+ private runHealthCheck(): void {
991
+ if (!this.daemonActive) return;
992
+
993
+ for (const [id, entry] of this.agents) {
994
+ if (entry.config.type === 'input') continue;
995
+ if (entry.state.mode === 'manual') continue;
996
+
997
+ // Check 1: Worker should exist for all active agents
998
+ if (!entry.worker) {
999
+ console.log(`[health] ${entry.config.label}: no worker โ€” recreating`);
1000
+ this.enterDaemonListening(id);
1001
+ entry.state.smithStatus = 'active';
1002
+ this.emit('event', { type: 'smith_status', agentId: id, smithStatus: 'active', mode: entry.state.mode } as any);
1003
+ continue;
1004
+ }
1005
+
1006
+ // Check 2: SmithStatus should be active
1007
+ if (entry.state.smithStatus !== 'active') {
1008
+ console.log(`[health] ${entry.config.label}: smith=${entry.state.smithStatus} โ€” setting active`);
1009
+ entry.state.smithStatus = 'active';
1010
+ this.emit('event', { type: 'smith_status', agentId: id, smithStatus: 'active', mode: entry.state.mode } as any);
1011
+ }
1012
+
1013
+ // Check 3: Message loop should be running
1014
+ if (!this.messageLoopTimers.has(id)) {
1015
+ console.log(`[health] ${entry.config.label}: message loop stopped โ€” restarting`);
1016
+ this.startMessageLoop(id);
1017
+ }
1018
+
1019
+ // Check 4: Stale running messages (agent not actually running) โ†’ mark failed
1020
+ if (entry.state.taskStatus !== 'running') {
1021
+ const staleRunning = this.bus.getLog().filter(m => m.to === id && m.status === 'running' && m.type !== 'ack');
1022
+ for (const m of staleRunning) {
1023
+ const age = Date.now() - m.timestamp;
1024
+ if (age > 60_000) { // running for 60s+ but agent is idle = stale
1025
+ console.log(`[health] ${entry.config.label}: stale running message ${m.id.slice(0, 8)} (${Math.round(age/1000)}s) โ€” marking failed`);
1026
+ m.status = 'failed';
1027
+ this.emit('event', { type: 'bus_message_status', messageId: m.id, status: 'failed' } as any);
1028
+ }
1029
+ }
1030
+ }
1031
+
1032
+ // Check 5: Pending messages but agent idle โ€” try wake
1033
+ if (entry.state.taskStatus !== 'running' && entry.state.mode === 'auto') {
1034
+ const pending = this.bus.getPendingMessagesFor(id).filter(m => m.from !== id && m.type !== 'ack');
1035
+ if (pending.length > 0 && entry.worker.isListening()) {
1036
+ // Message loop should handle this, but if it didn't, log it
1037
+ const age = Date.now() - pending[0].timestamp;
1038
+ if (age > 30_000) { // stuck for 30+ seconds
1039
+ console.log(`[health] ${entry.config.label}: ${pending.length} pending msg(s) stuck for ${Math.round(age/1000)}s โ€” message loop should pick up`);
1040
+ }
1041
+ }
1042
+ }
1043
+ }
1044
+ }
1045
+
966
1046
  /** Handle watch alert based on agent's configured action */
967
1047
  private handleWatchAlert(agentId: string, summary: string): void {
968
1048
  const entry = this.agents.get(agentId);
@@ -976,8 +1056,15 @@ export class WorkspaceOrchestrator extends EventEmitter {
976
1056
 
977
1057
  if (action === 'analyze') {
978
1058
  // Auto-wake agent to analyze changes (skip if busy/manual)
979
- if (entry.state.mode === 'manual' || entry.state.taskStatus === 'running') return;
980
- if (!entry.worker?.isListening()) return;
1059
+ if (entry.state.mode === 'manual' || entry.state.taskStatus === 'running') {
1060
+ console.log(`[watch] ${entry.config.label}: skipped analyze (mode=${entry.state.mode} task=${entry.state.taskStatus})`);
1061
+ return;
1062
+ }
1063
+ if (!entry.worker?.isListening()) {
1064
+ console.log(`[watch] ${entry.config.label}: skipped analyze (worker=${!!entry.worker} listening=${entry.worker?.isListening()})`);
1065
+ return;
1066
+ }
1067
+ console.log(`[watch] ${entry.config.label}: triggering analyze`);
981
1068
 
982
1069
  const prompt = entry.config.watch?.prompt || 'Analyze the following changes and produce a report:';
983
1070
  const logEntry = {
@@ -992,13 +1079,13 @@ export class WorkspaceOrchestrator extends EventEmitter {
992
1079
  }
993
1080
 
994
1081
  if (action === 'approve') {
995
- // Create pending approval โ€” user must click to trigger analysis
996
- this.bus.send('_watch', agentId, 'notify', {
1082
+ // Create message with pending_approval status โ€” user must approve to execute
1083
+ const msg = this.bus.send('_watch', agentId, 'notify', {
997
1084
  action: 'watch_changes',
998
1085
  content: `Watch detected changes (awaiting approval):\n${summary}`,
999
1086
  });
1000
- this.approvalQueue.add(agentId);
1001
- this.emit('event', { type: 'approval_required', agentId, upstreamId: '_watch' } satisfies OrchestratorEvent);
1087
+ msg.status = 'pending_approval';
1088
+ this.emit('event', { type: 'bus_message_status', messageId: msg.id, status: 'pending_approval' } as any);
1002
1089
  console.log(`[watch] ${entry.config.label}: changes detected, awaiting approval`);
1003
1090
  }
1004
1091
  }
@@ -1501,6 +1588,14 @@ export class WorkspaceOrchestrator extends EventEmitter {
1501
1588
  return;
1502
1589
  }
1503
1590
 
1591
+ // โ”€โ”€ requiresApproval โ†’ set pending_approval on arrival โ”€โ”€
1592
+ if (target.config.requiresApproval) {
1593
+ msg.status = 'pending_approval';
1594
+ this.emit('event', { type: 'bus_message_status', messageId: msg.id, status: 'pending_approval' } as any);
1595
+ console.log(`[bus] ${target.config.label}: received ${action} โ€” pending approval`);
1596
+ return;
1597
+ }
1598
+
1504
1599
  // โ”€โ”€ Message stays pending โ€” message loop will consume it when smith is ready โ”€โ”€
1505
1600
  console.log(`[bus] ${target.config.label}: received ${action} โ€” queued in inbox (${msg.status})`);
1506
1601
  }
@@ -1515,28 +1610,40 @@ export class WorkspaceOrchestrator extends EventEmitter {
1515
1610
  let debugTick = 0;
1516
1611
  const tick = () => {
1517
1612
  const entry = this.agents.get(agentId);
1518
- if (!entry || entry.state.smithStatus !== 'active') {
1613
+ if (!entry) {
1519
1614
  this.stopMessageLoop(agentId);
1520
1615
  return;
1521
1616
  }
1522
1617
 
1618
+ // Don't stop loop if smith is down โ€” just skip this tick
1619
+ // (loop stays alive so it works when smith comes back)
1620
+ if (entry.state.smithStatus !== 'active') return;
1621
+
1523
1622
  // Skip if manual (user in terminal) or running (already busy)
1524
1623
  if (entry.state.mode === 'manual') return;
1525
1624
  if (entry.state.taskStatus === 'running') return;
1526
1625
 
1527
- // Skip if no worker ready
1528
- if (!entry.worker?.isListening()) {
1626
+ // Skip if no worker ready โ€” recreate if needed
1627
+ if (!entry.worker) {
1628
+ if (this.daemonActive) {
1629
+ console.log(`[inbox] ${entry.config.label}: no worker, recreating...`);
1630
+ this.enterDaemonListening(agentId);
1631
+ }
1632
+ return;
1633
+ }
1634
+ if (!entry.worker.isListening()) {
1529
1635
  if (++debugTick % 15 === 0) {
1530
- console.log(`[inbox] ${entry.config.label}: not listening (worker=${!!entry.worker} smith=${entry.state.smithStatus} task=${entry.state.taskStatus})`);
1636
+ console.log(`[inbox] ${entry.config.label}: not listening (smith=${entry.state.smithStatus} task=${entry.state.taskStatus})`);
1531
1637
  }
1532
1638
  return;
1533
1639
  }
1534
1640
 
1535
- // Skip if worker is currently processing a message
1536
- if (entry.worker?.getCurrentMessageId()) {
1537
- const currentMsg = this.bus.getLog().find(m => m.id === entry.worker!.getCurrentMessageId());
1538
- if (currentMsg && currentMsg.status === 'running') return;
1539
- }
1641
+ // Skip if any message is already running for this agent
1642
+ const hasRunning = this.bus.getLog().some(m => m.to === agentId && m.status === 'running' && m.type !== 'ack');
1643
+ if (hasRunning) return;
1644
+
1645
+ // requiresApproval is handled at message arrival time (routeMessageToAgent),
1646
+ // not in the message loop. Approved messages come through as normal 'pending'.
1540
1647
 
1541
1648
  // Find next pending message, applying causedBy rules
1542
1649
  const allPending = this.bus.getPendingMessagesFor(agentId).filter(m => m.from !== agentId && m.type !== 'ack');
@@ -1554,6 +1661,9 @@ export class WorkspaceOrchestrator extends EventEmitter {
1554
1661
  return true;
1555
1662
  }
1556
1663
 
1664
+ // System messages (from _watch, _system, user) bypass causedBy rules
1665
+ if (m.from.startsWith('_') || m.from === 'user') return true;
1666
+
1557
1667
  // Notifications: check causedBy for loop prevention
1558
1668
  if (m.causedBy) {
1559
1669
  // Rule 1: Is this a response to something I sent? โ†’ accept (for verification)
@@ -141,7 +141,7 @@ export interface BusMessage {
141
141
  };
142
142
  timestamp: number;
143
143
  // Delivery tracking
144
- status?: 'pending' | 'running' | 'done' | 'failed';
144
+ status?: 'pending' | 'pending_approval' | 'running' | 'done' | 'failed';
145
145
  retries?: number;
146
146
  // Message classification
147
147
  category?: MessageCategory; // 'notification' (default, follows DAG) | 'ticket' (1-to-1, ignores DAG)
@@ -347,14 +347,38 @@ async function handleAgentsPost(id: string, body: any, res: ServerResponse): Pro
347
347
  if (!orch.isDaemonActive()) return jsonError(res, 'Start daemon first before retrying messages');
348
348
  const msg = orch.getBus().retryMessage(messageId);
349
349
  if (!msg) return jsonError(res, 'Message not found or already pending');
350
+ orch.emit('event', { type: 'bus_message_status', messageId: msg.id, status: 'pending' });
350
351
  return json(res, { ok: true, messageId: msg.id, action: msg.payload.action });
351
352
  }
352
353
  case 'abort_message': {
353
354
  const { messageId } = body;
354
355
  if (!messageId) return jsonError(res, 'messageId required');
355
356
  const abortMsg = orch.getBus().abortMessage(messageId);
357
+ if (abortMsg) {
358
+ orch.emit('event', { type: 'bus_message_status', messageId, status: 'failed' });
359
+ }
356
360
  return json(res, { ok: true, messageId, aborted: !!abortMsg });
357
361
  }
362
+ case 'approve_message': {
363
+ const { messageId } = body;
364
+ if (!messageId) return jsonError(res, 'messageId required');
365
+ const approveMsg = orch.getBus().getLog().find(m => m.id === messageId);
366
+ if (!approveMsg) return jsonError(res, 'Message not found');
367
+ if (approveMsg.status !== 'pending_approval') return jsonError(res, 'Message is not pending approval');
368
+ if (body.content) approveMsg.payload.content = body.content;
369
+ approveMsg.status = 'pending';
370
+ orch.emit('event', { type: 'bus_message_status', messageId, status: 'pending' });
371
+ return json(res, { ok: true });
372
+ }
373
+ case 'reject_message': {
374
+ const { messageId } = body;
375
+ if (!messageId) return jsonError(res, 'messageId required');
376
+ const rejectMsg = orch.getBus().getLog().find(m => m.id === messageId);
377
+ if (!rejectMsg) return jsonError(res, 'Message not found');
378
+ rejectMsg.status = 'failed';
379
+ orch.emit('event', { type: 'bus_message_status', messageId, status: 'failed' });
380
+ return json(res, { ok: true });
381
+ }
358
382
  case 'delete_message': {
359
383
  const { messageId } = body;
360
384
  if (!messageId) return jsonError(res, 'messageId required');
package/next-env.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /// <reference types="next" />
2
2
  /// <reference types="next/image-types/global" />
3
- import "./.next/types/routes.d.ts";
3
+ import "./.next/dev/types/routes.d.ts";
4
4
 
5
5
  // NOTE: This file should not be edited
6
6
  // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aion0/forge",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Unified AI workflow platform โ€” multi-model task orchestration, persistent sessions, web terminal, remote access",
5
5
  "type": "module",
6
6
  "scripts": {