@snappedly-tools/shipyard 0.7.0-staging.36082887921 → 0.7.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 (41) hide show
  1. package/README.md +41 -34
  2. package/dist/MountConfig-bZoCs4Dd.d.ts +26 -0
  3. package/dist/SandboxProvider-XJQqEdSf.d.ts +261 -0
  4. package/dist/{chunk-JI3HDDMS.js → chunk-56TDSWFU.js} +3 -3
  5. package/dist/{chunk-JI3HDDMS.js.map → chunk-56TDSWFU.js.map} +1 -1
  6. package/dist/chunk-ACD46ZM4.js +136 -0
  7. package/dist/chunk-ACD46ZM4.js.map +1 -0
  8. package/dist/{chunk-L6PX5QTU.js → chunk-FDYOTN55.js} +887 -262
  9. package/dist/chunk-FDYOTN55.js.map +1 -0
  10. package/dist/chunk-KMGNFXKN.js +38 -0
  11. package/dist/chunk-KMGNFXKN.js.map +1 -0
  12. package/dist/chunk-SOJTAJTF.js +78 -0
  13. package/dist/chunk-SOJTAJTF.js.map +1 -0
  14. package/dist/{chunk-44I2BL6E.js → chunk-WAXEMZUV.js} +33 -225
  15. package/dist/chunk-WAXEMZUV.js.map +1 -0
  16. package/dist/index.d.ts +97 -117
  17. package/dist/index.js +344 -322
  18. package/dist/index.js.map +1 -1
  19. package/dist/main.js +139 -235
  20. package/dist/main.js.map +1 -1
  21. package/dist/sandboxes/docker.d.ts +3 -1
  22. package/dist/sandboxes/docker.js +4 -2
  23. package/dist/sandboxes/no-sandbox.d.ts +37 -0
  24. package/dist/sandboxes/no-sandbox.js +4 -0
  25. package/dist/sandboxes/no-sandbox.js.map +1 -0
  26. package/dist/sandboxes/vercel.d.ts +104 -0
  27. package/dist/sandboxes/vercel.js +166 -0
  28. package/dist/sandboxes/vercel.js.map +1 -0
  29. package/dist/templates/blank/main.mts +13 -0
  30. package/dist/templates/blank/prompt.md +12 -0
  31. package/dist/templates/blank/template.json +4 -0
  32. package/dist/templates/parallel-planner/main.mts +9 -67
  33. package/dist/templates/parallel-planner-with-review/main.mts +12 -70
  34. package/dist/templates/sequential-reviewer/main.mts +3 -52
  35. package/dist/templates/simple-loop/main.mts +2 -51
  36. package/package.json +17 -1
  37. package/dist/MountConfig-BHnKnA4h.d.ts +0 -145
  38. package/dist/chunk-44I2BL6E.js.map +0 -1
  39. package/dist/chunk-L6PX5QTU.js.map +0 -1
  40. package/dist/templates/parallel-planner/planner-branch.mts +0 -151
  41. package/dist/templates/parallel-planner-with-review/planner-branch.mts +0 -151
package/dist/main.js CHANGED
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env node
2
- import { CODEX_MODELS, purgeRunLogs, withFriendlyErrors, DEFAULT_LOG_RETENTION_DAYS, resolveEnv } from './chunk-JI3HDDMS.js';
3
- import { buildImage, removeImage, RunnerLifecycleError, removeRepositoryRunner, defaultImageName, validateExistingRepositoryRunner, RUNNER_INSTALL_METADATA, RUNNER_CONTROLLER_LOCK, recoverRepositoryRunner, REPOSITORY_RUNNER_OWNER_ENV, removeOwnedRepositoryRunnerContainers, RUNNER_WORK_DIR } from './chunk-44I2BL6E.js';
4
- import { SANDBOX_REPO_DIR, Display, InitError, ACTIVATION_LABEL, PRODUCT_NAME, CONFIG_DIR, CLI_NAME, ClackDisplay, ConfigDirError, ExecHostError, LOCKS_DIR, RUNNER_DIR, assertProtectedDirectoryIdentities, repositoryRunnerEnvironment } from './chunk-L6PX5QTU.js';
2
+ import { CODEX_MODELS, purgeRunLogs, withFriendlyErrors, DEFAULT_LOG_RETENTION_DAYS, resolveEnv } from './chunk-56TDSWFU.js';
3
+ import { buildImage, removeImage, validateExistingRepositoryRunner, RunnerLifecycleError, RUNNER_INSTALL_METADATA, removeRepositoryRunner, RUNNER_CONTROLLER_LOCK, recoverRepositoryRunner, REPOSITORY_RUNNER_OWNER_ENV, removeOwnedRepositoryRunnerContainers, RUNNER_WORK_DIR } from './chunk-WAXEMZUV.js';
4
+ import { SANDBOX_REPO_DIR, Display, InitError, ACTIVATION_LABEL, PRODUCT_NAME, CONFIG_DIR, CLI_NAME, ClackDisplay, RUNNER_DIR, RUNNER_SANDBOX_MASK_DIR, assertProtectedDirectoryIdentities, repositoryRunnerEnvironment, ConfigDirError, defaultImageName, ExecHostError, LOCKS_DIR } from './chunk-FDYOTN55.js';
5
+ import './chunk-KMGNFXKN.js';
6
+ import './chunk-ACD46ZM4.js';
5
7
  import { NodeContext, NodeRuntime, NodeFileSystem } from '@effect/platform-node';
