@snappedly-tools/shipyard 0.7.0 → 0.9.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 (63) hide show
  1. package/README.md +46 -43
  2. package/dist/{MountConfig-bZoCs4Dd.d.ts → MountConfig-K5ILnfht.d.ts} +1 -1
  3. package/dist/SandboxProvider-oUAwYlWm.d.ts +120 -0
  4. package/dist/{chunk-FDYOTN55.js → chunk-57EEKW3R.js} +361 -984
  5. package/dist/chunk-57EEKW3R.js.map +1 -0
  6. package/dist/{chunk-56TDSWFU.js → chunk-HXSZM52J.js} +3 -3
  7. package/dist/{chunk-56TDSWFU.js.map → chunk-HXSZM52J.js.map} +1 -1
  8. package/dist/{chunk-WAXEMZUV.js → chunk-JZUBT4WG.js} +241 -33
  9. package/dist/chunk-JZUBT4WG.js.map +1 -0
  10. package/dist/chunk-NQRFVKCU.js +1840 -0
  11. package/dist/chunk-NQRFVKCU.js.map +1 -0
  12. package/dist/chunk-Z5C4LHVP.js +137 -0
  13. package/dist/chunk-Z5C4LHVP.js.map +1 -0
  14. package/dist/createSandbox-DmbnWAZv.d.ts +739 -0
  15. package/dist/index.d.ts +99 -2606
  16. package/dist/index.js +412 -7487
  17. package/dist/index.js.map +1 -1
  18. package/dist/integrations/github.d.ts +52 -0
  19. package/dist/integrations/github.js +1049 -0
  20. package/dist/integrations/github.js.map +1 -0
  21. package/dist/integrations/releases.d.ts +136 -0
  22. package/dist/integrations/releases.js +500 -0
  23. package/dist/integrations/releases.js.map +1 -0
  24. package/dist/main.js +826 -472
  25. package/dist/main.js.map +1 -1
  26. package/dist/publication-BPoy_M9M.d.ts +1200 -0
  27. package/dist/sandboxes/docker.d.ts +2 -3
  28. package/dist/sandboxes/docker.js +2 -4
  29. package/dist/templates/parallel-planner/main.mts +78 -16
  30. package/dist/templates/parallel-planner/planner-branch.mts +151 -0
  31. package/dist/templates/parallel-planner/setup.sh +1 -0
  32. package/dist/templates/parallel-planner-with-review/main.mts +81 -19
  33. package/dist/templates/parallel-planner-with-review/planner-branch.mts +151 -0
  34. package/dist/templates/parallel-planner-with-review/setup.sh +1 -0
  35. package/dist/templates/sequential-reviewer/main.mts +59 -6
  36. package/dist/templates/sequential-reviewer/setup.sh +1 -0
  37. package/dist/templates/shared/setup.sh +1 -0
  38. package/dist/templates/simple-loop/main.mts +58 -5
  39. package/dist/templates/simple-loop/setup.sh +1 -0
  40. package/dist/workflow/coordinator/migrations/002_workflow_phase_records.sql +11 -0
  41. package/dist/workflow/coordinator/migrations/003_phase_record_schema_version.sql +6 -0
  42. package/dist/workflow.d.ts +472 -0
  43. package/dist/workflow.js +4345 -0
  44. package/dist/workflow.js.map +1 -0
  45. package/package.json +11 -15
  46. package/dist/SandboxProvider-XJQqEdSf.d.ts +0 -261
  47. package/dist/chunk-ACD46ZM4.js +0 -136
  48. package/dist/chunk-ACD46ZM4.js.map +0 -1
  49. package/dist/chunk-FDYOTN55.js.map +0 -1
  50. package/dist/chunk-KMGNFXKN.js +0 -38
  51. package/dist/chunk-KMGNFXKN.js.map +0 -1
  52. package/dist/chunk-SOJTAJTF.js +0 -78
  53. package/dist/chunk-SOJTAJTF.js.map +0 -1
  54. package/dist/chunk-WAXEMZUV.js.map +0 -1
  55. package/dist/sandboxes/no-sandbox.d.ts +0 -37
  56. package/dist/sandboxes/no-sandbox.js +0 -4
  57. package/dist/sandboxes/no-sandbox.js.map +0 -1
  58. package/dist/sandboxes/vercel.d.ts +0 -104
  59. package/dist/sandboxes/vercel.js +0 -166
  60. package/dist/sandboxes/vercel.js.map +0 -1
  61. package/dist/templates/blank/main.mts +0 -13
  62. package/dist/templates/blank/prompt.md +0 -12
  63. package/dist/templates/blank/template.json +0 -4
