@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 +26 -1
- package/RELEASE_NOTES.md +20 -169
- package/components/WorkspaceView.tsx +23 -2
- package/lib/help-docs/11-workspace.md +52 -2
- package/lib/help-docs/CLAUDE.md +3 -1
- package/lib/workspace/agent-bus.ts +1 -1
- package/lib/workspace/orchestrator.ts +128 -18
- package/lib/workspace/types.ts +1 -1
- package/lib/workspace-standalone.ts +24 -0
- package/next-env.d.ts +1 -1
- package/package.json +1 -1
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
|
-
| **
|
|
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.
|
|
1
|
+
# Forge v0.5.1
|
|
2
2
|
|
|
3
3
|
Released: 2026-03-28
|
|
4
4
|
|
|
5
|
-
## Changes since v0.
|
|
5
|
+
## Changes since v0.5.0
|
|
6
6
|
|
|
7
7
|
### Features
|
|
8
|
-
- feat:
|
|
9
|
-
- feat:
|
|
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:
|
|
37
|
-
- fix:
|
|
38
|
-
- fix:
|
|
39
|
-
- fix:
|
|
40
|
-
- fix:
|
|
41
|
-
- fix:
|
|
42
|
-
- fix:
|
|
43
|
-
- fix:
|
|
44
|
-
- fix:
|
|
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:
|
|
113
|
-
- 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
|
-
-
|
|
117
|
-
-
|
|
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.
|
|
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. **
|
|
204
|
-
8. **
|
|
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
|
package/lib/help-docs/CLAUDE.md
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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')
|
|
980
|
-
|
|
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
|
|
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
|
-
|
|
1001
|
-
this.emit('event', { type: '
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
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)
|
package/lib/workspace/types.ts
CHANGED
|
@@ -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