@outerlayer/cli 0.23.0 → 0.25.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 (117) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +13 -0
  3. package/dist/{agent-setup-SNDCEHG2.js → agent-setup-PDIIGMKE.js} +7 -7
  4. package/dist/{bridge-QFN3KJP6.js → bridge-5KZ36YDG.js} +3 -3
  5. package/dist/build-Q2OBF77R.js +12 -0
  6. package/dist/build-info.json +1 -1
  7. package/dist/{builder-7FBQAWQZ.js → builder-PRSCX5AH.js} +7 -7
  8. package/dist/{check-3CGLWMHM.js → check-EPYRTE3R.js} +40 -37
  9. package/dist/{chunk-URWYRAIC.js → chunk-2K3TCQHW.js} +1 -3
  10. package/dist/{chunk-Z42JY3LQ.js → chunk-2N72GPS2.js} +3 -3
  11. package/dist/{chunk-WY7HFY62.js → chunk-34ANPNVB.js} +3 -3
  12. package/dist/{chunk-QSEPKZXR.js → chunk-3I44PQ26.js} +206 -57
  13. package/dist/{chunk-ZKWKQIJU.js → chunk-4S4VBDSO.js} +1 -1
  14. package/dist/chunk-5222PA6S.js +66 -0
  15. package/dist/{chunk-GXV73IIS.js → chunk-6CNHXHI2.js} +17 -5
  16. package/dist/{chunk-NYTZ7HVN.js → chunk-7OWPLRJR.js} +1 -1
  17. package/dist/{chunk-EFB3FGLC.js → chunk-ATGHVEU4.js} +1 -1
  18. package/dist/{chunk-QILR6TZP.js → chunk-AXPCGD46.js} +5 -5
  19. package/dist/{chunk-CBCKD6DO.js → chunk-D4AMAEZ7.js} +12 -11
  20. package/dist/{chunk-XACXXKQ5.js → chunk-DEBOL6ON.js} +1 -1
  21. package/dist/{chunk-XGPC3IOR.js → chunk-FXUVXSY2.js} +2 -2
  22. package/dist/{chunk-6AOZD3LY.js → chunk-HBLLZLOH.js} +3 -3
  23. package/dist/{chunk-ZLZ3XVPI.js → chunk-HWEIFHSW.js} +3 -3
  24. package/dist/{chunk-D64E222W.js → chunk-IDJIWKTY.js} +3 -3
  25. package/dist/{chunk-OBCFK4QD.js → chunk-IGRRD7PX.js} +11 -7
  26. package/dist/{chunk-GISNHSYL.js → chunk-JGD5TWQ2.js} +4 -2
  27. package/dist/{chunk-2TT7DEMZ.js → chunk-JKILLWZF.js} +11 -9
  28. package/dist/{chunk-V5CG75NX.js → chunk-JWRX5ZTT.js} +6 -2
  29. package/dist/{chunk-XOEV4FVK.js → chunk-K5WVK7IC.js} +1 -1
  30. package/dist/{chunk-TFGXOHF6.js → chunk-L2FZLBF7.js} +278 -40
  31. package/dist/{chunk-QS43O4DR.js → chunk-M4B6QKNZ.js} +1 -1
  32. package/dist/{chunk-UF5L4BAI.js → chunk-O2WYUIK7.js} +1 -1
  33. package/dist/{chunk-276D7BJ7.js → chunk-QMQC6JCO.js} +1 -1
  34. package/dist/{chunk-OGMZJWWD.js → chunk-QUGYGO6L.js} +6 -6
  35. package/dist/{chunk-JV4YGAAW.js → chunk-QXNJWSTM.js} +5 -5
  36. package/dist/{chunk-3W333NFO.js → chunk-RGW7ERF3.js} +15 -4
  37. package/dist/{chunk-I2UOAV7X.js → chunk-TQMI335S.js} +4 -1
  38. package/dist/{chunk-WXJXHZVB.js → chunk-UNF7MWEL.js} +1 -1
  39. package/dist/{chunk-FX6IHR5C.js → chunk-VMF5LK3D.js} +8 -8
  40. package/dist/{chunk-C2XZBNHY.js → chunk-VNGENFOG.js} +19 -6
  41. package/dist/{chunk-G3LHWUBK.js → chunk-VPUNOGXO.js} +6 -6
  42. package/dist/{chunk-C64D7HR3.js → chunk-X5LNCIZT.js} +19 -10
  43. package/dist/{chunk-2Q3PZ42G.js → chunk-XGJUNNXL.js} +1 -1
  44. package/dist/{chunk-BOG4ENP7.js → chunk-XHXDCYSK.js} +185 -38
  45. package/dist/{chunk-CQAEV3Z5.js → chunk-XRYUJTAT.js} +8 -1
  46. package/dist/{chunk-KGKFEONR.js → chunk-YMKQVCKC.js} +5 -5
  47. package/dist/{chunk-DJYOCD65.js → chunk-YTTHDPWF.js} +5 -5
  48. package/dist/{chunk-4AVDD7VX.js → chunk-ZGMQQWCV.js} +2 -2
  49. package/dist/{chunk-IHNQWF6F.js → chunk-ZI2ZVWKT.js} +1 -1
  50. package/dist/{chunk-OW52QLK3.js → chunk-ZODVSFI4.js} +1 -1
  51. package/dist/chunk-ZVB57WI6.js +19 -0
  52. package/dist/{claude-launcher-KEESTJHO.js → claude-launcher-WGOCLTG6.js} +1 -1
  53. package/dist/{cli-44WGGICQ.js → cli-2LGSPAYZ.js} +131 -97
  54. package/dist/command-ITLPJ5XU.js +533 -0
  55. package/dist/{config-H4QS332G.js → config-JJ3EO7SB.js} +3 -3
  56. package/dist/connect-cmd-JFDZKPND.js +25 -0
  57. package/dist/container-start-X22OXY6E.js +179 -0
  58. package/dist/{control-plane-launch-SEYCOZFH.js → control-plane-launch-ZFT6SI4N.js} +6 -6
  59. package/dist/{dist-SOH3O24Y.js → dist-2APO44JG.js} +1 -1
  60. package/dist/{docker-KMK4LOET.js → docker-PQHWAY5J.js} +7 -7
  61. package/dist/{doctor-4H77CEB7.js → doctor-73GNT4VV.js} +90 -31
  62. package/dist/{emit-artifact-cmd-XCZ3ALLV.js → emit-artifact-cmd-AIOTX6YT.js} +14 -11
  63. package/dist/{emit-criteria-cmd-JE72JHKJ.js → emit-criteria-cmd-7PJINOHA.js} +11 -8
  64. package/dist/{emit-finding-cmd-ILIDRMYN.js → emit-finding-cmd-JRDSZ4DR.js} +11 -8
  65. package/dist/{emit-result-cmd-WPKG5YAS.js → emit-result-cmd-ZGF6Q6OP.js} +14 -11
  66. package/dist/{emit-step-cmd-OVMLQSL6.js → emit-step-cmd-DUR34STQ.js} +12 -9
  67. package/dist/{exec-client-ATPF2DDB.js → exec-client-S5IA7JRG.js} +4 -4
  68. package/dist/{governing-source-EQBKPME7.js → governing-source-DKJABQAT.js} +12 -11
  69. package/dist/{guest-init-HWSE7W6L.js → guest-init-TOSKPGPF.js} +5 -5
  70. package/dist/{hook-fast-27SHD72I.js → hook-fast-NEELST6G.js} +1 -1
  71. package/dist/host-key-ICJEQBWH.js +9 -0
  72. package/dist/image-tag-6KJOGBQK.js +10 -0
  73. package/dist/index.js +4 -4
  74. package/dist/{init-HAAHIQFN.js → init-NE3VR3EY.js} +26 -23
  75. package/dist/{init-WQ62BXDN.js → init-SKOXNJND.js} +26 -23
  76. package/dist/{init-cmd-VPFIDY64.js → init-cmd-UEDOJ7LG.js} +12 -12
  77. package/dist/{init-update-UZIJ2FMU.js → init-update-VKZ4S6HH.js} +36 -10
  78. package/dist/{install-SFACZU7E.js → install-MPEJJ7X6.js} +5 -5
  79. package/dist/install-cmd-SE6NCTJK.js +12 -0
  80. package/dist/job-state-format-WJQGQB6F.js +6 -0
  81. package/dist/{logs-N3ZX7HFV.js → logs-T4ERSOQ4.js} +3 -3
  82. package/dist/loop-BPVEA5OC.js +78 -0
  83. package/dist/{machine-PGDRWPI2.js → machine-G3GICXQN.js} +22 -19
  84. package/dist/{mcp-serve-cmd-CNRC5AL5.js → mcp-serve-cmd-7M4VLNI4.js} +5 -5
  85. package/dist/{paths-AKX2OOLF.js → paths-2KGM2LZV.js} +3 -3
  86. package/dist/pending-BGLMTB5Q.js +9 -0
  87. package/dist/{pidfile-G2J5WBQS.js → pidfile-VYBH3IMN.js} +4 -4
  88. package/dist/{record-Q4IEYIVV.js → record-GNOHDJ2O.js} +4 -4
  89. package/dist/{relay-PSZ4LGMW.js → relay-GFPPD6GF.js} +1 -1
  90. package/dist/relay-protocol-HELO5SNH.js +6 -0
  91. package/dist/{runner-env-GZHXM27A.js → runner-env-GUFV544G.js} +6 -6
  92. package/dist/{runner-identity-IIZ3RK7M.js → runner-identity-J6DXJ6MF.js} +7 -7
  93. package/dist/{runner-key-BPP5XRYR.js → runner-key-L55O7Y7W.js} +26 -23
  94. package/dist/{session-sender-EKKZ5DEU.js → session-sender-LXTASWBY.js} +15 -12
  95. package/dist/{spec-cmd-JDHGNFVJ.js → spec-cmd-G2JNXKD7.js} +12 -9
  96. package/dist/{status-UCQDEMFX.js → status-U6DXOR6C.js} +43 -28
  97. package/dist/{sync-cmd-NSVXOYFJ.js → sync-cmd-VPNS5ITY.js} +14 -11
  98. package/dist/{watch-NQVZWPBJ.js → watch-ZOCYD2NE.js} +14 -11
  99. package/dist/{work-claim-cmd-FPJXQAXP.js → work-claim-cmd-BUMIPJNF.js} +15 -12
  100. package/dist/{work-cmd-FMPDXPU6.js → work-cmd-LRAXUOJV.js} +12 -9
  101. package/dist/{work-comment-cmd-ZVUE2IUN.js → work-comment-cmd-4RDIBUYO.js} +12 -9
  102. package/dist/{work-logs-cmd-CQH2SYT4.js → work-logs-cmd-YYN2CPJQ.js} +12 -9
  103. package/dist/{work-open-pr-cmd-YY2OGJOI.js → work-open-pr-cmd-3MCND2LZ.js} +13 -10
  104. package/dist/{work-pr-cmd-EHFTEVJO.js → work-pr-cmd-BODEZAXN.js} +12 -9
  105. package/dist/{work-stop-cmd-LGWSBSBT.js → work-stop-cmd-P7HXVKEG.js} +12 -9
  106. package/package.json +2 -2
  107. package/skill-pack/maintained/outerlayer-build/SKILL.md +1 -1
  108. package/skill-pack/maintained/outerlayer-build/references/agent-briefs.md +3 -2
  109. package/skill-pack/maintained/outerlayer-build/references/review-briefs.md +3 -1
  110. package/dist/build-WHHFA7KM.js +0 -12
  111. package/dist/command-4FAP2D3P.js +0 -207
  112. package/dist/connect-cmd-6DUTUK63.js +0 -22
  113. package/dist/host-key-ATB23GI3.js +0 -9
  114. package/dist/install-cmd-6HTP7I43.js +0 -12
  115. package/dist/loop-JPW2QJ64.js +0 -75
  116. package/dist/pending-7SGWZCEH.js +0 -9
  117. package/dist/relay-protocol-K7KP2SPI.js +0 -6
