@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.
Files changed (221) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +80 -0
  3. package/dist/bin/forgeai.d.ts +3 -0
  4. package/dist/bin/forgeai.js +8 -0
  5. package/dist/cli.d.ts +4 -0
  6. package/dist/cli.js +180 -0
  7. package/dist/commands/auth-login.d.ts +4 -0
  8. package/dist/commands/auth-login.js +202 -0
  9. package/dist/commands/auth-logout.d.ts +2 -0
  10. package/dist/commands/auth-logout.js +41 -0
  11. package/dist/commands/auth-status.d.ts +4 -0
  12. package/dist/commands/auth-status.js +81 -0
  13. package/dist/commands/cli-list.d.ts +4 -0
  14. package/dist/commands/cli-list.js +40 -0
  15. package/dist/commands/cli-unlink.d.ts +4 -0
  16. package/dist/commands/cli-unlink.js +45 -0
  17. package/dist/commands/config-set.d.ts +5 -0
  18. package/dist/commands/config-set.js +63 -0
  19. package/dist/commands/connect.d.ts +9 -0
  20. package/dist/commands/connect.js +165 -0
  21. package/dist/commands/disconnect.d.ts +5 -0
  22. package/dist/commands/disconnect.js +126 -0
  23. package/dist/commands/journal-clear.d.ts +2 -0
  24. package/dist/commands/journal-clear.js +16 -0
  25. package/dist/commands/journal-list.d.ts +2 -0
  26. package/dist/commands/journal-list.js +20 -0
  27. package/dist/commands/journal-status.d.ts +2 -0
  28. package/dist/commands/journal-status.js +18 -0
  29. package/dist/commands/mcp-serve.d.ts +13 -0
  30. package/dist/commands/mcp-serve.js +74 -0
  31. package/dist/commands/org-list.d.ts +4 -0
  32. package/dist/commands/org-list.js +56 -0
  33. package/dist/commands/org-unlink.d.ts +4 -0
  34. package/dist/commands/org-unlink.js +63 -0
  35. package/dist/commands/reconnect.d.ts +4 -0
  36. package/dist/commands/reconnect.js +46 -0
  37. package/dist/commands/release-open.d.ts +2 -0
  38. package/dist/commands/release-open.js +43 -0
  39. package/dist/commands/session-respond.d.ts +2 -0
  40. package/dist/commands/session-respond.js +29 -0
  41. package/dist/commands/task-open.d.ts +2 -0
  42. package/dist/commands/task-open.js +42 -0
  43. package/dist/commands/vm.d.ts +21 -0
  44. package/dist/commands/vm.js +122 -0
  45. package/dist/commands/workflow-open.d.ts +2 -0
  46. package/dist/commands/workflow-open.js +43 -0
  47. package/dist/config/config-manager.d.ts +31 -0
  48. package/dist/config/config-manager.js +70 -0
  49. package/dist/config/token-manager.d.ts +11 -0
  50. package/dist/config/token-manager.js +87 -0
  51. package/dist/index.d.ts +11 -0
  52. package/dist/index.js +22 -0
  53. package/dist/lib/auth-client.d.ts +1670 -0
  54. package/dist/lib/auth-client.js +10 -0
  55. package/dist/lib/branch-manager.d.ts +79 -0
  56. package/dist/lib/branch-manager.js +620 -0
  57. package/dist/lib/clone-and-checkout.d.ts +6 -0
  58. package/dist/lib/clone-and-checkout.js +41 -0
  59. package/dist/lib/connection-manager.d.ts +58 -0
  60. package/dist/lib/connection-manager.js +436 -0
  61. package/dist/lib/daemon-entry.d.ts +2 -0
  62. package/dist/lib/daemon-entry.js +210 -0
  63. package/dist/lib/daemon.d.ts +38 -0
  64. package/dist/lib/daemon.js +148 -0
  65. package/dist/lib/event-journal.d.ts +24 -0
  66. package/dist/lib/event-journal.js +76 -0
  67. package/dist/lib/ipc-client.d.ts +10 -0
  68. package/dist/lib/ipc-client.js +70 -0
  69. package/dist/lib/ipc-server.d.ts +36 -0
  70. package/dist/lib/ipc-server.js +70 -0
  71. package/dist/lib/node-version-guard.d.ts +8 -0
  72. package/dist/lib/node-version-guard.js +60 -0
  73. package/dist/lib/open-directory.d.ts +7 -0
  74. package/dist/lib/open-directory.js +41 -0
  75. package/dist/lib/repo-manager.d.ts +29 -0
  76. package/dist/lib/repo-manager.js +103 -0
  77. package/dist/lib/resilient-emitter.d.ts +53 -0
  78. package/dist/lib/resilient-emitter.js +160 -0
  79. package/dist/lib/rest-client.d.ts +164 -0
  80. package/dist/lib/rest-client.js +139 -0
  81. package/dist/lib/skill-manager.d.ts +23 -0
  82. package/dist/lib/skill-manager.js +167 -0
  83. package/dist/lib/socket-client.d.ts +68 -0
  84. package/dist/lib/socket-client.js +160 -0
  85. package/dist/mcp/mcp-probe.d.ts +11 -0
  86. package/dist/mcp/mcp-probe.js +106 -0
  87. package/dist/mcp/mcp-response.d.ts +29 -0
  88. package/dist/mcp/mcp-response.js +67 -0
  89. package/dist/mcp/mcp-server.d.ts +19 -0
  90. package/dist/mcp/mcp-server.js +76 -0
  91. package/dist/mcp/resources/project-conventions.d.ts +4 -0
  92. package/dist/mcp/resources/project-conventions.js +50 -0
  93. package/dist/mcp/resources/task-current.d.ts +4 -0
  94. package/dist/mcp/resources/task-current.js +51 -0
  95. package/dist/mcp/tools/forge-add-group-dependency.d.ts +4 -0
  96. package/dist/mcp/tools/forge-add-group-dependency.js +20 -0
  97. package/dist/mcp/tools/forge-add-task-dependency.d.ts +4 -0
  98. package/dist/mcp/tools/forge-add-task-dependency.js +20 -0
  99. package/dist/mcp/tools/forge-ask-human.d.ts +4 -0
  100. package/dist/mcp/tools/forge-ask-human.js +80 -0
  101. package/dist/mcp/tools/forge-column-done.d.ts +4 -0
  102. package/dist/mcp/tools/forge-column-done.js +46 -0
  103. package/dist/mcp/tools/forge-complete-step.d.ts +4 -0
  104. package/dist/mcp/tools/forge-complete-step.js +139 -0
  105. package/dist/mcp/tools/forge-create-task-group.d.ts +4 -0
  106. package/dist/mcp/tools/forge-create-task-group.js +22 -0
  107. package/dist/mcp/tools/forge-create-task.d.ts +4 -0
  108. package/dist/mcp/tools/forge-create-task.js +28 -0
  109. package/dist/mcp/tools/forge-delete-task-group.d.ts +4 -0
  110. package/dist/mcp/tools/forge-delete-task-group.js +17 -0
  111. package/dist/mcp/tools/forge-delete-task.d.ts +4 -0
  112. package/dist/mcp/tools/forge-delete-task.js +17 -0
  113. package/dist/mcp/tools/forge-get-task.d.ts +4 -0
  114. package/dist/mcp/tools/forge-get-task.js +17 -0
  115. package/dist/mcp/tools/forge-merge-back.d.ts +4 -0
  116. package/dist/mcp/tools/forge-merge-back.js +179 -0
  117. package/dist/mcp/tools/forge-release-merge.d.ts +4 -0
  118. package/dist/mcp/tools/forge-release-merge.js +159 -0
  119. package/dist/mcp/tools/forge-release-notes.d.ts +4 -0
  120. package/dist/mcp/tools/forge-release-notes.js +51 -0
  121. package/dist/mcp/tools/forge-release-tag.d.ts +4 -0
  122. package/dist/mcp/tools/forge-release-tag.js +125 -0
  123. package/dist/mcp/tools/forge-remove-group-dependency.d.ts +4 -0
  124. package/dist/mcp/tools/forge-remove-group-dependency.js +19 -0
  125. package/dist/mcp/tools/forge-remove-task-dependency.d.ts +4 -0
  126. package/dist/mcp/tools/forge-remove-task-dependency.js +19 -0
  127. package/dist/mcp/tools/forge-requeue.d.ts +4 -0
  128. package/dist/mcp/tools/forge-requeue.js +39 -0
  129. package/dist/mcp/tools/forge-submit-pr.d.ts +4 -0
  130. package/dist/mcp/tools/forge-submit-pr.js +68 -0
  131. package/dist/mcp/tools/forge-update-status.d.ts +4 -0
  132. package/dist/mcp/tools/forge-update-status.js +77 -0
  133. package/dist/mcp/tools/forge-update-task-group.d.ts +4 -0
  134. package/dist/mcp/tools/forge-update-task-group.js +21 -0
  135. package/dist/mcp/tools/forge-update-task.d.ts +4 -0
  136. package/dist/mcp/tools/forge-update-task.js +25 -0
  137. package/dist/mcp/tools/forge-validate-merge.d.ts +4 -0
  138. package/dist/mcp/tools/forge-validate-merge.js +194 -0
  139. package/dist/mcp/types.d.ts +39 -0
  140. package/dist/mcp/types.js +8 -0
  141. package/dist/methodology/artifact-manager.d.ts +14 -0
  142. package/dist/methodology/artifact-manager.js +55 -0
  143. package/dist/orchestrator/agent-registry.d.ts +53 -0
  144. package/dist/orchestrator/agent-registry.js +96 -0
  145. package/dist/orchestrator/agent-spawner.d.ts +112 -0
  146. package/dist/orchestrator/agent-spawner.js +1518 -0
  147. package/dist/orchestrator/connectors/claude-code-connector.d.ts +3 -0
  148. package/dist/orchestrator/connectors/claude-code-connector.js +60 -0
  149. package/dist/orchestrator/connectors/connector.d.ts +95 -0
  150. package/dist/orchestrator/connectors/connector.js +25 -0
  151. package/dist/orchestrator/connectors/docker-connector.d.ts +14 -0
  152. package/dist/orchestrator/connectors/docker-connector.js +337 -0
  153. package/dist/orchestrator/connectors/native-vm-connector.d.ts +11 -0
  154. package/dist/orchestrator/connectors/native-vm-connector.js +162 -0
  155. package/dist/orchestrator/connectors/opencode-connector.d.ts +3 -0
  156. package/dist/orchestrator/connectors/opencode-connector.js +39 -0
  157. package/dist/orchestrator/connectors/vm-connector.d.ts +3 -0
  158. package/dist/orchestrator/connectors/vm-connector.js +139 -0
  159. package/dist/orchestrator/dispatch-queue.d.ts +43 -0
  160. package/dist/orchestrator/dispatch-queue.js +93 -0
  161. package/dist/orchestrator/resume-handler.d.ts +34 -0
  162. package/dist/orchestrator/resume-handler.js +159 -0
  163. package/dist/orchestrator/stream-batcher.d.ts +16 -0
  164. package/dist/orchestrator/stream-batcher.js +50 -0
  165. package/dist/orchestrator/stream-json-extractor.d.ts +97 -0
  166. package/dist/orchestrator/stream-json-extractor.js +579 -0
  167. package/dist/vm/claude-token-manager.d.ts +36 -0
  168. package/dist/vm/claude-token-manager.js +223 -0
  169. package/dist/vm/docker-executor.d.ts +131 -0
  170. package/dist/vm/docker-executor.js +360 -0
  171. package/dist/vm/docker-process-proxy.d.ts +31 -0
  172. package/dist/vm/docker-process-proxy.js +105 -0
  173. package/dist/vm/forge-vm-agent.d.ts +11 -0
  174. package/dist/vm/forge-vm-agent.js +159 -0
  175. package/dist/vm/git-credentials.d.ts +43 -0
  176. package/dist/vm/git-credentials.js +53 -0
  177. package/dist/vm/image-manager.d.ts +7 -0
  178. package/dist/vm/image-manager.js +61 -0
  179. package/dist/vm/log.d.ts +9 -0
  180. package/dist/vm/log.js +18 -0
  181. package/dist/vm/mcp-bridge.d.ts +63 -0
  182. package/dist/vm/mcp-bridge.js +243 -0
  183. package/dist/vm/mcp-stub.d.ts +15 -0
  184. package/dist/vm/mcp-stub.js +67 -0
  185. package/dist/vm/native-addon.d.ts +69 -0
  186. package/dist/vm/native-addon.js +68 -0
  187. package/dist/vm/provider-resolver.d.ts +7 -0
  188. package/dist/vm/provider-resolver.js +27 -0
  189. package/dist/vm/providers/lima.d.ts +17 -0
  190. package/dist/vm/providers/lima.js +191 -0
  191. package/dist/vm/providers/qemu.d.ts +28 -0
  192. package/dist/vm/providers/qemu.js +260 -0
  193. package/dist/vm/providers/vfkit.d.ts +40 -0
  194. package/dist/vm/providers/vfkit.js +371 -0
  195. package/dist/vm/providers/wsl2.d.ts +27 -0
  196. package/dist/vm/providers/wsl2.js +226 -0
  197. package/dist/vm/session-manager.d.ts +130 -0
  198. package/dist/vm/session-manager.js +494 -0
  199. package/dist/vm/session-snapshot.d.ts +31 -0
  200. package/dist/vm/session-snapshot.js +180 -0
  201. package/dist/vm/types.d.ts +73 -0
  202. package/dist/vm/types.js +2 -0
  203. package/dist/vm/vm-helper-client.d.ts +81 -0
  204. package/dist/vm/vm-helper-client.js +335 -0
  205. package/dist/vm/vm-helper.d.ts +11 -0
  206. package/dist/vm/vm-helper.js +156 -0
  207. package/dist/vm/vm-ipc-protocol.d.ts +84 -0
  208. package/dist/vm/vm-ipc-protocol.js +9 -0
  209. package/dist/vm/vm-manager-facade.d.ts +23 -0
  210. package/dist/vm/vm-manager-facade.js +43 -0
  211. package/dist/vm/vm-manager.d.ts +74 -0
  212. package/dist/vm/vm-manager.js +515 -0
  213. package/dist/vm/vm-process-proxy.d.ts +34 -0
  214. package/dist/vm/vm-process-proxy.js +64 -0
  215. package/dist/vm/vsock-bridge.d.ts +44 -0
  216. package/dist/vm/vsock-bridge.js +140 -0
  217. package/dist/vm/vsock-protocol.d.ts +98 -0
  218. package/dist/vm/vsock-protocol.js +57 -0
  219. package/package.json +65 -0
  220. package/rootfs/Dockerfile +50 -0
  221. 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,3 @@
1
+ import type { AgentConnector } from "./connector.js";
2
+ export declare const openCodeConnector: AgentConnector;
3
+ //# sourceMappingURL=opencode-connector.d.ts.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,3 @@
1
+ import type { AgentConnector } from "./connector.js";
2
+ export declare const vmConnector: AgentConnector;
3
+ //# sourceMappingURL=vm-connector.d.ts.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