@getforgeai/cli 0.1.0-beta.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/LICENSE +201 -0
- package/README.md +80 -0
- package/dist/bin/forgeai.d.ts +3 -0
- package/dist/bin/forgeai.js +8 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +180 -0
- package/dist/commands/auth-login.d.ts +4 -0
- package/dist/commands/auth-login.js +202 -0
- package/dist/commands/auth-logout.d.ts +2 -0
- package/dist/commands/auth-logout.js +41 -0
- package/dist/commands/auth-status.d.ts +4 -0
- package/dist/commands/auth-status.js +81 -0
- package/dist/commands/cli-list.d.ts +4 -0
- package/dist/commands/cli-list.js +40 -0
- package/dist/commands/cli-unlink.d.ts +4 -0
- package/dist/commands/cli-unlink.js +45 -0
- package/dist/commands/config-set.d.ts +5 -0
- package/dist/commands/config-set.js +63 -0
- package/dist/commands/connect.d.ts +9 -0
- package/dist/commands/connect.js +165 -0
- package/dist/commands/disconnect.d.ts +5 -0
- package/dist/commands/disconnect.js +126 -0
- package/dist/commands/journal-clear.d.ts +2 -0
- package/dist/commands/journal-clear.js +16 -0
- package/dist/commands/journal-list.d.ts +2 -0
- package/dist/commands/journal-list.js +20 -0
- package/dist/commands/journal-status.d.ts +2 -0
- package/dist/commands/journal-status.js +18 -0
- package/dist/commands/mcp-serve.d.ts +13 -0
- package/dist/commands/mcp-serve.js +74 -0
- package/dist/commands/org-list.d.ts +4 -0
- package/dist/commands/org-list.js +56 -0
- package/dist/commands/org-unlink.d.ts +4 -0
- package/dist/commands/org-unlink.js +63 -0
- package/dist/commands/reconnect.d.ts +4 -0
- package/dist/commands/reconnect.js +46 -0
- package/dist/commands/release-open.d.ts +2 -0
- package/dist/commands/release-open.js +43 -0
- package/dist/commands/session-respond.d.ts +2 -0
- package/dist/commands/session-respond.js +29 -0
- package/dist/commands/task-open.d.ts +2 -0
- package/dist/commands/task-open.js +42 -0
- package/dist/commands/vm.d.ts +21 -0
- package/dist/commands/vm.js +122 -0
- package/dist/commands/workflow-open.d.ts +2 -0
- package/dist/commands/workflow-open.js +43 -0
- package/dist/config/config-manager.d.ts +31 -0
- package/dist/config/config-manager.js +70 -0
- package/dist/config/token-manager.d.ts +11 -0
- package/dist/config/token-manager.js +87 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +22 -0
- package/dist/lib/auth-client.d.ts +1670 -0
- package/dist/lib/auth-client.js +10 -0
- package/dist/lib/branch-manager.d.ts +79 -0
- package/dist/lib/branch-manager.js +620 -0
- package/dist/lib/clone-and-checkout.d.ts +6 -0
- package/dist/lib/clone-and-checkout.js +41 -0
- package/dist/lib/connection-manager.d.ts +58 -0
- package/dist/lib/connection-manager.js +436 -0
- package/dist/lib/daemon-entry.d.ts +2 -0
- package/dist/lib/daemon-entry.js +210 -0
- package/dist/lib/daemon.d.ts +38 -0
- package/dist/lib/daemon.js +148 -0
- package/dist/lib/event-journal.d.ts +24 -0
- package/dist/lib/event-journal.js +76 -0
- package/dist/lib/ipc-client.d.ts +10 -0
- package/dist/lib/ipc-client.js +70 -0
- package/dist/lib/ipc-server.d.ts +36 -0
- package/dist/lib/ipc-server.js +70 -0
- package/dist/lib/node-version-guard.d.ts +8 -0
- package/dist/lib/node-version-guard.js +60 -0
- package/dist/lib/open-directory.d.ts +7 -0
- package/dist/lib/open-directory.js +41 -0
- package/dist/lib/repo-manager.d.ts +29 -0
- package/dist/lib/repo-manager.js +103 -0
- package/dist/lib/resilient-emitter.d.ts +53 -0
- package/dist/lib/resilient-emitter.js +160 -0
- package/dist/lib/rest-client.d.ts +164 -0
- package/dist/lib/rest-client.js +139 -0
- package/dist/lib/skill-manager.d.ts +23 -0
- package/dist/lib/skill-manager.js +167 -0
- package/dist/lib/socket-client.d.ts +68 -0
- package/dist/lib/socket-client.js +160 -0
- package/dist/mcp/mcp-probe.d.ts +11 -0
- package/dist/mcp/mcp-probe.js +106 -0
- package/dist/mcp/mcp-response.d.ts +29 -0
- package/dist/mcp/mcp-response.js +67 -0
- package/dist/mcp/mcp-server.d.ts +19 -0
- package/dist/mcp/mcp-server.js +76 -0
- package/dist/mcp/resources/project-conventions.d.ts +4 -0
- package/dist/mcp/resources/project-conventions.js +50 -0
- package/dist/mcp/resources/task-current.d.ts +4 -0
- package/dist/mcp/resources/task-current.js +51 -0
- package/dist/mcp/tools/forge-add-group-dependency.d.ts +4 -0
- package/dist/mcp/tools/forge-add-group-dependency.js +20 -0
- package/dist/mcp/tools/forge-add-task-dependency.d.ts +4 -0
- package/dist/mcp/tools/forge-add-task-dependency.js +20 -0
- package/dist/mcp/tools/forge-ask-human.d.ts +4 -0
- package/dist/mcp/tools/forge-ask-human.js +80 -0
- package/dist/mcp/tools/forge-column-done.d.ts +4 -0
- package/dist/mcp/tools/forge-column-done.js +46 -0
- package/dist/mcp/tools/forge-complete-step.d.ts +4 -0
- package/dist/mcp/tools/forge-complete-step.js +139 -0
- package/dist/mcp/tools/forge-create-task-group.d.ts +4 -0
- package/dist/mcp/tools/forge-create-task-group.js +22 -0
- package/dist/mcp/tools/forge-create-task.d.ts +4 -0
- package/dist/mcp/tools/forge-create-task.js +28 -0
- package/dist/mcp/tools/forge-delete-task-group.d.ts +4 -0
- package/dist/mcp/tools/forge-delete-task-group.js +17 -0
- package/dist/mcp/tools/forge-delete-task.d.ts +4 -0
- package/dist/mcp/tools/forge-delete-task.js +17 -0
- package/dist/mcp/tools/forge-get-task.d.ts +4 -0
- package/dist/mcp/tools/forge-get-task.js +17 -0
- package/dist/mcp/tools/forge-merge-back.d.ts +4 -0
- package/dist/mcp/tools/forge-merge-back.js +179 -0
- package/dist/mcp/tools/forge-release-merge.d.ts +4 -0
- package/dist/mcp/tools/forge-release-merge.js +159 -0
- package/dist/mcp/tools/forge-release-notes.d.ts +4 -0
- package/dist/mcp/tools/forge-release-notes.js +51 -0
- package/dist/mcp/tools/forge-release-tag.d.ts +4 -0
- package/dist/mcp/tools/forge-release-tag.js +125 -0
- package/dist/mcp/tools/forge-remove-group-dependency.d.ts +4 -0
- package/dist/mcp/tools/forge-remove-group-dependency.js +19 -0
- package/dist/mcp/tools/forge-remove-task-dependency.d.ts +4 -0
- package/dist/mcp/tools/forge-remove-task-dependency.js +19 -0
- package/dist/mcp/tools/forge-requeue.d.ts +4 -0
- package/dist/mcp/tools/forge-requeue.js +39 -0
- package/dist/mcp/tools/forge-submit-pr.d.ts +4 -0
- package/dist/mcp/tools/forge-submit-pr.js +68 -0
- package/dist/mcp/tools/forge-update-status.d.ts +4 -0
- package/dist/mcp/tools/forge-update-status.js +77 -0
- package/dist/mcp/tools/forge-update-task-group.d.ts +4 -0
- package/dist/mcp/tools/forge-update-task-group.js +21 -0
- package/dist/mcp/tools/forge-update-task.d.ts +4 -0
- package/dist/mcp/tools/forge-update-task.js +25 -0
- package/dist/mcp/tools/forge-validate-merge.d.ts +4 -0
- package/dist/mcp/tools/forge-validate-merge.js +194 -0
- package/dist/mcp/types.d.ts +39 -0
- package/dist/mcp/types.js +8 -0
- package/dist/methodology/artifact-manager.d.ts +14 -0
- package/dist/methodology/artifact-manager.js +55 -0
- package/dist/orchestrator/agent-registry.d.ts +53 -0
- package/dist/orchestrator/agent-registry.js +96 -0
- package/dist/orchestrator/agent-spawner.d.ts +112 -0
- package/dist/orchestrator/agent-spawner.js +1518 -0
- package/dist/orchestrator/connectors/claude-code-connector.d.ts +3 -0
- package/dist/orchestrator/connectors/claude-code-connector.js +60 -0
- package/dist/orchestrator/connectors/connector.d.ts +95 -0
- package/dist/orchestrator/connectors/connector.js +25 -0
- package/dist/orchestrator/connectors/docker-connector.d.ts +14 -0
- package/dist/orchestrator/connectors/docker-connector.js +337 -0
- package/dist/orchestrator/connectors/native-vm-connector.d.ts +11 -0
- package/dist/orchestrator/connectors/native-vm-connector.js +162 -0
- package/dist/orchestrator/connectors/opencode-connector.d.ts +3 -0
- package/dist/orchestrator/connectors/opencode-connector.js +39 -0
- package/dist/orchestrator/connectors/vm-connector.d.ts +3 -0
- package/dist/orchestrator/connectors/vm-connector.js +139 -0
- package/dist/orchestrator/dispatch-queue.d.ts +43 -0
- package/dist/orchestrator/dispatch-queue.js +93 -0
- package/dist/orchestrator/resume-handler.d.ts +34 -0
- package/dist/orchestrator/resume-handler.js +159 -0
- package/dist/orchestrator/stream-batcher.d.ts +16 -0
- package/dist/orchestrator/stream-batcher.js +50 -0
- package/dist/orchestrator/stream-json-extractor.d.ts +97 -0
- package/dist/orchestrator/stream-json-extractor.js +579 -0
- package/dist/vm/claude-token-manager.d.ts +36 -0
- package/dist/vm/claude-token-manager.js +223 -0
- package/dist/vm/docker-executor.d.ts +131 -0
- package/dist/vm/docker-executor.js +360 -0
- package/dist/vm/docker-process-proxy.d.ts +31 -0
- package/dist/vm/docker-process-proxy.js +105 -0
- package/dist/vm/forge-vm-agent.d.ts +11 -0
- package/dist/vm/forge-vm-agent.js +159 -0
- package/dist/vm/git-credentials.d.ts +43 -0
- package/dist/vm/git-credentials.js +53 -0
- package/dist/vm/image-manager.d.ts +7 -0
- package/dist/vm/image-manager.js +61 -0
- package/dist/vm/log.d.ts +9 -0
- package/dist/vm/log.js +18 -0
- package/dist/vm/mcp-bridge.d.ts +63 -0
- package/dist/vm/mcp-bridge.js +243 -0
- package/dist/vm/mcp-stub.d.ts +15 -0
- package/dist/vm/mcp-stub.js +67 -0
- package/dist/vm/native-addon.d.ts +69 -0
- package/dist/vm/native-addon.js +68 -0
- package/dist/vm/provider-resolver.d.ts +7 -0
- package/dist/vm/provider-resolver.js +27 -0
- package/dist/vm/providers/lima.d.ts +17 -0
- package/dist/vm/providers/lima.js +191 -0
- package/dist/vm/providers/qemu.d.ts +28 -0
- package/dist/vm/providers/qemu.js +260 -0
- package/dist/vm/providers/vfkit.d.ts +40 -0
- package/dist/vm/providers/vfkit.js +371 -0
- package/dist/vm/providers/wsl2.d.ts +27 -0
- package/dist/vm/providers/wsl2.js +226 -0
- package/dist/vm/session-manager.d.ts +130 -0
- package/dist/vm/session-manager.js +494 -0
- package/dist/vm/session-snapshot.d.ts +31 -0
- package/dist/vm/session-snapshot.js +180 -0
- package/dist/vm/types.d.ts +73 -0
- package/dist/vm/types.js +2 -0
- package/dist/vm/vm-helper-client.d.ts +81 -0
- package/dist/vm/vm-helper-client.js +335 -0
- package/dist/vm/vm-helper.d.ts +11 -0
- package/dist/vm/vm-helper.js +156 -0
- package/dist/vm/vm-ipc-protocol.d.ts +84 -0
- package/dist/vm/vm-ipc-protocol.js +9 -0
- package/dist/vm/vm-manager-facade.d.ts +23 -0
- package/dist/vm/vm-manager-facade.js +43 -0
- package/dist/vm/vm-manager.d.ts +74 -0
- package/dist/vm/vm-manager.js +515 -0
- package/dist/vm/vm-process-proxy.d.ts +34 -0
- package/dist/vm/vm-process-proxy.js +64 -0
- package/dist/vm/vsock-bridge.d.ts +44 -0
- package/dist/vm/vsock-bridge.js +140 -0
- package/dist/vm/vsock-protocol.d.ts +98 -0
- package/dist/vm/vsock-protocol.js +57 -0
- package/package.json +65 -0
- package/rootfs/Dockerfile +50 -0
- package/rootfs/forge-mcp-relay.mjs +158 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentConnector implementation that runs agents inside bubblewrap sandboxes
|
|
3
|
+
* within the shared native VM (Virtualization.framework via Swift N-API addon).
|
|
4
|
+
*
|
|
5
|
+
* Replaces the old vm-connector that used one VM per session with SSH streaming.
|
|
6
|
+
* This connector uses vsock for all communication and shares a single VM across
|
|
7
|
+
* all concurrent agent sessions.
|
|
8
|
+
*/
|
|
9
|
+
import { stopAgentProcess } from "./connector.js";
|
|
10
|
+
import { getVmManager } from "../../vm/vm-manager-facade.js";
|
|
11
|
+
import { vmLog } from "../../vm/log.js";
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Helpers
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
/**
|
|
16
|
+
* Build MCP config JSON that points to forge-mcp-relay running inside the sandbox.
|
|
17
|
+
* The relay connects to vminitd which bridges to the host via vsock.
|
|
18
|
+
*/
|
|
19
|
+
function buildMcpConfigJson(sessionIndex) {
|
|
20
|
+
const mcpPort = 20000 + sessionIndex;
|
|
21
|
+
return JSON.stringify({
|
|
22
|
+
mcpServers: {
|
|
23
|
+
forgeai: {
|
|
24
|
+
command: "node",
|
|
25
|
+
args: ["/opt/forgeai/forge-mcp-relay.mjs", String(mcpPort)],
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
}, null, 2);
|
|
29
|
+
}
|
|
30
|
+
function buildDefaultPrompt(skillName) {
|
|
31
|
+
return `You are a ForgeAI agent. Read and follow the ${skillName} skill in .claude/skills/${skillName}/SKILL.md to implement your assigned task.`;
|
|
32
|
+
}
|
|
33
|
+
function buildResumePrompt(basePrompt, ctx) {
|
|
34
|
+
const parts = [basePrompt];
|
|
35
|
+
if (ctx?.agentQuestion ?? ctx?.response) {
|
|
36
|
+
parts.push("\n\n## Human Interaction Resume\n");
|
|
37
|
+
if (ctx?.agentQuestion) {
|
|
38
|
+
parts.push(`**Your previous question:**\n${ctx.agentQuestion}\n`);
|
|
39
|
+
}
|
|
40
|
+
if (ctx?.response) {
|
|
41
|
+
parts.push(`**Human response:**\n${ctx.response}\n`);
|
|
42
|
+
}
|
|
43
|
+
parts.push("Continue your work using the human's response above. Do not re-ask the same question.");
|
|
44
|
+
}
|
|
45
|
+
return parts.join("\n");
|
|
46
|
+
}
|
|
47
|
+
/** Standard Claude CLI args for agent execution */
|
|
48
|
+
function buildAgentArgs(mcpConfigPath, prompt) {
|
|
49
|
+
return [
|
|
50
|
+
"--dangerously-skip-permissions",
|
|
51
|
+
"--mcp-config",
|
|
52
|
+
mcpConfigPath,
|
|
53
|
+
"-p",
|
|
54
|
+
prompt,
|
|
55
|
+
"--output-format",
|
|
56
|
+
"stream-json",
|
|
57
|
+
"--verbose",
|
|
58
|
+
"--include-partial-messages",
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Connector
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
export const nativeVmConnector = {
|
|
65
|
+
name: "native-vm-claude-code",
|
|
66
|
+
async start(params) {
|
|
67
|
+
const sm = getVmManager();
|
|
68
|
+
// 1. Ensure the shared VM is booted
|
|
69
|
+
await sm.ensureVmRunning();
|
|
70
|
+
// 2. Create session (allocates index, host dirs)
|
|
71
|
+
const sessionId = await sm.createSession(params.taskId);
|
|
72
|
+
const session = sm.getSessionByTaskId(params.taskId);
|
|
73
|
+
// 3. Clone repo inside the VM
|
|
74
|
+
await sm.cloneRepo(sessionId, params.repoUrl, params.taskBranch, params.workflowBaseBranch);
|
|
75
|
+
// 4. Prepare workspace files on the host (available in VM via VirtioFS)
|
|
76
|
+
const skillFiles = [];
|
|
77
|
+
if (params.skill) {
|
|
78
|
+
for (const file of params.skill.files) {
|
|
79
|
+
skillFiles.push({
|
|
80
|
+
relativePath: `${params.skill.name}/${file.path}`,
|
|
81
|
+
content: file.content,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
sm.prepareWorkspace(sessionId, {
|
|
86
|
+
contextJson: JSON.stringify(params.taskContext ?? {}, null, 2),
|
|
87
|
+
mcpConfigJson: buildMcpConfigJson(session.sessionIndex),
|
|
88
|
+
skillFiles,
|
|
89
|
+
});
|
|
90
|
+
// 5. Copy workspace files from VirtioFS mount into the workspace inside VM
|
|
91
|
+
await sm.exec("bash", [
|
|
92
|
+
"-c",
|
|
93
|
+
[
|
|
94
|
+
`mkdir -p /sessions/${sessionId}/workspace/.forgeai /sessions/${sessionId}/workspace/.claude`,
|
|
95
|
+
`cp /sessions/${sessionId}/context.json /sessions/${sessionId}/workspace/.forgeai/context.json`,
|
|
96
|
+
`cp /sessions/${sessionId}/mcp_config.json /sessions/${sessionId}/workspace/.claude/mcp_config.json`,
|
|
97
|
+
].join(" && "),
|
|
98
|
+
]);
|
|
99
|
+
if (skillFiles.length > 0) {
|
|
100
|
+
await sm.exec("bash", [
|
|
101
|
+
"-c",
|
|
102
|
+
[
|
|
103
|
+
`mkdir -p /sessions/${sessionId}/workspace/.claude/skills`,
|
|
104
|
+
`cp -r /sessions/${sessionId}/skills/. /sessions/${sessionId}/workspace/.claude/skills/`,
|
|
105
|
+
].join(" && "),
|
|
106
|
+
]);
|
|
107
|
+
}
|
|
108
|
+
// 6. Spawn agent in bubblewrap sandbox
|
|
109
|
+
const prompt = params.prompt ?? buildDefaultPrompt(params.skillName);
|
|
110
|
+
const agentArgs = buildAgentArgs("/workspace/.claude/mcp_config.json", prompt);
|
|
111
|
+
const env = {};
|
|
112
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
113
|
+
env.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
|
114
|
+
}
|
|
115
|
+
vmLog.magenta("native-vm-connector", `Spawning agent in sandbox ${sessionId}`);
|
|
116
|
+
const proxy = await sm.spawnAgent(params.taskId, "claude", agentArgs, env);
|
|
117
|
+
// Auto-destroy session on agent exit
|
|
118
|
+
proxy.once("exit", () => {
|
|
119
|
+
sm.destroySession(params.taskId).catch((err) => {
|
|
120
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
121
|
+
vmLog.warn("native-vm-connector", `Cleanup failed for ${sessionId}: ${message}`);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
return {
|
|
125
|
+
pid: proxy.pid,
|
|
126
|
+
process: proxy,
|
|
127
|
+
vmId: sessionId,
|
|
128
|
+
};
|
|
129
|
+
},
|
|
130
|
+
async stop(entry) {
|
|
131
|
+
// Stop the agent process using shared SIGTERM -> wait -> SIGKILL logic
|
|
132
|
+
await stopAgentProcess(entry);
|
|
133
|
+
// Destroy the VM session (cleanup sandbox + host files)
|
|
134
|
+
if (entry.vmId) {
|
|
135
|
+
const sm = getVmManager();
|
|
136
|
+
await sm.destroySession(entry.taskId);
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
async resume(params, ctx) {
|
|
140
|
+
const sm = getVmManager();
|
|
141
|
+
const session = sm.getSessionByTaskId(params.taskId);
|
|
142
|
+
// If the sandbox is still alive (idle window), spawn fresh agent in same session
|
|
143
|
+
if (session && session.status === "RUNNING") {
|
|
144
|
+
vmLog.cyan("native-vm-connector", `Resuming in existing sandbox ${session.id}`);
|
|
145
|
+
const basePrompt = params.prompt ?? buildDefaultPrompt(params.skillName);
|
|
146
|
+
const agentArgs = buildAgentArgs("/workspace/.claude/mcp_config.json", buildResumePrompt(basePrompt, ctx));
|
|
147
|
+
const proxy = await sm.spawnAgent(params.taskId, "claude", agentArgs);
|
|
148
|
+
return {
|
|
149
|
+
pid: proxy.pid,
|
|
150
|
+
process: proxy,
|
|
151
|
+
vmId: session.id,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
// No existing session — full start pipeline with resume context baked into prompt
|
|
155
|
+
const basePrompt = params.prompt ?? buildDefaultPrompt(params.skillName);
|
|
156
|
+
return this.start({
|
|
157
|
+
...params,
|
|
158
|
+
prompt: buildResumePrompt(basePrompt, ctx),
|
|
159
|
+
});
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
//# sourceMappingURL=native-vm-connector.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { stopAgentProcess } from "./connector.js";
|
|
3
|
+
function buildDefaultPrompt(skillName) {
|
|
4
|
+
return `You are a ForgeAI agent. Read and follow the ${skillName} skill in .opencode/skills/${skillName}/SKILL.md to implement your assigned task.`;
|
|
5
|
+
}
|
|
6
|
+
function spawnOpenCode(params, prompt) {
|
|
7
|
+
const child = spawn("opencode", ["--mcp-config", params.mcpConfigPath, "-p", prompt], {
|
|
8
|
+
cwd: params.worktreePath,
|
|
9
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
10
|
+
detached: false,
|
|
11
|
+
});
|
|
12
|
+
if (child.pid === undefined) {
|
|
13
|
+
throw new Error("Failed to spawn opencode process — no PID assigned");
|
|
14
|
+
}
|
|
15
|
+
return { pid: child.pid, process: child };
|
|
16
|
+
}
|
|
17
|
+
export const openCodeConnector = {
|
|
18
|
+
name: "opencode",
|
|
19
|
+
async start(params) {
|
|
20
|
+
return spawnOpenCode(params, params.prompt ?? buildDefaultPrompt(params.skillName));
|
|
21
|
+
},
|
|
22
|
+
stop: stopAgentProcess,
|
|
23
|
+
async resume(params, ctx) {
|
|
24
|
+
const basePrompt = params.prompt ?? buildDefaultPrompt(params.skillName);
|
|
25
|
+
const parts = [basePrompt];
|
|
26
|
+
if (ctx?.agentQuestion || ctx?.response) {
|
|
27
|
+
parts.push("\n\n## Human Interaction Resume\n");
|
|
28
|
+
if (ctx.agentQuestion) {
|
|
29
|
+
parts.push(`**Your previous question:**\n${ctx.agentQuestion}\n`);
|
|
30
|
+
}
|
|
31
|
+
if (ctx.response) {
|
|
32
|
+
parts.push(`**Human response:**\n${ctx.response}\n`);
|
|
33
|
+
}
|
|
34
|
+
parts.push("Continue your work using the human's response above. Do not re-ask the same question.");
|
|
35
|
+
}
|
|
36
|
+
return spawnOpenCode(params, parts.join("\n"));
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
//# sourceMappingURL=opencode-connector.js.map
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { stopAgentProcess } from "./connector.js";
|
|
2
|
+
import { getVmManager } from "../../vm/vm-manager.js";
|
|
3
|
+
import { vmLog } from "../../vm/log.js";
|
|
4
|
+
function buildDefaultPrompt(skillName) {
|
|
5
|
+
return `You are a ForgeAI agent. Read and follow the ${skillName} skill in .claude/skills/${skillName}/SKILL.md to implement your assigned task.`;
|
|
6
|
+
}
|
|
7
|
+
function buildResumePrompt(basePrompt, ctx) {
|
|
8
|
+
const parts = [basePrompt];
|
|
9
|
+
if (ctx?.agentQuestion || ctx?.response) {
|
|
10
|
+
parts.push("\n\n## Human Interaction Resume\n");
|
|
11
|
+
if (ctx.agentQuestion) {
|
|
12
|
+
parts.push(`**Your previous question:**\n${ctx.agentQuestion}\n`);
|
|
13
|
+
}
|
|
14
|
+
if (ctx.response) {
|
|
15
|
+
parts.push(`**Human response:**\n${ctx.response}\n`);
|
|
16
|
+
}
|
|
17
|
+
parts.push("Continue your work using the human's response above. Do not re-ask the same question.");
|
|
18
|
+
}
|
|
19
|
+
return parts.join("\n");
|
|
20
|
+
}
|
|
21
|
+
/** MCP tunnel port inside the VM (must match MCP_TUNNEL_PORT in vm-manager) */
|
|
22
|
+
const VM_MCP_PORT = 19876;
|
|
23
|
+
function buildMcpConfigJson() {
|
|
24
|
+
// MCP config points to a Node.js one-liner that connects to the
|
|
25
|
+
// reverse-tunneled TCP port where the host's `forge mcp` is listening.
|
|
26
|
+
const config = {
|
|
27
|
+
mcpServers: {
|
|
28
|
+
forgeai: {
|
|
29
|
+
command: "node",
|
|
30
|
+
args: [
|
|
31
|
+
"-e",
|
|
32
|
+
`const s=require("net").createConnection(${VM_MCP_PORT},"127.0.0.1");process.stdin.pipe(s);s.pipe(process.stdout);s.on("error",()=>process.exit(1));`,
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
return JSON.stringify(config, null, 2);
|
|
38
|
+
}
|
|
39
|
+
export const vmConnector = {
|
|
40
|
+
name: "vm-claude-code",
|
|
41
|
+
async start(params) {
|
|
42
|
+
const vmManager = getVmManager();
|
|
43
|
+
// 1. Create session
|
|
44
|
+
const sessionId = await vmManager.createSession(params.taskId, {
|
|
45
|
+
repoUrl: params.repoUrl,
|
|
46
|
+
branch: params.taskBranch,
|
|
47
|
+
workflowBaseBranch: params.workflowBaseBranch,
|
|
48
|
+
workflowContext: params.workflowContext,
|
|
49
|
+
});
|
|
50
|
+
// 2. Boot VM
|
|
51
|
+
await vmManager.bootVm(sessionId);
|
|
52
|
+
// 3. Clone repo + checkout branch inside VM (creates branch if it doesn't exist)
|
|
53
|
+
await vmManager.cloneRepo(sessionId, params.repoUrl, params.taskBranch, params.workflowBaseBranch);
|
|
54
|
+
// 4. Prepare workspace (context, skills, MCP config)
|
|
55
|
+
const skillFiles = [];
|
|
56
|
+
if (params.skill) {
|
|
57
|
+
for (const file of params.skill.files) {
|
|
58
|
+
skillFiles.push({
|
|
59
|
+
relativePath: `${params.skill.name}/${file.path}`,
|
|
60
|
+
content: file.content,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
await vmManager.prepareWorkspace(sessionId, {
|
|
65
|
+
contextJson: JSON.stringify(params.taskContext ?? {}, null, 2),
|
|
66
|
+
mcpConfigJson: buildMcpConfigJson(),
|
|
67
|
+
skillFiles,
|
|
68
|
+
});
|
|
69
|
+
// 5. Spawn agent inside VM (with MCP bridge on host via reverse tunnel)
|
|
70
|
+
const prompt = params.prompt ?? buildDefaultPrompt(params.skillName);
|
|
71
|
+
const agentArgs = [
|
|
72
|
+
"--dangerously-skip-permissions",
|
|
73
|
+
"--mcp-config",
|
|
74
|
+
"/workspace/.claude/mcp_config.json",
|
|
75
|
+
"-p",
|
|
76
|
+
prompt,
|
|
77
|
+
"--output-format",
|
|
78
|
+
"stream-json",
|
|
79
|
+
"--verbose",
|
|
80
|
+
"--include-partial-messages",
|
|
81
|
+
];
|
|
82
|
+
vmLog.magenta("vm-connector", `Spawning agent in VM ${sessionId}\n prompt="${prompt.slice(0, 200)}${prompt.length > 200 ? "..." : ""}"`);
|
|
83
|
+
const agent = await vmManager.spawnAgent(sessionId, "claude", agentArgs, {
|
|
84
|
+
taskId: params.taskId,
|
|
85
|
+
projectId: params.projectId,
|
|
86
|
+
orgId: params.orgId,
|
|
87
|
+
workflowId: params.workflowContext?.workflowId,
|
|
88
|
+
stepId: params.workflowContext?.stepId,
|
|
89
|
+
});
|
|
90
|
+
return {
|
|
91
|
+
pid: agent.pid,
|
|
92
|
+
process: agent.process,
|
|
93
|
+
vmId: sessionId,
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
async stop(entry) {
|
|
97
|
+
// Stop the agent process using shared logic
|
|
98
|
+
await stopAgentProcess(entry);
|
|
99
|
+
// Destroy the VM session
|
|
100
|
+
if (entry.vmId) {
|
|
101
|
+
const vmManager = getVmManager();
|
|
102
|
+
await vmManager.destroySession(entry.vmId);
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
async resume(params, ctx) {
|
|
106
|
+
const vmManager = getVmManager();
|
|
107
|
+
const session = vmManager.getSession(params.taskId);
|
|
108
|
+
// If VM is still running (idle window), spawn fresh agent in same VM
|
|
109
|
+
if (session && session.status === "RUNNING") {
|
|
110
|
+
vmLog.cyan("vm-connector", `Resuming agent in existing VM ${session.id} for task ${params.taskId}`);
|
|
111
|
+
const basePrompt = params.prompt ?? buildDefaultPrompt(params.skillName);
|
|
112
|
+
const resumePrompt = buildResumePrompt(basePrompt, ctx);
|
|
113
|
+
const agentArgs = [
|
|
114
|
+
"--dangerously-skip-permissions",
|
|
115
|
+
"--mcp-config",
|
|
116
|
+
"/workspace/.claude/mcp_config.json",
|
|
117
|
+
"-p",
|
|
118
|
+
resumePrompt,
|
|
119
|
+
"--output-format",
|
|
120
|
+
"stream-json",
|
|
121
|
+
"--verbose",
|
|
122
|
+
"--include-partial-messages",
|
|
123
|
+
];
|
|
124
|
+
const agent = await vmManager.spawnAgent(session.id, "claude", agentArgs);
|
|
125
|
+
return {
|
|
126
|
+
pid: agent.pid,
|
|
127
|
+
process: agent.process,
|
|
128
|
+
vmId: session.id,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
// VM not running — full start pipeline
|
|
132
|
+
vmLog.cyan("vm-connector", `No running VM for task ${params.taskId}, starting fresh`);
|
|
133
|
+
// Rebuild params with resume context baked into prompt
|
|
134
|
+
const basePrompt = params.prompt ?? buildDefaultPrompt(params.skillName);
|
|
135
|
+
const resumePrompt = buildResumePrompt(basePrompt, ctx);
|
|
136
|
+
return this.start({ ...params, prompt: resumePrompt });
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
//# sourceMappingURL=vm-connector.js.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { DispatchParams } from "./agent-spawner.js";
|
|
2
|
+
export type DrainContext = {
|
|
3
|
+
spawnAgent: (params: DispatchParams) => Promise<void>;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* In-memory FIFO queue that buffers incoming task:dispatch events
|
|
7
|
+
* when the agent registry is at capacity.
|
|
8
|
+
*
|
|
9
|
+
* Global singleton — agent slots are machine-level, shared across all org connections.
|
|
10
|
+
*/
|
|
11
|
+
export declare class DispatchQueue {
|
|
12
|
+
private readonly queue;
|
|
13
|
+
private readonly maxSize;
|
|
14
|
+
private paused;
|
|
15
|
+
private resumeTimer;
|
|
16
|
+
constructor(maxSize?: number);
|
|
17
|
+
/**
|
|
18
|
+
* Pause the queue (e.g. due to billing cooldown).
|
|
19
|
+
* While paused, drainNext() is a no-op and spawnAgent calls should be skipped.
|
|
20
|
+
* Optionally schedule auto-resume based on a future reset timestamp.
|
|
21
|
+
*/
|
|
22
|
+
pause(resetAt?: string): void;
|
|
23
|
+
resume(): void;
|
|
24
|
+
isPaused(): boolean;
|
|
25
|
+
enqueue(params: DispatchParams): void;
|
|
26
|
+
/**
|
|
27
|
+
* Pop head of queue and attempt to spawn via the provided context.
|
|
28
|
+
* If spawn fails, the item is NOT re-enqueued (spawn errors are terminal).
|
|
29
|
+
*
|
|
30
|
+
* DEVIATION FROM SPEC (subtask 1.3): The original spec suggested "re-enqueue at head"
|
|
31
|
+
* on spawn failure. This is intentionally NOT implemented because:
|
|
32
|
+
* - Re-enqueuing failed spawns creates infinite retry loops (spawn will fail again)
|
|
33
|
+
* - Better to fail fast with observable logs than silently retry
|
|
34
|
+
* - Spawn failures are terminal errors (bad git state, missing config, etc.)
|
|
35
|
+
* - If the spawn environment recovers, the next dispatch event will retry
|
|
36
|
+
*/
|
|
37
|
+
drainNext(ctx: DrainContext): Promise<void>;
|
|
38
|
+
size(): number;
|
|
39
|
+
clear(): void;
|
|
40
|
+
}
|
|
41
|
+
/** Global singleton instance */
|
|
42
|
+
export declare const dispatchQueue: DispatchQueue;
|
|
43
|
+
//# sourceMappingURL=dispatch-queue.d.ts.map
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
const DEFAULT_MAX_QUEUE_SIZE = 20;
|
|
3
|
+
/**
|
|
4
|
+
* In-memory FIFO queue that buffers incoming task:dispatch events
|
|
5
|
+
* when the agent registry is at capacity.
|
|
6
|
+
*
|
|
7
|
+
* Global singleton — agent slots are machine-level, shared across all org connections.
|
|
8
|
+
*/
|
|
9
|
+
export class DispatchQueue {
|
|
10
|
+
queue = [];
|
|
11
|
+
maxSize;
|
|
12
|
+
paused = false;
|
|
13
|
+
resumeTimer = null;
|
|
14
|
+
constructor(maxSize = DEFAULT_MAX_QUEUE_SIZE) {
|
|
15
|
+
this.maxSize = maxSize;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Pause the queue (e.g. due to billing cooldown).
|
|
19
|
+
* While paused, drainNext() is a no-op and spawnAgent calls should be skipped.
|
|
20
|
+
* Optionally schedule auto-resume based on a future reset timestamp.
|
|
21
|
+
*/
|
|
22
|
+
pause(resetAt) {
|
|
23
|
+
this.paused = true;
|
|
24
|
+
if (this.resumeTimer) {
|
|
25
|
+
clearTimeout(this.resumeTimer);
|
|
26
|
+
this.resumeTimer = null;
|
|
27
|
+
}
|
|
28
|
+
if (resetAt) {
|
|
29
|
+
const resumeMs = new Date(resetAt).getTime() - Date.now() + 60_000; // +1min buffer
|
|
30
|
+
if (resumeMs > 0 && resumeMs < 86_400_000) {
|
|
31
|
+
this.resumeTimer = setTimeout(() => {
|
|
32
|
+
this.resume();
|
|
33
|
+
console.log(chalk.green("[dispatch-queue] Cooldown expired — queue auto-resumed"));
|
|
34
|
+
}, resumeMs);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
console.log(chalk.yellow("[dispatch-queue] Queue paused (billing cooldown)"));
|
|
38
|
+
}
|
|
39
|
+
resume() {
|
|
40
|
+
this.paused = false;
|
|
41
|
+
if (this.resumeTimer) {
|
|
42
|
+
clearTimeout(this.resumeTimer);
|
|
43
|
+
this.resumeTimer = null;
|
|
44
|
+
}
|
|
45
|
+
console.log(chalk.green("[dispatch-queue] Queue resumed"));
|
|
46
|
+
}
|
|
47
|
+
isPaused() {
|
|
48
|
+
return this.paused;
|
|
49
|
+
}
|
|
50
|
+
enqueue(params) {
|
|
51
|
+
if (this.queue.length >= this.maxSize) {
|
|
52
|
+
const dropped = this.queue.shift();
|
|
53
|
+
console.warn(chalk.yellow(`[dispatch-queue] Queue full (${this.maxSize}), dropping oldest dispatch for task ${dropped?.taskId}`));
|
|
54
|
+
}
|
|
55
|
+
this.queue.push(params);
|
|
56
|
+
console.log(chalk.dim(`[dispatch-queue] Enqueued task ${params.taskId} (queue size: ${this.queue.length})`));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Pop head of queue and attempt to spawn via the provided context.
|
|
60
|
+
* If spawn fails, the item is NOT re-enqueued (spawn errors are terminal).
|
|
61
|
+
*
|
|
62
|
+
* DEVIATION FROM SPEC (subtask 1.3): The original spec suggested "re-enqueue at head"
|
|
63
|
+
* on spawn failure. This is intentionally NOT implemented because:
|
|
64
|
+
* - Re-enqueuing failed spawns creates infinite retry loops (spawn will fail again)
|
|
65
|
+
* - Better to fail fast with observable logs than silently retry
|
|
66
|
+
* - Spawn failures are terminal errors (bad git state, missing config, etc.)
|
|
67
|
+
* - If the spawn environment recovers, the next dispatch event will retry
|
|
68
|
+
*/
|
|
69
|
+
async drainNext(ctx) {
|
|
70
|
+
if (this.paused) {
|
|
71
|
+
console.log(chalk.dim("[dispatch-queue] Drain skipped — queue is paused"));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (this.queue.length === 0)
|
|
75
|
+
return;
|
|
76
|
+
const next = this.queue.shift();
|
|
77
|
+
console.log(chalk.dim(`[dispatch-queue] Draining next: task ${next.taskId} (remaining: ${this.queue.length})`));
|
|
78
|
+
await ctx.spawnAgent(next);
|
|
79
|
+
}
|
|
80
|
+
size() {
|
|
81
|
+
return this.queue.length;
|
|
82
|
+
}
|
|
83
|
+
clear() {
|
|
84
|
+
const count = this.queue.length;
|
|
85
|
+
this.queue.length = 0;
|
|
86
|
+
if (count > 0) {
|
|
87
|
+
console.log(chalk.dim(`[dispatch-queue] Cleared ${count} queued dispatches`));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** Global singleton instance */
|
|
92
|
+
export const dispatchQueue = new DispatchQueue();
|
|
93
|
+
//# sourceMappingURL=dispatch-queue.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { AgentRegistry } from "./agent-registry.js";
|
|
2
|
+
import type { createRestClient } from "../lib/rest-client.js";
|
|
3
|
+
import type { createSocketClient } from "../lib/socket-client.js";
|
|
4
|
+
import type { DispatchQueue } from "./dispatch-queue.js";
|
|
5
|
+
export type ResumePayload = {
|
|
6
|
+
taskId: string;
|
|
7
|
+
projectId?: string;
|
|
8
|
+
orgId: string;
|
|
9
|
+
response?: string;
|
|
10
|
+
/** Original agent question (before clear) — for context in the resume prompt */
|
|
11
|
+
agentQuestion?: string;
|
|
12
|
+
/** Original task command from skill pipeline — so the agent keeps its instruction */
|
|
13
|
+
taskCommand?: string;
|
|
14
|
+
skillSlug?: string;
|
|
15
|
+
taskSlug?: string;
|
|
16
|
+
projectSlug?: string;
|
|
17
|
+
taskBranch?: string;
|
|
18
|
+
repoUrl?: string;
|
|
19
|
+
};
|
|
20
|
+
export type ResumeContext = {
|
|
21
|
+
restClient: ReturnType<typeof createRestClient>;
|
|
22
|
+
socketClient: ReturnType<typeof createSocketClient>;
|
|
23
|
+
agentType: string;
|
|
24
|
+
agentSlots: number;
|
|
25
|
+
registry: AgentRegistry;
|
|
26
|
+
queue: DispatchQueue;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Handle a task:resume event — uses VM-based execution to resume an agent.
|
|
30
|
+
* The VM connector handles environment setup (boot, clone, workspace, spawn).
|
|
31
|
+
* Uses slot reservation to prevent over-allocation during async pipeline.
|
|
32
|
+
*/
|
|
33
|
+
export declare function handleResume(payload: ResumePayload, ctx: ResumeContext): Promise<void>;
|
|
34
|
+
//# sourceMappingURL=resume-handler.d.ts.map
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { DEFAULT_SKILL_NAME, resolveConnector, attachProcessHandlers, spawnAgent, } from "./agent-spawner.js";
|
|
3
|
+
import { pullSkill } from "../lib/skill-manager.js";
|
|
4
|
+
import { getVmManager } from "../vm/vm-manager-facade.js";
|
|
5
|
+
// Helper: Stop and replace existing agent for same taskId
|
|
6
|
+
async function stopExistingAgent(payload, registry) {
|
|
7
|
+
const existingEntry = registry.getByTaskId(payload.taskId);
|
|
8
|
+
if (existingEntry?.status === "RUNNING") {
|
|
9
|
+
console.log(chalk.cyan(`[resume] Stopping existing agent for task ${payload.taskId} (PID: ${existingEntry.pid}) before resume`));
|
|
10
|
+
registry.unregister(payload.taskId);
|
|
11
|
+
try {
|
|
12
|
+
const connector = resolveConnector(existingEntry.connectorName);
|
|
13
|
+
await connector.stop(existingEntry);
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
console.warn(chalk.yellow(`[resume] Error stopping old agent for ${payload.taskId}: ${err instanceof Error ? err.message : String(err)}`));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
// Helper: Handle case when no slot capacity available
|
|
21
|
+
function enqueueWhenNoCapacity(payload, ctx) {
|
|
22
|
+
console.warn(chalk.yellow(`[resume] No capacity for task ${payload.taskId}, enqueuing`));
|
|
23
|
+
if (payload.projectId) {
|
|
24
|
+
ctx.queue.enqueue({
|
|
25
|
+
taskId: payload.taskId,
|
|
26
|
+
projectId: payload.projectId,
|
|
27
|
+
assigneeId: "",
|
|
28
|
+
orgId: payload.orgId,
|
|
29
|
+
taskSlug: payload.taskSlug,
|
|
30
|
+
projectSlug: payload.projectSlug,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
ctx.socketClient.emitAgentStatus({
|
|
34
|
+
agentId: payload.taskId,
|
|
35
|
+
status: "QUEUED",
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Handle a task:resume event — uses VM-based execution to resume an agent.
|
|
40
|
+
* The VM connector handles environment setup (boot, clone, workspace, spawn).
|
|
41
|
+
* Uses slot reservation to prevent over-allocation during async pipeline.
|
|
42
|
+
*/
|
|
43
|
+
export async function handleResume(payload, ctx) {
|
|
44
|
+
const { registry } = ctx;
|
|
45
|
+
// Guard: if a spawn is already in flight for this taskId, skip
|
|
46
|
+
if (registry.hasPendingReservation(payload.taskId)) {
|
|
47
|
+
console.log(chalk.dim(`[resume] Spawn already in flight for task ${payload.taskId}, skipping duplicate`));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
// Stop-and-replace: if an agent is already running for this taskId,
|
|
51
|
+
// stop it first to prevent two agents in the same environment.
|
|
52
|
+
await stopExistingAgent(payload, registry);
|
|
53
|
+
// Reserve slot (atomic capacity check)
|
|
54
|
+
if (!registry.reserveSlot(payload.taskId)) {
|
|
55
|
+
enqueueWhenNoCapacity(payload, ctx);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
if (!payload.projectId) {
|
|
60
|
+
throw new Error(`Cannot resume task ${payload.taskId} — no projectId provided`);
|
|
61
|
+
}
|
|
62
|
+
const validatedPayload = payload;
|
|
63
|
+
// Check if VM session exists (same-CLI detection)
|
|
64
|
+
const sm = getVmManager();
|
|
65
|
+
const session = sm.getSessionByTaskId(payload.taskId);
|
|
66
|
+
const isSameCli = session?.status === "RUNNING";
|
|
67
|
+
if (isSameCli) {
|
|
68
|
+
console.log(chalk.cyan(`[resume] Same-CLI resume for task ${payload.taskId} — VM session exists`));
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
console.log(chalk.cyan(`[resume] Cross-CLI resume for task ${payload.taskId} — will boot new VM`));
|
|
72
|
+
}
|
|
73
|
+
// Pull skill on host
|
|
74
|
+
const resumeSkillName = payload.skillSlug ?? DEFAULT_SKILL_NAME;
|
|
75
|
+
const skill = await pullSkill(resumeSkillName, ctx.restClient);
|
|
76
|
+
// Resolve connector
|
|
77
|
+
const connector = resolveConnector(ctx.agentType);
|
|
78
|
+
const taskSlug = payload.taskSlug ?? payload.taskId;
|
|
79
|
+
const taskBranch = payload.taskBranch ?? taskSlug;
|
|
80
|
+
// If no session exists, try to fetch snapshot for --continue resume
|
|
81
|
+
let sessionSnapshotBase64;
|
|
82
|
+
if (!isSameCli) {
|
|
83
|
+
try {
|
|
84
|
+
const snapshotData = await new Promise((resolve) => {
|
|
85
|
+
const timeout = setTimeout(() => resolve(null), 10_000);
|
|
86
|
+
ctx.socketClient.socket.emit("workflow:step:snapshot:fetch", { workflowId: payload.taskId, stepId: payload.taskId }, (data) => {
|
|
87
|
+
clearTimeout(timeout);
|
|
88
|
+
resolve(data);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
if (snapshotData?.snapshot) {
|
|
92
|
+
sessionSnapshotBase64 = snapshotData.snapshot;
|
|
93
|
+
console.log(chalk.cyan(`[resume] Snapshot found for task ${payload.taskId} — will restore with --continue`));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// No snapshot available
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const vmSpawnParams = {
|
|
101
|
+
taskId: validatedPayload.taskId,
|
|
102
|
+
projectId: validatedPayload.projectId,
|
|
103
|
+
orgId: validatedPayload.orgId,
|
|
104
|
+
workingDir: "/workspace",
|
|
105
|
+
mcpConfigPath: "/workspace/.claude/mcp_config.json",
|
|
106
|
+
skillName: skill?.name ?? resumeSkillName,
|
|
107
|
+
prompt: validatedPayload.taskCommand,
|
|
108
|
+
repoUrl: payload.repoUrl,
|
|
109
|
+
taskBranch,
|
|
110
|
+
skill: skill ? { name: skill.name, files: skill.files } : undefined,
|
|
111
|
+
sessionSnapshotBase64,
|
|
112
|
+
};
|
|
113
|
+
const result = await connector.resume(vmSpawnParams, {
|
|
114
|
+
response: validatedPayload.response,
|
|
115
|
+
agentQuestion: validatedPayload.agentQuestion,
|
|
116
|
+
});
|
|
117
|
+
// Confirm slot + register + attach handlers
|
|
118
|
+
registry.confirmSlot(payload.taskId, {
|
|
119
|
+
taskId: payload.taskId,
|
|
120
|
+
pid: result.pid,
|
|
121
|
+
process: result.process,
|
|
122
|
+
connectorName: connector.name,
|
|
123
|
+
workingDir: "/workspace",
|
|
124
|
+
orgId: payload.orgId,
|
|
125
|
+
startedAt: new Date().toISOString(),
|
|
126
|
+
status: "RUNNING",
|
|
127
|
+
vmId: result.vmId,
|
|
128
|
+
});
|
|
129
|
+
ctx.socketClient.emitAgentStatus({
|
|
130
|
+
agentId: payload.taskId,
|
|
131
|
+
status: "RUNNING",
|
|
132
|
+
});
|
|
133
|
+
console.log(chalk.green(`[resume] Agent resumed for task ${payload.taskId} (PID: ${result.pid}, same-cli: ${isSameCli})`));
|
|
134
|
+
// Attach process handlers
|
|
135
|
+
const boundSpawnAgent = (p) => spawnAgent(p, {
|
|
136
|
+
restClient: ctx.restClient,
|
|
137
|
+
socketClient: ctx.socketClient,
|
|
138
|
+
agentType: ctx.agentType,
|
|
139
|
+
agentSlots: ctx.agentSlots,
|
|
140
|
+
queue: ctx.queue,
|
|
141
|
+
});
|
|
142
|
+
attachProcessHandlers(result.process, {
|
|
143
|
+
taskId: payload.taskId,
|
|
144
|
+
projectId: validatedPayload.projectId,
|
|
145
|
+
registry: ctx.registry,
|
|
146
|
+
socketClient: ctx.socketClient,
|
|
147
|
+
restClient: ctx.restClient,
|
|
148
|
+
queue: ctx.queue,
|
|
149
|
+
spawnAgentFn: boundSpawnAgent,
|
|
150
|
+
label: "resume",
|
|
151
|
+
agentSlots: ctx.agentSlots,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
registry.releaseSlot(payload.taskId);
|
|
156
|
+
throw err;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=resume-handler.js.map
|