@@ -1,30 +1,32 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
- import { releaseLease, isUnfinished, setStage, jobEnv, runHook, describeHookEnd, provisionMicroVm, deliverCommitHooks, provisionProcess, provisionEnvironment, readProvisionOut, resolveHostBuild, workdirProblem, listenOnBroker, hostEnvironment, hostCommandEnvironment, execArgv, microVmCommandLine, runCommand, commandLogOffset, execCommandLine, startSender, startHostServices, finishJob, isContainerTracked, probeCommand, appendCommandLog, judgeProbe, EXIT_CODE_FILE, isGroupAlive, outcomeFromExitCode, isTimedOut, readCpuUsec, isIdle, stopOrphanedMicroVms, unfinishedJobs, listAdoptableJobs, retryFailedReleases, recoverLostJobs, hookLimitsFor, AttemptLogBook, needsRenew, renewLease, RENEW_INTERVAL_SECONDS, listTrackedJobs, restartSender, ALREADY_RELEASED } from './chunk-QSEPKZXR.js';
4
- import { writePendingUpdate } from './chunk-CQAEV3Z5.js';
5
- import { readHousekeepingRecord, writeHousekeepingRecord } from './chunk-UF5L4BAI.js';
3
+ import { releaseLease, isUnfinished, setStage, jobEnv, runHook, describeHookEnd, provisionMicroVm, deliverCommitHooks, provisionProcess, provisionEnvironment, readProvisionOut, resolveHostBuild, workdirProblem, listenOnBroker, hostEnvironment, hostCommandEnvironment, execArgv, microVmCommandLine, runCommand, commandLogOffset, execCommandLine, startSender, startHostServices, finishJob, isContainerTracked, probeCommand, appendCommandLog, judgeProbe, EXIT_CODE_FILE, isGroupAlive, outcomeFromExitCode, isTimedOut, readCpuUsec, isIdle, stopOrphanedMicroVms, unfinishedJobs, listUnreadableJobs, listAdoptableJobs, retryFailedReleases, recoverLostJobs, hookLimitsFor, AttemptLogBook, needsRenew, renewLease, RENEW_INTERVAL_SECONDS, listTrackedJobs, restartSender, ALREADY_RELEASED, inTeardown } from './chunk-3I44PQ26.js';
4
+ import { writePendingUpdate } from './chunk-XRYUJTAT.js';
5
+ import { JOB_STATE_FORMAT, readsJobState, jobStateFormatOf } from './chunk-ZVB57WI6.js';
6
+ import { readHousekeepingRecord, writeHousekeepingRecord } from './chunk-O2WYUIK7.js';
6
7
  import { decideInstalledSwitch } from './chunk-3JVYMD4G.js';
