@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 @@
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../../src/cli/main.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\nimport { parseArgs } from \"node:util\";\nimport { createInterface } from \"node:readline/promises\";\nimport { initialize, manageImage, type InitOptions } from \"./scaffold.ts\";\n\nconst help = `Outpost — sandboxed coding agents and workflows\n\noutpost init [--yes] [--agent codex|claude] [--provider docker|podman|vercel|daytona|local]\n [--template blank|iterate|review|plan|plan-review] [--tracker github|beads|custom]\n [--manager npm|pnpm|yarn|bun] [--model NAME] [--install] [--build] [--image NAME] [--label NAME] [--directory PATH]\noutpost image build|remove [--engine docker|podman] [--file PATH] [--image NAME] [--uid ID] [--gid ID]\n\nNode.js 24+ and Git are required. Run generated scripts with Node.js.\n`;\n\ntry {\n const { values, positionals } = parseArgs({\n allowPositionals: true,\n options: {\n help: { type: \"boolean\", short: \"h\" },\n yes: { type: \"boolean\", short: \"y\" },\n install: { type: \"boolean\" },\n build: { type: \"boolean\" },\n agent: { type: \"string\" },\n provider: { type: \"string\" },\n template: { type: \"string\" },\n tracker: { type: \"string\" },\n manager: { type: \"string\" },\n model: { type: \"string\" },\n label: { type: \"string\" },\n directory: { type: \"string\" },\n engine: { type: \"string\" },\n file: { type: \"string\" },\n image: { type: \"string\" },\n uid: { type: \"string\" },\n gid: { type: \"string\" },\n },\n });\n if (values.help || !positionals.length) process.stdout.write(help);\n else if (positionals[0] === \"init\") {\n const options: Record<string, unknown> = Object.fromEntries(\n Object.entries(values).filter(([key]) => ![\"help\", \"yes\"].includes(key)),\n );\n if (\n !values.yes &&\n !process.stdin.isTTY &&\n (!values.agent || !values.provider || !values.template || !values.tracker)\n )\n throw new Error(\n \"Headless initialization requires --yes for defaults, or --agent, --provider, --template and --tracker.\",\n );\n if (!values.yes && process.stdin.isTTY) {\n const terminal = createInterface({\n input: process.stdin,\n output: process.stdout,\n });\n try {\n for (const [key, label, fallback] of [\n [\"agent\", \"Agent (codex/claude)\", \"codex\"],\n [\n \"provider\",\n \"Sandbox (docker/podman/vercel/daytona/local)\",\n \"docker\",\n ],\n [\n \"template\",\n \"Template (blank/iterate/review/plan/plan-review)\",\n \"blank\",\n ],\n [\"tracker\", \"Issue tracker (github/beads/custom)\", \"github\"],\n ] as const) {\n options[key] ??=\n (await terminal.question(`${label} [${fallback}]: `)).trim() ||\n fallback;\n }\n } finally {\n terminal.close();\n }\n }\n const result = await initialize(options as InitOptions);\n process.stdout.write(\n `Created ${result.files.length} files.\\nRun: ${result.run}\\n`,\n );\n } else if (\n positionals[0] === \"image\" &&\n [\"build\", \"remove\"].includes(positionals[1] ?? \"\")\n ) {\n const options = Object.fromEntries(\n Object.entries(values).filter(([key]) =>\n [\"directory\", \"engine\", \"file\", \"image\"].includes(key),\n ),\n );\n const image = await manageImage(positionals[1] as \"build\" | \"remove\", {\n ...options,\n ...(values.uid ? { uid: Number(values.uid) } : {}),\n ...(values.gid ? { gid: Number(values.gid) } : {}),\n });\n process.stdout.write(`${positionals[1]}: ${image}\\n`);\n } else throw new Error(\"Unknown command. Run outpost --help.\");\n} catch (error) {\n process.stderr.write(\n `${error instanceof Error ? error.message : String(error)}\\n`,\n );\n process.exitCode = 1;\n}\n"]}
@@ -0,0 +1,28 @@
1
+ import { type Executor } from "../infrastructure/process.ts";
2
+ export type Template = "blank" | "iterate" | "review" | "plan" | "plan-review";
3
+ export interface InitOptions {
4
+ readonly directory?: string;
5
+ readonly agent?: "claude" | "codex";
6
+ readonly provider?: "docker" | "podman" | "vercel" | "daytona" | "local";
7
+ readonly template?: Template;
8
+ readonly tracker?: "github" | "beads" | "custom";
9
+ readonly manager?: "npm" | "pnpm" | "yarn" | "bun";
10
+ readonly model?: string;
11
+ readonly install?: boolean;
12
+ readonly label?: string;
13
+ readonly build?: boolean;
14
+ readonly image?: string;
15
+ }
16
+ export declare const imageRecipe: string;
17
+ export declare function initialize(options?: InitOptions, executor?: Executor): Promise<{
18
+ files: readonly string[];
19
+ run: string;
20
+ }>;
21
+ export declare function manageImage(action: "build" | "remove", options?: {
22
+ directory?: string;
23
+ engine?: "docker" | "podman";
24
+ image?: string;
25
+ file?: string;
26
+ uid?: number;
27
+ gid?: number;
28
+ }, executor?: Executor): Promise<string>;
@@ -0,0 +1,169 @@
1
+ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { join, resolve } from "node:path";
3
+ import { OutpostError, invariant } from "../domain/errors.js";
4
+ import { imageName } from "../providers/container.js";
5
+ import { agentVersions } from "../providers/versions.js";
6
+ import { requireSuccess } from "../infrastructure/process.js";
7
+ import { starter, tracker } from "./starters.js";
8
+ export const imageRecipe = `FROM node:24-bookworm-slim
9
+ ARG AGENT_UID=1000
10
+ ARG AGENT_GID=1000
11
+ RUN apt-get update && apt-get install -y --no-install-recommends git gh openssh-client ca-certificates curl procps util-linux python3 && rm -rf /var/lib/apt/lists/*
12
+ RUN npm install -g --allow-scripts=@anthropic-ai/claude-code @openai/codex@${agentVersions.codex} @anthropic-ai/claude-code@${agentVersions.claude}
13
+ RUN groupmod -o -g "$AGENT_GID" node && usermod -o -u "$AGENT_UID" -g "$AGENT_GID" node
14
+ ENV HOME=/home/agent
15
+ USER $AGENT_UID:$AGENT_GID
16
+ WORKDIR /workspace
17
+ `;
18
+ export async function initialize(options = {}, executor) {
19
+ const root = resolve(options.directory ?? process.cwd()), folder = join(root, ".outpost");
20
+ const agent = options.agent ?? "codex", provider = options.provider ?? "docker", template = options.template ?? "blank";
21
+ invariant(["codex", "claude"].includes(agent), "Choose codex or claude");
22
+ invariant(["docker", "podman", "vercel", "daytona", "local"].includes(provider), "Unknown sandbox provider");
23
+ invariant(["blank", "iterate", "review", "plan", "plan-review"].includes(template), "Unknown starter template");
24
+ if (options.tracker)
25
+ invariant(["github", "beads", "custom"].includes(options.tracker), "Unknown tracker");
26
+ let pkg = {};
27
+ try {
28
+ pkg = JSON.parse(await readFile(join(root, "package.json"), "utf8"));
29
+ if (!pkg || typeof pkg !== "object")
30
+ pkg = {};
31
+ }
32
+ catch (cause) {
33
+ if (!(cause instanceof SyntaxError) &&
34
+ cause.code !== "ENOENT")
35
+ throw cause;
36
+ }
37
+ const extension = pkg.type === "module" ? "ts" : "mts";
38
+ let detected = typeof pkg.packageManager === "string"
39
+ ? pkg.packageManager.split("@")[0]
40
+ : undefined;
41
+ if (!detected || !["npm", "pnpm", "yarn", "bun"].includes(detected)) {
42
+ detected = undefined;
43
+ for (const [file, manager] of [
44
+ ["pnpm-lock.yaml", "pnpm"],
45
+ ["yarn.lock", "yarn"],
46
+ ["bun.lock", "bun"],
47
+ ["bun.lockb", "bun"],
48
+ ]) {
49
+ if (await access(join(root, file))
50
+ .then(() => true)
51
+ .catch(() => false)) {
52
+ detected = manager;
53
+ break;
54
+ }
55
+ }
56
+ }
57
+ const manager = options.manager ?? detected ?? "npm";
58
+ invariant(["npm", "pnpm", "yarn", "bun"].includes(manager), "Unknown package manager");
59
+ const files = {
60
+ [`run.${extension}`]: starter(options, extension),
61
+ "brief.md": "Objective: {{OBJECTIVE}}\n\nWork on {{WORK_BRANCH}} from {{BASE_BRANCH}}. Inspect the repository, implement the objective, run relevant tests and commit your changes. When finished, write <outpost>done</outpost>.\n",
62
+ ".env.example": (agent === "codex" ? "OPENAI_API_KEY=\n" : "ANTHROPIC_API_KEY=\n") +
63
+ (options.tracker === "github" ? "GH_TOKEN=\n" : ""),
64
+ ".gitignore": ".env\nworkspaces/\nlocks/\nrecovery/\nlogs/\n",
65
+ };
66
+ if (provider === "docker" || provider === "podman")
67
+ files[provider === "docker" ? "Dockerfile" : "Containerfile"] =
68
+ options.tracker === "beads"
69
+ ? imageRecipe.replace("RUN groupmod", "RUN npm install -g --allow-scripts=@beads/bd @beads/bd@1.2.2\nRUN groupmod")
70
+ : imageRecipe;
71
+ if (template !== "blank")
72
+ files["STANDARDS.md"] =
73
+ "# Engineering standards\n\nKeep domain rules separate from infrastructure. Test observable behavior, handle failure and cancellation, and preserve existing user changes. Run the repository checks before committing. Prefer clear names to comments; keep necessary comments at most two lines. Review the complete diff against the supplied base commit.\n";
74
+ if (options.tracker)
75
+ files[`tickets.${extension}`] = tracker(options.tracker, options.label);
76
+ if (options.tracker === "custom")
77
+ files["TRACKER.md"] =
78
+ `# Custom issue tracker\n\nSet OUTPOST_TRACKER_URL in the host environment before running the starter. Implement GET issues?state=open (Issue[]), GET issues/:id (Issue), and POST issues/:id/close (any successful status). Issue has id, title, optional body and optional blockedBy ids. The generated tickets.${extension} is yours to adapt: add authentication, pagination and filtering for your tracker there. Never commit credentials.\n\nThe campaign reloads the backlog each cycle, reads an issue before implementation, and closes it only after its commits reach the host branch. Configure authentication in tickets.${extension}. A failed close is reported and must be reconciled before rerunning.\n`;
79
+ for (const name of Object.keys(files))
80
+ if (await access(join(folder, name))
81
+ .then(() => true)
82
+ .catch(() => false))
83
+ throw new OutpostError("configuration", `Initialization would overwrite .outpost/${name}`);
84
+ await mkdir(folder, { recursive: true });
85
+ for (const [name, content] of Object.entries(files))
86
+ await writeFile(join(folder, name), content, { flag: "wx" });
87
+ if (options.install) {
88
+ const packages = [
89
+ "@elie-laloum/outpost",
90
+ ...(provider === "vercel"
91
+ ? ["@vercel/sandbox"]
92
+ : provider === "daytona"
93
+ ? ["@daytona/sdk"]
94
+ : []),
95
+ ];
96
+ const args = manager === "npm"
97
+ ? ["install", "--save-dev", ...packages]
98
+ : ["add", "--dev", ...packages];
99
+ await requireSuccess(process.platform === "win32"
100
+ ? {
101
+ executable: "cmd.exe",
102
+ arguments: ["/d", "/c", [manager, ...args].join(" ")],
103
+ directory: root,
104
+ }
105
+ : { executable: manager, arguments: args, directory: root }, executor);
106
+ }
107
+ if (options.label) {
108
+ invariant(options.tracker === "github", "Labels require the GitHub tracker");
109
+ await requireSuccess({
110
+ executable: "gh",
111
+ arguments: [
112
+ "label",
113
+ "create",
114
+ options.label,
115
+ "--color",
116
+ "4969ED",
117
+ "--description",
118
+ "Ready for Outpost",
119
+ "--force",
120
+ ],
121
+ directory: root,
122
+ }, executor);
123
+ }
124
+ if (options.build && (provider === "docker" || provider === "podman"))
125
+ await manageImage("build", {
126
+ directory: root,
127
+ engine: provider,
128
+ ...(options.image ? { image: options.image } : {}),
129
+ }, executor);
130
+ return {
131
+ files: Object.keys(files).map((name) => join(folder, name)),
132
+ run: `node .outpost/run.${extension}`,
133
+ };
134
+ }
135
+ export async function manageImage(action, options = {}, executor) {
136
+ const root = resolve(options.directory ?? process.cwd()), engine = options.engine ?? "docker", image = options.image ?? imageName(root);
137
+ invariant(engine === "docker" || engine === "podman", "Image engine must be docker or podman");
138
+ const uid = options.uid ?? process.getuid?.() ?? 1000, gid = options.gid ?? process.getgid?.() ?? 1000;
139
+ invariant(Number.isSafeInteger(uid) &&
140
+ uid >= 0 &&
141
+ Number.isSafeInteger(gid) &&
142
+ gid >= 0, "UID and GID must be nonnegative integers");
143
+ const args = action === "remove"
144
+ ? ["image", "rm", image]
145
+ : [
146
+ "build",
147
+ "--tag",
148
+ image,
149
+ "--file",
150
+ resolve(root, options.file ??
151
+ `.outpost/${engine === "docker" ? "Dockerfile" : "Containerfile"}`),
152
+ "--build-arg",
153
+ `AGENT_UID=${uid}`,
154
+ "--build-arg",
155
+ `AGENT_GID=${gid}`,
156
+ root,
157
+ ];
158
+ await requireSuccess({
159
+ executable: engine,
160
+ arguments: args,
161
+ directory: root,
162
+ deadlineMs: 1_800_000,
163
+ observe(channel, text) {
164
+ (channel === "stdout" ? process.stdout : process.stderr).write(text);
165
+ },
166
+ }, executor);
167
+ return image;
168
+ }
169
+ //# sourceMappingURL=scaffold.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../../src/cli/scaffold.ts"],"names":[],"mappings":"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","sourcesContent":["import { access, mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\nimport { OutpostError, invariant } from \"../domain/errors.ts\";\nimport { imageName } from \"../providers/container.ts\";\nimport { agentVersions } from \"../providers/versions.ts\";\nimport { requireSuccess, type Executor } from \"../infrastructure/process.ts\";\n\nimport { starter, tracker } from \"./starters.ts\";\n\nexport type Template = \"blank\" | \"iterate\" | \"review\" | \"plan\" | \"plan-review\";\nexport interface InitOptions {\n readonly directory?: string;\n readonly agent?: \"claude\" | \"codex\";\n readonly provider?: \"docker\" | \"podman\" | \"vercel\" | \"daytona\" | \"local\";\n readonly template?: Template;\n readonly tracker?: \"github\" | \"beads\" | \"custom\";\n readonly manager?: \"npm\" | \"pnpm\" | \"yarn\" | \"bun\";\n readonly model?: string;\n readonly install?: boolean;\n readonly label?: string;\n readonly build?: boolean;\n readonly image?: string;\n}\n\nexport const imageRecipe = `FROM node:24-bookworm-slim\nARG AGENT_UID=1000\nARG AGENT_GID=1000\nRUN apt-get update && apt-get install -y --no-install-recommends git gh openssh-client ca-certificates curl procps util-linux python3 && rm -rf /var/lib/apt/lists/*\nRUN npm install -g --allow-scripts=@anthropic-ai/claude-code @openai/codex@${agentVersions.codex} @anthropic-ai/claude-code@${agentVersions.claude}\nRUN groupmod -o -g \"$AGENT_GID\" node && usermod -o -u \"$AGENT_UID\" -g \"$AGENT_GID\" node\nENV HOME=/home/agent\nUSER $AGENT_UID:$AGENT_GID\nWORKDIR /workspace\n`;\n\nexport async function initialize(\n options: InitOptions = {},\n executor?: Executor,\n): Promise<{ files: readonly string[]; run: string }> {\n const root = resolve(options.directory ?? process.cwd()),\n folder = join(root, \".outpost\");\n const agent = options.agent ?? \"codex\",\n provider = options.provider ?? \"docker\",\n template = options.template ?? \"blank\";\n invariant([\"codex\", \"claude\"].includes(agent), \"Choose codex or claude\");\n invariant(\n [\"docker\", \"podman\", \"vercel\", \"daytona\", \"local\"].includes(provider),\n \"Unknown sandbox provider\",\n );\n invariant(\n [\"blank\", \"iterate\", \"review\", \"plan\", \"plan-review\"].includes(template),\n \"Unknown starter template\",\n );\n if (options.tracker)\n invariant(\n [\"github\", \"beads\", \"custom\"].includes(options.tracker),\n \"Unknown tracker\",\n );\n let pkg: { type?: string; packageManager?: string } = {};\n try {\n pkg = JSON.parse(await readFile(join(root, \"package.json\"), \"utf8\"));\n if (!pkg || typeof pkg !== \"object\") pkg = {};\n } catch (cause) {\n if (\n !(cause instanceof SyntaxError) &&\n (cause as NodeJS.ErrnoException).code !== \"ENOENT\"\n )\n throw cause;\n }\n const extension = pkg.type === \"module\" ? \"ts\" : \"mts\";\n let detected =\n typeof pkg.packageManager === \"string\"\n ? pkg.packageManager.split(\"@\")[0]\n : undefined;\n if (!detected || ![\"npm\", \"pnpm\", \"yarn\", \"bun\"].includes(detected)) {\n detected = undefined;\n for (const [file, manager] of [\n [\"pnpm-lock.yaml\", \"pnpm\"],\n [\"yarn.lock\", \"yarn\"],\n [\"bun.lock\", \"bun\"],\n [\"bun.lockb\", \"bun\"],\n ]) {\n if (\n await access(join(root, file!))\n .then(() => true)\n .catch(() => false)\n ) {\n detected = manager;\n break;\n }\n }\n }\n const manager = options.manager ?? detected ?? \"npm\";\n invariant(\n [\"npm\", \"pnpm\", \"yarn\", \"bun\"].includes(manager),\n \"Unknown package manager\",\n );\n const files: Record<string, string> = {\n [`run.${extension}`]: starter(options, extension),\n \"brief.md\":\n \"Objective: {{OBJECTIVE}}\\n\\nWork on {{WORK_BRANCH}} from {{BASE_BRANCH}}. Inspect the repository, implement the objective, run relevant tests and commit your changes. When finished, write <outpost>done</outpost>.\\n\",\n \".env.example\":\n (agent === \"codex\" ? \"OPENAI_API_KEY=\\n\" : \"ANTHROPIC_API_KEY=\\n\") +\n (options.tracker === \"github\" ? \"GH_TOKEN=\\n\" : \"\"),\n \".gitignore\": \".env\\nworkspaces/\\nlocks/\\nrecovery/\\nlogs/\\n\",\n };\n if (provider === \"docker\" || provider === \"podman\")\n files[provider === \"docker\" ? \"Dockerfile\" : \"Containerfile\"] =\n options.tracker === \"beads\"\n ? imageRecipe.replace(\n \"RUN groupmod\",\n \"RUN npm install -g --allow-scripts=@beads/bd @beads/bd@1.2.2\\nRUN groupmod\",\n )\n : imageRecipe;\n if (template !== \"blank\")\n files[\"STANDARDS.md\"] =\n \"# Engineering standards\\n\\nKeep domain rules separate from infrastructure. Test observable behavior, handle failure and cancellation, and preserve existing user changes. Run the repository checks before committing. Prefer clear names to comments; keep necessary comments at most two lines. Review the complete diff against the supplied base commit.\\n\";\n if (options.tracker)\n files[`tickets.${extension}`] = tracker(options.tracker, options.label);\n if (options.tracker === \"custom\")\n files[\"TRACKER.md\"] =\n `# Custom issue tracker\\n\\nSet OUTPOST_TRACKER_URL in the host environment before running the starter. Implement GET issues?state=open (Issue[]), GET issues/:id (Issue), and POST issues/:id/close (any successful status). Issue has id, title, optional body and optional blockedBy ids. The generated tickets.${extension} is yours to adapt: add authentication, pagination and filtering for your tracker there. Never commit credentials.\\n\\nThe campaign reloads the backlog each cycle, reads an issue before implementation, and closes it only after its commits reach the host branch. Configure authentication in tickets.${extension}. A failed close is reported and must be reconciled before rerunning.\\n`;\n for (const name of Object.keys(files))\n if (\n await access(join(folder, name))\n .then(() => true)\n .catch(() => false)\n )\n throw new OutpostError(\n \"configuration\",\n `Initialization would overwrite .outpost/${name}`,\n );\n await mkdir(folder, { recursive: true });\n for (const [name, content] of Object.entries(files))\n await writeFile(join(folder, name), content, { flag: \"wx\" });\n if (options.install) {\n const packages = [\n \"@elie-laloum/outpost\",\n ...(provider === \"vercel\"\n ? [\"@vercel/sandbox\"]\n : provider === \"daytona\"\n ? [\"@daytona/sdk\"]\n : []),\n ];\n const args =\n manager === \"npm\"\n ? [\"install\", \"--save-dev\", ...packages]\n : [\"add\", \"--dev\", ...packages];\n await requireSuccess(\n process.platform === \"win32\"\n ? {\n executable: \"cmd.exe\",\n arguments: [\"/d\", \"/c\", [manager, ...args].join(\" \")],\n directory: root,\n }\n : { executable: manager, arguments: args, directory: root },\n executor,\n );\n }\n if (options.label) {\n invariant(\n options.tracker === \"github\",\n \"Labels require the GitHub tracker\",\n );\n await requireSuccess(\n {\n executable: \"gh\",\n arguments: [\n \"label\",\n \"create\",\n options.label,\n \"--color\",\n \"4969ED\",\n \"--description\",\n \"Ready for Outpost\",\n \"--force\",\n ],\n directory: root,\n },\n executor,\n );\n }\n if (options.build && (provider === \"docker\" || provider === \"podman\"))\n await manageImage(\n \"build\",\n {\n directory: root,\n engine: provider,\n ...(options.image ? { image: options.image } : {}),\n },\n executor,\n );\n return {\n files: Object.keys(files).map((name) => join(folder, name)),\n run: `node .outpost/run.${extension}`,\n };\n}\n\nexport async function manageImage(\n action: \"build\" | \"remove\",\n options: {\n directory?: string;\n engine?: \"docker\" | \"podman\";\n image?: string;\n file?: string;\n uid?: number;\n gid?: number;\n } = {},\n executor?: Executor,\n): Promise<string> {\n const root = resolve(options.directory ?? process.cwd()),\n engine = options.engine ?? \"docker\",\n image = options.image ?? imageName(root);\n invariant(\n engine === \"docker\" || engine === \"podman\",\n \"Image engine must be docker or podman\",\n );\n const uid = options.uid ?? process.getuid?.() ?? 1000,\n gid = options.gid ?? process.getgid?.() ?? 1000;\n invariant(\n Number.isSafeInteger(uid) &&\n uid >= 0 &&\n Number.isSafeInteger(gid) &&\n gid >= 0,\n \"UID and GID must be nonnegative integers\",\n );\n const args =\n action === \"remove\"\n ? [\"image\", \"rm\", image]\n : [\n \"build\",\n \"--tag\",\n image,\n \"--file\",\n resolve(\n root,\n options.file ??\n `.outpost/${engine === \"docker\" ? \"Dockerfile\" : \"Containerfile\"}`,\n ),\n \"--build-arg\",\n `AGENT_UID=${uid}`,\n \"--build-arg\",\n `AGENT_GID=${gid}`,\n root,\n ];\n await requireSuccess(\n {\n executable: engine,\n arguments: args,\n directory: root,\n deadlineMs: 1_800_000,\n observe(channel, text) {\n (channel === \"stdout\" ? process.stdout : process.stderr).write(text);\n },\n },\n executor,\n );\n return image;\n}\n"]}
@@ -0,0 +1,3 @@
1
+ import type { InitOptions } from "./scaffold.ts";
2
+ export declare function starter(options: InitOptions, extension: string): string;
3
+ export declare function tracker(kind: NonNullable<InitOptions["tracker"]>, label?: string): string;
@@ -0,0 +1,34 @@
1
+ export function starter(options, extension) {
2
+ const agent = options.agent ?? "codex", provider = options.provider ?? "docker", template = options.template ?? "blank";
3
+ const planning = template === "plan" || template === "plan-review";
4
+ const reviewing = template === "review" || template === "plan-review";
5
+ const imports = `import { campaign, dispatch, ${agent} } from "@elie-laloum/outpost";\nimport type { Backlog } from "@elie-laloum/outpost";\nimport { ${provider} } from "@elie-laloum/outpost/providers/${provider}";\nimport { readFile } from "node:fs/promises";\n${options.tracker ? `import { backlog } from "./tickets.${extension}";\n` : ""}`;
6
+ const config = `const runtime = { agent: ${agent}(${options.model ? `{ model: ${JSON.stringify(options.model)} }` : ""}), provider: ${provider}(${options.image && (provider === "docker" || provider === "podman") ? `{ image: ${JSON.stringify(options.image)} }` : ""}) };\nconst objective = process.argv.slice(2).join(" ") || "Inspect this repository and implement one useful improvement.";\n`;
7
+ if (template === "blank")
8
+ return `${imports}\n${config}\nconst result = await dispatch({ ...runtime, branch: { mode: "integrate" }, brief: { file: ".outpost/brief.md", values: { OBJECTIVE: objective } } });\nconsole.log({ branch: result.branch, commits: result.commits, conversation: result.conversation });\n`;
9
+ const backlog = options.tracker
10
+ ? ""
11
+ : `let pending = true;\nconst requested = { id: "objective", title: objective };\nconst backlog: Backlog = {\n async list() { return pending ? [requested] : []; },\n async get() { return requested; },\n async close() { pending = false; },\n};\n`;
12
+ return `${imports}\n${config}\n${backlog}\nconst result = await campaign({\n ...runtime, backlog,\n cycles: 10, concurrency: ${planning ? 3 : 1},\n implementationPasses: ${planning ? 100 : 1}, reviewPasses: 1,\n planner: ${planning ? "runtime.agent" : "false"}, reviewer: ${reviewing ? "runtime.agent" : "false"},\n copies: ["node_modules"],\n standards: await readFile(new URL("./STANDARDS.md", import.meta.url), "utf8"),\n observe: event => console.log(\`[cycle \${event.cycle}] \${event.phase}\${event.issue ? " " + event.issue : ""}\`),\n});\nconsole.log(result);\nif (result.issues.some(issue => issue.state === "failed")) process.exitCode = 1;\n`;
13
+ }
14
+ export function tracker(kind, label) {
15
+ if (kind !== "custom") {
16
+ const factory = kind === "github" ? "githubBacklog" : "beadsBacklog";
17
+ return `import { ${factory} } from "@elie-laloum/outpost";\nexport const backlog = ${factory}(${label ? `{ label: ${JSON.stringify(label)} }` : ""});\n`;
18
+ }
19
+ return `import type { Backlog, Issue } from "@elie-laloum/outpost";
20
+ async function request(path: string, method = "GET", signal?: AbortSignal) {
21
+ const base = process.env.OUTPOST_TRACKER_URL;
22
+ if (!base) throw new Error("Set OUTPOST_TRACKER_URL");
23
+ const reply = await fetch(new URL(path, base.endsWith("/") ? base : base + "/"), { method, signal });
24
+ if (!reply.ok) throw new Error(\`Tracker returned \${reply.status}\`);
25
+ return reply;
26
+ }
27
+ export const backlog: Backlog = {
28
+ async list(signal) { return await (await request("issues?state=open", "GET", signal)).json() as Issue[]; },
29
+ async get(id, signal) { return await (await request("issues/" + encodeURIComponent(id), "GET", signal)).json() as Issue; },
30
+ async close(id, signal) { await request("issues/" + encodeURIComponent(id) + "/close", "POST", signal); },
31
+ };
32
+ `;
33
+ }
34
+ //# sourceMappingURL=starters.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"starters.js","sourceRoot":"","sources":["../../src/cli/starters.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,OAAO,CAAC,OAAoB,EAAE,SAAiB;IAC7D,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,OAAO,EACpC,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,QAAQ,EACvC,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC;IACzC,MAAM,QAAQ,GAAG,QAAQ,KAAK,MAAM,IAAI,QAAQ,KAAK,aAAa,CAAC;IACnE,MAAM,SAAS,GAAG,QAAQ,KAAK,QAAQ,IAAI,QAAQ,KAAK,aAAa,CAAC;IACtE,MAAM,OAAO,GAAG,gCAAgC,KAAK,mGAAmG,QAAQ,2CAA2C,QAAQ,qDAAqD,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,sCAAsC,SAAS,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;IACvV,MAAM,MAAM,GAAG,4BAA4B,KAAK,IAAI,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,YAAY,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,gBAAgB,QAAQ,IAAI,OAAO,CAAC,KAAK,IAAI,CAAC,QAAQ,KAAK,QAAQ,IAAI,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,YAAY,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,+HAA+H,CAAC;IACxY,IAAI,QAAQ,KAAK,OAAO;QACtB,OAAO,GAAG,OAAO,KAAK,MAAM,gQAAgQ,CAAC;IAC/R,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO;QAC7B,CAAC,CAAC,EAAE;QACJ,CAAC,CAAC,sPAAsP,CAAC;IAC3P,OAAO,GAAG,OAAO,KAAK,MAAM,KAAK,OAAO,yFAAyF,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,8BAA8B,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,kCAAkC,QAAQ,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,OAAO,eAAe,SAAS,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,OAAO,wVAAwV,CAAC;AACtpB,CAAC;AAED,MAAM,UAAU,OAAO,CACrB,IAAyC,EACzC,KAAc;IAEd,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;QACtB,MAAM,OAAO,GAAG,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,cAAc,CAAC;QACrE,OAAO,YAAY,OAAO,2DAA2D,OAAO,IAAI,KAAK,CAAC,CAAC,CAAC,YAAY,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC;IAC3J,CAAC;IACD,OAAO;;;;;;;;;;;;;CAaR,CAAC;AACF,CAAC","sourcesContent":["import type { InitOptions } from \"./scaffold.ts\";\n\nexport function starter(options: InitOptions, extension: string): string {\n const agent = options.agent ?? \"codex\",\n provider = options.provider ?? \"docker\",\n template = options.template ?? \"blank\";\n const planning = template === \"plan\" || template === \"plan-review\";\n const reviewing = template === \"review\" || template === \"plan-review\";\n const imports = `import { campaign, dispatch, ${agent} } from \"@elie-laloum/outpost\";\\nimport type { Backlog } from \"@elie-laloum/outpost\";\\nimport { ${provider} } from \"@elie-laloum/outpost/providers/${provider}\";\\nimport { readFile } from \"node:fs/promises\";\\n${options.tracker ? `import { backlog } from \"./tickets.${extension}\";\\n` : \"\"}`;\n const config = `const runtime = { agent: ${agent}(${options.model ? `{ model: ${JSON.stringify(options.model)} }` : \"\"}), provider: ${provider}(${options.image && (provider === \"docker\" || provider === \"podman\") ? `{ image: ${JSON.stringify(options.image)} }` : \"\"}) };\\nconst objective = process.argv.slice(2).join(\" \") || \"Inspect this repository and implement one useful improvement.\";\\n`;\n if (template === \"blank\")\n return `${imports}\\n${config}\\nconst result = await dispatch({ ...runtime, branch: { mode: \"integrate\" }, brief: { file: \".outpost/brief.md\", values: { OBJECTIVE: objective } } });\\nconsole.log({ branch: result.branch, commits: result.commits, conversation: result.conversation });\\n`;\n const backlog = options.tracker\n ? \"\"\n : `let pending = true;\\nconst requested = { id: \"objective\", title: objective };\\nconst backlog: Backlog = {\\n async list() { return pending ? [requested] : []; },\\n async get() { return requested; },\\n async close() { pending = false; },\\n};\\n`;\n return `${imports}\\n${config}\\n${backlog}\\nconst result = await campaign({\\n ...runtime, backlog,\\n cycles: 10, concurrency: ${planning ? 3 : 1},\\n implementationPasses: ${planning ? 100 : 1}, reviewPasses: 1,\\n planner: ${planning ? \"runtime.agent\" : \"false\"}, reviewer: ${reviewing ? \"runtime.agent\" : \"false\"},\\n copies: [\"node_modules\"],\\n standards: await readFile(new URL(\"./STANDARDS.md\", import.meta.url), \"utf8\"),\\n observe: event => console.log(\\`[cycle \\${event.cycle}] \\${event.phase}\\${event.issue ? \" \" + event.issue : \"\"}\\`),\\n});\\nconsole.log(result);\\nif (result.issues.some(issue => issue.state === \"failed\")) process.exitCode = 1;\\n`;\n}\n\nexport function tracker(\n kind: NonNullable<InitOptions[\"tracker\"]>,\n label?: string,\n): string {\n if (kind !== \"custom\") {\n const factory = kind === \"github\" ? \"githubBacklog\" : \"beadsBacklog\";\n return `import { ${factory} } from \"@elie-laloum/outpost\";\\nexport const backlog = ${factory}(${label ? `{ label: ${JSON.stringify(label)} }` : \"\"});\\n`;\n }\n return `import type { Backlog, Issue } from \"@elie-laloum/outpost\";\nasync function request(path: string, method = \"GET\", signal?: AbortSignal) {\n const base = process.env.OUTPOST_TRACKER_URL;\n if (!base) throw new Error(\"Set OUTPOST_TRACKER_URL\");\n const reply = await fetch(new URL(path, base.endsWith(\"/\") ? base : base + \"/\"), { method, signal });\n if (!reply.ok) throw new Error(\\`Tracker returned \\${reply.status}\\`);\n return reply;\n}\nexport const backlog: Backlog = {\n async list(signal) { return await (await request(\"issues?state=open\", \"GET\", signal)).json() as Issue[]; },\n async get(id, signal) { return await (await request(\"issues/\" + encodeURIComponent(id), \"GET\", signal)).json() as Issue; },\n async close(id, signal) { await request(\"issues/\" + encodeURIComponent(id) + \"/close\", \"POST\", signal); },\n};\n`;\n}\n"]}
@@ -0,0 +1,16 @@
1
+ export interface Issue {
2
+ readonly id: string;
3
+ readonly title: string;
4
+ readonly body?: string;
5
+ readonly blockedBy?: readonly string[];
6
+ }
7
+ export interface Backlog {
8
+ list(signal?: AbortSignal): Promise<readonly Issue[]>;
9
+ get(id: string, signal?: AbortSignal): Promise<Issue>;
10
+ close(id: string, signal?: AbortSignal): Promise<void>;
11
+ }
12
+ export interface Assignment {
13
+ readonly id: string;
14
+ readonly branch: string;
15
+ }
16
+ export declare function readPlan(input: unknown): readonly Assignment[];
@@ -0,0 +1,23 @@
1
+ import { invariant } from "./errors.js";
2
+ export function readPlan(input) {
3
+ invariant(!!input &&
4
+ typeof input === "object" &&
5
+ "issues" in input &&
6
+ Array.isArray(input.issues), "Plan must contain an issues array");
7
+ const ids = new Set(), branches = new Set();
8
+ return input.issues.map((entry) => {
9
+ invariant(!!entry &&
10
+ typeof entry === "object" &&
11
+ "id" in entry &&
12
+ typeof entry.id === "string" &&
13
+ entry.id.trim().length > 0 &&
14
+ "branch" in entry &&
15
+ typeof entry.branch === "string" &&
16
+ entry.branch.startsWith("outpost/"), "Each assignment needs an issue id and an outpost/ branch");
17
+ invariant(!ids.has(entry.id) && !branches.has(entry.branch), "Plan contains duplicate issues or branches");
18
+ ids.add(entry.id);
19
+ branches.add(entry.branch);
20
+ return { id: entry.id, branch: entry.branch };
21
+ });
22
+ }
23
+ //# sourceMappingURL=backlog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backlog.js","sourceRoot":"","sources":["../../src/domain/backlog.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAoBxC,MAAM,UAAU,QAAQ,CAAC,KAAc;IACrC,SAAS,CACP,CAAC,CAAC,KAAK;QACL,OAAO,KAAK,KAAK,QAAQ;QACzB,QAAQ,IAAI,KAAK;QACjB,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,EAC7B,mCAAmC,CACpC,CAAC;IACF,MAAM,GAAG,GAAG,IAAI,GAAG,EAAU,EAC3B,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAc,EAAE,EAAE;QACzC,SAAS,CACP,CAAC,CAAC,KAAK;YACL,OAAO,KAAK,KAAK,QAAQ;YACzB,IAAI,IAAI,KAAK;YACb,OAAO,KAAK,CAAC,EAAE,KAAK,QAAQ;YAC5B,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC;YAC1B,QAAQ,IAAI,KAAK;YACjB,OAAO,KAAK,CAAC,MAAM,KAAK,QAAQ;YAChC,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC,UAAU,CAAC,EACrC,0DAA0D,CAC3D,CAAC;QACF,SAAS,CACP,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,EACjD,4CAA4C,CAC7C,CAAC;QACF,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAClB,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAC3B,OAAO,EAAE,EAAE,EAAE,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC;IAChD,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["import { invariant } from \"./errors.ts\";\n\nexport interface Issue {\n readonly id: string;\n readonly title: string;\n readonly body?: string;\n readonly blockedBy?: readonly string[];\n}\n\nexport interface Backlog {\n list(signal?: AbortSignal): Promise<readonly Issue[]>;\n get(id: string, signal?: AbortSignal): Promise<Issue>;\n close(id: string, signal?: AbortSignal): Promise<void>;\n}\n\nexport interface Assignment {\n readonly id: string;\n readonly branch: string;\n}\n\nexport function readPlan(input: unknown): readonly Assignment[] {\n invariant(\n !!input &&\n typeof input === \"object\" &&\n \"issues\" in input &&\n Array.isArray(input.issues),\n \"Plan must contain an issues array\",\n );\n const ids = new Set<string>(),\n branches = new Set<string>();\n return input.issues.map((entry: unknown) => {\n invariant(\n !!entry &&\n typeof entry === \"object\" &&\n \"id\" in entry &&\n typeof entry.id === \"string\" &&\n entry.id.trim().length > 0 &&\n \"branch\" in entry &&\n typeof entry.branch === \"string\" &&\n entry.branch.startsWith(\"outpost/\"),\n \"Each assignment needs an issue id and an outpost/ branch\",\n );\n invariant(\n !ids.has(entry.id) && !branches.has(entry.branch),\n \"Plan contains duplicate issues or branches\",\n );\n ids.add(entry.id);\n branches.add(entry.branch);\n return { id: entry.id, branch: entry.branch };\n });\n}\n"]}
@@ -0,0 +1,11 @@
1
+ export type FaultCode = "configuration" | "process" | "timeout" | "aborted" | "workspace" | "conflict" | "prompt" | "response" | "session" | "provider";
2
+ export declare class OutpostError extends Error {
3
+ recovery: Readonly<Record<string, unknown>>;
4
+ readonly code: FaultCode;
5
+ readonly details: Readonly<Record<string, unknown>>;
6
+ constructor(code: FaultCode, message: string, details?: Record<string, unknown>, cause?: unknown);
7
+ }
8
+ export declare function recordRecovery(error: unknown, details: Record<string, unknown>): void;
9
+ export declare function recoveryDetails(error: unknown): Readonly<Record<string, unknown>> | undefined;
10
+ export declare function invariant(condition: unknown, message: string): asserts condition;
11
+ export declare function positive(value: number, label: string): number;
@@ -0,0 +1,37 @@
1
+ export class OutpostError extends Error {
2
+ recovery = {};
3
+ code;
4
+ details;
5
+ constructor(code, message, details = {}, cause) {
6
+ super(message, { cause });
7
+ this.name = "OutpostError";
8
+ this.code = code;
9
+ this.details = Object.freeze({ ...details });
10
+ }
11
+ }
12
+ const recoveryRecords = new WeakMap();
13
+ export function recordRecovery(error, details) {
14
+ if (error && (typeof error === "object" || typeof error === "function")) {
15
+ const previous = recoveryDetails(error);
16
+ const recovery = Object.freeze({ ...previous, ...details });
17
+ recoveryRecords.set(error, recovery);
18
+ if (error instanceof OutpostError)
19
+ error.recovery = recovery;
20
+ }
21
+ }
22
+ export function recoveryDetails(error) {
23
+ return error instanceof OutpostError
24
+ ? error.recovery
25
+ : error && (typeof error === "object" || typeof error === "function")
26
+ ? recoveryRecords.get(error)
27
+ : undefined;
28
+ }
29
+ export function invariant(condition, message) {
30
+ if (!condition)
31
+ throw new OutpostError("configuration", message);
32
+ }
33
+ export function positive(value, label) {
34
+ invariant(Number.isSafeInteger(value) && value > 0, `${label} must be a positive integer`);
35
+ return value;
36
+ }
37
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/domain/errors.ts"],"names":[],"mappings":"AAYA,MAAM,OAAO,YAAa,SAAQ,KAAK;IACrC,QAAQ,GAAsC,EAAE,CAAC;IACxC,IAAI,CAAY;IAChB,OAAO,CAAoC;IACpD,YACE,IAAe,EACf,OAAe,EACf,UAAmC,EAAE,EACrC,KAAe;QAEf,KAAK,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;QAC1B,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC;QAC3B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,OAAO,EAAE,CAAC,CAAC;IAC/C,CAAC;CACF;AAED,MAAM,eAAe,GAAG,IAAI,OAAO,EAGhC,CAAC;AAEJ,MAAM,UAAU,cAAc,CAC5B,KAAc,EACd,OAAgC;IAEhC,IAAI,KAAK,IAAI,CAAC,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,KAAK,KAAK,UAAU,CAAC,EAAE,CAAC;QACxE,MAAM,QAAQ,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;QACxC,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,QAAQ,EAAE,GAAG,OAAO,EAAE,CAAC,CAAC;QAC5D,eAAe,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;QACrC,IAAI,KAAK,YAAY,YAAY;YAAE,KAAK,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC/D,CAAC;AACH,CAAC;AAED,MAAM,UAAU,eAAe,CAC7B,KAAc;IAEd,OAAO,KAAK,YAAY,YAAY;QAClC,CAAC,CAAC,KAAK,CAAC,QAAQ;QAChB,CAAC,CAAC,KAAK,IAAI,CAAC,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,KAAK,KAAK,UAAU,CAAC;YACnE,CAAC,CAAC,eAAe,CAAC,GAAG,CAAC,KAAK,CAAC;YAC5B,CAAC,CAAC,SAAS,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,SAAS,CACvB,SAAkB,EAClB,OAAe;IAEf,IAAI,CAAC,SAAS;QAAE,MAAM,IAAI,YAAY,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC;AACnE,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,KAAa,EAAE,KAAa;IACnD,SAAS,CACP,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,EACxC,GAAG,KAAK,6BAA6B,CACtC,CAAC;IACF,OAAO,KAAK,CAAC;AACf,CAAC","sourcesContent":["export type FaultCode =\n | \"configuration\"\n | \"process\"\n | \"timeout\"\n | \"aborted\"\n | \"workspace\"\n | \"conflict\"\n | \"prompt\"\n | \"response\"\n | \"session\"\n | \"provider\";\n\nexport class OutpostError extends Error {\n recovery: Readonly<Record<string, unknown>> = {};\n readonly code: FaultCode;\n readonly details: Readonly<Record<string, unknown>>;\n constructor(\n code: FaultCode,\n message: string,\n details: Record<string, unknown> = {},\n cause?: unknown,\n ) {\n super(message, { cause });\n this.name = \"OutpostError\";\n this.code = code;\n this.details = Object.freeze({ ...details });\n }\n}\n\nconst recoveryRecords = new WeakMap<\n object,\n Readonly<Record<string, unknown>>\n>();\n\nexport function recordRecovery(\n error: unknown,\n details: Record<string, unknown>,\n): void {\n if (error && (typeof error === \"object\" || typeof error === \"function\")) {\n const previous = recoveryDetails(error);\n const recovery = Object.freeze({ ...previous, ...details });\n recoveryRecords.set(error, recovery);\n if (error instanceof OutpostError) error.recovery = recovery;\n }\n}\n\nexport function recoveryDetails(\n error: unknown,\n): Readonly<Record<string, unknown>> | undefined {\n return error instanceof OutpostError\n ? error.recovery\n : error && (typeof error === \"object\" || typeof error === \"function\")\n ? recoveryRecords.get(error)\n : undefined;\n}\n\nexport function invariant(\n condition: unknown,\n message: string,\n): asserts condition {\n if (!condition) throw new OutpostError(\"configuration\", message);\n}\n\nexport function positive(value: number, label: string): number {\n invariant(\n Number.isSafeInteger(value) && value > 0,\n `${label} must be a positive integer`,\n );\n return value;\n}\n"]}
@@ -0,0 +1,183 @@
1
+ import type { Readable, Writable } from "node:stream";
2
+ export type Variables = Readonly<Record<string, string>>;
3
+ export type Channel = "stdout" | "stderr";
4
+ export interface Command {
5
+ readonly executable: string;
6
+ readonly arguments?: readonly string[];
7
+ readonly stdin?: string;
8
+ readonly directory?: string;
9
+ readonly variables?: Variables;
10
+ readonly signal?: AbortSignal;
11
+ readonly deadlineMs?: number;
12
+ readonly interactive?: boolean;
13
+ readonly terminal?: {
14
+ readonly input?: Readable;
15
+ readonly output?: Writable;
16
+ readonly error?: Writable;
17
+ };
18
+ readonly elevated?: boolean;
19
+ readonly retain?: number;
20
+ readonly observe?: (channel: Channel, text: string) => void;
21
+ }
22
+ export interface CommandResult {
23
+ readonly status: number;
24
+ readonly stdout: string;
25
+ readonly stderr: string;
26
+ }
27
+ export interface Volume {
28
+ readonly source: string;
29
+ readonly target: string;
30
+ readonly readOnly?: boolean;
31
+ }
32
+ export interface SandboxContext {
33
+ readonly repository: string;
34
+ readonly directory: string;
35
+ readonly gitDirectories: readonly string[];
36
+ readonly variables: Variables;
37
+ readonly signal?: AbortSignal;
38
+ }
39
+ export interface TransferOptions {
40
+ readonly signal?: AbortSignal;
41
+ readonly deadlineMs?: number;
42
+ }
43
+ export interface SandboxLease {
44
+ readonly root: string;
45
+ readonly home: string;
46
+ invoke(command: Command): Promise<CommandResult>;
47
+ upload(source: string, destination: string, options?: TransferOptions): Promise<void>;
48
+ download(source: string, destination: string, options?: TransferOptions): Promise<void>;
49
+ release(): Promise<void>;
50
+ }
51
+ export interface SandboxProvider {
52
+ readonly name: string;
53
+ readonly placement: "mounted" | "remote" | "host";
54
+ readonly variables?: Variables;
55
+ acquire(context: SandboxContext): Promise<SandboxLease>;
56
+ }
57
+ export interface Usage {
58
+ readonly input: number;
59
+ readonly cached: number;
60
+ readonly cacheCreated?: number;
61
+ readonly output: number;
62
+ }
63
+ export type AgentEvent = {
64
+ readonly kind: "phase";
65
+ readonly name: string;
66
+ readonly agent?: string;
67
+ readonly branch?: string;
68
+ readonly directory?: string;
69
+ } | {
70
+ readonly kind: "summary";
71
+ readonly durationMs: number;
72
+ readonly status: number;
73
+ readonly tokens: Usage;
74
+ } | {
75
+ readonly kind: "warning";
76
+ readonly message: string;
77
+ } | {
78
+ readonly kind: "text";
79
+ readonly text: string;
80
+ } | {
81
+ readonly kind: "result";
82
+ readonly text: string;
83
+ } | {
84
+ readonly kind: "prompt";
85
+ readonly text: string;
86
+ } | {
87
+ readonly kind: "tool";
88
+ readonly name: string;
89
+ readonly input: unknown;
90
+ } | {
91
+ readonly kind: "conversation";
92
+ readonly id: string;
93
+ } | {
94
+ readonly kind: "usage";
95
+ readonly tokens: Usage;
96
+ } | {
97
+ readonly kind: "failure";
98
+ readonly message: string;
99
+ } | {
100
+ readonly kind: "finished";
101
+ } | {
102
+ readonly kind: "raw";
103
+ readonly value: unknown;
104
+ };
105
+ export type AgentObservation = AgentEvent & {
106
+ readonly pass: number;
107
+ readonly at: string;
108
+ };
109
+ export interface AgentInput {
110
+ readonly text?: string;
111
+ readonly interactive?: boolean;
112
+ readonly continuation?: {
113
+ readonly id: string;
114
+ readonly fork?: boolean;
115
+ };
116
+ }
117
+ export interface ConversationRecord {
118
+ readonly id: string;
119
+ readonly file: string;
120
+ readonly format: string;
121
+ }
122
+ export interface ConversationContext {
123
+ readonly repository: string;
124
+ readonly sandbox: SandboxLease;
125
+ readonly staging: string;
126
+ readonly home?: string;
127
+ readonly local?: boolean;
128
+ readonly warn?: (message: string) => void;
129
+ }
130
+ export interface ConversationStore {
131
+ readonly name: string;
132
+ locate(id: string, repository: string, home?: string): Promise<ConversationRecord>;
133
+ capture(id: string, context: ConversationContext): Promise<ConversationRecord>;
134
+ restore(record: ConversationRecord, context: ConversationContext): Promise<void>;
135
+ }
136
+ export interface AgentAdapter {
137
+ readonly name: string;
138
+ readonly variables?: Variables;
139
+ readonly conversations?: "claude" | "codex";
140
+ readonly storage?: ConversationStore;
141
+ readonly capture?: boolean;
142
+ readonly resumable?: boolean;
143
+ transcriptUsage?(text: string): Usage | undefined;
144
+ request(input: AgentInput): Command;
145
+ events(line: string): readonly AgentEvent[];
146
+ }
147
+ export interface StageLimits {
148
+ readonly copyMs?: number;
149
+ readonly gitMs?: number;
150
+ readonly collectMs?: number;
151
+ readonly mergeMs?: number;
152
+ }
153
+ export type BranchPolicy = {
154
+ readonly mode: "current";
155
+ } | {
156
+ readonly mode: "named";
157
+ readonly name: string;
158
+ readonly from?: string;
159
+ } | {
160
+ readonly mode: "integrate";
161
+ readonly from?: string;
162
+ };
163
+ export interface WorkspaceRecord {
164
+ readonly repository: string;
165
+ readonly directory: string;
166
+ readonly branch: string;
167
+ readonly baseBranch: string;
168
+ readonly baseline: string;
169
+ readonly gitDirectories: readonly string[];
170
+ readonly policy: BranchPolicy;
171
+ }
172
+ export interface Commit {
173
+ readonly oid: string;
174
+ readonly subject: string;
175
+ }
176
+ export interface Disposal {
177
+ readonly retainedDirectory?: string;
178
+ }
179
+ export interface LifecycleHooks {
180
+ readonly workspaceReady?: readonly Command[];
181
+ readonly hostReady?: readonly Command[];
182
+ readonly sandboxReady?: readonly Command[];
183
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ports.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ports.js","sourceRoot":"","sources":["../../src/domain/ports.ts"],"names":[],"mappings":"","sourcesContent":["import type { Readable, Writable } from \"node:stream\";\n\nexport type Variables = Readonly<Record<string, string>>;\nexport type Channel = \"stdout\" | \"stderr\";\n\nexport interface Command {\n readonly executable: string;\n readonly arguments?: readonly string[];\n readonly stdin?: string;\n readonly directory?: string;\n readonly variables?: Variables;\n readonly signal?: AbortSignal;\n readonly deadlineMs?: number;\n readonly interactive?: boolean;\n readonly terminal?: {\n readonly input?: Readable;\n readonly output?: Writable;\n readonly error?: Writable;\n };\n readonly elevated?: boolean;\n readonly retain?: number;\n readonly observe?: (channel: Channel, text: string) => void;\n}\n\nexport interface CommandResult {\n readonly status: number;\n readonly stdout: string;\n readonly stderr: string;\n}\n\nexport interface Volume {\n readonly source: string;\n readonly target: string;\n readonly readOnly?: boolean;\n}\n\nexport interface SandboxContext {\n readonly repository: string;\n readonly directory: string;\n readonly gitDirectories: readonly string[];\n readonly variables: Variables;\n readonly signal?: AbortSignal;\n}\n\nexport interface TransferOptions {\n readonly signal?: AbortSignal;\n readonly deadlineMs?: number;\n}\n\nexport interface SandboxLease {\n readonly root: string;\n readonly home: string;\n invoke(command: Command): Promise<CommandResult>;\n upload(\n source: string,\n destination: string,\n options?: TransferOptions,\n ): Promise<void>;\n download(\n source: string,\n destination: string,\n options?: TransferOptions,\n ): Promise<void>;\n release(): Promise<void>;\n}\n\nexport interface SandboxProvider {\n readonly name: string;\n readonly placement: \"mounted\" | \"remote\" | \"host\";\n readonly variables?: Variables;\n acquire(context: SandboxContext): Promise<SandboxLease>;\n}\n\nexport interface Usage {\n readonly input: number;\n readonly cached: number;\n readonly cacheCreated?: number;\n readonly output: number;\n}\n\nexport type AgentEvent =\n | {\n readonly kind: \"phase\";\n readonly name: string;\n readonly agent?: string;\n readonly branch?: string;\n readonly directory?: string;\n }\n | {\n readonly kind: \"summary\";\n readonly durationMs: number;\n readonly status: number;\n readonly tokens: Usage;\n }\n | { readonly kind: \"warning\"; readonly message: string }\n | { readonly kind: \"text\"; readonly text: string }\n | { readonly kind: \"result\"; readonly text: string }\n | { readonly kind: \"prompt\"; readonly text: string }\n | { readonly kind: \"tool\"; readonly name: string; readonly input: unknown }\n | { readonly kind: \"conversation\"; readonly id: string }\n | { readonly kind: \"usage\"; readonly tokens: Usage }\n | { readonly kind: \"failure\"; readonly message: string }\n | { readonly kind: \"finished\" }\n | { readonly kind: \"raw\"; readonly value: unknown };\n\nexport type AgentObservation = AgentEvent & {\n readonly pass: number;\n readonly at: string;\n};\n\nexport interface AgentInput {\n readonly text?: string;\n readonly interactive?: boolean;\n readonly continuation?: { readonly id: string; readonly fork?: boolean };\n}\n\nexport interface ConversationRecord {\n readonly id: string;\n readonly file: string;\n readonly format: string;\n}\n\nexport interface ConversationContext {\n readonly repository: string;\n readonly sandbox: SandboxLease;\n readonly staging: string;\n readonly home?: string;\n readonly local?: boolean;\n readonly warn?: (message: string) => void;\n}\n\nexport interface ConversationStore {\n readonly name: string;\n locate(\n id: string,\n repository: string,\n home?: string,\n ): Promise<ConversationRecord>;\n capture(\n id: string,\n context: ConversationContext,\n ): Promise<ConversationRecord>;\n restore(\n record: ConversationRecord,\n context: ConversationContext,\n ): Promise<void>;\n}\n\nexport interface AgentAdapter {\n readonly name: string;\n readonly variables?: Variables;\n readonly conversations?: \"claude\" | \"codex\";\n readonly storage?: ConversationStore;\n readonly capture?: boolean;\n readonly resumable?: boolean;\n transcriptUsage?(text: string): Usage | undefined;\n request(input: AgentInput): Command;\n events(line: string): readonly AgentEvent[];\n}\n\nexport interface StageLimits {\n readonly copyMs?: number;\n readonly gitMs?: number;\n readonly collectMs?: number;\n readonly mergeMs?: number;\n}\n\nexport type BranchPolicy =\n | { readonly mode: \"current\" }\n | { readonly mode: \"named\"; readonly name: string; readonly from?: string }\n | { readonly mode: \"integrate\"; readonly from?: string };\n\nexport interface WorkspaceRecord {\n readonly repository: string;\n readonly directory: string;\n readonly branch: string;\n readonly baseBranch: string;\n readonly baseline: string;\n readonly gitDirectories: readonly string[];\n readonly policy: BranchPolicy;\n}\n\nexport interface Commit {\n readonly oid: string;\n readonly subject: string;\n}\n\nexport interface Disposal {\n readonly retainedDirectory?: string;\n}\n\nexport interface LifecycleHooks {\n readonly workspaceReady?: readonly Command[];\n readonly hostReady?: readonly Command[];\n readonly sandboxReady?: readonly Command[];\n}\n"]}