@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.
- package/README.md +46 -43
- package/dist/{MountConfig-bZoCs4Dd.d.ts → MountConfig-K5ILnfht.d.ts} +1 -1
- package/dist/SandboxProvider-oUAwYlWm.d.ts +120 -0
- package/dist/{chunk-FDYOTN55.js → chunk-57EEKW3R.js} +361 -984
- package/dist/chunk-57EEKW3R.js.map +1 -0
- package/dist/{chunk-56TDSWFU.js → chunk-HXSZM52J.js} +3 -3
- package/dist/{chunk-56TDSWFU.js.map → chunk-HXSZM52J.js.map} +1 -1
- package/dist/{chunk-WAXEMZUV.js → chunk-JZUBT4WG.js} +241 -33
- package/dist/chunk-JZUBT4WG.js.map +1 -0
- package/dist/chunk-NQRFVKCU.js +1840 -0
- package/dist/chunk-NQRFVKCU.js.map +1 -0
- package/dist/chunk-Z5C4LHVP.js +137 -0
- package/dist/chunk-Z5C4LHVP.js.map +1 -0
- package/dist/createSandbox-DmbnWAZv.d.ts +739 -0
- package/dist/index.d.ts +99 -2606
- package/dist/index.js +412 -7487
- package/dist/index.js.map +1 -1
- package/dist/integrations/github.d.ts +52 -0
- package/dist/integrations/github.js +1049 -0
- package/dist/integrations/github.js.map +1 -0
- package/dist/integrations/releases.d.ts +136 -0
- package/dist/integrations/releases.js +500 -0
- package/dist/integrations/releases.js.map +1 -0
- package/dist/main.js +826 -472
- package/dist/main.js.map +1 -1
- package/dist/publication-BPoy_M9M.d.ts +1200 -0
- package/dist/sandboxes/docker.d.ts +2 -3
- package/dist/sandboxes/docker.js +2 -4
- package/dist/templates/parallel-planner/main.mts +78 -16
- package/dist/templates/parallel-planner/planner-branch.mts +151 -0
- package/dist/templates/parallel-planner/setup.sh +1 -0
- package/dist/templates/parallel-planner-with-review/main.mts +81 -19
- package/dist/templates/parallel-planner-with-review/planner-branch.mts +151 -0
- package/dist/templates/parallel-planner-with-review/setup.sh +1 -0
- package/dist/templates/sequential-reviewer/main.mts +59 -6
- package/dist/templates/sequential-reviewer/setup.sh +1 -0
- package/dist/templates/shared/setup.sh +1 -0
- package/dist/templates/simple-loop/main.mts +58 -5
- package/dist/templates/simple-loop/setup.sh +1 -0
- package/dist/workflow/coordinator/migrations/002_workflow_phase_records.sql +11 -0
- package/dist/workflow/coordinator/migrations/003_phase_record_schema_version.sql +6 -0
- package/dist/workflow.d.ts +472 -0
- package/dist/workflow.js +4345 -0
- package/dist/workflow.js.map +1 -0
- package/package.json +11 -15
- package/dist/SandboxProvider-XJQqEdSf.d.ts +0 -261
- package/dist/chunk-ACD46ZM4.js +0 -136
- package/dist/chunk-ACD46ZM4.js.map +0 -1
- package/dist/chunk-FDYOTN55.js.map +0 -1
- package/dist/chunk-KMGNFXKN.js +0 -38
- package/dist/chunk-KMGNFXKN.js.map +0 -1
- package/dist/chunk-SOJTAJTF.js +0 -78
- package/dist/chunk-SOJTAJTF.js.map +0 -1
- package/dist/chunk-WAXEMZUV.js.map +0 -1
- package/dist/sandboxes/no-sandbox.d.ts +0 -37
- package/dist/sandboxes/no-sandbox.js +0 -4
- package/dist/sandboxes/no-sandbox.js.map +0 -1
- package/dist/sandboxes/vercel.d.ts +0 -104
- package/dist/sandboxes/vercel.js +0 -166
- package/dist/sandboxes/vercel.js.map +0 -1
- package/dist/templates/blank/main.mts +0 -13
- package/dist/templates/blank/prompt.md +0 -12
- package/dist/templates/blank/template.json +0 -4
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { I as IsolatedSandboxProvider } from '../SandboxProvider-
|
|
2
|
-
import { M as MountConfig } from '../MountConfig-
|
|
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,
|
package/dist/sandboxes/docker.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
export { docker } from '../chunk-
|
|
2
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
210
|
+
sandbox: sandboxProvider,
|
|
149
211
|
name: "planner",
|
|
150
|
-
branchStrategy: { type: "branch", branch:
|
|
212
|
+
branchStrategy: { type: "branch", branch: plannerBranch },
|
|
151
213
|
maxIterations: 1,
|
|
152
|
-
agent:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
233
|
+
sandbox: sandboxProvider,
|
|
172
234
|
name: "planner",
|
|
173
|
-
branchStrategy: { type: "branch", branch:
|
|
235
|
+
branchStrategy: { type: "branch", branch: plannerBranch },
|
|
174
236
|
maxIterations: 1,
|
|
175
|
-
agent:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
526
|
+
sandbox: sandboxProvider,
|
|
465
527
|
});
|
|
466
528
|
try {
|
|
467
529
|
const scopeIds = [
|