7
8
  import { fetchVersionMeta, npmVersion, npmSupportsProvenance, fetchTarballDigest, fetchProvenance, installVersion, auditSignatures } from './chunk-YETTWTAY.js';
8
- import { compareVersions, satisfiesRange } from './chunk-I2UOAV7X.js';
9
9
  import { RUNNER_PROTOCOL } from './chunk-TPKYTORP.js';
10
- import { currentSkillCommand, formerSkillProblem, placeholderQueues, placeholderProblem } from './chunk-G3LHWUBK.js';
10
+ import { currentSkillCommand, formerSkillProblem, placeholderQueues, placeholderProblem } from './chunk-VPUNOGXO.js';
11
+ import { writeMarker, readMarker, currentVersion, removeMarker, entryIn, versionDir, startedThroughCurrent, switchCurrent, isInLayout, cleanVersions, stagingDir } from './chunk-QMQC6JCO.js';
11
12
  import { freeBytes, formatBytes } from './chunk-YYYRBYWG.js';
12
- import { writeMarker, readMarker, currentVersion, removeMarker, entryIn, versionDir, startedThroughCurrent, switchCurrent, cleanVersions, isInLayout, stagingDir } from './chunk-276D7BJ7.js';
13
13
  import { RepoFilter } from './chunk-TBT347UY.js';
14
- import { LEASE_SECONDS, provisionContainer, leaseExpiresAtMs, deliverContainerHooks, lifecycleSteps, lifecycleFailureReason, prepareBranch, containerCommandExec, measureDisk, setUpRunnerCgroup, createContainerRuntime, parseCpuList, ensureTunnel, keepGhConfigCurrent, filesAreCopied } from './chunk-BOG4ENP7.js';
15
- import { checkFit } from './chunk-OBCFK4QD.js';
16
- import { claimPidFile, readPidFile, releasePidFile } from './chunk-XACXXKQ5.js';
14
+ import { LEASE_SECONDS, provisionContainer, leaseExpiresAtMs, deliverContainerHooks, lifecycleSteps, lifecycleFailureReason, prepareBranch, containerCommandExec, measureDisk, setUpRunnerCgroup, createContainerRuntime, parseCpuList, ensureTunnel, keepGhConfigCurrent, filesAreCopied } from './chunk-XHXDCYSK.js';
15
+ import { tagToPull, pullInstruction, imageTag, writeUpdateStatus } from './chunk-5222PA6S.js';
16
+ import { isVersion, compareVersions, satisfiesRange } from './chunk-TQMI335S.js';
17
+ import { checkFit } from './chunk-IGRRD7PX.js';
18
+ import { claimPidFile, readPidFile, releasePidFile } from './chunk-DEBOL6ON.js';
17
19
  import { startTimeOf, isSameProcess } from './chunk-JBJTC3UU.js';
18
- import { writeJobFile, ensureRunnerIdentity, readJobFile, builderName, runnerLabelValue, ensureFailureCount, builderSpec, ensureBuilder, spawnRun, BUILD_LABEL, RUNNER_LABEL, readJobFileChecked } from './chunk-OGMZJWWD.js';
19
- import { describeCallFailure, retrySend, shortClaim, relativeToRunner, formatDuration, BuildTokens, classifyRefusal, refusalCode, createRunnerLog, formatClock } from './chunk-KGKFEONR.js';
20
- import { isSignatureRefusal, WorkCommandError, getJson, postJson } from './chunk-C64D7HR3.js';
21
- import { isBuiltinContainer, isBuiltinVm, isBuiltinProcess, loadRunnerConfig, checkHooks, builtinKind, DEFAULT_UPDATE_MAX_WAIT_HOURS } from './chunk-V5CG75NX.js';
20
+ import { writeJobFile, ensureRunnerIdentity, readJobFile, builderName, runnerLabelValue, ensureFailureCount, builderSpec, ensureBuilder, spawnRun, BUILD_LABEL, RUNNER_LABEL, readJobFileChecked } from './chunk-QUGYGO6L.js';
21
+ import { describeCallFailure, retrySend, shortClaim, relativeToRunner, formatDuration, BuildTokens, classifyRefusal, refusalCode, createRunnerLog, formatClock } from './chunk-YMKQVCKC.js';
22
+ import { isSignatureRefusal, WorkCommandError, getJson, postJson } from './chunk-X5LNCIZT.js';
23
+ import { isBuiltinContainer, isBuiltinVm, isBuiltinProcess, loadRunnerConfig, checkHooks, builtinKind, DEFAULT_UPDATE_MAX_WAIT_HOURS } from './chunk-JWRX5ZTT.js';
22
24
  import { exceedsDisk } from './chunk-7RQ73DLO.js';
23
25
  import { resolveVersion } from './chunk-RA3O54FC.js';
24
- import { loadHostKey, ensureHostKey, signerFor } from './chunk-OW52QLK3.js';
25
- import { RUNNER_DIR_MODE, runnerDir, buildDir, attemptStamp, jobDir, RUNNER_FILE_MODE, jobsDir, pidFilePath, spoolDir, rootfsDir } from './chunk-IHNQWF6F.js';
26
- import { parseClaimIdentity, parseClaimedRepositories, parseUnreachable, repositoriesOf, primaryOf, sharedUserEnvironment, JOB_MOUNT, claudeCredential } from './chunk-URWYRAIC.js';
27
- import { COMMAND_LOST_REASON, CONTAINER_STOPPED_REASON } from './chunk-GISNHSYL.js';
26
+ import { loadHostKey, ensureHostKey, signerFor } from './chunk-ZODVSFI4.js';
27
+ import { RUNNER_DIR_MODE, runnerDir, buildDir, attemptStamp, jobDir, RUNNER_FILE_MODE, jobsDir, pidFilePath, spoolDir, rootfsDir } from './chunk-ZI2ZVWKT.js';
28
+ import { parseClaimIdentity, parseClaimedRepositories, parseUnreachable, repositoriesOf, primaryOf, sharedUserEnvironment, JOB_MOUNT, claudeCredential } from './chunk-2K3TCQHW.js';
29
+ import { COMMAND_LOST_REASON, CONTAINER_STOPPED_REASON } from './chunk-JGD5TWQ2.js';
28
30
  import { randomBytes } from 'crypto';
29
31
  import { mkdirSync, existsSync, chmodSync, writeFileSync, renameSync, statSync, readdirSync, readFileSync, rmSync, closeSync, openSync, constants, lstatSync } from 'fs';
30
32
  import { join, dirname } from 'path';