6
- import { Effect, Layer, Option, Exit, Cause } from 'effect';
8
+ import { Effect, Layer, Option } from 'effect';
7
9
  import { Options, Command } from '@effect/cli';
8
- import * as clack2 from '@clack/prompts';
10
+ import * as clack from '@clack/prompts';
9
11
  import { execFile, execSync, spawn, execFileSync } from 'child_process';
10
12
  import { existsSync, constants, statSync } from 'fs';
11
13
  import { join, resolve, dirname } from 'path';
@@ -17,12 +19,18 @@ import { createHash } from 'crypto';
17
19
  import { chmod, lstat, realpath, rm, mkdir, writeFile, readFile, access } from 'fs/promises';
18
20
  import 'module';
19
21
 
20
- var configDirExists = (repoDir) => Effect.gen(function* () {
22
+ var pathExists = (path) => Effect.gen(function* () {
21
23
  const fs = yield* FileSystem.FileSystem;
22
- return yield* fs.exists(join(repoDir, CONFIG_DIR)).pipe(Effect.catchAll(() => Effect.succeed(false)));
24
+ return yield* fs.exists(path).pipe(Effect.catchAll(() => Effect.succeed(false)));
25
+ });
26
+ var getConfigDirState = (repoDir) => Effect.gen(function* () {
27
+ const canonicalExists = yield* pathExists(join(repoDir, CONFIG_DIR));
28
+ if (canonicalExists) return "canonical";
29
+ return "missing";
23
30
  });
24
31
  var requireCanonicalConfigDir = (repoDir) => Effect.gen(function* () {
25
- if (!(yield* configDirExists(repoDir))) {
32
+ const state = yield* getConfigDirState(repoDir);
33
+ if (state === "missing") {
26
34
  return yield* Effect.fail(
27
35
  new ConfigDirError({
28
36
  message: `No ${CONFIG_DIR}/ found. Run \`${CLI_NAME} init\` first.`
@@ -32,7 +40,8 @@ var requireCanonicalConfigDir = (repoDir) => Effect.gen(function* () {
32
40
  return join(repoDir, CONFIG_DIR);
33
41
  });
34
42
  var assertConfigDirAvailable = (repoDir) => Effect.gen(function* () {
35
- if (yield* configDirExists(repoDir)) {
43
+ const state = yield* getConfigDirState(repoDir);
44
+ if (state === "canonical") {
36
45
  return yield* Effect.fail(
37
46
  new ConfigDirError({
38
47
  message: `${CONFIG_DIR}/ directory already exists. ${CLI_NAME} did not modify it. Use the existing configuration or remove it deliberately after preserving any active worktrees before re-initializing.`
@@ -62,16 +71,11 @@ var CODEX_CHATGPT_AUTH_OPTIONS = `{
62
71
  },
63
72
  ],
64
73
  }`;
65
- var roleModelEnvExample = (agentName) => agentName === "codex" ? `# Optional model choices for Shipyard workflows.
66
- # Choose models available to your Codex CLI account: https://learn.chatgpt.com/docs/models
67
- # The provider must support each value. Aliases can change their target over time.
68
- # SHIPYARD_ROUTINE_MODEL=gpt-6-luna
69
- # SHIPYARD_STRONG_MODEL=gpt-6-sol` : `# Optional model choices for Shipyard workflows.
70
- # Choose aliases or model IDs available to your Claude Code provider: https://code.claude.com/docs/en/model-config
71
- # The provider must support each value. Aliases can change their target over time.
72
- # SHIPYARD_ROUTINE_MODEL=sonnet
73
- # SHIPYARD_STRONG_MODEL=opus`;
74
74
  var TEMPLATES = [
75
+ {
76
+ name: "blank",
77
+ description: "Bare scaffold \u2014 write your own prompt and orchestration"
78
+ },
75
79
  {
76
80
  name: "simple-loop",
77
81
  description: "Implements standalone issues one by one and opens review PRs"
@@ -171,7 +175,7 @@ RUN apt-get update && apt-get install -y \\
171
175
 
172
176
  # Build-args for UID/GID alignment: shipyard docker build-image
173
177
  # defaults these to the host user's UID/GID so image-built files
174
- # and explicit mounted files share an owner without runtime chown.
178
+ # and bind-mounted files share an owner without runtime chown.
175
179
  ARG AGENT_UID=1000
176
180
  ARG AGENT_GID=1000
177
181
 
@@ -187,8 +191,8 @@ ENV PATH="/home/agent/.local/bin:$PATH"
187
191
 
188
192
  WORKDIR /home/agent
189
193
 
190
- # Shipyard syncs Git history into a sandbox-owned repository at ${SANDBOX_REPO_DIR}
191
- # and runs agent commands from ${SANDBOX_REPO_DIR}.
194
+ # In worktree sandbox mode, Shipyard bind-mounts the git worktree at ${SANDBOX_REPO_DIR}
195
+ # and overrides the working directory to ${SANDBOX_REPO_DIR} at container start.
192
196
  # Structure your Dockerfile so that ${SANDBOX_REPO_DIR} can serve as the project root.
193
197
  ENTRYPOINT ["sleep", "infinity"]
194
198
  `;
@@ -205,7 +209,7 @@ RUN apt-get update && apt-get install -y \\
205
209
 
206
210
  # Build-args for UID/GID alignment: shipyard docker build-image
207
211
  # defaults these to the host user's UID/GID so image-built files
208
- # and explicit mounted files share an owner without runtime chown.
212
+ # and bind-mounted files share an owner without runtime chown.
209
213
  ARG AGENT_UID=1000
210
214
  ARG AGENT_GID=1000
211
215
 
@@ -219,8 +223,8 @@ USER \${AGENT_UID}:\${AGENT_GID}
219
223
 
220
224
  WORKDIR /home/agent
221
225
 
222
- # Shipyard syncs Git history into a sandbox-owned repository at ${SANDBOX_REPO_DIR}
223
- # and runs agent commands from ${SANDBOX_REPO_DIR}.
226
+ # In worktree sandbox mode, Shipyard bind-mounts the git worktree at ${SANDBOX_REPO_DIR}
227
+ # and overrides the working directory to ${SANDBOX_REPO_DIR} at container start.
224
228
  # Structure your Dockerfile so that ${SANDBOX_REPO_DIR} can serve as the project root.
225
229
  ENTRYPOINT ["sleep", "infinity"]
226
230
  `;
@@ -335,26 +339,34 @@ var copyTemplateFiles = (templateDir, destDir, mainFilename) => Effect.gen(funct
335
339
  { concurrency: "unbounded" }
336
340
  );
337
341
  });
338
- var rewriteMainTs = (configDir, agent, model, modelExplicit, sandboxProvider, mainFilename, codexAuth) => Effect.gen(function* () {
342
+ var rewriteMainTs = (configDir, agent, model, sandboxProvider, mainFilename, codexAuth) => Effect.gen(function* () {
339
343
  const fs = yield* FileSystem.FileSystem;
340
344
  const mainTsPath = join(configDir, mainFilename);
341
345
  const exists = yield* fs.exists(mainTsPath).pipe(Effect.mapError((e) => new Error(e.message)));
342
346
  if (!exists) return;
343
347
  let content = yield* fs.readFileString(mainTsPath).pipe(Effect.mapError((e) => new Error(e.message)));
348
+ if (mainFilename === "main.ts") {
349
+ content = content.replace(/main\.mts/g, "main.ts");
350
+ }
344
351
  content = content.replace(
345
352
  new RegExp(`\\b${TEMPLATE_AGENT_FACTORY}\\b`, "g"),
346
353
  agent.factoryImport
347
354
  );
355
+ const factoryCallRe = new RegExp(
356
+ `${agent.factoryImport}\\(([^)\\n]*)\\)`,
357
+ "g"
358
+ );
348
359
  content = content.replace(
349
- "const CODEX_PROVIDER = true;",
350
- `const CODEX_PROVIDER = ${agent.name === "codex"};`
360
+ factoryCallRe,
361
+ (match, modelExpression) => {
362
+ const keepsConfiguredModel = agent.name === "codex" && model === CODEX_MODELS.routine.model && /^(?:[A-Za-z_$][\w$]*\.)*CODEX_MODELS\.[A-Za-z_$][\w$]*$/.test(
363
+ modelExpression.trim()
364
+ );
365
+ return keepsConfiguredModel ? match : `${agent.factoryImport}("${model}")`;
366
+ }
351
367
  );
352
- if (agent.name !== "codex" || modelExplicit) {
353
- const modelLiteral = JSON.stringify(model);
354
- content = content.replace(
355
- /shipyard\.CODEX_MODELS\.(?:routine|strong)/g,
356
- modelLiteral
357
- );
368
+ if (agent.name !== "codex" || model !== CODEX_MODELS.routine.model) {
369
+ content = content.replace(/\bCODEX_MODELS,\s*/g, "");
358
370
  }
359
371
  if (agent.name === "codex" && codexAuth === "chatgpt") {
360
372
  content = content.replace(
@@ -422,20 +434,13 @@ var scaffold = (repoDir, options) => Effect.gen(function* () {
422
434
  const {
423
435
  agent,
424
436
  model,
425
- modelExplicit = false,
426
- templateName = "simple-loop",
437
+ templateName = "blank",
427
438
  issueTracker = ISSUE_TRACKER_REGISTRY[0],
428
439
  // default: github-issues
429
440
  sandboxProvider = SANDBOX_PROVIDER_REGISTRY[0],
430
441
  // default: docker
431
442
  codexAuth = "api-key"
432
443
  } = options;
433
- const totalProgressSteps = 5;
434
- const reportProgress2 = (current, message) => options.onProgress?.({
435
- current,
436
- total: totalProgressSteps,
437
- message
438
- });
439
444
  if (codexAuth === "chatgpt" && agent.name !== "codex") {
440
445
  return yield* Effect.fail(
441
446
  new Error(
@@ -447,14 +452,11 @@ var scaffold = (repoDir, options) => Effect.gen(function* () {
447
452
  yield* assertConfigDirAvailable(repoDir);
448
453
  const configDir = join(repoDir, CONFIG_DIR);
449
454
  const mainFilename = yield* detectMainFilename(repoDir);
450
- reportProgress2(1, "Validated repository and configuration");
451
455
  yield* fs.makeDirectory(configDir, { recursive: false }).pipe(Effect.mapError((e) => new Error(e.message)));
452
456
  const templateDir = yield* getTemplateDir(templateName);
453
- reportProgress2(2, `Prepared ${CONFIG_DIR}/ directory`);
454
457
  const envExampleParts = [
455
458
  agent.name === "codex" && codexAuth === "chatgpt" ? CODEX_CHATGPT_ENV_EXAMPLE : agent.envExample
456
459
  ];
457
- envExampleParts.push(roleModelEnvExample(agent.name));
458
460
  if (issueTracker.envExample) {
459
461
  envExampleParts.push(issueTracker.envExample);
460
462
  }
@@ -473,19 +475,15 @@ var scaffold = (repoDir, options) => Effect.gen(function* () {
473
475
  ],
474
476
  { concurrency: "unbounded" }
475
477
  );
476
- reportProgress2(3, "Wrote configuration and template files");
477
478
  yield* rewriteMainTs(
478
479
  configDir,
479
480
  agent,
480
481
  model,
481
- modelExplicit,
482
482
  sandboxProvider,
483
483
  mainFilename,
484
484
  codexAuth
485
485
  );
486
- reportProgress2(4, "Configured selected agent and sandbox");
487
486
  yield* substituteTemplateArgs(configDir, issueTracker);
488
- reportProgress2(5, "Applied issue tracker settings");
489
487
  return { mainFilename };
490
488
  });
491
489
  var resolveCodexAuthMode = async (options) => {
@@ -564,11 +562,6 @@ var ensureCodexChatGptAuth = (options) => {
564
562
  });
565
563
  }
566
564
  };
567
- var runEffectPromise = async (effect) => {
568
- const exit = await Effect.runPromiseExit(effect);
569
- if (Exit.isFailure(exit)) throw Cause.squash(exit.cause);
570
- return exit.value;
571
- };
572
565
  var REPOSITORY_RUNNER_WORKFLOW_PATH = ".github/workflows/shipyard-wake.yml";
573
566
  var REPOSITORY_RUNNER_WAKE_EXECUTABLE = "shipyard-wake";
574
567
  var REPOSITORY_RUNNER_WORKFLOW = `name: Shipyard wake-up
@@ -763,22 +756,6 @@ var RELEASES_API = "https://api.github.com/repos/actions/runner/releases/latest"
763
756
  var RunnerInstallError = class extends Error {
764
757
  name = "RunnerInstallError";
765
758
  };
766
- var RunnerInstallConflictError = class extends RunnerInstallError {
767
- constructor(repository, runners) {
768
- const names = runners.map(({ name }) => name).join(", ");
769
- super(
770
- `A repository runner labeled \`${ACTIVATION_LABEL}\` is already registered for ${repository}: ${names}. Remove it before installing another.`
771
- );
772
- this.repository = repository;
773
- this.runners = runners;
774
- const details = runners.map(
775
- ({ id, name, status, busy }) => `${name}${id === void 0 ? "" : ` (#${id})`} (${status}${busy ? ", busy" : ""})`
776
- ).join(", ");
777
- this.confirmationMessage = `GitHub has ${runners.length} repository runner${runners.length === 1 ? "" : "s"} labeled \`${ACTIVATION_LABEL}\` for ${repository}: ${details}. Delete ${runners.length === 1 ? "this registration" : "these registrations"} and install a replacement?`;
778
- }
779
- name = "RunnerInstallConflictError";
780
- confirmationMessage;
781
- };
782
759
  var defaultAdapters = {
783
760
  platform: () => process.platform,
784
761
  arch: () => process.arch,
@@ -902,7 +879,9 @@ var selectArm64Asset = (release) => {
902
879
  var appendRunnerIgnores = async (gitignorePath, adapters) => {
903
880
  const current = await adapters.exists(gitignorePath) ? await adapters.readText(gitignorePath) : "";
904
881
  const lines = new Set(current.split(/\r?\n/));
905
- const additions = [`${RUNNER_DIR}/`].filter((line) => !lines.has(line));
882
+ const additions = [`${RUNNER_DIR}/`, `${RUNNER_SANDBOX_MASK_DIR}/`].filter(
883
+ (line) => !lines.has(line)
884
+ );
906
885
  if (additions.length === 0) return;
907
886
  const prefix = current.length === 0 || current.endsWith("\n") ? current : `${current}
908
887
  `;
@@ -913,88 +892,6 @@ var commandFailure = (purpose, error) => new RunnerInstallError(
913
892
  `${purpose} failed: ${error instanceof Error ? error.message : String(error)}`
914
893
  );
915
894
  var reportProgress = (options, current, total, message) => options.onProgress?.({ current, total, message });
916
- var parseExistingRunnerRegistrations = (stdout) => {
917
- const lines = stdout.trim().split(/\r?\n/).filter(Boolean);
918
- return lines.map((line) => {
919
- let value;
920
- try {
921
- value = JSON.parse(line);
922
- } catch {
923
- return { id: void 0, name: line, status: "unknown", busy: false };
924
- }
925
- if (typeof value !== "object" || value === null) {
926
- throw new RunnerInstallError(
927
- "GitHub returned invalid repository runner information."
928
- );
929
- }
930
- const registration = value;
931
- if (typeof registration.id !== "number" || !Number.isInteger(registration.id) || registration.id <= 0 || typeof registration.name !== "string" || registration.name.length === 0 || typeof registration.status !== "string" || typeof registration.busy !== "boolean") {
932
- throw new RunnerInstallError(
933
- "GitHub returned invalid repository runner information."
934
- );
935
- }
936
- return {
937
- id: registration.id,
938
- name: registration.name,
939
- status: registration.status,
940
- busy: registration.busy
941
- };
942
- });
943
- };
944
- var removeExistingRepositoryRunnerRegistrations = async (options, adapters = defaultAdapters) => {
945
- if (options.conflict.runners.some(({ id }) => id === void 0)) {
946
- throw new RunnerInstallError(
947
- "GitHub did not provide runner IDs, so Shipyard cannot safely remove the existing registrations. Remove them in GitHub Settings > Actions > Runners, then retry."
948
- );
949
- }
950
- const environment = adapters.environment();
951
- const removed = [];
952
- for (const runner of options.conflict.runners) {
953
- try {
954
- await adapters.run(
955
- "gh",
956
- [
957
- "api",
958
- "--method",
959
- "DELETE",
960
- `repos/${options.conflict.repository}/actions/runners/${runner.id}`
961
- ],
962
- { cwd: options.repoDir, env: environment }
963
- );
964
- removed.push(runner.name);
965
- } catch (error) {
966
- const previous = removed.length === 0 ? "" : ` Earlier registrations were already removed: ${removed.join(", ")}.`;
967
- throw new RunnerInstallError(
968
- `${commandFailure(
969
- `Removing GitHub runner ${runner.name} (#${runner.id}) from ${options.conflict.repository}`,
970
- error
971
- ).message}${previous}`
972
- );
973
- }
974
- }
975
- };
976
- var installRepositoryRunnerWithReplacement = async (options, adapters = defaultAdapters) => {
977
- try {
978
- return await options.install();
979
- } catch (error) {
980
- if (!(error instanceof RunnerInstallConflictError) || !options.interactive) {
981
- throw error;
982
- }
983
- if (!await options.confirmReplacement(error)) {
984
- throw new RunnerInstallError(
985
- `${error.message} Existing runner registrations were left unchanged; installation cancelled.`
986
- );
987
- }
988
- await removeExistingRepositoryRunnerRegistrations(
989
- {
990
- repoDir: options.repoDir,
991
- conflict: error
992
- },
993
- adapters
994
- );
995
- return options.install();
996
- }
997
- };
998
895
  var installRepositoryRunner = async (options, adapters = defaultAdapters) => {
999
896
  if (adapters.platform() !== "darwin" || adapters.arch() !== "arm64") {
1000
897
  throw new RunnerInstallError(
@@ -1003,6 +900,7 @@ var installRepositoryRunner = async (options, adapters = defaultAdapters) => {
1003
900
  }
1004
901
  const configDir = join(options.repoDir, CONFIG_DIR);
1005
902
  const runnerDir = join(configDir, RUNNER_DIR);
903
+ const maskDir = join(configDir, RUNNER_SANDBOX_MASK_DIR);
1006
904
  if (!await adapters.exists(configDir)) {
1007
905
  throw new RunnerInstallError(
1008
906
  `No ${CONFIG_DIR}/ found. Run \`shipyard init\` in this repository first.`
@@ -1058,6 +956,7 @@ var installRepositoryRunner = async (options, adapters = defaultAdapters) => {
1058
956
  {
1059
957
  repoDir: options.repoDir,
1060
958
  runnerDir,
959
+ maskDir,
1061
960
  repository,
1062
961
  runnerName
1063
962
  },
@@ -1070,8 +969,10 @@ var installRepositoryRunner = async (options, adapters = defaultAdapters) => {
1070
969
  }
1071
970
  await appendRunnerIgnores(join(configDir, ".gitignore"), adapters);
1072
971
  await adapters.chmod(runnerDir, 448);
972
+ await adapters.chmod(maskDir, 448);
1073
973
  await assertProtectedDirectoryIdentities(
1074
974
  runnerDir,
975
+ maskDir,
1075
976
  adapters.inspectDirectory
1076
977
  ).catch((error) => {
1077
978
  throw commandFailure("Validating protected runner directories", error);
@@ -1102,7 +1003,7 @@ var installRepositoryRunner = async (options, adapters = defaultAdapters) => {
1102
1003
  "--paginate",
1103
1004
  `repos/${repository}/actions/runners`,
1104
1005
  "--jq",
1105
- `.runners[] | select(any(.labels[]; .name == "${ACTIVATION_LABEL}")) | {id, name, status, busy}`
1006
+ `.runners[] | select(any(.labels[]; .name == "${ACTIVATION_LABEL}")) | .name`
1106
1007
  ],
1107
1008
  { cwd: options.repoDir, env: hostEnv }
1108
1009
  ).catch((error) => {
@@ -1111,11 +1012,10 @@ var installRepositoryRunner = async (options, adapters = defaultAdapters) => {
1111
1012
  error
1112
1013
  );
1113
1014
  });
1114
- const existingRegistrations = parseExistingRunnerRegistrations(
1115
- existing.stdout
1116
- );
1117
- if (existingRegistrations.length > 0) {
1118
- throw new RunnerInstallConflictError(repository, existingRegistrations);
1015
+ if (existing.stdout.trim().length > 0) {
1016
+ throw new RunnerInstallError(
1017
+ `A repository runner labeled \`${ACTIVATION_LABEL}\` is already registered for ${repository}. Remove it before installing another.`
1018
+ );
1119
1019
  }
1120
1020
  reportProgress(
1121
1021
  options,
@@ -1188,13 +1088,18 @@ var installRepositoryRunner = async (options, adapters = defaultAdapters) => {
1188
1088
  await adapters.makeDirectory(runnerDir).catch((error) => {
1189
1089
  throw commandFailure("Creating the protected runner directory", error);
1190
1090
  });
1091
+ if (!await adapters.exists(maskDir)) {
1092
+ await adapters.makeDirectory(maskDir);
1093
+ }
1191
1094
  await assertProtectedDirectoryIdentities(
1192
1095
  runnerDir,
1096
+ maskDir,
1193
1097
  adapters.inspectDirectory
1194
1098
  ).catch((error) => {
1195
1099
  throw commandFailure("Validating protected runner directories", error);
1196
1100
  });
1197
1101
  await adapters.chmod(runnerDir, 448);
1102
+ await adapters.chmod(maskDir, 448);
1198
1103
  reportProgress(
1199
1104
  options,
1200
1105
  5,
@@ -1234,12 +1139,15 @@ var installRepositoryRunner = async (options, adapters = defaultAdapters) => {
1234
1139
  { cwd: runnerDir, env: repositoryRunnerEnvironment(hostEnv) }
1235
1140
  );
1236
1141
  } catch {
1237
- const cleanup = await Promise.allSettled([adapters.remove(runnerDir)]);
1142
+ const cleanup = await Promise.allSettled([
1143
+ adapters.remove(runnerDir),
1144
+ adapters.remove(maskDir)
1145
+ ]);
1238
1146
  const cleanupSucceeded = cleanup.every(
1239
1147
  (result) => result.status === "fulfilled"
1240
1148
  );
1241
1149
  throw new RunnerInstallError(
1242
- cleanupSucceeded ? `Registering ${runnerName} failed. Partial local runner files were removed so installation can be retried. Check GitHub Settings > Actions > Runners for an orphan registration; the one-time token was not stored.` : `Registering ${runnerName} failed and partial local runner files could not be fully removed. Remove only ${runnerDir}, check GitHub Settings > Actions > Runners for an orphan registration, then retry. The one-time token was not stored.`
1150
+ cleanupSucceeded ? `Registering ${runnerName} failed. Partial local runner files were removed so installation can be retried. Check GitHub Settings > Actions > Runners for an orphan registration; the one-time token was not stored.` : `Registering ${runnerName} failed and partial local runner files could not be fully removed. Remove only ${runnerDir} and ${maskDir}, check GitHub Settings > Actions > Runners for an orphan registration, then retry. The one-time token was not stored.`
1243
1151
  );
1244
1152
  }
1245
1153
  reportProgress(options, 7, progressTotal, "Registered runner with GitHub");
@@ -1481,6 +1389,7 @@ var requireRunnerContext = async (repoDir, adapters) => {
1481
1389
  }
1482
1390
  const configDir = join(repoDir, CONFIG_DIR);
1483
1391
  const runnerDir = join(configDir, RUNNER_DIR);
1392
+ const maskDir = join(configDir, RUNNER_SANDBOX_MASK_DIR);
1484
1393
  if (!await adapters.exists(configDir)) {
1485
1394
  throw new RunnerControlError(
1486
1395
  `No ${CONFIG_DIR}/ found. Run \`shipyard init\` in this repository first.`
@@ -1630,6 +1539,7 @@ var requireRunnerContext = async (repoDir, adapters) => {
1630
1539
  });
1631
1540
  return {
1632
1541
  runnerDir,
1542
+ maskDir,
1633
1543
  repository,
1634
1544
  metadata,
1635
1545
  hostEnvironment,
@@ -1683,6 +1593,7 @@ var startRepositoryRunnerManaged = async (options, adapters = defaultAdapters2)
1683
1593
  {
1684
1594
  repoDir: options.repoDir,
1685
1595
  runnerDir: context.runnerDir,
1596
+ maskDir: context.maskDir,
1686
1597
  metadata: context.metadata,
1687
1598
  runnerEnvironment: repositoryRunnerEnvironment(context.hostEnvironment),
1688
1599
  dockerEnvironment: context.runtimeEnvironment
@@ -2212,38 +2123,19 @@ var installRunnerCommand = Command.make(
2212
2123
  { registrationToken: registrationTokenOption },
2213
2124
  ({ registrationToken }) => Effect.gen(function* () {
2214
2125
  const display = yield* Display;
2215
- const repoDir = process.cwd();
2216
- const result = yield* Effect.tryPromise({
2217
- try: () => installRepositoryRunnerWithReplacement({
2218
- repoDir,
2219
- interactive: process.stdin.isTTY === true,
2220
- confirmReplacement: async (conflict) => {
2221
- const confirmed = await clack2.confirm({
2222
- message: conflict.confirmationMessage,
2223
- initialValue: false
2224
- });
2225
- return !clack2.isCancel(confirmed) && confirmed === true;
2226
- },
2227
- install: () => runEffectPromise(
2228
- display.progress(
2229
- "Installing repository runner",
2230
- (report) => Effect.tryPromise({
2231
- try: () => installRepositoryRunner({
2232
- repoDir,
2233
- registrationToken: registrationToken._tag === "Some" ? registrationToken.value : void 0,
2234
- onProgress: report
2235
- }),
2236
- catch: (error) => error instanceof RunnerInstallError ? error : new InitError({
2237
- message: `Repository runner installation failed: ${error instanceof Error ? error.message : String(error)}`
2238
- })
2239
- })
2240
- )
2241
- )
2242
- }),
2243
- catch: (error) => new InitError({
2244
- message: error instanceof RunnerInstallError ? error.message : `Repository runner installation failed: ${error instanceof Error ? error.message : String(error)}`
2126
+ const result = yield* display.progress(
2127
+ "Installing repository runner",
2128
+ (report) => Effect.tryPromise({
2129
+ try: () => installRepositoryRunner({
2130
+ repoDir: process.cwd(),
2131
+ registrationToken: registrationToken._tag === "Some" ? registrationToken.value : void 0,
2132
+ onProgress: report
2133
+ }),
2134
+ catch: (error) => new InitError({
2135
+ message: error instanceof RunnerInstallError ? error.message : `Repository runner installation failed: ${error instanceof Error ? error.message : String(error)}`
2136
+ })
2245
2137
  })
2246
- });
2138
+ );
2247
2139
  yield* display.status(
2248
2140
  `Installed ${result.name} for ${result.repository}.`,
2249
2141
  "success"
@@ -2367,7 +2259,7 @@ var runnerCommand = Command.make(
2367
2259
  purgeRunnerCommand
2368
2260
  ])
2369
2261
  );
2370
- var VERSION = "0.7.0-staging.36082887921" ;
2262
+ var VERSION = "0.7.0" ;
2371
2263
 
2372
2264
  // src/cli.ts
2373
2265
  var imageNameOption = Options.text("image-name").pipe(
@@ -2535,7 +2427,7 @@ var runCommand2 = Command.make(
2535
2427
  );
2536
2428
  var templateOption = Options.text("template").pipe(
2537
2429
  Options.withDescription(
2538
- "Template to scaffold (e.g. simple-loop, parallel-planner)"
2430
+ "Template to scaffold (e.g. blank, simple-loop, parallel-planner)"
2539
2431
  ),
2540
2432
  Options.optional
2541
2433
  );
@@ -2669,12 +2561,12 @@ var initCommand = Command.make(
2669
2561
  yield* failIfNonInteractive(params.flag);
2670
2562
  }
2671
2563
  const confirmed = yield* Effect.promise(
2672
- () => clack2.confirm({
2564
+ () => clack.confirm({
2673
2565
  message: params.promptMessage,
2674
2566
  initialValue: true
2675
2567
  })
2676
2568
  );
2677
- if (clack2.isCancel(confirmed)) {
2569
+ if (clack.isCancel(confirmed)) {
2678
2570
  yield* Effect.fail(
2679
2571
  new InitError({ message: params.cancelMessage })
2680
2572
  );
@@ -2699,7 +2591,7 @@ var initCommand = Command.make(
2699
2591
  yield* failIfNonInteractive("--agent");
2700
2592
  }
2701
2593
  const selected = yield* Effect.promise(
2702
- () => clack2.select({
2594
+ () => clack.select({
2703
2595
  message: "Select an agent:",
2704
2596
  initialValue: DEFAULT_AGENT_NAME,
2705
2597
  options: agents.map((a) => ({
@@ -2709,7 +2601,7 @@ var initCommand = Command.make(
2709
2601
  }))
2710
2602
  })
2711
2603
  );
2712
- if (clack2.isCancel(selected)) {
2604
+ if (clack.isCancel(selected)) {
2713
2605
  yield* Effect.fail(
2714
2606
  new InitError({ message: "Agent selection cancelled." })
2715
2607
  );
@@ -2722,7 +2614,7 @@ var initCommand = Command.make(
2722
2614
  requested: codexAuthFlag._tag === "Some" ? codexAuthFlag.value : void 0,
2723
2615
  interactive: isInteractive,
2724
2616
  select: async () => {
2725
- const selected = await clack2.select({
2617
+ const selected = await clack.select({
2726
2618
  message: "How will you authenticate Codex?",
2727
2619
  initialValue: "chatgpt",
2728
2620
  options: [
@@ -2738,7 +2630,7 @@ var initCommand = Command.make(
2738
2630
  }
2739
2631
  ]
2740
2632
  });
2741
- return clack2.isCancel(selected) ? void 0 : selected;
2633
+ return clack.isCancel(selected) ? void 0 : selected;
2742
2634
  }
2743
2635
  }),
2744
2636
  catch: (error) => error instanceof InitError ? error : new InitError({
@@ -2757,7 +2649,32 @@ var initCommand = Command.make(
2757
2649
  })
2758
2650
  });
2759
2651
  }
2760
- const selectedSandboxProvider = getSandboxProvider("docker");
2652
+ const sandboxProviders = listSandboxProviders();
2653
+ let selectedSandboxProvider;
2654
+ if (sandboxFlag._tag === "Some") {
2655
+ selectedSandboxProvider = getSandboxProvider(sandboxFlag.value);
2656
+ } else {
2657
+ if (!isInteractive) {
2658
+ yield* failIfNonInteractive("--sandbox");
2659
+ }
2660
+ const selected = yield* Effect.promise(
2661
+ () => clack.select({
2662
+ message: "Select a sandbox provider:",
2663
+ options: sandboxProviders.map((p) => ({
2664
+ value: p.name,
2665
+ label: p.label
2666
+ }))
2667
+ })
2668
+ );
2669
+ if (clack.isCancel(selected)) {
2670
+ yield* Effect.fail(
2671
+ new InitError({
2672
+ message: "Sandbox provider selection cancelled."
2673
+ })
2674
+ );
2675
+ }
2676
+ selectedSandboxProvider = getSandboxProvider(selected);
2677
+ }
2761
2678
  const issueTrackers = listIssueTrackers();
2762
2679
  let selectedIssueTracker;
2763
2680
  if (issueTrackerFlag._tag === "Some") {
@@ -2767,7 +2684,7 @@ var initCommand = Command.make(
2767
2684
  yield* failIfNonInteractive("--issue-tracker");
2768
2685
  }
2769
2686
  const selected = yield* Effect.promise(
2770
- () => clack2.select({
2687
+ () => clack.select({
2771
2688
  message: "Select an issue tracker:",
2772
2689
  initialValue: "github-issues",
2773
2690
  options: issueTrackers.map((b) => ({
@@ -2776,7 +2693,7 @@ var initCommand = Command.make(
2776
2693
  }))
2777
2694
  })
2778
2695
  );
2779
- if (clack2.isCancel(selected)) {
2696
+ if (clack.isCancel(selected)) {
2780
2697
  yield* Effect.fail(
2781
2698
  new InitError({
2782
2699
  message: "Issue tracker selection cancelled."
@@ -2793,9 +2710,9 @@ var initCommand = Command.make(
2793
2710
  yield* failIfNonInteractive("--template");
2794
2711
  }
2795
2712
  const selected = yield* Effect.promise(
2796
- () => clack2.select({
2713
+ () => clack.select({
2797
2714
  message: "Select a template:",
2798
- initialValue: "simple-loop",
2715
+ initialValue: "blank",
2799
2716
  options: templates.map((tmpl) => ({
2800
2717
  value: tmpl.name,
2801
2718
  label: tmpl.name,
@@ -2803,7 +2720,7 @@ var initCommand = Command.make(
2803
2720
  }))
2804
2721
  })
2805
2722
  );
2806
- if (clack2.isCancel(selected)) {
2723
+ if (clack.isCancel(selected)) {
2807
2724
  yield* Effect.fail(
2808
2725
  new InitError({ message: "Template selection cancelled." })
2809
2726
  );
@@ -2855,17 +2772,15 @@ var initCommand = Command.make(
2855
2772
  console.warn(message);
2856
2773
  }
2857
2774
  }
2858
- yield* d.progress(
2775
+ yield* d.spinner(
2859
2776
  `Scaffolding ${CONFIG_DIR}/ config directory...`,
2860
- (report) => scaffold(cwd, {
2777
+ scaffold(cwd, {
2861
2778
  agent: selectedAgent,
2862
2779
  model: selectedModel,
2863
- modelExplicit: modelFlag._tag === "Some",
2864
2780
  templateName: selectedTemplate,
2865
2781
  issueTracker: selectedIssueTracker,
2866
2782
  sandboxProvider: selectedSandboxProvider,
2867
- codexAuth: selectedCodexAuth,
2868
- onProgress: report
2783
+ codexAuth: selectedCodexAuth
2869
2784
  }).pipe(
2870
2785
  Effect.mapError(
2871
2786
  (e) => new InitError({
@@ -2928,39 +2843,28 @@ var initCommand = Command.make(
2928
2843
  interactive: isInteractive,
2929
2844
  requested: installRunnerChoice._tag === "Some" ? installRunnerChoice.value : void 0,
2930
2845
  confirm: async ({ message, initialValue }) => {
2931
- const confirmed = await clack2.confirm({ message, initialValue });
2932
- if (clack2.isCancel(confirmed)) {
2846
+ const confirmed = await clack.confirm({ message, initialValue });
2847
+ if (clack.isCancel(confirmed)) {
2933
2848
  throw new InitError({
2934
2849
  message: "Repository-runner installation selection cancelled."
2935
2850
  });
2936
2851
  }
2937
2852
  return confirmed === true;
2938
2853
  },
2939
- install: () => installRepositoryRunnerWithReplacement({
2940
- repoDir: cwd,
2941
- interactive: isInteractive,
2942
- confirmReplacement: async (conflict) => {
2943
- const confirmed = await clack2.confirm({
2944
- message: conflict.confirmationMessage,
2945
- initialValue: false
2946
- });
2947
- return !clack2.isCancel(confirmed) && confirmed === true;
2948
- },
2949
- install: () => runEffectPromise(
2950
- d.progress(
2951
- "Installing repository runner",
2952
- (report) => Effect.tryPromise({
2953
- try: () => installRepositoryRunner({
2954
- repoDir: cwd,
2955
- onProgress: report
2956
- }),
2957
- catch: (error) => error instanceof RunnerInstallError ? error : new InitError({
2958
- message: `Repository runner installation failed: ${error instanceof Error ? error.message : String(error)}`
2959
- })
2854
+ install: () => Effect.runPromise(
2855
+ d.progress(
2856
+ "Installing repository runner",
2857
+ (report) => Effect.tryPromise({
2858
+ try: () => installRepositoryRunner({
2859
+ repoDir: cwd,
2860
+ onProgress: report
2861
+ }),
2862
+ catch: (error) => new InitError({
2863
+ message: error instanceof RunnerInstallError ? error.message : `Repository runner installation failed: ${error instanceof Error ? error.message : String(error)}`
2960
2864
  })
2961
- )
2865
+ })
2962
2866
  )
2963
- })
2867
+ )
2964
2868
  }),
2965
2869
  catch: (error) => error instanceof InitError ? error : new InitError({
2966
2870
  message: `Repository-runner setup failed: ${error instanceof Error ? error.message : String(error)}`