@elie-laloum/outpost 1.1.0

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 (128) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/LICENSE +21 -0
  3. package/README.fr.md +251 -0
  4. package/README.md +226 -0
  5. package/ROADMAP.md +34 -0
  6. package/SECURITY.md +15 -0
  7. package/dist/application/campaign.d.ts +33 -0
  8. package/dist/application/campaign.js +195 -0
  9. package/dist/application/campaign.js.map +1 -0
  10. package/dist/application/execution.d.ts +48 -0
  11. package/dist/application/execution.js +322 -0
  12. package/dist/application/execution.js.map +1 -0
  13. package/dist/application/interactive-brief.d.ts +3 -0
  14. package/dist/application/interactive-brief.js +35 -0
  15. package/dist/application/interactive-brief.js.map +1 -0
  16. package/dist/application/outpost.d.ts +91 -0
  17. package/dist/application/outpost.js +629 -0
  18. package/dist/application/outpost.js.map +1 -0
  19. package/dist/application/remote-workspace.d.ts +11 -0
  20. package/dist/application/remote-workspace.js +264 -0
  21. package/dist/application/remote-workspace.js.map +1 -0
  22. package/dist/application/tasks.d.ts +17 -0
  23. package/dist/application/tasks.js +36 -0
  24. package/dist/application/tasks.js.map +1 -0
  25. package/dist/cli/main.d.ts +2 -0
  26. package/dist/cli/main.js +94 -0
  27. package/dist/cli/main.js.map +1 -0
  28. package/dist/cli/scaffold.d.ts +28 -0
  29. package/dist/cli/scaffold.js +169 -0
  30. package/dist/cli/scaffold.js.map +1 -0
  31. package/dist/cli/starters.d.ts +3 -0
  32. package/dist/cli/starters.js +34 -0
  33. package/dist/cli/starters.js.map +1 -0
  34. package/dist/domain/backlog.d.ts +16 -0
  35. package/dist/domain/backlog.js +23 -0
  36. package/dist/domain/backlog.js.map +1 -0
  37. package/dist/domain/errors.d.ts +11 -0
  38. package/dist/domain/errors.js +37 -0
  39. package/dist/domain/errors.js.map +1 -0
  40. package/dist/domain/ports.d.ts +183 -0
  41. package/dist/domain/ports.js +2 -0
  42. package/dist/domain/ports.js.map +1 -0
  43. package/dist/domain/prompts.d.ts +22 -0
  44. package/dist/domain/prompts.js +45 -0
  45. package/dist/domain/prompts.js.map +1 -0
  46. package/dist/domain/response.d.ts +37 -0
  47. package/dist/domain/response.js +50 -0
  48. package/dist/domain/response.js.map +1 -0
  49. package/dist/domain/workflow.d.ts +68 -0
  50. package/dist/domain/workflow.js +246 -0
  51. package/dist/domain/workflow.js.map +1 -0
  52. package/dist/index.d.ts +27 -0
  53. package/dist/index.js +13 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/infrastructure/abort.d.ts +1 -0
  56. package/dist/infrastructure/abort.js +16 -0
  57. package/dist/infrastructure/abort.js.map +1 -0
  58. package/dist/infrastructure/conversations.d.ts +30 -0
  59. package/dist/infrastructure/conversations.js +222 -0
  60. package/dist/infrastructure/conversations.js.map +1 -0
  61. package/dist/infrastructure/files.d.ts +5 -0
  62. package/dist/infrastructure/files.js +85 -0
  63. package/dist/infrastructure/files.js.map +1 -0
  64. package/dist/infrastructure/git.d.ts +14 -0
  65. package/dist/infrastructure/git.js +304 -0
  66. package/dist/infrastructure/git.js.map +1 -0
  67. package/dist/infrastructure/journal.d.ts +10 -0
  68. package/dist/infrastructure/journal.js +53 -0
  69. package/dist/infrastructure/journal.js.map +1 -0
  70. package/dist/infrastructure/process.d.ts +6 -0
  71. package/dist/infrastructure/process.js +133 -0
  72. package/dist/infrastructure/process.js.map +1 -0
  73. package/dist/infrastructure/reporter.d.ts +10 -0
  74. package/dist/infrastructure/reporter.js +47 -0
  75. package/dist/infrastructure/reporter.js.map +1 -0
  76. package/dist/infrastructure/settings.d.ts +3 -0
  77. package/dist/infrastructure/settings.js +41 -0
  78. package/dist/infrastructure/settings.js.map +1 -0
  79. package/dist/infrastructure/shutdown.d.ts +1 -0
  80. package/dist/infrastructure/shutdown.js +67 -0
  81. package/dist/infrastructure/shutdown.js.map +1 -0
  82. package/dist/infrastructure/terminal.d.ts +11 -0
  83. package/dist/infrastructure/terminal.js +19 -0
  84. package/dist/infrastructure/terminal.js.map +1 -0
  85. package/dist/infrastructure/transfer.d.ts +3 -0
  86. package/dist/infrastructure/transfer.js +29 -0
  87. package/dist/infrastructure/transfer.js.map +1 -0
  88. package/dist/providers/agents.d.ts +17 -0
  89. package/dist/providers/agents.js +204 -0
  90. package/dist/providers/agents.js.map +1 -0
  91. package/dist/providers/backlogs.d.ts +9 -0
  92. package/dist/providers/backlogs.js +82 -0
  93. package/dist/providers/backlogs.js.map +1 -0
  94. package/dist/providers/cloud-files.d.ts +4 -0
  95. package/dist/providers/cloud-files.js +66 -0
  96. package/dist/providers/cloud-files.js.map +1 -0
  97. package/dist/providers/container.d.ts +21 -0
  98. package/dist/providers/container.js +310 -0
  99. package/dist/providers/container.js.map +1 -0
  100. package/dist/providers/daytona.d.ts +10 -0
  101. package/dist/providers/daytona.js +153 -0
  102. package/dist/providers/daytona.js.map +1 -0
  103. package/dist/providers/docker.d.ts +3 -0
  104. package/dist/providers/docker.js +3 -0
  105. package/dist/providers/docker.js.map +1 -0
  106. package/dist/providers/factories.d.ts +9 -0
  107. package/dist/providers/factories.js +12 -0
  108. package/dist/providers/factories.js.map +1 -0
  109. package/dist/providers/local.d.ts +4 -0
  110. package/dist/providers/local.js +64 -0
  111. package/dist/providers/local.js.map +1 -0
  112. package/dist/providers/podman.d.ts +3 -0
  113. package/dist/providers/podman.js +3 -0
  114. package/dist/providers/podman.js.map +1 -0
  115. package/dist/providers/vercel.d.ts +9 -0
  116. package/dist/providers/vercel.js +158 -0
  117. package/dist/providers/vercel.js.map +1 -0
  118. package/dist/providers/versions.d.ts +4 -0
  119. package/dist/providers/versions.js +5 -0
  120. package/dist/providers/versions.js.map +1 -0
  121. package/docs/api.md +147 -0
  122. package/docs/architecture.md +21 -0
  123. package/docs/migration-1.1.fr.md +44 -0
  124. package/docs/migration-1.1.md +38 -0
  125. package/docs/operations.md +101 -0
  126. package/docs/providers.md +119 -0
  127. package/docs/workflows.md +101 -0
  128. package/package.json +95 -0