@@ -237,6 +239,61 @@ function writeLastUpdate(configPath, update) {
237
239
  `, { mode: RUNNER_FILE_MODE });
238
240
  renameSync(temporary, path);
239
241
  }
242
+
243
+ // src/runner/update/ask-formats.ts
244
+ async function askJobStateFormats(runNode, entry, cwd) {
245
+ let result;
246
+ try {
247
+ result = await runNode([entry, "runner", "job-state-formats"], cwd);
248
+ } catch {
249
+ return void 0;
250
+ }
251
+ if (result.status !== 0) return void 0;
252
+ try {
253
+ const body = JSON.parse(result.stdout);
254
+ if (!Array.isArray(body.reads) || !body.reads.every((n) => Number.isInteger(n))) return void 0;
255
+ return body.reads;
256
+ } catch {
257
+ return void 0;
258
+ }
259
+ }
260
+
261
+ // src/runner/update/survives-restart.ts
262
+ function stageReason(job) {
263
+ if (job.stage === "command") return void 0;
264
+ if (!inTeardown(job)) return "is still in setup";
265
+ switch (job.stage) {
266
+ case "judge":
267
+ return "is running the criteria judge";
268
+ case "checks":
269
+ return "is running its checks";
270
+ default:
271
+ return `is finishing (${job.stage})`;
272
+ }
273
+ }
274
+ function ownReason({ job, ending }) {
275
+ if (job.microvm !== void 0) return "runs in a microVM, which does not carry on through a restart";
276
+ if (!isContainerTracked(job)) return "is not a container build tracked inside its container";
277
+ if (ending) return "is being ended";
278
+ return stageReason(job);
279
+ }
280
+ function buildBlockers(builds) {
281
+ return builds.flatMap((build) => {
282
+ const reason = ownReason(build);
283
+ return reason === void 0 ? [] : [{ item: build.job.item, reason }];
284
+ });
285
+ }
286
+ function restartBlockers(builds, reads) {
287
+ return builds.flatMap((build) => {
288
+ const reason = ownReason(build);
289
+ if (reason !== void 0) return [{ item: build.job.item, reason }];
290
+ if (reads === void 0) return [{ item: build.job.item, reason: "has job state the new version may not read: it did not say which formats it reads" }];
291
+ if (!readsJobState(build.job, reads)) {
292
+ return [{ item: build.job.item, reason: `has job-state format ${jobStateFormatOf(build.job)}, which the new version does not read` }];
293
+ }
294
+ return [];
295
+ });
296
+ }
240
297
  var RELEASE_REPOSITORY = "https://github.com/outerlayerai/outerlayer";
241
298
  var RELEASE_WORKFLOW_PATH = ".github/workflows/outerlayer-release.yml";
242
299
  var UpdateStepError = class extends Error {
@@ -410,8 +467,8 @@ async function preflightUpdate(args) {
410
467
  const failure2 = await earlyFailure(args);
411
468
  return failure2 === void 0 ? void 0 : finish(args, failure2);
412
469
  }
413
- async function performUpdate(args) {
414
- const { configPath, own, version, deps } = args;
470
+ async function prepareUpdate(args) {
471
+ const { configPath, version, deps } = args;
415
472
  const early = await earlyFailure(args);
416
473
  if (early !== void 0) return finish(args, early);
417
474
  let verified;
@@ -423,24 +480,31 @@ async function performUpdate(args) {
423
480
  if (!verified.ok) return finish(args, { outcome: verified.outcome, reason: verified.reason });
424
481
  const staged = await deps.runNode([entryIn(verified.dir), "runner", "check", "--config", configPath], verified.dir);
425
482
  if (staged.status !== 0) {
426
- try {
427
- rmSync(verified.dir, { recursive: true, force: true });
428
- } catch {
429
- }
483
+ removeStaging(verified.dir);
430
484
  return finish(args, {
431
485
  outcome: "retrying",
432
486
  reason: `${version} failed \`runner check\` on this host (exit ${staged.status})`,
433
487
  detail: `${version} failed \`runner check\` on this host: ${(staged.stderr.trim() || staged.stdout.trim()).slice(0, 500)}`
434
488
  });
435
489
  }