@@ -1,5 +1,5 @@
1
- import { I as IsolatedSandboxProvider } from '../SandboxProvider-XJQqEdSf.js';
2
- import { M as MountConfig } from '../MountConfig-bZoCs4Dd.js';
1
+ import { I as IsolatedSandboxProvider } from '../SandboxProvider-oUAwYlWm.js';
2
+ import { M as MountConfig } from '../MountConfig-K5ILnfht.js';
3
3
 
4
4
  /**
5
5
  * Shared mount utilities for the Docker sandbox provider.
@@ -16,7 +16,6 @@ import { M as MountConfig } from '../MountConfig-bZoCs4Dd.js';
16
16
  * - `false` — disable labeling entirely.
17
17
  */
18
18
  type SelinuxLabel = "z" | "Z" | false;
19
-
20
19
  /**
21
20
  * Derive the default image name from the repo directory.
22
21
  * Returns `shipyard:<dir-name>` where dir-name is the last path segment,
@@ -1,6 +1,4 @@
1
- export { docker } from '../chunk-WAXEMZUV.js';
2
- export { defaultImageName } from '../chunk-FDYOTN55.js';
3
- import '../chunk-KMGNFXKN.js';
4
- import '../chunk-ACD46ZM4.js';
1
+ export { defaultImageName, docker } from '../chunk-JZUBT4WG.js';
2
+ import '../chunk-57EEKW3R.js';
5
3
  //# sourceMappingURL=docker.js.map
6
4
  //# sourceMappingURL=docker.js.map
@@ -5,9 +5,59 @@ import { existsSync } from "node:fs";
5
5
  import * as shipyard from "@snappedly-tools/shipyard";
6
6
  import { docker } from "@snappedly-tools/shipyard/sandboxes/docker";
7
7
  import { z } from "zod";
8
+ import { resolvePlannerBranch } from "./planner-branch.mjs";
8
9
 
9
10
  if (process.loadEnvFile && existsSync(".shipyard/.env"))
10
11
  process.loadEnvFile(".shipyard/.env");
12
+ type ModelRole = "routine" | "strong";
13
+ const CODEX_PROVIDER = true;
14
+ const agentFactory = shipyard.codex;
15
+ type AgentModel = Parameters<typeof agentFactory>[0];
16
+ const readRoleModel = (role: ModelRole): string | undefined => {
17
+ const envName = `SHIPYARD_${role.toUpperCase()}_MODEL`;
18
+ const model = process.env[envName];
19
+ if (model !== undefined && model.trim().length === 0)
20
+ throw new Error(`${envName} must not be empty`);
21
+ return model;
22
+ };
23
+ const roleModels = {
24
+ routine: readRoleModel("routine"),
25
+ strong: readRoleModel("strong"),
26
+ };
27
+ const CODEX_REASONING_EFFORTS = shipyard.CODEX_REASONING_EFFORTS;
28
+ type CodexReasoningEffort = shipyard.CodexReasoningEffort;
29
+ const readCodexReasoningEffort = (
30
+ role: ModelRole,
31
+ ): CodexReasoningEffort | undefined => {
32
+ if (!CODEX_PROVIDER) return undefined;
33
+ const envName = `SHIPYARD_CODEX_${role.toUpperCase()}_REASONING_EFFORT`;
34
+ const effort = process.env[envName]?.trim();
35
+ if (!effort) return undefined;
36
+ if (!(CODEX_REASONING_EFFORTS as readonly string[]).includes(effort))
37
+ throw new Error(
38
+ `${envName} must be one of ${CODEX_REASONING_EFFORTS.join(", ")}; received "${effort}"`,
39
+ );
40
+ return effort as CodexReasoningEffort;
41
+ };
42
+ const roleEfforts = {
43
+ routine: readCodexReasoningEffort("routine"),
44
+ strong: readCodexReasoningEffort("strong"),
45
+ };
46
+ const readCodexRoleModel = (role: ModelRole, defaultModel: AgentModel) => {
47
+ if (!CODEX_PROVIDER || typeof defaultModel === "string") return defaultModel;
48
+ const envName = `SHIPYARD_CODEX_${role.toUpperCase()}_MODEL`;
49
+ const model = process.env[envName]?.trim();
50
+ return model ? { ...defaultModel, model } : defaultModel;
51
+ };
52
+ const roleAgent = (role: ModelRole, defaultModel: AgentModel) => {
53
+ const model = roleModels[role] ?? readCodexRoleModel(role, defaultModel);
54
+ const effort = roleEfforts[role];
55
+ if (typeof model !== "string")
56
+ return effort === undefined
57
+ ? agentFactory(model)
58
+ : agentFactory(model, { effort });
59
+ return agentFactory(model, { effort: effort ?? null });
60
+ };
11
61
  const targetBranch = execFileSync("git", ["branch", "--show-current"], {
12
62
  encoding: "utf8",
13
63
  }).trim();
@@ -21,7 +71,11 @@ if (
21
71
  !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository)
22
72
  )
23
73
  throw new Error("Invalid target branch or GitHub repository");
24
- process.env.GH_REPO = repository;
74
+ const sandboxAuthOptions = {};
75
+ const sandboxProvider = docker({
76
+ env: { GH_REPO: repository },
77
+ ...sandboxAuthOptions,
78
+ });
25
79
  const hooks = {
26
80
  sandbox: {
27
81
  onSandboxReady: [
@@ -107,14 +161,14 @@ const runWorker = async (scope: Scope, ticket: Ticket) => {
107
161
  const sandbox = await shipyard.createSandbox({
108
162
  branch,
109
163
  baseBranch: scope.branch,
110
- sandbox: docker(),
164
+ sandbox: sandboxProvider,
111
165
  hooks,
112
166
  });
113
167
  try {
114
168
  await sandbox.run({
115
169
  name: `triage #${ticket.id}`,
116
170
  maxIterations: 1,
117
- agent: shipyard.codex(shipyard.CODEX_MODELS.strong),
171
+ agent: roleAgent("routine", shipyard.CODEX_MODELS.routine),
118
172
  promptFile: "./.shipyard/triage-prompt.md",
119
173
  promptArgs: { TASK_ID: ticket.id },
120
174
  });
@@ -122,7 +176,7 @@ const runWorker = async (scope: Scope, ticket: Ticket) => {
122
176
  const implementation = await sandbox.run({
123
177
  name: "implementer",
124
178
  maxIterations: 100,
125
- agent: shipyard.codex(shipyard.CODEX_MODELS.routine),
179
+ agent: roleAgent("routine", shipyard.CODEX_MODELS.routine),
126
180
  promptFile: "./.shipyard/implement-prompt.md",
127
181
  promptArgs: {
128
182
  TASK_ID: ticket.id,
@@ -143,13 +197,21 @@ for (let iteration = 0; iteration < 10; iteration++) {
143
197
  execFileSync("node", [".shipyard/select-issues.mjs"], { encoding: "utf8" }),
144
198
  ) as Scope[];
145
199
  if (!scopes.length) break;
200
+ const localBranches = execFileSync(
201
+ "git",
202
+ ["for-each-ref", "--format=%(refname:short)", "refs/heads"],
203
+ { encoding: "utf8" },
204
+ )
205
+ .split(/\r?\n/)
206
+ .filter(Boolean);
207
+ const plannerBranch = await resolvePlannerBranch(localBranches);
146
208
  const plan = await shipyard.run({
147
209
  hooks,
148
- sandbox: docker(),
210
+ sandbox: sandboxProvider,
149
211
  name: "planner",
150
- branchStrategy: { type: "branch", branch: "shipyard/planner" },
212
+ branchStrategy: { type: "branch", branch: plannerBranch },
151
213
  maxIterations: 1,
152
- agent: shipyard.codex(shipyard.CODEX_MODELS.strong),
214
+ agent: roleAgent("strong", shipyard.CODEX_MODELS.strong),
153
215
  promptFile: "./.shipyard/plan-prompt.md",
154
216
  output: shipyard.Output.object({ tag: "plan", schema: planSchema }),
155
217
  });
@@ -203,7 +265,7 @@ for (let iteration = 0; iteration < 10; iteration++) {
203
265
  if (scope.kind === "spec") {
204
266
  const seed = await shipyard.createSandbox({
205
267
  branch: scope.branch,
206
- sandbox: docker(),
268
+ sandbox: sandboxProvider,
207
269
  hooks,
208
270
  });
209
271
  await closeClean(seed);
@@ -237,7 +299,7 @@ for (let iteration = 0; iteration < 10; iteration++) {
237
299
  if (failures.length) throw new TicketFailures(failures);
238
300
  const wave = await shipyard.createSandbox({
239
301
  branch: scope.branch,
240
- sandbox: docker(),
302
+ sandbox: sandboxProvider,
241
303
  hooks,
242
304
  });
243
305
  try {
@@ -277,7 +339,7 @@ for (let iteration = 0; iteration < 10; iteration++) {
277
339
  const resolution = await wave.run({
278
340
  name: "conflict-resolver",
279
341
  maxIterations: 10,
280
- agent: shipyard.codex(shipyard.CODEX_MODELS.strong),
342
+ agent: roleAgent("strong", shipyard.CODEX_MODELS.strong),
281
343
  promptFile: "./.shipyard/conflict-prompt.md",
282
344
  promptArgs: {
283
345
  TASK_ID: ready[index]!.id,
@@ -331,7 +393,7 @@ for (let iteration = 0; iteration < 10; iteration++) {
331
393
  const integrated = await wave.run({
332
394
  name: "spec-integrator",
333
395
  maxIterations: 10,
334
- agent: shipyard.codex(shipyard.CODEX_MODELS.strong),
396
+ agent: roleAgent("strong", shipyard.CODEX_MODELS.strong),
335
397
  promptFile: "./.shipyard/spec-wave-prompt.md",
336
398
  promptArgs: {
337
399
  TASK_ID: id,
@@ -362,7 +424,7 @@ for (let iteration = 0; iteration < 10; iteration++) {
362
424
  }
363
425
  const integration = await shipyard.createSandbox({
364
426
  branch: scope.branch,
365
- sandbox: docker(),
427
+ sandbox: sandboxProvider,
366
428
  hooks,
367
429
  });
368
430
  let handoffEvidence: string;
@@ -371,7 +433,7 @@ for (let iteration = 0; iteration < 10; iteration++) {
371
433
  await integration.run({
372
434
  name: `triage #${id}`,
373
435
  maxIterations: 1,
374
- agent: shipyard.codex(shipyard.CODEX_MODELS.strong),
436
+ agent: roleAgent("routine", shipyard.CODEX_MODELS.routine),
375
437
  promptFile: "./.shipyard/triage-prompt.md",
376
438
  promptArgs: { TASK_ID: id },
377
439
  });
@@ -379,7 +441,7 @@ for (let iteration = 0; iteration < 10; iteration++) {
379
441
  const standalone = await integration.run({
380
442
  name: "implementer",
381
443
  maxIterations: 100,
382
- agent: shipyard.codex(shipyard.CODEX_MODELS.routine),
444
+ agent: roleAgent("routine", shipyard.CODEX_MODELS.routine),
383
445
  promptFile: "./.shipyard/implement-prompt.md",
384
446
  promptArgs: {
385
447
  TASK_ID: id,
@@ -393,7 +455,7 @@ for (let iteration = 0; iteration < 10; iteration++) {
393
455
  const final = await integration.run({
394
456
  name: "merger",
395
457
  maxIterations: 1,
396
- agent: shipyard.codex(shipyard.CODEX_MODELS.strong),
458
+ agent: roleAgent("strong", shipyard.CODEX_MODELS.strong),
397
459
  promptFile: "./.shipyard/merge-prompt.md",
398
460
  promptArgs: {
399
461
  TASK_ID: id,
@@ -410,7 +472,7 @@ for (let iteration = 0; iteration < 10; iteration++) {
410
472
  }
411
473
  const publication = await shipyard.createSandbox({
412
474
  branch: scope.branch,
413
- sandbox: docker(),
475
+ sandbox: sandboxProvider,
414
476
  });
415
477
  try {
416
478
  const scopeIds = [
@@ -0,0 +1,151 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { createInterface } from "node:readline/promises";
3
+
4
+ const PLANNER_BRANCH = "shipyard/planner";
5
+
6
+ type BranchPrompt = (message: string) => Promise<string>;
7
+
8
+ const askInTerminal: BranchPrompt = async (message) => {
9
+ const readline = createInterface({
10
+ input: process.stdin,
11
+ output: process.stdout,
12
+ });
13
+ try {
14
+ return await readline.question(message);
15
+ } finally {
16
+ readline.close();
17
+ }
18
+ };
19
+
20
+ const refsConflict = (left: string, right: string): boolean =>
21
+ left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
22
+
23
+ const nextPlannerBranch = (localBranches: readonly string[]): string => {
24
+ for (let suffix = 2; ; suffix++) {
25
+ for (const candidate of [
26
+ `${PLANNER_BRANCH}-${suffix}`,
27
+ `shipyard-planner-${suffix}`,
28
+ ]) {
29
+ if (!localBranches.some((branch) => refsConflict(candidate, branch)))
30
+ return candidate;
31
+ }
32
+ }
33
+ };
34
+
35
+ const reusablePlannerBranch = (
36
+ localBranches: readonly string[],
37
+ ): string | undefined => {
38
+ const candidates = localBranches
39
+ .map((branch) => ({
40
+ branch,
41
+ suffix: /^(?:shipyard\/planner-|shipyard-planner-)(\d+)$/.exec(
42
+ branch,
43
+ )?.[1],
44
+ }))
45
+ .filter((candidate) => candidate.suffix !== undefined)
46
+ .sort((left, right) => Number(left.suffix) - Number(right.suffix));
47
+
48
+ return candidates.find(({ branch }) =>
49
+ localBranches.every(
50
+ (other) => other === branch || !refsConflict(branch, other),
51
+ ),
52
+ )?.branch;
53
+ };
54
+
55
+ export const resolvePlannerBranch = async (
56
+ localBranches: readonly string[],
57
+ options: {
58
+ isInteractive?: boolean;
59
+ ask?: BranchPrompt;
60
+ checkedOutBranches?: readonly string[];
61
+ mergedBranches?: readonly string[];
62
+ deleteBranches?: (branches: readonly string[]) => void;
63
+ } = {},
64
+ ): Promise<string> => {
65
+ const conflicts = localBranches.filter((branch) =>
66
+ refsConflict(PLANNER_BRANCH, branch),
67
+ );
68
+ if (conflicts.length === 0) return PLANNER_BRANCH;
69
+
70
+ const reusable = reusablePlannerBranch(localBranches);
71
+ if (reusable) return reusable;
72
+
73
+ const alternate = nextPlannerBranch(localBranches);
74
+
75
+ const conflictList = conflicts.map((branch) => ` - ${branch}`).join("\n");
76
+ const context =
77
+ `Planner branch '${PLANNER_BRANCH}' conflicts with local branch refs:\n${conflictList}\n` +
78
+ `Git cannot create both names. The listed local branches will be kept unless you choose deletion.`;
79
+ const isInteractive =
80
+ options.isInteractive ??
81
+ Boolean(process.stdin.isTTY && process.stdout.isTTY);
82
+ if (!isInteractive) {
83
+ throw new Error(
84
+ `${context}\nRerun in an interactive terminal to keep them and use '${alternate}', or request local deletion after Git checks that they are merged and not checked out. No branches were changed.`,
85
+ );
86
+ }
87
+
88
+ const checkedOutBranches = new Set(
89
+ options.checkedOutBranches ??
90
+ execFileSync("git", ["worktree", "list", "--porcelain"], {
91
+ encoding: "utf8",
92
+ })
93
+ .split(/\r?\n/)
94
+ .filter((line) => line.startsWith("branch refs/heads/"))
95
+ .map((line) => line.slice("branch refs/heads/".length)),
96
+ );
97
+ const mergedBranches = new Set(
98
+ options.mergedBranches ??
99
+ execFileSync(
100
+ "git",
101
+ ["branch", "--merged", "HEAD", "--format=%(refname:short)"],
102
+ { encoding: "utf8" },
103
+ )
104
+ .split(/\r?\n/)
105
+ .filter(Boolean),
106
+ );
107
+ const branchesSafeToDelete = conflicts.every(
108
+ (branch) => !checkedOutBranches.has(branch) && mergedBranches.has(branch),
109
+ );
110
+ const unsafeDeleteReasons = conflicts.flatMap((branch) => {
111
+ if (checkedOutBranches.has(branch))
112
+ return [`'${branch}' is checked out in a worktree`];
113
+ if (!mergedBranches.has(branch))
114
+ return [`'${branch}' has commits not merged into HEAD`];
115
+ return [];
116
+ });
117
+
118
+ const ask = options.ask ?? askInTerminal;
119
+ const answer = await ask(
120
+ `${context}\n[Y] Keep them and use '${alternate}' (recommended)\n` +
121
+ `[d] Delete these local branches and use '${PLANNER_BRANCH}'${
122
+ branchesSafeToDelete
123
+ ? ""
124
+ : ` (unavailable: ${unsafeDeleteReasons.join("; ")})`
125
+ }\n` +
126
+ `[n] Abort without changes\nChoose [Y/d/n]: `,
127
+ );
128
+ const choice = answer.trim().toLowerCase();
129
+ if (choice === "" || choice === "y" || choice === "yes") return alternate;
130
+ if (choice === "d" || choice === "delete") {
131
+ if (!branchesSafeToDelete) {
132
+ throw new Error(
133
+ `Cannot safely delete the conflicting branches: ${unsafeDeleteReasons.join("; ")}. No branches were changed. Use '${alternate}' or inspect the listed refs manually.`,
134
+ );
135
+ }
136
+ const deleteBranches =
137
+ options.deleteBranches ??
138
+ ((branches: readonly string[]) => {
139
+ for (const branch of branches)
140
+ execFileSync("git", ["branch", "-d", branch], {
141
+ encoding: "utf8",
142
+ });
143
+ });
144
+ deleteBranches(conflicts);
145
+ return PLANNER_BRANCH;
146
+ }
147
+
148
+ throw new Error(
149
+ `Planner branch selection cancelled. No branches were changed. Inspect with 'git worktree list' and 'git branch --list "shipyard/planner*"', then rerun.`,
150
+ );
151
+ };
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env bash
2
2
  set -euo pipefail
3
3
 
4
+ # The sandbox starts from a temporary Git bundle, so reset its origin before Git pushes.
4
5
  if [[ -n ${GH_REPO:-} ]]; then
5
6
  [[ "$GH_REPO" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]] || { echo "Invalid GH_REPO" >&2; exit 1; }
6
7
  git remote set-url origin "https://github.com/$GH_REPO.git"
@@ -5,9 +5,59 @@ import { existsSync } from "node:fs";
5
5
  import * as shipyard from "@snappedly-tools/shipyard";
6
6
  import { docker } from "@snappedly-tools/shipyard/sandboxes/docker";
7
7
  import { z } from "zod";
8
+ import { resolvePlannerBranch } from "./planner-branch.mjs";
8
9
 
9
10
  if (process.loadEnvFile && existsSync(".shipyard/.env"))
10
11
  process.loadEnvFile(".shipyard/.env");
12
+ type ModelRole = "routine" | "strong";
13
+ const CODEX_PROVIDER = true;
14
+ const agentFactory = shipyard.codex;
15
+ type AgentModel = Parameters<typeof agentFactory>[0];
16
+ const readRoleModel = (role: ModelRole): string | undefined => {
17
+ const envName = `SHIPYARD_${role.toUpperCase()}_MODEL`;
18
+ const model = process.env[envName];
19
+ if (model !== undefined && model.trim().length === 0)
20
+ throw new Error(`${envName} must not be empty`);
21
+ return model;
22
+ };
23
+ const roleModels = {
24
+ routine: readRoleModel("routine"),
25
+ strong: readRoleModel("strong"),
26
+ };
27
+ const CODEX_REASONING_EFFORTS = shipyard.CODEX_REASONING_EFFORTS;
28
+ type CodexReasoningEffort = shipyard.CodexReasoningEffort;
29
+ const readCodexReasoningEffort = (
30
+ role: ModelRole,
31
+ ): CodexReasoningEffort | undefined => {
32
+ if (!CODEX_PROVIDER) return undefined;
33
+ const envName = `SHIPYARD_CODEX_${role.toUpperCase()}_REASONING_EFFORT`;
34
+ const effort = process.env[envName]?.trim();
35
+ if (!effort) return undefined;
36
+ if (!(CODEX_REASONING_EFFORTS as readonly string[]).includes(effort))
37
+ throw new Error(
38
+ `${envName} must be one of ${CODEX_REASONING_EFFORTS.join(", ")}; received "${effort}"`,
39
+ );
40
+ return effort as CodexReasoningEffort;
41
+ };
42
+ const roleEfforts = {
43
+ routine: readCodexReasoningEffort("routine"),
44
+ strong: readCodexReasoningEffort("strong"),
45
+ };
46
+ const readCodexRoleModel = (role: ModelRole, defaultModel: AgentModel) => {
47
+ if (!CODEX_PROVIDER || typeof defaultModel === "string") return defaultModel;
48
+ const envName = `SHIPYARD_CODEX_${role.toUpperCase()}_MODEL`;
49
+ const model = process.env[envName]?.trim();
50
+ return model ? { ...defaultModel, model } : defaultModel;
51
+ };
52
+ const roleAgent = (role: ModelRole, defaultModel: AgentModel) => {
53
+ const model = roleModels[role] ?? readCodexRoleModel(role, defaultModel);
54
+ const effort = roleEfforts[role];
55
+ if (typeof model !== "string")
56
+ return effort === undefined
57
+ ? agentFactory(model)
58
+ : agentFactory(model, { effort });
59
+ return agentFactory(model, { effort: effort ?? null });
60
+ };
11
61
  const targetBranch = execFileSync("git", ["branch", "--show-current"], {
12
62
  encoding: "utf8",
13
63
  }).trim();
@@ -21,7 +71,11 @@ if (
21
71
  !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository)
22
72
  )
23
73
  throw new Error("Invalid target branch or GitHub repository");
24
- process.env.GH_REPO = repository;
74
+ const sandboxAuthOptions = {};
75
+ const sandboxProvider = docker({
76
+ env: { GH_REPO: repository },
77
+ ...sandboxAuthOptions,
78
+ });
25
79
  const hooks = {
26
80
  sandbox: {
27
81
  onSandboxReady: [
@@ -117,14 +171,14 @@ const runWorker = async (scope: Scope, ticket: Ticket) => {
117
171
  const sandbox = await shipyard.createSandbox({
118
172
  branch,
119
173
  baseBranch: scope.branch,
120
- sandbox: docker(),
174
+ sandbox: sandboxProvider,
121
175
  hooks,
122
176
  });
123
177
  try {
124
178
  await sandbox.run({
125
179
  name: `triage #${ticket.id}`,
126
180
  maxIterations: 1,
127
- agent: shipyard.codex(shipyard.CODEX_MODELS.strong),
181
+ agent: roleAgent("routine", shipyard.CODEX_MODELS.routine),
128
182
  promptFile: "./.shipyard/triage-prompt.md",
129
183
  promptArgs: { TASK_ID: ticket.id },
130
184
  });
@@ -132,7 +186,7 @@ const runWorker = async (scope: Scope, ticket: Ticket) => {
132
186
  const implementation = await sandbox.run({
133
187
  name: "implementer",
134
188
  maxIterations: 100,
135
- agent: shipyard.codex(shipyard.CODEX_MODELS.routine),
189
+ agent: roleAgent("routine", shipyard.CODEX_MODELS.routine),
136
190
  promptFile: "./.shipyard/implement-prompt.md",
137
191
  promptArgs: {
138
192
  TASK_ID: ticket.id,
@@ -145,7 +199,7 @@ const runWorker = async (scope: Scope, ticket: Ticket) => {
145
199
  const review = await sandbox.run({
146
200
  name: "reviewer",
147
201
  maxIterations: 1,
148
- agent: shipyard.codex(shipyard.CODEX_MODELS.strong),
202
+ agent: roleAgent("strong", shipyard.CODEX_MODELS.strong),
149
203
  promptFile: "./.shipyard/review-prompt.md",
150
204
  promptArgs: {
151
205
  TASK_ID: ticket.id,
@@ -166,13 +220,21 @@ for (let iteration = 0; iteration < 10; iteration++) {
166
220
  execFileSync("node", [".shipyard/select-issues.mjs"], { encoding: "utf8" }),
167
221
  ) as Scope[];
168
222
  if (!scopes.length) break;
223
+ const localBranches = execFileSync(
224
+ "git",
225
+ ["for-each-ref", "--format=%(refname:short)", "refs/heads"],
226
+ { encoding: "utf8" },
227
+ )
228
+ .split(/\r?\n/)
229
+ .filter(Boolean);
230
+ const plannerBranch = await resolvePlannerBranch(localBranches);
169
231
  const plan = await shipyard.run({
170
232
  hooks,
171
- sandbox: docker(),
233
+ sandbox: sandboxProvider,
172
234
  name: "planner",
173
- branchStrategy: { type: "branch", branch: "shipyard/planner" },
235
+ branchStrategy: { type: "branch", branch: plannerBranch },
174
236
  maxIterations: 1,
175
- agent: shipyard.codex(shipyard.CODEX_MODELS.strong),
237
+ agent: roleAgent("strong", shipyard.CODEX_MODELS.strong),
176
238
  promptFile: "./.shipyard/plan-prompt.md",
177
239
  output: shipyard.Output.object({ tag: "plan", schema: planSchema }),
178
240
  });
@@ -226,7 +288,7 @@ for (let iteration = 0; iteration < 10; iteration++) {
226
288
  if (scope.kind === "spec") {
227
289
  const seed = await shipyard.createSandbox({
228
290
  branch: scope.branch,
229
- sandbox: docker(),
291
+ sandbox: sandboxProvider,
230
292
  hooks,
231
293
  });
232
294
  await closeClean(seed);
@@ -260,7 +322,7 @@ for (let iteration = 0; iteration < 10; iteration++) {
260
322
  if (failures.length) throw new TicketFailures(failures);
261
323
  const wave = await shipyard.createSandbox({
262
324
  branch: scope.branch,
263
- sandbox: docker(),
325
+ sandbox: sandboxProvider,
264
326
  hooks,
265
327
  });
266
328
  try {
@@ -300,7 +362,7 @@ for (let iteration = 0; iteration < 10; iteration++) {
300
362
  const resolution = await wave.run({
301
363
  name: "conflict-resolver",
302
364
  maxIterations: 10,
303
- agent: shipyard.codex(shipyard.CODEX_MODELS.strong),
365
+ agent: roleAgent("strong", shipyard.CODEX_MODELS.strong),
304
366
  promptFile: "./.shipyard/conflict-prompt.md",
305
367
  promptArgs: {
306
368
  TASK_ID: ready[index]!.id,
@@ -354,7 +416,7 @@ for (let iteration = 0; iteration < 10; iteration++) {
354
416
  const integrated = await wave.run({
355
417
  name: "spec-integrator",
356
418
  maxIterations: 10,
357
- agent: shipyard.codex(shipyard.CODEX_MODELS.strong),
419
+ agent: roleAgent("strong", shipyard.CODEX_MODELS.strong),
358
420
  promptFile: "./.shipyard/spec-wave-prompt.md",
359
421
  promptArgs: {
360
422
  TASK_ID: id,
@@ -385,7 +447,7 @@ for (let iteration = 0; iteration < 10; iteration++) {
385
447
  }
386
448
  const integration = await shipyard.createSandbox({
387
449
  branch: scope.branch,
388
- sandbox: docker(),
450
+ sandbox: sandboxProvider,
389
451
  hooks,
390
452
  });
391
453
  let handoffEvidence: string;
@@ -394,7 +456,7 @@ for (let iteration = 0; iteration < 10; iteration++) {
394
456
  await integration.run({
395
457
  name: `triage #${id}`,
396
458
  maxIterations: 1,
397
- agent: shipyard.codex(shipyard.CODEX_MODELS.strong),
459
+ agent: roleAgent("routine", shipyard.CODEX_MODELS.routine),
398
460
  promptFile: "./.shipyard/triage-prompt.md",
399
461
  promptArgs: { TASK_ID: id },
400
462
  });
@@ -402,7 +464,7 @@ for (let iteration = 0; iteration < 10; iteration++) {
402
464
  const standalone = await integration.run({
403
465
  name: "implementer",
404
466
  maxIterations: 100,
405
- agent: shipyard.codex(shipyard.CODEX_MODELS.routine),
467
+ agent: roleAgent("routine", shipyard.CODEX_MODELS.routine),
406
468
  promptFile: "./.shipyard/implement-prompt.md",
407
469
  promptArgs: {
408
470
  TASK_ID: id,
@@ -416,7 +478,7 @@ for (let iteration = 0; iteration < 10; iteration++) {
416
478
  const review = await integration.run({
417
479
  name: "reviewer",
418
480
  maxIterations: 1,
419
- agent: shipyard.codex(shipyard.CODEX_MODELS.strong),
481
+ agent: roleAgent("strong", shipyard.CODEX_MODELS.strong),
420
482
  promptFile: "./.shipyard/review-prompt.md",
421
483
  promptArgs: {
422
484
  TASK_ID: id,
@@ -429,7 +491,7 @@ for (let iteration = 0; iteration < 10; iteration++) {
429
491
  const final = await integration.run({
430
492
  name: "merger",
431
493
  maxIterations: 1,
432
- agent: shipyard.codex(shipyard.CODEX_MODELS.strong),
494
+ agent: roleAgent("strong", shipyard.CODEX_MODELS.strong),
433
495
  promptFile: "./.shipyard/merge-prompt.md",
434
496
  promptArgs: {
435
497
  TASK_ID: id,
@@ -445,7 +507,7 @@ for (let iteration = 0; iteration < 10; iteration++) {
445
507
  const finalReview = await integration.run({
446
508
  name: "reviewer",
447
509
  maxIterations: 1,
448
- agent: shipyard.codex(shipyard.CODEX_MODELS.strong),
510
+ agent: roleAgent("strong", shipyard.CODEX_MODELS.strong),
449
511
  promptFile: "./.shipyard/review-prompt.md",
450
512
  promptArgs: {
451
513
  TASK_ID: id,
@@ -461,7 +523,7 @@ for (let iteration = 0; iteration < 10; iteration++) {
461
523
  }
462
524
  const publication = await shipyard.createSandbox({
463
525
  branch: scope.branch,
464
- sandbox: docker(),
526
+ sandbox: sandboxProvider,
465
527
  });
466
528
  try {
467
529
  const scopeIds = [