@aion0/forge 0.5.14 → 0.5.15
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/.forge/agent-context.json +6 -0
- package/RELEASE_NOTES.md +16 -7
- package/components/WorkspaceView.tsx +14 -2
- package/lib/workspace/orchestrator.ts +106 -1
- package/lib/workspace/session-monitor.ts +23 -3
- package/lib/workspace/skill-installer.ts +87 -0
- package/lib/workspace-standalone.ts +16 -0
- package/package.json +1 -1
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
|
-
# Forge v0.5.
|
|
1
|
+
# Forge v0.5.15
|
|
2
2
|
|
|
3
3
|
Released: 2026-03-31
|
|
4
4
|
|
|
5
|
-
## Changes since v0.5.
|
|
5
|
+
## Changes since v0.5.14
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
- feat: replace Stop button with Done/Failed/Idle for running agents
|
|
9
|
+
- feat: Claude Code Stop hook for agent completion detection
|
|
6
10
|
|
|
7
11
|
### Bug Fixes
|
|
8
|
-
- fix:
|
|
9
|
-
- fix:
|
|
10
|
-
- fix:
|
|
11
|
-
- fix:
|
|
12
|
+
- fix: suppress session monitor for 10s after manual state change
|
|
13
|
+
- fix: reset session monitor state when task status manually changed
|
|
14
|
+
- fix: stop button resets task to idle for terminal agents, not smith down
|
|
15
|
+
- fix: session monitor fallback timeout to 60min
|
|
16
|
+
- fix: session monitor fallback timeout to 10min
|
|
17
|
+
- fix: session monitor done threshold to 5min (hook is primary detection)
|
|
18
|
+
- fix: add logging to agent-context.json write for debugging
|
|
19
|
+
- fix: hook uses correct Claude Code schema + date-stamped backup
|
|
20
|
+
- fix: hook reads agent context from file instead of env vars
|
|
12
21
|
|
|
13
22
|
|
|
14
|
-
**Full Changelog**: https://github.com/aiwatching/forge/compare/v0.5.
|
|
23
|
+
**Full Changelog**: https://github.com/aiwatching/forge/compare/v0.5.14...v0.5.15
|
|
@@ -2212,6 +2212,9 @@ interface AgentNodeData {
|
|
|
2212
2212
|
onShowInbox: () => void;
|
|
2213
2213
|
onOpenTerminal: () => void;
|
|
2214
2214
|
onSwitchSession: () => void;
|
|
2215
|
+
onMarkIdle?: () => void;
|
|
2216
|
+
onMarkDone?: (notify: boolean) => void;
|
|
2217
|
+
onMarkFailed?: (notify: boolean) => void;
|
|
2215
2218
|
inboxPending?: number;
|
|
2216
2219
|
inboxFailed?: number;
|
|
2217
2220
|
[key: string]: unknown;
|
|
@@ -2325,8 +2328,14 @@ function AgentFlowNode({ data }: NodeProps<Node<AgentNodeData>>) {
|
|
|
2325
2328
|
{/* Actions */}
|
|
2326
2329
|
<div className="flex items-center gap-1 px-2 py-1.5" style={{ borderTop: `1px solid ${c.border}15` }}>
|
|
2327
2330
|
{taskStatus === 'running' && (
|
|
2328
|
-
|
|
2329
|
-
|
|
2331
|
+
<>
|
|
2332
|
+
<button onPointerDown={e => e.stopPropagation()} onClick={e => { e.stopPropagation(); data.onMarkIdle?.(); }}
|
|
2333
|
+
className="text-[9px] px-1 py-0.5 rounded bg-gray-600/20 text-gray-400 hover:bg-gray-600/30" title="Silent stop — no notifications">■</button>
|
|
2334
|
+
<button onPointerDown={e => e.stopPropagation()} onClick={e => { e.stopPropagation(); data.onMarkDone?.(true); }}
|
|
2335
|
+
className="text-[9px] px-1 py-0.5 rounded bg-green-600/20 text-green-400 hover:bg-green-600/30" title="Mark done + notify">✓</button>
|
|
2336
|
+
<button onPointerDown={e => e.stopPropagation()} onClick={e => { e.stopPropagation(); data.onMarkFailed?.(true); }}
|
|
2337
|
+
className="text-[9px] px-1 py-0.5 rounded bg-red-600/20 text-red-400 hover:bg-red-600/30" title="Mark failed + notify">✕</button>
|
|
2338
|
+
</>
|
|
2330
2339
|
)}
|
|
2331
2340
|
{/* Message button — send instructions to agent */}
|
|
2332
2341
|
{smithStatus === 'active' && taskStatus !== 'running' && (
|
|
@@ -2469,6 +2478,9 @@ function WorkspaceViewInner({ projectPath, projectName, onClose }: {
|
|
|
2469
2478
|
},
|
|
2470
2479
|
onPause: () => wsApi(workspaceId!, 'pause', { agentId: agent.id }),
|
|
2471
2480
|
onStop: () => wsApi(workspaceId!, 'stop', { agentId: agent.id }),
|
|
2481
|
+
onMarkIdle: () => wsApi(workspaceId!, 'mark_done', { agentId: agent.id, notify: false }),
|
|
2482
|
+
onMarkDone: (notify: boolean) => wsApi(workspaceId!, 'mark_done', { agentId: agent.id, notify }),
|
|
2483
|
+
onMarkFailed: (notify: boolean) => wsApi(workspaceId!, 'mark_failed', { agentId: agent.id, notify }),
|
|
2472
2484
|
onRetry: () => wsApi(workspaceId!, 'retry', { agentId: agent.id }),
|
|
2473
2485
|
onEdit: () => setModal({ mode: 'edit', initial: agent, editId: agent.id }),
|
|
2474
2486
|
onRemove: () => {
|
|
@@ -1103,6 +1103,31 @@ export class WorkspaceOrchestrator extends EventEmitter {
|
|
|
1103
1103
|
console.log('[workspace] Daemon stopped');
|
|
1104
1104
|
}
|
|
1105
1105
|
|
|
1106
|
+
// ─── Hook-based completion ─────────────────────────────
|
|
1107
|
+
|
|
1108
|
+
/** Called by Claude Code Stop hook via HTTP — agent finished a turn */
|
|
1109
|
+
handleHookDone(agentId: string): void {
|
|
1110
|
+
const entry = this.agents.get(agentId);
|
|
1111
|
+
if (!entry) return;
|
|
1112
|
+
if (!this.daemonActive) return;
|
|
1113
|
+
|
|
1114
|
+
// Only transition running → done (ignore if already idle/done)
|
|
1115
|
+
if (entry.state.taskStatus !== 'running') {
|
|
1116
|
+
console.log(`[hook] ${entry.config.label}: Stop hook fired but task=${entry.state.taskStatus}, ignoring`);
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
console.log(`[hook] ${entry.config.label}: Stop hook → done`);
|
|
1121
|
+
entry.state.taskStatus = 'done';
|
|
1122
|
+
entry.state.completedAt = Date.now();
|
|
1123
|
+
this.emit('event', { type: 'task_status', agentId, taskStatus: 'done' } as any);
|
|
1124
|
+
this.emit('event', { type: 'log', agentId, entry: { type: 'system', subtype: 'hook_done', content: 'Claude Code Stop hook: turn completed', timestamp: new Date().toISOString() } } as any);
|
|
1125
|
+
this.handleAgentDone(agentId, entry, 'Stop hook');
|
|
1126
|
+
this.sessionMonitor?.resetState(agentId);
|
|
1127
|
+
this.saveNow();
|
|
1128
|
+
this.emitAgentsChanged();
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1106
1131
|
// ─── Session File Monitor ──────────────────────────────
|
|
1107
1132
|
|
|
1108
1133
|
private async startSessionMonitors(): Promise<void> {
|
|
@@ -1502,9 +1527,63 @@ export class WorkspaceOrchestrator extends EventEmitter {
|
|
|
1502
1527
|
}
|
|
1503
1528
|
|
|
1504
1529
|
/** Stop a running agent */
|
|
1530
|
+
/** Mark agent task as done (user manually confirms completion) */
|
|
1531
|
+
markAgentDone(agentId: string, notify: boolean): void {
|
|
1532
|
+
const entry = this.agents.get(agentId);
|
|
1533
|
+
if (!entry) return;
|
|
1534
|
+
|
|
1535
|
+
// Mark running inbox messages as done
|
|
1536
|
+
const runningMsgs = this.bus.getLog().filter(m => m.to === agentId && m.status === 'running' && m.type !== 'ack');
|
|
1537
|
+
for (const m of runningMsgs) {
|
|
1538
|
+
m.status = 'done' as any;
|
|
1539
|
+
this.emit('event', { type: 'bus_message_status', messageId: m.id, status: 'done' } as any);
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
entry.state.taskStatus = notify ? 'done' : 'idle';
|
|
1543
|
+
entry.state.completedAt = Date.now();
|
|
1544
|
+
this.emit('event', { type: 'task_status', agentId, taskStatus: entry.state.taskStatus } as any);
|
|
1545
|
+
if (notify) {
|
|
1546
|
+
this.handleAgentDone(agentId, entry, 'Manually marked done');
|
|
1547
|
+
}
|
|
1548
|
+
this.sessionMonitor?.resetState(agentId);
|
|
1549
|
+
this.saveNow();
|
|
1550
|
+
this.emitAgentsChanged();
|
|
1551
|
+
console.log(`[workspace] ${entry.config.label}: manually marked ${notify ? 'done' : 'idle'} (${runningMsgs.length} messages completed)`);
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
/** Mark agent task as failed (user manually marks failure) */
|
|
1555
|
+
markAgentFailed(agentId: string, notify: boolean): void {
|
|
1556
|
+
const entry = this.agents.get(agentId);
|
|
1557
|
+
if (!entry) return;
|
|
1558
|
+
|
|
1559
|
+
// Mark running inbox messages as failed
|
|
1560
|
+
const runningMsgs = this.bus.getLog().filter(m => m.to === agentId && m.status === 'running' && m.type !== 'ack');
|
|
1561
|
+
for (const m of runningMsgs) {
|
|
1562
|
+
m.status = 'failed' as any;
|
|
1563
|
+
this.emit('event', { type: 'bus_message_status', messageId: m.id, status: 'failed' } as any);
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
entry.state.taskStatus = 'failed';
|
|
1567
|
+
entry.state.error = 'Manually marked as failed';
|
|
1568
|
+
this.emit('event', { type: 'task_status', agentId, taskStatus: 'failed' } as any);
|
|
1569
|
+
if (notify) {
|
|
1570
|
+
this.bus.notifyTaskComplete(agentId, [], 'Task failed');
|
|
1571
|
+
}
|
|
1572
|
+
this.sessionMonitor?.resetState(agentId);
|
|
1573
|
+
this.saveNow();
|
|
1574
|
+
this.emitAgentsChanged();
|
|
1575
|
+
console.log(`[workspace] ${entry.config.label}: manually marked failed (${runningMsgs.length} messages failed)`);
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
/** Legacy stop — for headless mode */
|
|
1505
1579
|
stopAgent(agentId: string): void {
|
|
1506
1580
|
const entry = this.agents.get(agentId);
|
|
1507
|
-
entry
|
|
1581
|
+
if (!entry) return;
|
|
1582
|
+
if (entry.config.persistentSession) {
|
|
1583
|
+
this.markAgentDone(agentId, false);
|
|
1584
|
+
} else {
|
|
1585
|
+
entry.worker?.stop();
|
|
1586
|
+
}
|
|
1508
1587
|
}
|
|
1509
1588
|
|
|
1510
1589
|
/** Retry a failed agent from its last checkpoint */
|
|
@@ -1967,12 +2046,38 @@ export class WorkspaceOrchestrator extends EventEmitter {
|
|
|
1967
2046
|
}
|
|
1968
2047
|
}
|
|
1969
2048
|
|
|
2049
|
+
// Write agent context file for hooks to read (workDir/.forge/agent-context.json)
|
|
2050
|
+
try {
|
|
2051
|
+
const forgeDir = join(workDir, '.forge');
|
|
2052
|
+
const { mkdirSync: mkdirS } = require('node:fs');
|
|
2053
|
+
mkdirS(forgeDir, { recursive: true });
|
|
2054
|
+
const ctxPath = join(forgeDir, 'agent-context.json');
|
|
2055
|
+
writeFileSync(ctxPath, JSON.stringify({
|
|
2056
|
+
workspaceId: this.workspaceId,
|
|
2057
|
+
agentId: config.id,
|
|
2058
|
+
agentLabel: config.label,
|
|
2059
|
+
forgePort: Number(process.env.PORT) || 8403,
|
|
2060
|
+
}, null, 2));
|
|
2061
|
+
console.log(`[daemon] ${config.label}: wrote agent-context.json to ${ctxPath}`);
|
|
2062
|
+
} catch (err: any) {
|
|
2063
|
+
console.error(`[daemon] ${config.label}: failed to write agent-context.json: ${err.message}`);
|
|
2064
|
+
}
|
|
2065
|
+
|
|
1970
2066
|
// Check if tmux session already exists
|
|
1971
2067
|
let sessionAlreadyExists = false;
|
|
1972
2068
|
try {
|
|
1973
2069
|
execSync(`tmux has-session -t "${sessionName}" 2>/dev/null`, { timeout: 3000 });
|
|
1974
2070
|
sessionAlreadyExists = true;
|
|
1975
2071
|
console.log(`[daemon] ${config.label}: persistent session already exists (${sessionName})`);
|
|
2072
|
+
// Ensure FORGE env vars are set in the tmux session environment
|
|
2073
|
+
// (for hooks that read them — set-environment makes them available to new processes in this session)
|
|
2074
|
+
try {
|
|
2075
|
+
execSync(`tmux set-environment -t "${sessionName}" FORGE_WORKSPACE_ID "${this.workspaceId}"`, { timeout: 3000 });
|
|
2076
|
+
execSync(`tmux set-environment -t "${sessionName}" FORGE_AGENT_ID "${config.id}"`, { timeout: 3000 });
|
|
2077
|
+
execSync(`tmux set-environment -t "${sessionName}" FORGE_PORT "${Number(process.env.PORT) || 8403}"`, { timeout: 3000 });
|
|
2078
|
+
} catch {}
|
|
2079
|
+
// Note: set-environment affects new processes in this tmux session.
|
|
2080
|
+
// Claude Code hooks run as child processes of the shell, which inherits tmux environment.
|
|
1976
2081
|
} catch {
|
|
1977
2082
|
// Create new tmux session and start the CLI agent
|
|
1978
2083
|
try {
|
|
@@ -27,9 +27,9 @@ export interface SessionMonitorEvent {
|
|
|
27
27
|
detail?: string; // e.g., result summary
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
const POLL_INTERVAL =
|
|
31
|
-
const IDLE_THRESHOLD =
|
|
32
|
-
const STABLE_THRESHOLD =
|
|
30
|
+
const POLL_INTERVAL = 3000; // check every 3s
|
|
31
|
+
const IDLE_THRESHOLD = 3540000; // 59min of no file change → check for result entry
|
|
32
|
+
const STABLE_THRESHOLD = 3600000; // 60min of no change → force done (fallback if hook missed)
|
|
33
33
|
|
|
34
34
|
export class SessionFileMonitor extends EventEmitter {
|
|
35
35
|
private timers = new Map<string, NodeJS.Timeout>();
|
|
@@ -86,6 +86,20 @@ export class SessionFileMonitor extends EventEmitter {
|
|
|
86
86
|
return this.currentState.get(agentId) || 'idle';
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Reset monitor state to idle and pause detection briefly.
|
|
91
|
+
* Call when orchestrator manually changes taskStatus (button/hook).
|
|
92
|
+
* Suppresses detection for 10s to avoid immediately flipping back.
|
|
93
|
+
*/
|
|
94
|
+
private suppressUntil = new Map<string, number>();
|
|
95
|
+
|
|
96
|
+
resetState(agentId: string): void {
|
|
97
|
+
this.currentState.set(agentId, 'idle');
|
|
98
|
+
this.lastStableTime.set(agentId, Date.now());
|
|
99
|
+
// Suppress state changes for 10s after manual reset
|
|
100
|
+
this.suppressUntil.set(agentId, Date.now() + 10_000);
|
|
101
|
+
}
|
|
102
|
+
|
|
89
103
|
/**
|
|
90
104
|
* Resolve session file path for a project + session ID.
|
|
91
105
|
*/
|
|
@@ -202,6 +216,12 @@ export class SessionFileMonitor extends EventEmitter {
|
|
|
202
216
|
const prev = this.currentState.get(agentId);
|
|
203
217
|
if (prev === state) return;
|
|
204
218
|
|
|
219
|
+
// Suppress state changes if recently reset by orchestrator
|
|
220
|
+
const suppressed = this.suppressUntil.get(agentId);
|
|
221
|
+
if (suppressed && Date.now() < suppressed) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
205
225
|
this.currentState.set(agentId, state);
|
|
206
226
|
const event: SessionMonitorEvent = { agentId, state, sessionFile: filePath, detail };
|
|
207
227
|
this.emit('stateChange', event);
|
|
@@ -63,6 +63,9 @@ export function installForgeSkills(
|
|
|
63
63
|
ensureForgePermissions(projectClaudeDir);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
// Install Stop hook in user-level settings (for agent completion detection)
|
|
67
|
+
installForgeStopHook(forgePort);
|
|
68
|
+
|
|
66
69
|
return { installed };
|
|
67
70
|
}
|
|
68
71
|
|
|
@@ -162,6 +165,90 @@ export function applyProfileToProject(
|
|
|
162
165
|
}
|
|
163
166
|
}
|
|
164
167
|
|
|
168
|
+
const FORGE_HOOK_MARKER = '# forge-stop-hook';
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Install a Stop hook in user-level ~/.claude/settings.json.
|
|
172
|
+
* When Claude Code finishes a turn, the hook notifies Forge via HTTP.
|
|
173
|
+
* Preserves existing user hooks. Creates backup before modifying.
|
|
174
|
+
*/
|
|
175
|
+
function installForgeStopHook(forgePort: number): void {
|
|
176
|
+
const settingsFile = join(homedir(), '.claude', 'settings.json');
|
|
177
|
+
const now = new Date();
|
|
178
|
+
const dateStr = `${now.getFullYear()}${String(now.getMonth()+1).padStart(2,'0')}${String(now.getDate()).padStart(2,'0')}-${String(now.getHours()).padStart(2,'0')}${String(now.getMinutes()).padStart(2,'0')}`;
|
|
179
|
+
const backupFile = join(homedir(), '.claude', `settings.json.forge-backup-${dateStr}`);
|
|
180
|
+
const daemonPort = forgePort + 2; // 8403 → 8405
|
|
181
|
+
|
|
182
|
+
// Hook reads agent context from .forge/agent-context.json in the project dir.
|
|
183
|
+
// This file is written by ensurePersistentSession for each agent's workDir.
|
|
184
|
+
// Falls back to env vars if file doesn't exist.
|
|
185
|
+
const hookCommand = `${FORGE_HOOK_MARKER}\nCTX_FILE="$(pwd)/.forge/agent-context.json"; if [ -f "$CTX_FILE" ]; then WS_ID=$(python3 -c "import json;print(json.load(open('$CTX_FILE')).get('workspaceId',''))" 2>/dev/null); AG_ID=$(python3 -c "import json;print(json.load(open('$CTX_FILE')).get('agentId',''))" 2>/dev/null); elif [ -n "$FORGE_WORKSPACE_ID" ]; then WS_ID="$FORGE_WORKSPACE_ID"; AG_ID="$FORGE_AGENT_ID"; fi; if [ -n "$WS_ID" ] && [ -n "$AG_ID" ]; then curl -s -X POST "http://localhost:${daemonPort}/workspace/$WS_ID/agents" -H "Content-Type: application/json" -d "{\\"action\\":\\"agent_done\\",\\"agentId\\":\\"$AG_ID\\"}" > /dev/null 2>&1 & fi`;
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
let settings: any = {};
|
|
189
|
+
if (existsSync(settingsFile)) {
|
|
190
|
+
const raw = readFileSync(settingsFile, 'utf-8');
|
|
191
|
+
settings = JSON.parse(raw);
|
|
192
|
+
|
|
193
|
+
// Check if hook already installed
|
|
194
|
+
// Remove old forge hook if present (will re-add with latest version)
|
|
195
|
+
if (settings.hooks?.Stop) {
|
|
196
|
+
settings.hooks.Stop = settings.hooks.Stop.filter((h: any) => {
|
|
197
|
+
if (h.command?.includes(FORGE_HOOK_MARKER) || h.command?.includes('agent_done')) return false;
|
|
198
|
+
if (h.hooks?.some((sub: any) => sub.command?.includes(FORGE_HOOK_MARKER) || sub.command?.includes('agent_done'))) return false;
|
|
199
|
+
return true;
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Backup before modifying
|
|
204
|
+
writeFileSync(backupFile, raw, 'utf-8');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (!settings.hooks) settings.hooks = {};
|
|
208
|
+
if (!settings.hooks.Stop) settings.hooks.Stop = [];
|
|
209
|
+
|
|
210
|
+
// Add forge hook (Claude Code hooks schema: matcher + hooks array)
|
|
211
|
+
settings.hooks.Stop.push({
|
|
212
|
+
matcher: '',
|
|
213
|
+
hooks: [{
|
|
214
|
+
type: 'command',
|
|
215
|
+
command: hookCommand,
|
|
216
|
+
timeout: 5000,
|
|
217
|
+
}],
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
mkdirSync(join(homedir(), '.claude'), { recursive: true });
|
|
221
|
+
writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
222
|
+
console.log('[skills] Installed Forge Stop hook in ~/.claude/settings.json');
|
|
223
|
+
} catch (err: any) {
|
|
224
|
+
console.error('[skills] Failed to install Stop hook:', err.message);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Remove Forge Stop hook from user-level settings (cleanup).
|
|
230
|
+
*/
|
|
231
|
+
export function removeForgeStopHook(): void {
|
|
232
|
+
const settingsFile = join(homedir(), '.claude', 'settings.json');
|
|
233
|
+
try {
|
|
234
|
+
if (!existsSync(settingsFile)) return;
|
|
235
|
+
const settings = JSON.parse(readFileSync(settingsFile, 'utf-8'));
|
|
236
|
+
if (!settings.hooks?.Stop) return;
|
|
237
|
+
|
|
238
|
+
settings.hooks.Stop = settings.hooks.Stop.filter((h: any) => {
|
|
239
|
+
// Remove entries matching either old flat format or new nested format
|
|
240
|
+
if (h.command?.includes(FORGE_HOOK_MARKER) || h.command?.includes('agent_done')) return false;
|
|
241
|
+
if (h.hooks?.some((sub: any) => sub.command?.includes(FORGE_HOOK_MARKER) || sub.command?.includes('agent_done'))) return false;
|
|
242
|
+
return true;
|
|
243
|
+
});
|
|
244
|
+
if (settings.hooks.Stop.length === 0) delete settings.hooks.Stop;
|
|
245
|
+
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
246
|
+
|
|
247
|
+
writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
248
|
+
console.log('[skills] Removed Forge Stop hook from ~/.claude/settings.json');
|
|
249
|
+
} catch {}
|
|
250
|
+
}
|
|
251
|
+
|
|
165
252
|
/**
|
|
166
253
|
* Check if forge skills are already installed for this agent.
|
|
167
254
|
*/
|
|
@@ -190,6 +190,12 @@ async function handleAgentsPost(id: string, body: any, res: ServerResponse): Pro
|
|
|
190
190
|
return jsonError(res, err.message);
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
|
+
case 'agent_done': {
|
|
194
|
+
// Called by Claude Code Stop hook — agent finished a turn
|
|
195
|
+
if (!agentId) return jsonError(res, 'agentId required');
|
|
196
|
+
orch.handleHookDone(agentId);
|
|
197
|
+
return json(res, { ok: true });
|
|
198
|
+
}
|
|
193
199
|
case 'run': {
|
|
194
200
|
if (!agentId) return jsonError(res, 'agentId required');
|
|
195
201
|
if (!orch.isDaemonActive()) return jsonError(res, 'Start daemon first before running agents');
|
|
@@ -226,6 +232,16 @@ async function handleAgentsPost(id: string, body: any, res: ServerResponse): Pro
|
|
|
226
232
|
orch.stopAgent(agentId);
|
|
227
233
|
return json(res, { ok: true });
|
|
228
234
|
}
|
|
235
|
+
case 'mark_done': {
|
|
236
|
+
if (!agentId) return jsonError(res, 'agentId required');
|
|
237
|
+
orch.markAgentDone(agentId, body.notify !== false);
|
|
238
|
+
return json(res, { ok: true });
|
|
239
|
+
}
|
|
240
|
+
case 'mark_failed': {
|
|
241
|
+
if (!agentId) return jsonError(res, 'agentId required');
|
|
242
|
+
orch.markAgentFailed(agentId, body.notify !== false);
|
|
243
|
+
return json(res, { ok: true });
|
|
244
|
+
}
|
|
229
245
|
case 'retry': {
|
|
230
246
|
if (!agentId) return jsonError(res, 'agentId required');
|
|
231
247
|
if (!orch.isDaemonActive()) return jsonError(res, 'Start daemon first before retrying agents');
|
package/package.json
CHANGED