490
+ return { kind: "prepared", dir: verified.dir };
491
+ }
492
+ function removeStaging(dir) {
493
+ try {
494
+ rmSync(dir, { recursive: true });
495
+ } catch {
496
+ }
497
+ }
498
+ function installPrepared(args, stagedDir) {
499
+ const { configPath, own, version } = args;
436
500
  try {
437
501
  if (version !== own) rmSync(versionDir(configPath, version), { recursive: true, force: true });
438
- renameSync(verified.dir, versionDir(configPath, version));
502
+ renameSync(stagedDir, versionDir(configPath, version));
439
503
  writeMarker(configPath, { from: own, to: version, starts: 0 });
440
504
  switchCurrent(configPath, version);
441
505
  } catch (err) {
442
506
  try {
443
- rmSync(verified.dir, { recursive: true, force: true });
507
+ rmSync(stagedDir, { recursive: true, force: true });
444
508
  if (currentVersion(configPath) !== version) removeMarker(configPath);
445
509
  } catch {
446
510
  }
@@ -454,6 +518,10 @@ async function performUpdate(args) {
454
518
  args.log.line(`updating ${own} -> ${version} \xB7 restarting`);
455
519
  return { kind: "switched" };
456
520
  }
521
+ async function performUpdate(args) {
522
+ const prepared = await prepareUpdate(args);
523
+ return prepared.kind === "prepared" ? installPrepared(args, prepared.dir) : prepared;
524
+ }
457
525
  function rollBack(configPath, marker, reason, detail, now, log) {
458
526
  try {
459
527
  writeLastUpdate(configPath, { from: marker.from, to: marker.to, outcome: "rolled_back", reason, at: isoAt(now) });
@@ -1165,6 +1233,10 @@ function selfUpdateBlocker(configPath, entryPath, own) {
1165
1233
  return void 0;
1166
1234
  }
1167
1235
  function considerNamedVersion(state, named) {
1236
+ if (state.deps.image !== void 0) {
1237
+ considerNamedImage(state, named);
1238
+ return;
1239
+ }
1168
1240
  const update = state.deps.update;
1169
1241
  if (update === void 0 || named === void 0) return;
1170
1242
  const configPath = state.config.configPath;
@@ -1187,6 +1259,34 @@ function considerNamedVersion(state, named) {
1187
1259
  logOf(state).line(`this runner cannot update itself to ${named}: ${blocker}`);
1188
1260
  }
1189
1261
  }
1262
+ function considerNamedImage(state, named) {
1263
+ if (named !== void 0 && !isVersion(named)) named = void 0;
1264
+ if (named !== void 0) state.imageNamed = named;
1265
+ recordImageStatus(state);
1266
+ const own = ownVersionOf(state.deps);
1267
+ if (named === void 0 || state.imageLogged === named || (compareVersions(own, named) ?? 0) >= 0) return;
1268
+ state.imageLogged = named;
1269
+ logOf(state).line(`the factory names ${named}, newer than the ${own} this image runs \xB7 ${pullInstruction(imageTag(named))}`);
1270
+ }
1271
+ function recordImageStatus(state) {
1272
+ if (state.deps.image === void 0) return;
1273
+ const named = state.imageNamed;
1274
+ const record = {
1275
+ own: ownVersionOf(state.deps),
1276
+ ...named === void 0 ? {} : { named, tag: imageTag(named) },
1277
+ upgradeRequired: state.upgradeRefused === true,
1278
+ at: nowOf(state).toISOString()
1279
+ };
1280
+ const key = JSON.stringify({ ...record, at: void 0 });
1281
+ if (state.imageRecord === key) return;
1282
+ try {
1283
+ writeUpdateStatus(runnerDir(state.config.configPath), record);
1284
+ state.imageRecord = key;
1285
+ reportRepeating(state, "the update-status.json record", void 0);
1286
+ } catch (err) {
1287
+ reportRepeating(state, "the update-status.json record", `could not write update-status.json (${describeCallFailure(err)}): \`runner status\` will not name the image to pull`);
1288
+ }
1289
+ }
1190
1290
  function adoptUpdateWait(state, version, nowMs) {
1191
1291
  const previous = state.updateWait;
1192
1292
  const wait = { version, sinceMs: previous?.sinceMs ?? nowMs, holdSinceMs: previous?.holdSinceMs };
@@ -1209,12 +1309,20 @@ function updateArgsOf(state, update, version) {
1209
1309
  function dropPendingUpdate(state) {
1210
1310
  state.pendingUpdate = void 0;
1211
1311
  state.preflightedUpdate = void 0;
1312
+ discardPrepared(state);
1212
1313
  clearUpdateWait(state);
1213
1314
  }
1315
+ function discardPrepared(state) {
1316
+ if (state.preparedUpdate === void 0) return;
1317
+ removeStaging(state.preparedUpdate.dir);
1318
+ state.preparedUpdate = void 0;
1319
+ }
1214
1320
  function clearUpdateWait(state) {
1215
1321
  state.updateWait = void 0;
1216
1322
  state.updateLogged = void 0;
1217
1323
  state.upgradeRefused = false;
1324
+ if (state.installWait !== void 0) return;
1325
+ state.waitsLogged = void 0;
1218
1326
  recordPendingUpdate(state, void 0);
1219
1327
  }
1220
1328
  function recordPendingUpdate(state, record) {
@@ -1238,7 +1346,7 @@ function runningItems(state) {
1238
1346
  const numbers = [...state.jobs.values()].map((entry) => Number(entry.job.item)).filter((n) => Number.isFinite(n));
1239
1347
  return numbers.sort((a, b) => a - b);
1240
1348
  }
1241
- function holdForUpdate(state, version, mayHold = true) {
1349
+ function holdForUpdate(state, version, mayHold = true, waits = []) {
1242
1350
  const log = logOf(state);
1243
1351
  const nowMs = nowOf(state).getTime();
1244
1352
  const wait = adoptUpdateWait(state, version, nowMs);
@@ -1253,6 +1361,11 @@ function holdForUpdate(state, version, mayHold = true) {
1253
1361
  const maxWaitMs = (state.config.update?.maxWaitHours ?? DEFAULT_UPDATE_MAX_WAIT_HOURS) * 60 * 6e4;
1254
1362
  const holding = mayHold && (state.upgradeRefused === true || nowMs - wait.sinceMs >= maxWaitMs);
1255
1363
  const record = { version, since: new Date(wait.sinceMs).toISOString() };
1364
+ if (waits.length > 0) {
1365
+ record.via = "self-update";
1366
+ record.waits = recordedWaits(waits);
1367
+ }
1368
+ noteWaits(state, version, waits);
1256
1369
  if (holding) {
1257
1370
  wait.holdSinceMs ??= nowMs;
1258
1371
  const builds = runningItems(state);
@@ -1269,6 +1382,16 @@ function holdForUpdate(state, version, mayHold = true) {
1269
1382
  recordPendingUpdate(state, record);
1270
1383
  return holding;
1271
1384
  }
1385
+ function recordedWaits(waits) {
1386
+ return waits.flatMap((wait) => Number.isFinite(Number(wait.item)) ? [{ item: Number(wait.item), reason: wait.reason }] : []);
1387
+ }
1388
+ function noteWaits(state, version, waits) {
1389
+ const phrase = waits.map((wait) => `#${wait.item} ${wait.reason}`).join("; ");
1390
+ const key = waits.length === 0 ? void 0 : `${version}:${phrase}`;
1391
+ if (state.waitsLogged === key) return;
1392
+ state.waitsLogged = key;
1393
+ if (key !== void 0) logOf(state).line(`update to ${version} waits: ${phrase}`);
1394
+ }
1272
1395
  function buildsPhrase(builds) {
1273
1396
  return builds.length === 0 ? "the running builds" : builds.map((n) => `#${n}`).join(", ");
1274
1397
  }
@@ -1292,7 +1415,7 @@ function installedSwitchOf(state, update) {
1292
1415
  installed: current !== void 0 && existsSync(entryIn(versionDir(configPath, current)))
1293
1416
  });
1294
1417
  }
1295
- function restartOntoInstalled(state) {
1418
+ async function restartOntoInstalled(state) {
1296
1419
  const update = state.deps.update;
1297
1420
  if (update === void 0) return false;
1298
1421
  const configPath = state.config.configPath;
@@ -1300,6 +1423,8 @@ function restartOntoInstalled(state) {
1300
1423
  const decision = installedSwitchOf(state, update);
1301
1424
  if (decision.kind === "none") {
1302
1425
  state.installedSwitch = void 0;
1426
+ state.installedFormats = void 0;
1427
+ state.installWait = void 0;
1303
1428
  return false;
1304
1429
  }
1305
1430
  const said = `${decision.kind}:${decision.to}`;
@@ -1315,8 +1440,14 @@ function restartOntoInstalled(state) {
1315
1440
  return false;
1316
1441
  }
1317
1442
  if (state.jobs.size > 0) {
1318
- if (first) log.line(`${decision.to} is installed \xB7 taking no new work, restarting onto it once ${state.jobs.size} running build(s) finish`);
1319
- return true;
1443
+ const waits = await installBlockers(state, update, decision.to);
1444
+ if (waits.length > 0) {
1445
+ if (first) log.line(`${decision.to} is installed \xB7 taking no new work, restarting onto it once ${state.jobs.size} running build(s) finish`);
1446
+ const wait = state.installWait = state.installWait?.version === decision.to ? state.installWait : { version: decision.to, sinceMs: nowOf(state).getTime() };
1447
+ noteWaits(state, decision.to, waits);
1448
+ recordPendingUpdate(state, { version: decision.to, since: new Date(wait.sinceMs).toISOString(), via: "install", waits: recordedWaits(waits) });
1449
+ return true;
1450
+ }
1320
1451
  }
1321
1452
  try {
1322
1453
  writeMarker(configPath, { from: own, to: decision.to, starts: 0 });
@@ -1327,10 +1458,66 @@ function restartOntoInstalled(state) {
1327
1458
  (state.exitForUpdate ?? (() => update.exit(RESTART_EXIT_STATUS)))();
1328
1459
  return true;
1329
1460
  }
1461
+ function runningBuilds(state) {
1462
+ return [...state.jobs.values()].map((active) => ({ job: active.job, ending: active.ending }));
1463
+ }
1464
+ async function installBlockers(state, update, to) {
1465
+ const stage = buildBlockers(runningBuilds(state));
1466
+ if (stage.length > 0) return stage;
1467
+ if (state.installedFormats?.version !== to) {
1468
+ const dir = versionDir(state.config.configPath, to);
1469
+ state.installedFormats = { version: to, reads: await askJobStateFormats(update.runNode, entryIn(dir), dir) };
1470
+ }
1471
+ return restartBlockers(runningBuilds(state), state.installedFormats.reads);
1472
+ }
1473
+ function formatBlocked(builds, reads) {
1474
+ const byStage = new Set(buildBlockers(builds).map((blocker) => blocker.item));
1475
+ return restartBlockers(builds, reads).some((blocker) => !byStage.has(blocker.item));
1476
+ }
1477
+ async function restartNow(state, update, version) {
1478
+ const early = buildBlockers(runningBuilds(state));
1479
+ if (early.length > 0) return { kind: "waiting", waits: early };
1480
+ if (state.preparedUpdate !== void 0 && state.preparedUpdate.version !== version) discardPrepared(state);
1481
+ const args = updateArgsOf(state, update, version);
1482
+ let reads;
1483
+ const declined = state.declinedFormats?.has(version) === true;
1484
+ if (state.preparedUpdate !== void 0) {
1485
+ reads = state.preparedUpdate.reads;
1486
+ } else if (declined && restartBlockers(runningBuilds(state), state.declinedFormats.get(version)).length > 0) {
1487
+ reads = state.declinedFormats.get(version);
1488
+ } else {
1489
+ const prepared = await prepareUpdate(args);
1490
+ if (prepared.kind !== "prepared") {
1491
+ dropPendingUpdate(state);
1492
+ reportUpdateResult(state, version, prepared);
1493
+ return { kind: "failed" };
1494
+ }
1495
+ reads = await askJobStateFormats(update.runNode, entryIn(prepared.dir), prepared.dir);
1496
+ if (formatBlocked(runningBuilds(state), reads)) {
1497
+ removeStaging(prepared.dir);
1498
+ (state.declinedFormats ??= /* @__PURE__ */ new Map()).set(version, reads);
1499
+ } else {
1500
+ state.preparedUpdate = { version, dir: prepared.dir, reads };
1501
+ }
1502
+ }
1503
+ const waits = restartBlockers(runningBuilds(state), reads);
1504
+ if (waits.length > 0) return { kind: "waiting", waits };
1505
+ const staged = state.preparedUpdate;
1506
+ state.preparedUpdate = void 0;
1507
+ dropPendingUpdate(state);
1508
+ const result = installPrepared(args, staged.dir);
1509
+ if (result.kind === "switched") {
1510
+ (state.exitForUpdate ?? (() => update.exit(RESTART_EXIT_STATUS)))();
1511
+ return { kind: "restarted" };
1512
+ }
1513
+ reportUpdateResult(state, version, result);
1514
+ return { kind: "failed" };
1515
+ }
1330
1516
  async function updateWhenIdle(state) {
1331
1517
  const update = state.deps.update;
1332
1518
  const version = state.pendingUpdate;
1333
1519
  if (update === void 0 || version === void 0) {
1520
+ discardPrepared(state);
1334
1521
  recordPendingUpdate(state, void 0);
1335
1522
  return false;
1336
1523
  }
@@ -1356,6 +1543,12 @@ async function updateWhenIdle(state) {
1356
1543
  }
1357
1544
  state.preflightedUpdate = version;
1358
1545
  }
1546
+ if (update.underService === true) {
1547
+ const outcome = await restartNow(state, update, version);
1548
+ if (outcome.kind === "restarted") return true;
1549
+ if (outcome.kind === "failed") return false;
1550
+ return holdForUpdate(state, version, true, outcome.waits);
1551
+ }
1359
1552
  return holdForUpdate(state, version);
1360
1553
  }
1361
1554
  dropPendingUpdate(state);
@@ -1488,8 +1681,9 @@ async function housekeepingPass(state) {
1488
1681
  async function runTick(state) {
1489
1682
  state.restarting = false;
1490
1683
  state.holdingForUpdate = false;
1684
+ if (state.draining === true) return;
1491
1685
  reloadConfig(state);
1492
- if (restartOntoInstalled(state)) {
1686
+ if (await restartOntoInstalled(state)) {
1493
1687
  state.restarting = true;
1494
1688
  return;
1495
1689
  }
@@ -1552,7 +1746,7 @@ var CLAIM_REFUSAL_CODES = new Set(
1552
1746
  "pull_request_from_fork pull_request_on_default_branch pull_request_ambiguous pull_request_repository_not_connected repository_not_reachable branch_claimed_elsewhere".split(" ")
1553
1747
  );
1554
1748
  async function tryClaimAndStart(state, item, queue) {
1555
- const { postJson: postJson2 } = await import('./work-cmd-FMPDXPU6.js');
1749
+ const { postJson: postJson2 } = await import('./work-cmd-LRAXUOJV.js');
1556
1750
  const log = logOf(state);
1557
1751
  const hostKey = await loadHostKey(state.config.configPath);
1558
1752
  const claim = await retrySend(
@@ -1589,14 +1783,21 @@ async function tryClaimAndStart(state, item, queue) {
1589
1783
  return { kind: "signature" };
1590
1784
  }
1591
1785
  if (claim.code === "runner_upgrade_required") {
1592
- reportRepeating(state, "the runner protocol", claim.message ?? claim.reason);
1786
+ const tag = tagToPull(state.imageNamed);
1787
+ reportRepeating(
1788
+ state,
1789
+ "the runner protocol",
1790
+ state.deps.image === void 0 ? claim.message ?? claim.reason : `${claim.message ?? claim.reason} \xB7 ${pullInstruction(tag)}`
1791
+ );
1593
1792
  state.upgradeRefused = true;
1793
+ recordImageStatus(state);
1594
1794
  return { kind: "outdated" };
1595
1795
  }
1596
1796
  return { kind: "error", reason: `${claim.reason} (${claim.attempts} attempt(s))` };
1597
1797
  }
1598
1798
  reportRepeating(state, "the runner protocol", void 0);
1599
1799
  state.upgradeRefused = false;
1800
+ recordImageStatus(state);
1600
1801
  reportRepeating(state, "the request signature", void 0);
1601
1802
  const { data } = claim.value;
1602
1803
  const claimId = data.id === void 0 || data.id === null ? "" : String(data.id);
@@ -1605,7 +1806,7 @@ async function tryClaimAndStart(state, item, queue) {
1605
1806
  }
1606
1807
  if (wasExtended(data)) {
1607
1808
  log.line(
1608
- `#${item.number} skipped: the claim extended a live lease under host "${state.config.host}" \xB7 set a distinct runner.host per config file`
1809
+ `#${item.number} skipped: the claim extended a live lease under the runner name "${state.config.host}" \xB7 set a distinct runner.name per config file`
1609
1810
  );
1610
1811
  return { kind: "held" };
1611
1812
  }
@@ -1695,6 +1896,7 @@ async function startJob(state, item, queue, claimId, itemKey, branch, identity,
1695
1896
  const runnerRoot = runnerDir(state.config.configPath);
1696
1897
  const jobFilePath = join(dirPath, "job.json");
1697
1898
  let job = {
1899
+ stateFormat: state.deps.jobStateFormat ?? JOB_STATE_FORMAT,
1698
1900
  item: item.number,
1699
1901
  queue,
1700
1902
  claimId,
@@ -2514,6 +2716,13 @@ async function finishTrackedWhileAway(state, dirPath, job, where) {
2514
2716
  `recovered job ${finished.queue} #${finished.item} (claim ${shortClaim(finished.claimId)}) as ${outcome}: ${code === void 0 ? `its container ${where === "missing" ? "is gone" : "stopped"} and its volume ${read.readable ? "holds no exit code" : "could not be read"}` : `its command recorded exit ${code}`}`
2515
2717
  );
2516
2718
  }
2719
+ async function logUnusableHelper(config, runtime, log) {
2720
+ if (!isBuiltinVm(config.hooks.provision)) return;
2721
+ const helper = await runtime.vm.helper.probe();
2722
+ if (helper.ok) return;
2723
+ if (helper.reason === "unsupported") log.line(helper.detail);
2724
+ else if (helper.unreachable === true) log.line(`${helper.detail}, so microVM builds run without the jailer`);
2725
+ }
2517
2726
  async function start(configPath, deps = { fetchImpl: fetch }) {
2518
2727
  const log = deps.log ??= createRunnerLog(deps.now ?? (() => /* @__PURE__ */ new Date()));
2519
2728
  const update = deps.update;
@@ -2521,7 +2730,7 @@ async function start(configPath, deps = { fetchImpl: fetch }) {
2521
2730
  const outcome = await onStartUnderMarker({
2522
2731
  configPath,
2523
2732
  own: ownVersionOf(deps),
2524
- check: update.check ? () => update.check(configPath) : async () => (await import('./check-3CGLWMHM.js')).runRunnerCheck(configPath, { fetchImpl: deps.fetchImpl }),
2733
+ check: update.check ? () => update.check(configPath) : async () => (await import('./check-EPYRTE3R.js')).runRunnerCheck(configPath, { fetchImpl: deps.fetchImpl }),
2525
2734
  now: () => (deps.now ?? (() => /* @__PURE__ */ new Date()))().getTime(),
2526
2735
  log
2527
2736
  });
@@ -2560,12 +2769,16 @@ async function start(configPath, deps = { fetchImpl: fetch }) {
2560
2769
  const state = { config, jobs: /* @__PURE__ */ new Map(), deps, configMtimeMs };
2561
2770
  log.line(`runner ${config.host} started \xB7 config ${configPath} \xB7 ${describeConfig(config)}`);
2562
2771
  const nowMs = () => deps.now ? deps.now().getTime() : Date.now();
2563
- const runningMicroVms = await stopOrphanedMicroVms({ jobs: unfinishedJobs(jobsDir(configPath)), runtime: runtimeOf(state) }).catch((err) => {
2772
+ await logUnusableHelper(config, runtimeOf(state), log);
2773
+ const runningMicroVms = await stopOrphanedMicroVms({ jobs: unfinishedJobs(jobsDir(configPath)), runtime: runtimeOf(state), onError: (line) => log.error(line) }).catch((err) => {
2564
2774
  log.error(`a microVM an earlier runner left could not be stopped: ${describeCallFailure(err)}`);
2565
2775
  return [];
2566
2776
  });
2567
2777
  for (const buildId of runningMicroVms) log.line(`stopped microVM build ${buildId}, left running by an earlier runner`);
2568
2778
  const attemptLogs = attemptLogsOf(state);
2779
+ for (const { job, format } of listUnreadableJobs(jobsDir(configPath))) {
2780
+ log.error(`leaving job ${job.queue} #${job.item} alone: its job-state format ${format} is not one this version reads`);
2781
+ }
2569
2782
  for (const { dirPath, job } of listAdoptableJobs(jobsDir(configPath))) {
2570
2783
  const adopted = { job, dirPath, lastRenewAtMs: nowMs(), cmd: null, cpuSample: null, ending: false };
2571
2784
  state.jobs.set(dirPath, adopted);
@@ -2625,8 +2838,9 @@ async function start(configPath, deps = { fetchImpl: fetch }) {
2625
2838
  const probeTimer = setInterval(() => {
2626
2839
  for (const active of state.jobs.values()) void probeBuild(state, active);
2627
2840
  }, deps.probeIntervalMs ?? PROBE_INTERVAL_MS);
2628
- const onSigterm = () => void drain();
2841
+ const onSigterm = () => deps.image === void 0 ? void drain() : leave();
2629
2842
  const onSigusr1 = () => void stopNow();
2843
+ const onSigusr2 = () => drainKeepingUp();
2630
2844
  const onSigint = () => {
2631
2845
  interrupts++;
2632
2846
  if (interrupts === 1) {
@@ -2639,6 +2853,7 @@ async function start(configPath, deps = { fetchImpl: fetch }) {
2639
2853
  const detachSignals = () => {
2640
2854
  process.off("SIGTERM", onSigterm);
2641
2855
  process.off("SIGUSR1", onSigusr1);
2856
+ process.off("SIGUSR2", onSigusr2);
2642
2857
  process.off("SIGINT", onSigint);
2643
2858
  };
2644
2859
  const stopIntake = () => {
@@ -2685,6 +2900,19 @@ async function start(configPath, deps = { fetchImpl: fetch }) {
2685
2900
  }
2686
2901
  finish2();
2687
2902
  };
2903
+ const drainKeepingUp = () => {
2904
+ if (state.draining === true) return;
2905
+ state.draining = true;
2906
+ stopIntake();
2907
+ log.line(`stopping: taking no more work, ${state.jobs.size} build(s) still managed \xB7 docker stop ends this container`);
2908
+ };
2909
+ const leave = () => {
2910
+ if (exited) return;
2911
+ log.line(`stopping: leaving ${state.jobs.size} running build(s) for the next runner to adopt`);
2912
+ state.draining = true;
2913
+ finish2();
2914
+ deps.image?.exit?.(0);
2915
+ };
2688
2916
  const stopNow = async () => {
2689
2917
  if (stopping) return;
2690
2918
  stopping = true;
@@ -2705,6 +2933,7 @@ async function start(configPath, deps = { fetchImpl: fetch }) {
2705
2933
  const handle = { state, drain, stopNow, close };
2706
2934
  process.on("SIGTERM", onSigterm);
2707
2935
  process.on("SIGUSR1", onSigusr1);
2936
+ if (deps.image !== void 0) process.on("SIGUSR2", onSigusr2);
2708
2937
  process.on("SIGINT", onSigint);
2709
2938
  deps.onReady?.();
2710
2939
  await tick(state);
@@ -2717,7 +2946,8 @@ function describeWaiting(active, nowMs) {
2717
2946
  }
2718
2947
  var RunnerStartError = class extends Error {
2719
2948
  };
2720
- async function requestStop(configPath, stopNow = false, pollMs = 5e3) {
2949
+ async function requestStop(configPath, stopNow = false, pollMs = 5e3, options = {}) {
2950
+ const kill = options.kill ?? ((pid, signal2) => process.kill(pid, signal2));
2721
2951
  const entry = readPidFile(pidFilePath(configPath));
2722
2952
  if (!entry) {
2723
2953
  process.stdout.write("no runner is running on this config\n");
@@ -2729,9 +2959,9 @@ async function requestStop(configPath, stopNow = false, pollMs = 5e3) {
2729
2959
  `);
2730
2960
  return;
2731
2961
  }
2732
- const signal = stopNow ? "SIGUSR1" : "SIGTERM";
2962
+ const signal = stopNow ? "SIGUSR1" : options.image === true ? "SIGUSR2" : "SIGTERM";
2733
2963
  try {
2734
- process.kill(entry.pid, signal);
2964
+ kill(entry.pid, signal);
2735
2965
  } catch (err) {
2736
2966
  const code = err?.code;
2737
2967
  if (code === "EPERM") {
@@ -2744,6 +2974,14 @@ async function requestStop(configPath, stopNow = false, pollMs = 5e3) {
2744
2974
  `);
2745
2975
  return;
2746
2976
  }
2977
+ if (signal === "SIGUSR2") {
2978
+ process.stdout.write(
2979
+ `sent SIGUSR2 to runner pid ${entry.pid}: it takes no more work and keeps managing its builds.
2980
+ The container stays up. Run \`outerlayer runner status\` to see what is left, then \`docker stop\` the container once nothing is.
2981
+ `
2982
+ );
2983
+ return;
2984
+ }
2747
2985
  process.stdout.write(
2748
2986
  stopNow ? `sent SIGUSR1 to runner pid ${entry.pid}, stopping now
2749
2987
  ` : `sent SIGTERM to runner pid ${entry.pid}, draining
@@ -2777,4 +3015,4 @@ function runnerProcessState(configPath) {
2777
3015
  return isSameProcess(entry.pid, entry.start) ? { state: "running", pid: entry.pid } : { state: "stale", pid: entry.pid };
2778
3016
  }
2779
3017
 
2780
- export { RunnerStartError, listJobFiles, probeBuild, probeCredentials, requestStop, runnerProcessState, start, tick, watchTick };
3018
+ export { RunnerStartError, listJobFiles, logUnusableHelper, probeBuild, probeCredentials, requestStop, runnerProcessState, start, tick, watchTick };
@@ -5,7 +5,7 @@ import { readCloudConfig, cloudConfigPath } from './chunk-EABW6AJQ.js';
5
5
  import { cliInvocation } from './chunk-CRB3POP4.js';
6
6
  import { homeFromEnv } from './chunk-VNVZDWO3.js';
7
7
  import { resolvePackageVersion } from './chunk-RA3O54FC.js';
8
- import { signRunnerRequest, signerFor } from './chunk-OW52QLK3.js';
8
+ import { signRunnerRequest, signerFor } from './chunk-ZODVSFI4.js';
9
9
 
10
10
  createRequire(import.meta.url);
11
11
 
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
- import { RUNNER_DIR_MODE, RUNNER_FILE_MODE } from './chunk-IHNQWF6F.js';
3
+ import { RUNNER_DIR_MODE, RUNNER_FILE_MODE } from './chunk-ZI2ZVWKT.js';
4
4
  import { readFileSync, mkdirSync, writeFileSync, renameSync } from 'fs';
5
5
  import { join } from 'path';
6
6
 
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
- import { runnerDir, RUNNER_DIR_MODE, RUNNER_FILE_MODE } from './chunk-IHNQWF6F.js';
3
+ import { runnerDir, RUNNER_DIR_MODE, RUNNER_FILE_MODE } from './chunk-ZI2ZVWKT.js';
4
4
  import { readlinkSync, realpathSync, mkdirSync, unlinkSync, symlinkSync, renameSync, readFileSync, writeFileSync, rmSync, existsSync, readdirSync } from 'fs';
5
5
  import { join, basename, sep, resolve } from 'path';
6
6
 
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
- import { RecipeError, BUILDKIT_IMAGE } from './chunk-3W333NFO.js';
3
+ import { RecipeError, BUILDKIT_IMAGE } from './chunk-RGW7ERF3.js';
4
4
  import { parseSize } from './chunk-7RQ73DLO.js';
5
- import { compareVersions } from './chunk-CBCKD6DO.js';
6
- import { ensureHostKey } from './chunk-OW52QLK3.js';
7
- import { RUNNER_DIR_MODE, RUNNER_FILE_MODE, runnerDir, jobsDir } from './chunk-IHNQWF6F.js';
8
- import { CLAUDE_PORT, JOB_MOUNT, GATEWAY_PORT, GATEWAY_SOCKET, CLAUDE_SOCKET, RELAY_PORT, DESTINATION_BASE_PORT } from './chunk-URWYRAIC.js';
9
- import { JOIN_DIR, RELAY_ID, JOIN_SOCKET, TARGET_GATEWAY, TARGET_CLAUDE, destinationTarget, RELAY_USER } from './chunk-GISNHSYL.js';
5
+ import { compareVersions } from './chunk-D4AMAEZ7.js';
6
+ import { ensureHostKey } from './chunk-ZODVSFI4.js';
7
+ import { RUNNER_DIR_MODE, RUNNER_FILE_MODE, runnerDir, jobsDir } from './chunk-ZI2ZVWKT.js';
8
+ import { CLAUDE_PORT, JOB_MOUNT, GATEWAY_PORT, GATEWAY_SOCKET, CLAUDE_SOCKET, RELAY_PORT, DESTINATION_BASE_PORT } from './chunk-2K3TCQHW.js';
9
+ import { JOIN_DIR, RELAY_ID, JOIN_SOCKET, TARGET_GATEWAY, TARGET_CLAUDE, destinationTarget, RELAY_USER } from './chunk-JGD5TWQ2.js';
10
10
  import { StringDecoder } from 'string_decoder';
11
11
  import { spawn } from 'child_process';
12
12
  import { existsSync, readFileSync, mkdirSync, createWriteStream, rmSync, chmodSync, writeFileSync, realpathSync, readdirSync, renameSync } from 'fs';
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
- import { compareVersions } from './chunk-I2UOAV7X.js';
4
- import { launcherPath, writeLauncher, LauncherWriteError, pathNotes } from './chunk-EFB3FGLC.js';
5
- import { currentVersion, currentLink, isInLayout, packageRootIn, versionDir, switchCurrent, execStartLine } from './chunk-276D7BJ7.js';
6
- import { RUNNER_DIR_MODE } from './chunk-IHNQWF6F.js';
3
+ import { launcherPath, writeLauncher, LauncherWriteError, pathNotes } from './chunk-ATGHVEU4.js';
4
+ import { currentVersion, currentLink, isInLayout, packageRootIn, versionDir, switchCurrent, execStartLine } from './chunk-QMQC6JCO.js';
5
+ import { compareVersions } from './chunk-TQMI335S.js';
6
+ import { RUNNER_DIR_MODE } from './chunk-ZI2ZVWKT.js';
7
7
  import { realpathSync, readFileSync, mkdirSync, rmSync, cpSync, existsSync } from 'fs';
8
8
  import { dirname, sep, join } from 'path';
9
9
 
@@ -92,7 +92,7 @@ function launcherSummary(launcher) {
92
92
  }
93
93
  function restartNote(result) {
94
94
  if (result.previous === void 0 || result.previous === result.version) return void 0;
95
- return `current moved from ${result.previous} to ${result.version}: a runner its service started takes no new work, waits for its running builds to finish, then restarts on ${result.version}. A runner started by hand keeps running until you restart it`;
95
+ return `current moved from ${result.previous} to ${result.version}: a runner its service started restarts on ${result.version} at once when its running builds are container builds that carry on through the restart. Otherwise it takes no new work and waits for them to finish. A runner started by hand keeps running until you restart it`;
96
96
  }
97
97
 
98
98
  export { RunnerDowngradeError, RunnerInstallError, launcherSummary, restartNote, runRunnerInstall };