@@ -0,0 +1,629 @@
1
+ import { join, posix } from "node:path";
2
+ import { readFile, stat } from "node:fs/promises";
3
+ import { OutpostError, invariant, recordRecovery } from "../domain/errors.js";
4
+ import { validateBrief } from "../domain/prompts.js";
5
+ import { acquireWorkspace, commits, git, } from "../infrastructure/git.js";
6
+ import { nativeConversations } from "../infrastructure/conversations.js";
7
+ import { resolveVariables } from "../infrastructure/settings.js";
8
+ import { executeProcess, quote, requireSuccess, } from "../infrastructure/process.js";
9
+ import { journal } from "../infrastructure/journal.js";
10
+ import { registerCleanup } from "../infrastructure/shutdown.js";
11
+ import { restoreTerminal } from "../infrastructure/terminal.js";
12
+ import { boundedTransfers } from "../infrastructure/transfer.js";
13
+ import { docker } from "../providers/docker.js";
14
+ import { agentVersions } from "../providers/versions.js";
15
+ import { execute, notify, renderBrief, validateDispatch, preflightDispatch, } from "./execution.js";
16
+ import { seedRemote } from "./remote-workspace.js";
17
+ import { completeBrief } from "./interactive-brief.js";
18
+ const workspaces = new WeakMap();
19
+ const storageFor = (agent) => agent.storage ??
20
+ (agent.conversations ? nativeConversations(agent.conversations) : undefined);
21
+ async function startupFailure(workspace, cause, options) {
22
+ try {
23
+ const log = await journal(workspace.repository, options.logging, options.label);
24
+ log.record({
25
+ kind: "phase",
26
+ name: "preparation failed",
27
+ branch: workspace.branch,
28
+ directory: workspace.directory,
29
+ });
30
+ log.record({
31
+ kind: "failure",
32
+ message: cause instanceof Error ? cause.message : String(cause),
33
+ });
34
+ await log.close();
35
+ recordRecovery(cause, {
36
+ branch: workspace.branch,
37
+ directory: workspace.directory,
38
+ log: log.file,
39
+ });
40
+ }
41
+ catch {
42
+ /* Logging must preserve the preparation error. */
43
+ }
44
+ }
45
+ export async function openWorkspace(options = {}) {
46
+ options.signal?.throwIfAborted();
47
+ const lease = await acquireWorkspace(options);
48
+ try {
49
+ await hooks(options.hooks?.workspaceReady ?? [], lease.directory, executeProcess, options.signal);
50
+ }
51
+ catch (cause) {
52
+ await startupFailure(lease, cause, options);
53
+ await lease.dispose();
54
+ throw cause;
55
+ }
56
+ const state = {
57
+ lease,
58
+ active: false,
59
+ closed: false,
60
+ ...(options.hooks ? { hooks: options.hooks } : {}),
61
+ };
62
+ const result = {
63
+ ...lease,
64
+ dispatch(options) {
65
+ return dispatch({ ...options, workspace: result });
66
+ },
67
+ sandbox(options = {}) {
68
+ return createSandbox({ ...options, workspace: result });
69
+ },
70
+ attach(options) {
71
+ return attach({ ...options, workspace: result });
72
+ },
73
+ async close(settings = {}) {
74
+ invariant(!state.active, "Close the sandbox before closing its workspace");
75
+ state.closed = true;
76
+ return lease.dispose(settings.preserve);
77
+ },
78
+ async [Symbol.asyncDispose]() {
79
+ await result.close();
80
+ },
81
+ };
82
+ workspaces.set(result, state);
83
+ return result;
84
+ }
85
+ function continuationConfiguration(configuration, next) {
86
+ if (next.repository || next.branch || next.copies) {
87
+ const { workspace, ...rest } = configuration;
88
+ return {
89
+ ...(workspace ? { repository: workspace.repository } : {}),
90
+ ...rest,
91
+ };
92
+ }
93
+ return configuration;
94
+ }
95
+ function warmContinuation(options) {
96
+ invariant(![
97
+ "repository",
98
+ "branch",
99
+ "provider",
100
+ "workspace",
101
+ "copies",
102
+ "hooks",
103
+ "limits",
104
+ "bootstrap",
105
+ "conversationHome",
106
+ "includeUncommitted",
107
+ ].some((key) => key in options), "Warm continuation uses its existing sandbox; use dispatch() to change sandbox settings");
108
+ }
109
+ async function hooks(commands, directory, invoke, signal, parallel = false) {
110
+ if (parallel) {
111
+ const controller = new AbortController();
112
+ const combined = signal
113
+ ? AbortSignal.any([signal, controller.signal])
114
+ : controller.signal;
115
+ const outcomes = await Promise.allSettled(commands.map((command) => hooks([command], directory, invoke, combined).catch((cause) => {
116
+ controller.abort(cause);
117
+ throw cause;
118
+ })));
119
+ const failure = outcomes.find((outcome) => outcome.status === "rejected");
120
+ if (failure?.status === "rejected")
121
+ throw failure.reason;
122
+ return;
123
+ }
124
+ for (const command of commands)
125
+ await requireSuccess({
126
+ ...command,
127
+ directory: command.directory ?? directory,
128
+ deadlineMs: command.deadlineMs ?? 600_000,
129
+ ...(signal ? { signal } : {}),
130
+ }, invoke);
131
+ }
132
+ async function prepareAdapter(agent, runtime, signal) {
133
+ if (!agent.conversations)
134
+ return agent;
135
+ const executable = agent.conversations;
136
+ const cli = executable === "claude"
137
+ ? `@anthropic-ai/claude-code@${agentVersions.claude}`
138
+ : `@openai/codex@${agentVersions.codex}`;
139
+ const prefix = posix.join(runtime.home, ".outpost-tools"), target = posix.join(prefix, "bin", executable);
140
+ const installed = await requireSuccess({
141
+ executable: "sh",
142
+ arguments: [
143
+ "-c",
144
+ `if command -v ${executable} >/dev/null 2>&1; then command -v ${executable}; elif test -x ${quote(target)}; then printf '%s\\n' ${quote(target)}; else npm install --global --allow-scripts=@anthropic-ai/claude-code --prefix ${quote(prefix)} ${cli} >&2 && printf '%s\\n' ${quote(target)}; fi`,
145
+ ],
146
+ signal,
147
+ }, runtime.invoke.bind(runtime));
148
+ const binary = installed.stdout.trim();
149
+ invariant(binary.startsWith("/") && !binary.includes("\n"), "Agent bootstrap returned an invalid executable path");
150
+ return {
151
+ ...agent,
152
+ request(input) {
153
+ return { ...agent.request(input), executable: binary };
154
+ },
155
+ };
156
+ }
157
+ export async function createSandbox(options = {}) {
158
+ options.signal?.throwIfAborted();
159
+ const provider = options.provider ?? docker();
160
+ if (provider.placement === "remote" && !options.workspace && !options.branch)
161
+ options = { ...options, branch: { mode: "integrate" } };
162
+ invariant(!options.workspace ||
163
+ (!options.repository && !options.branch && !options.copies), "A supplied workspace owns its repository, branch and copied inputs");
164
+ const owned = !options.workspace;
165
+ invariant(provider.placement !== "remote" ||
166
+ ((options.workspace?.policy ?? options.branch)?.mode !== "current" &&
167
+ (options.workspace || options.branch)), "Remote providers require a named or integration workspace");
168
+ const workspace = options.workspace ?? (await openWorkspace(options));
169
+ const state = workspaces.get(workspace);
170
+ invariant(state && !state.closed && !state.active, "Workspace must be open and cannot belong to another sandbox");
171
+ state.active = true;
172
+ const lifecycle = options.hooks ?? state.hooks;
173
+ const stop = new AbortController();
174
+ const setupSignal = options.signal
175
+ ? AbortSignal.any([options.signal, stop.signal])
176
+ : stop.signal;
177
+ let lease, sync;
178
+ const prepared = new Map();
179
+ const staging = join(workspace.repository, ".outpost", "recovery", "conversations");
180
+ try {
181
+ const configured = await resolveVariables(workspace.repository, {}, provider.variables);
182
+ const name = (await git(workspace.repository, ["config", "--get", "user.name"]).catch(() => "Outpost")).trim();
183
+ const email = (await git(workspace.repository, ["config", "--get", "user.email"]).catch(() => "outpost@localhost")).trim();
184
+ const variables = {
185
+ GIT_AUTHOR_NAME: name,
186
+ GIT_COMMITTER_NAME: name,
187
+ GIT_AUTHOR_EMAIL: email,
188
+ GIT_COMMITTER_EMAIL: email,
189
+ ...configured,
190
+ };
191
+ lease = boundedTransfers(await provider.acquire({
192
+ repository: workspace.repository,
193
+ directory: workspace.directory,
194
+ gitDirectories: workspace.gitDirectories,
195
+ variables,
196
+ signal: setupSignal,
197
+ }), {
198
+ ...(options.limits?.copyMs
199
+ ? { deadlineMs: options.limits.copyMs }
200
+ : {}),
201
+ });
202
+ if (provider.placement === "remote") {
203
+ sync = await seedRemote(workspace, lease, {
204
+ ...(options.includeUncommitted ? { includeUncommitted: true } : {}),
205
+ ...(options.limits ? { limits: options.limits } : {}),
206
+ signal: setupSignal,
207
+ });
208
+ for (const path of options.copies ?? [])
209
+ if (await stat(join(workspace.directory, path)).catch((error) => {
210
+ if (error.code === "ENOENT")
211
+ return undefined;
212
+ throw error;
213
+ }))
214
+ await lease.upload(join(workspace.directory, path), posix.join(lease.root, path.replaceAll("\\", "/")));
215
+ if (options.bootstrap !== false && options.agent)
216
+ prepared.set(options.agent, await prepareAdapter(options.agent, lease, setupSignal));
217
+ }
218
+ const initialized = await Promise.allSettled([
219
+ hooks(lifecycle?.hostReady ?? [], workspace.directory, executeProcess, setupSignal),
220
+ hooks(lifecycle?.sandboxReady ?? [], lease.root, lease.invoke.bind(lease), setupSignal, true),
221
+ ].map((pending) => pending.catch((cause) => {
222
+ stop.abort(cause);
223
+ throw cause;
224
+ })));
225
+ const failure = initialized.find((item) => item.status === "rejected");
226
+ if (failure?.status === "rejected")
227
+ throw failure.reason;
228
+ }
229
+ catch (cause) {
230
+ await startupFailure(workspace, cause, options);
231
+ await lease?.release().catch(() => undefined);
232
+ state.active = false;
233
+ if (owned)
234
+ await workspace.close();
235
+ throw cause;
236
+ }
237
+ const runtime = lease;
238
+ let busy, closing, closed = false;
239
+ const known = new Set();
240
+ const exclusive = (action) => {
241
+ invariant(!closed && !closing, "Sandbox is closed");
242
+ invariant(!busy, "Sandbox already has an active operation; use another sandbox for parallel work");
243
+ const pending = Promise.resolve().then(action);
244
+ busy = pending;
245
+ void pending.then(() => {
246
+ busy = undefined;
247
+ }, () => {
248
+ busy = undefined;
249
+ });
250
+ return pending;
251
+ };
252
+ const selectAgent = async (selected, signal) => {
253
+ invariant(selected, "Provide an agent on the sandbox or this operation");
254
+ if (!prepared.has(selected))
255
+ prepared.set(selected, provider.placement === "remote" && options.bootstrap !== false
256
+ ? await prepareAdapter(selected, runtime, signal)
257
+ : selected);
258
+ const variables = await resolveVariables(workspace.repository, selected.variables, provider.variables);
259
+ const adapter = prepared.get(selected);
260
+ return {
261
+ selected,
262
+ adapter,
263
+ executionLease: {
264
+ ...runtime,
265
+ invoke(command) {
266
+ return runtime.invoke({
267
+ ...command,
268
+ variables: { ...variables, ...command.variables },
269
+ });
270
+ },
271
+ },
272
+ };
273
+ };
274
+ const conversationKey = (agent, id) => `${agent.storage?.name ?? agent.conversations ?? agent.name}:${id}`;
275
+ const restore = async (id, agent) => {
276
+ if (known.has(conversationKey(agent, id)))
277
+ return;
278
+ const storage = storageFor(agent);
279
+ invariant(storage, "This adapter does not support native conversations");
280
+ const found = await storage.locate(id, workspace.repository, options.conversationHome);
281
+ if (provider.placement !== "host" ||
282
+ workspace.directory !== workspace.repository)
283
+ await storage.restore(found, {
284
+ repository: workspace.repository,
285
+ sandbox: runtime,
286
+ staging,
287
+ });
288
+ known.add(conversationKey(agent, id));
289
+ };
290
+ const result = {
291
+ workspace,
292
+ root: runtime.root,
293
+ dispatch(dispatch) {
294
+ validateDispatch(dispatch);
295
+ return exclusive(async () => {
296
+ await preflightDispatch(dispatch, workspace.repository, dispatch.agent ?? options.agent);
297
+ const signal = dispatch.signal
298
+ ? AbortSignal.any([dispatch.signal, stop.signal])
299
+ : stop.signal;
300
+ const { selected, adapter, executionLease } = await selectAgent(dispatch.agent ?? options.agent, signal);
301
+ if (dispatch.continuation)
302
+ await restore(dispatch.continuation.id, selected);
303
+ const baseline = (await git(workspace.directory, ["rev-parse", "HEAD"])).trim();
304
+ const log = await journal(workspace.repository, dispatch.logging ?? options.logging, dispatch.label);
305
+ let execution, transcript;
306
+ const captured = new Map();
307
+ const storage = storageFor(selected);
308
+ let failure;
309
+ let conversation = dispatch.continuation?.id;
310
+ const conversations = new Set(conversation ? [conversation] : []);
311
+ const save = async (id) => {
312
+ invariant(storage, "Conversation storage is unavailable");
313
+ const location = await storage.capture(id, {
314
+ repository: workspace.repository,
315
+ sandbox: runtime,
316
+ staging,
317
+ ...(options.conversationHome
318
+ ? { home: options.conversationHome }
319
+ : {}),
320
+ ...(dispatch.warn ? { warn: dispatch.warn } : {}),
321
+ local: provider.placement === "host",
322
+ });
323
+ captured.set(id, location);
324
+ transcript = location;
325
+ return location;
326
+ };
327
+ try {
328
+ execution = await execute(workspace, executionLease, adapter, provider.placement === "host", {
329
+ ...dispatch,
330
+ signal,
331
+ observe(event) {
332
+ if (event.kind === "conversation") {
333
+ conversation = event.id;
334
+ conversations.add(event.id);
335
+ known.add(conversationKey(selected, event.id));
336
+ }
337
+ log.record(event);
338
+ notify(dispatch.observe, event);
339
+ },
340
+ }, async (turn) => {
341
+ if (!turn.conversation || !storage || selected.capture === false)
342
+ return turn;
343
+ const location = await save(turn.conversation);
344
+ const usage = selected.transcriptUsage?.(await readFile(location.file, "utf8"));
345
+ return {
346
+ ...turn,
347
+ transcript: location.file,
348
+ ...(usage ? { usage } : {}),
349
+ };
350
+ });
351
+ }
352
+ catch (cause) {
353
+ failure = cause;
354
+ log.record({
355
+ kind: "failure",
356
+ message: cause instanceof Error ? cause.message : String(cause),
357
+ });
358
+ }
359
+ try {
360
+ await sync?.pull();
361
+ if (storage && selected.capture !== false)
362
+ for (const id of conversations)
363
+ if (failure || !captured.has(id))
364
+ await save(id);
365
+ }
366
+ catch (cause) {
367
+ failure = failure
368
+ ? new AggregateError([failure, cause], "Execution and recovery both failed")
369
+ : cause;
370
+ }
371
+ try {
372
+ await log.close();
373
+ }
374
+ catch (cause) {
375
+ failure ??= cause;
376
+ }
377
+ const changes = await commits(workspace.directory, baseline, options.limits?.collectMs);
378
+ if (failure) {
379
+ recordRecovery(failure, {
380
+ branch: workspace.branch,
381
+ directory: workspace.directory,
382
+ commits: changes,
383
+ transcript: transcript?.file,
384
+ log: log.file,
385
+ });
386
+ throw failure;
387
+ }
388
+ invariant(execution, "Execution did not produce a result");
389
+ return {
390
+ ...execution,
391
+ branch: workspace.branch,
392
+ directory: workspace.directory,
393
+ commits: changes,
394
+ ...(transcript ? { transcript: transcript.file } : {}),
395
+ ...(log.file ? { log: log.file } : {}),
396
+ resume(next) {
397
+ warmContinuation(next);
398
+ invariant(conversation, "No conversation was emitted");
399
+ return result.resume(conversation, { agent: selected, ...next });
400
+ },
401
+ fork(next) {
402
+ warmContinuation(next);
403
+ invariant(conversation, "No conversation was emitted");
404
+ return result.fork(conversation, { agent: selected, ...next });
405
+ },
406
+ };
407
+ });
408
+ },
409
+ resume(id, dispatch) {
410
+ return result.dispatch({ ...dispatch, continuation: { id } });
411
+ },
412
+ fork(id, dispatch) {
413
+ return result.dispatch({ ...dispatch, continuation: { id, fork: true } });
414
+ },
415
+ attach(settings = {}) {
416
+ settings.signal?.throwIfAborted();
417
+ validateBrief(settings.brief, true);
418
+ return exclusive(async () => {
419
+ const signal = settings.signal
420
+ ? AbortSignal.any([settings.signal, stop.signal])
421
+ : stop.signal;
422
+ const { selected, adapter, executionLease } = await selectAgent(settings.agent ?? options.agent, signal);
423
+ if (settings.continuation)
424
+ await restore(settings.continuation.id, selected);
425
+ const brief = await completeBrief(settings.brief, signal, settings.ask);
426
+ const text = brief
427
+ ? await renderBrief(brief, workspace, executionLease, provider.placement === "host", settings)
428
+ : undefined;
429
+ const command = adapter.request({
430
+ interactive: true,
431
+ ...(text === undefined ? {} : { text }),
432
+ ...(settings.continuation
433
+ ? { continuation: settings.continuation }
434
+ : {}),
435
+ });
436
+ const baseline = (await git(workspace.directory, ["rev-parse", "HEAD"])).trim();
437
+ let output;
438
+ try {
439
+ output = await executionLease.invoke({
440
+ ...command,
441
+ ...(settings.terminal ? { terminal: settings.terminal } : {}),
442
+ deadlineMs: 86_400_000,
443
+ signal: settings.signal
444
+ ? AbortSignal.any([settings.signal, stop.signal])
445
+ : stop.signal,
446
+ });
447
+ }
448
+ finally {
449
+ restoreTerminal();
450
+ await sync?.pull();
451
+ }
452
+ return {
453
+ ...output,
454
+ branch: workspace.branch,
455
+ directory: workspace.directory,
456
+ commits: await commits(workspace.directory, baseline, options.limits?.collectMs),
457
+ };
458
+ });
459
+ },
460
+ command(command) {
461
+ command.signal?.throwIfAborted();
462
+ return exclusive(async () => {
463
+ try {
464
+ return await runtime.invoke({
465
+ ...command,
466
+ signal: command.signal
467
+ ? AbortSignal.any([command.signal, stop.signal])
468
+ : stop.signal,
469
+ });
470
+ }
471
+ finally {
472
+ await sync?.pull();
473
+ }
474
+ });
475
+ },
476
+ close(settings = {}) {
477
+ if (closing)
478
+ return closing;
479
+ closing = (async () => {
480
+ stop.abort(new OutpostError("aborted", "Sandbox closed"));
481
+ await busy?.catch(() => undefined);
482
+ let failure;
483
+ try {
484
+ await runtime.release();
485
+ await sync?.close();
486
+ }
487
+ catch (cause) {
488
+ failure = cause;
489
+ }
490
+ state.active = false;
491
+ closed = true;
492
+ unregister();
493
+ const disposal = owned
494
+ ? await workspace.close({ preserve: settings.preserve || !!failure })
495
+ : {};
496
+ if (failure)
497
+ throw failure;
498
+ return disposal;
499
+ })();
500
+ return closing;
501
+ },
502
+ async [Symbol.asyncDispose]() {
503
+ await result.close();
504
+ },
505
+ };
506
+ const unregister = registerCleanup(() => result.close({ preserve: true }));
507
+ return result;
508
+ }
509
+ export async function dispatch(options) {
510
+ options.signal?.throwIfAborted();
511
+ await preflightDispatch(options, options.workspace?.repository ?? options.repository ?? process.cwd(), options.agent);
512
+ if ((options.passes ?? 1) > 1) {
513
+ const outputs = [];
514
+ for (let index = 0; index < options.passes; index++) {
515
+ options.signal?.throwIfAborted();
516
+ const output = await dispatch({
517
+ ...options,
518
+ passes: 1,
519
+ observe: (event) => notify(options.observe, { ...event, pass: index + 1 }),
520
+ });
521
+ outputs.push(output);
522
+ if (output.completed)
523
+ break;
524
+ }
525
+ const last = outputs.at(-1);
526
+ return {
527
+ ...last,
528
+ text: outputs.map((output) => output.text).join("\n"),
529
+ turns: outputs.flatMap((output) => output.turns),
530
+ commits: outputs.flatMap((output) => output.commits),
531
+ usage: outputs.reduce((sum, output) => ({
532
+ input: sum.input + output.usage.input,
533
+ cached: sum.cached + output.usage.cached,
534
+ output: sum.output + output.usage.output,
535
+ ...(sum.cacheCreated !== undefined ||
536
+ output.usage.cacheCreated !== undefined
537
+ ? {
538
+ cacheCreated: (sum.cacheCreated ?? 0) + (output.usage.cacheCreated ?? 0),
539
+ }
540
+ : {}),
541
+ }), {
542
+ input: 0,
543
+ cached: 0,
544
+ output: 0,
545
+ }),
546
+ };
547
+ }
548
+ if (options.continuation) {
549
+ const storage = storageFor(options.agent);
550
+ invariant(storage, "This adapter does not support native conversations");
551
+ await storage.locate(options.continuation.id, options.workspace?.repository ?? options.repository ?? process.cwd(), options.conversationHome);
552
+ }
553
+ const sandbox = await createSandbox(options);
554
+ const { brief: _brief, response: _response, continuation: _continuation, passes: _passes, ...configuration } = options;
555
+ let successful = false;
556
+ try {
557
+ const output = await sandbox.dispatch(options);
558
+ await sandbox.workspace.integrate();
559
+ successful = true;
560
+ const disposed = await sandbox.close();
561
+ const continuation = output.conversation;
562
+ return {
563
+ ...output,
564
+ ...disposed,
565
+ resume(next) {
566
+ invariant(continuation, "No conversation was emitted");
567
+ return dispatch({
568
+ ...continuationConfiguration(configuration, next),
569
+ ...next,
570
+ continuation: { id: continuation },
571
+ });
572
+ },
573
+ fork(next) {
574
+ invariant(continuation, "No conversation was emitted");
575
+ return dispatch({
576
+ ...continuationConfiguration(configuration, next),
577
+ ...next,
578
+ continuation: { id: continuation, fork: true },
579
+ });
580
+ },
581
+ };
582
+ }
583
+ catch (cause) {
584
+ recordRecovery(cause, {
585
+ branch: sandbox.workspace.branch,
586
+ directory: sandbox.workspace.directory,
587
+ });
588
+ throw cause;
589
+ }
590
+ finally {
591
+ if (!successful)
592
+ await sandbox.close({ preserve: true });
593
+ }
594
+ }
595
+ export async function attach(options) {
596
+ options.signal?.throwIfAborted();
597
+ validateBrief(options.brief, true);
598
+ const brief = await completeBrief(options.brief, options.signal, options.ask);
599
+ if (brief)
600
+ options = { ...options, brief };
601
+ if (options.continuation) {
602
+ const storage = storageFor(options.agent);
603
+ invariant(storage, "This adapter does not support native conversations");
604
+ await storage.locate(options.continuation.id, options.workspace?.repository ?? options.repository ?? process.cwd(), options.conversationHome);
605
+ }
606
+ const sandbox = await createSandbox(options);
607
+ let successful = false;
608
+ try {
609
+ const baseline = (await git(sandbox.workspace.directory, ["rev-parse", "HEAD"])).trim();
610
+ const result = await sandbox.attach(options);
611
+ const changes = await commits(sandbox.workspace.directory, baseline, options.limits?.collectMs);
612
+ if (result.status === 0)
613
+ await sandbox.workspace.integrate();
614
+ const disposal = await sandbox.close({ preserve: result.status !== 0 });
615
+ successful = true;
616
+ return {
617
+ ...result,
618
+ ...disposal,
619
+ branch: sandbox.workspace.branch,
620
+ directory: sandbox.workspace.directory,
621
+ commits: changes,
622
+ };
623
+ }
624
+ finally {
625
+ if (!successful)
626
+ await sandbox.close({ preserve: true });
627
+ }
628
+ }
629
+ //# sourceMappingURL=outpost.js.map