@outerlayer/cli 0.19.1 → 0.22.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 (146) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +26 -7
  3. package/dist/{agent-setup-AM42SA2D.js → agent-setup-XAJH6MPW.js} +13 -12
  4. package/dist/build-GTMEADQD.js +12 -0
  5. package/dist/build-command-ND2ENDBS.js +50 -0
  6. package/dist/build-info.json +1 -1
  7. package/dist/{builder-C4QNTYMS.js → builder-OAUSTZHL.js} +7 -5
  8. package/dist/{check-W2JGXN3B.js → check-3YHMJQ3P.js} +48 -50
  9. package/dist/{chunk-F2KXHIFI.js → chunk-22DYXLU7.js} +360 -66
  10. package/dist/{chunk-P53VEAOF.js → chunk-2CMJLYV6.js} +1 -1
  11. package/dist/{chunk-FBTGMSQF.js → chunk-2HT2XGJX.js} +16 -9
  12. package/dist/{chunk-S5PCA3SN.js → chunk-2J5JGLW5.js} +1 -1
  13. package/dist/{chunk-R22NZSGA.js → chunk-34XORKFD.js} +9 -9
  14. package/dist/{chunk-QOZKPLVJ.js → chunk-3W333NFO.js} +40 -1
  15. package/dist/{chunk-HOKXGFDR.js → chunk-4JHVPFH6.js} +22 -16
  16. package/dist/{chunk-AMFWYESY.js → chunk-4NM3I3VV.js} +1 -1
  17. package/dist/{chunk-PIZRPNKR.js → chunk-4QKLMNI3.js} +5 -5
  18. package/dist/{chunk-NNLA7LLQ.js → chunk-5OCIHFGE.js} +29 -5
  19. package/dist/{chunk-PSN33ERW.js → chunk-6IO62VJS.js} +3 -3
  20. package/dist/{chunk-7YP7T6GV.js → chunk-7V7KG3YM.js} +2 -2
  21. package/dist/{chunk-QNLX477Q.js → chunk-AEKSX4EY.js} +165 -72
  22. package/dist/{chunk-JHGEBPLE.js → chunk-ATRIVTRE.js} +3 -3
  23. package/dist/{chunk-X5H2WYMB.js → chunk-BLW6GPBZ.js} +1 -1
  24. package/dist/{chunk-MLAAT4S6.js → chunk-C3HKR67W.js} +3 -3
  25. package/dist/{chunk-XZ4H4X5F.js → chunk-C3OVGKVJ.js} +15 -8
  26. package/dist/{chunk-BM63HB2M.js → chunk-EAQSOE62.js} +2 -2
  27. package/dist/{chunk-LW7G5TJB.js → chunk-EXV37PKS.js} +2 -2
  28. package/dist/{chunk-B6SCN5NY.js → chunk-FGW2774C.js} +175 -44
  29. package/dist/{chunk-KYS3TQAG.js → chunk-G2WME4RG.js} +4 -4
  30. package/dist/{chunk-LRG3ZMZD.js → chunk-G6DZCKA6.js} +4 -4
  31. package/dist/chunk-GISNHSYL.js +41 -0
  32. package/dist/{chunk-G2YNJIGF.js → chunk-HCVLP7XA.js} +10 -1
  33. package/dist/{chunk-QEFKA2QD.js → chunk-I66MQDXH.js} +2 -2
  34. package/dist/{chunk-3CWUOFV6.js → chunk-IRBNIRQJ.js} +12 -7
  35. package/dist/{chunk-CG36IONK.js → chunk-JRCSET55.js} +2 -2
  36. package/dist/{chunk-CGGPS7SI.js → chunk-JVB63NIB.js} +5 -5
  37. package/dist/{chunk-M2ZWWTWZ.js → chunk-KDHYLBRR.js} +1 -1
  38. package/dist/{chunk-N2PXGQRL.js → chunk-KIGSGZBB.js} +1 -1
  39. package/dist/{chunk-EAEELQVQ.js → chunk-KOQELWQ6.js} +5 -5
  40. package/dist/{chunk-MTP2QEVO.js → chunk-KPBQYEND.js} +1 -1
  41. package/dist/{chunk-VYAL7PUE.js → chunk-LOB2SUM3.js} +1 -1
  42. package/dist/{chunk-GGOMT6KF.js → chunk-PHHBYAL6.js} +1 -1
  43. package/dist/{chunk-IF2OSI5T.js → chunk-QOALB6ZD.js} +2 -2
  44. package/dist/{chunk-BWSUPRDK.js → chunk-TPKYTORP.js} +1 -1
  45. package/dist/{chunk-XNGMQ27R.js → chunk-UD3EXMKB.js} +2 -2
  46. package/dist/{chunk-7UWVLZXD.js → chunk-USWVXQIH.js} +1 -1
  47. package/dist/{chunk-BQXFBI7T.js → chunk-V7MQKOZY.js} +32 -7
  48. package/dist/{chunk-6X4SQ5MY.js → chunk-V7SAQ645.js} +1 -1
  49. package/dist/{chunk-TWF3RICI.js → chunk-VSUXCMMH.js} +110 -11
  50. package/dist/{chunk-FVMYKL6L.js → chunk-VWLOQ6FI.js} +1 -1
  51. package/dist/{chunk-3ZR7Y23Y.js → chunk-VX74ND77.js} +650 -70
  52. package/dist/{chunk-Z7K3L4FC.js → chunk-W3SWJ2TD.js} +2 -2
  53. package/dist/chunk-WERZMIKA.js +718 -0
  54. package/dist/{chunk-GEMQLBMW.js → chunk-WMHYIBJW.js} +6 -6
  55. package/dist/{chunk-5VML45HH.js → chunk-XCRZTXRL.js} +2 -2
  56. package/dist/{chunk-ECODQ7II.js → chunk-ZFLOGMUC.js} +1 -1
  57. package/dist/chunk-ZNPE7FW5.js +137 -0
  58. package/dist/claude-launcher-4BQNXYAF.js +267 -0
  59. package/dist/{cli-EXX2R73G.js → cli-NMGVZQPB.js} +189 -119
  60. package/dist/{command-ZQNEPDS6.js → command-GE62AJZY.js} +32 -34
  61. package/dist/{config-CLSGABD5.js → config-7Q52COZ3.js} +3 -2
  62. package/dist/{connect-cmd-UNYKGDAL.js → connect-cmd-ZFOK6HEL.js} +5 -5
  63. package/dist/{context-cleanup-CHEWNYU6.js → context-cleanup-DSA3HGQU.js} +1 -1
  64. package/dist/{control-plane-launch-34LNRJWR.js → control-plane-launch-SZTE4TUS.js} +62 -28
  65. package/dist/{dist-WO7NOJNG.js → dist-7IH56ELA.js} +1 -1
  66. package/dist/{docker-BFU3AQGY.js → docker-2OVBKVRK.js} +7 -7
  67. package/dist/{doctor-DHUM5DBB.js → doctor-3SQGZVYP.js} +35 -36
  68. package/dist/{emit-artifact-cmd-XBHM647X.js → emit-artifact-cmd-R42BE6ID.js} +19 -18
  69. package/dist/{emit-cmd-W55AEBCA.js → emit-cmd-3W5IO5ET.js} +3 -3
  70. package/dist/{emit-criteria-cmd-CRTYEF7D.js → emit-criteria-cmd-MQZE7W4Y.js} +52 -16
  71. package/dist/{emit-finding-cmd-7JHVRYOP.js → emit-finding-cmd-ZS5JEGGM.js} +13 -12
  72. package/dist/{emit-result-cmd-5NZMLCJK.js → emit-result-cmd-FUE3DXBW.js} +19 -18
  73. package/dist/{emit-step-cmd-347XNGPQ.js → emit-step-cmd-T7PRA44X.js} +14 -13
  74. package/dist/{exec-client-7UMESIE2.js → exec-client-YIDTWXMZ.js} +4 -3
  75. package/dist/{governing-source-HAA4OW57.js → governing-source-TORXL4I4.js} +19 -18
  76. package/dist/{guest-init-KPBQOFE5.js → guest-init-TO6HAWIT.js} +4 -3
  77. package/dist/{hook-fast-RL5E5OXE.js → hook-fast-WFB2ZMRB.js} +2 -2
  78. package/dist/host-key-2LXQDSBO.js +9 -0
  79. package/dist/{import-capture-cmd-TK4XY7RD.js → import-capture-cmd-Q56DDZVO.js} +1 -1
  80. package/dist/index.js +5 -5
  81. package/dist/init-2CRAVIUU.js +52 -0
  82. package/dist/{init-V6FOMD2V.js → init-AHDZCMDC.js} +32 -33
  83. package/dist/{init-cmd-Z4O4PPLZ.js → init-cmd-IXDL2ZSL.js} +25 -23
  84. package/dist/{init-update-7DVAKWW3.js → init-update-YB3PODDH.js} +69 -24
  85. package/dist/{install-4SEFIIW2.js → install-CZUIW2AF.js} +5 -4
  86. package/dist/{install-cmd-QDOTR4OM.js → install-cmd-Q7RJGFUO.js} +6 -5
  87. package/dist/{login-browser-DX35MARZ.js → login-browser-BN5UKPHS.js} +3 -3
  88. package/dist/{logout-cmd-5V5MMBSY.js → logout-cmd-RZFWTNYD.js} +2 -2
  89. package/dist/{logs-AMLKC3VP.js → logs-RJPYTVQE.js} +3 -2
  90. package/dist/loop-ZDW6TRAA.js +75 -0
  91. package/dist/machine-JCOHLASP.js +47 -0
  92. package/dist/{mcp-install-cmd-GGWB3JJ2.js → mcp-install-cmd-GD4QUP7Z.js} +2 -2
  93. package/dist/{mcp-serve-cmd-N3UTY6EE.js → mcp-serve-cmd-IULQPVHL.js} +6 -5
  94. package/dist/{paths-7XP63OAX.js → paths-N3LU4O2Z.js} +3 -2
  95. package/dist/pending-GRCV3ZR4.js +9 -0
  96. package/dist/{pidfile-3DM4OBKO.js → pidfile-IF3TCL5A.js} +4 -3
  97. package/dist/receive-files-T2BIUBQL.js +254 -0
  98. package/dist/{record-3WP6V2K5.js → record-73PQE77B.js} +4 -3
  99. package/dist/relay-PSZ4LGMW.js +210 -0
  100. package/dist/relay-protocol-K7KP2SPI.js +6 -0
  101. package/dist/{runner-env-FRZFFHDF.js → runner-env-LDOBJESN.js} +6 -5
  102. package/dist/runner-identity-RWCBTY2A.js +14 -0
  103. package/dist/{runner-key-5FOWCW7J.js → runner-key-HMYLP6WN.js} +32 -33
  104. package/dist/{session-sender-U2BBUWNO.js → session-sender-2G25JYHI.js} +20 -19
  105. package/dist/{spec-cmd-FVLJOFGC.js → spec-cmd-LBVXM6XS.js} +13 -12
  106. package/dist/{starter-pack-6LQVLBIA.js → starter-pack-5EQYGSJF.js} +2 -2
  107. package/dist/{status-BINH7MBG.js → status-QZB2GWXJ.js} +36 -38
  108. package/dist/{statusline-fast-KN3BX67H.js → statusline-fast-2OK3DVAA.js} +2 -2
  109. package/dist/{sync-cmd-VXOUFS4I.js → sync-cmd-TQQIR4PN.js} +19 -18
  110. package/dist/{watch-OH645CV2.js → watch-4JES4PCS.js} +19 -18
  111. package/dist/{work-claim-cmd-ZRPSHMPT.js → work-claim-cmd-XG6O7B63.js} +17 -16
  112. package/dist/{work-cmd-5I3J2XQQ.js → work-cmd-L6WMPAA2.js} +13 -12
  113. package/dist/{work-comment-cmd-UCCPWUSR.js → work-comment-cmd-SNTDCPGH.js} +13 -12
  114. package/dist/{work-logs-cmd-7352CJPG.js → work-logs-cmd-3S7IAVII.js} +13 -12
  115. package/dist/{work-open-pr-cmd-LSH4VHJM.js → work-open-pr-cmd-E3WMO5XT.js} +15 -14
  116. package/dist/{work-pr-cmd-H4OQR26N.js → work-pr-cmd-22T6FG4T.js} +14 -13
  117. package/dist/work-stop-cmd-WR7ESS4Z.js +53 -0
  118. package/package.json +2 -2
  119. package/skill-pack/maintained/outerlayer-build/SKILL.md +347 -0
  120. package/skill-pack/{template/build → maintained/outerlayer-build}/references/evidence.md +2 -2
  121. package/skill-pack/{template/build → maintained/outerlayer-build}/references/release.md +4 -4
  122. package/skill-pack/{template/build → maintained/outerlayer-build}/references/review-loop.md +3 -1
  123. package/skill-pack/shipped.json +2 -0
  124. package/skill-pack/template/build/SKILL.md +4 -253
  125. package/skill-pack/validators/acceptance-criteria.yaml +1 -1
  126. package/dist/build-TQ6X5PYN.js +0 -12
  127. package/dist/chunk-3UZTIRLF.js +0 -320
  128. package/dist/chunk-CX22S5BA.js +0 -45
  129. package/dist/chunk-LLM3I5YU.js +0 -89
  130. package/dist/chunk-SADUQSO4.js +0 -50
  131. package/dist/chunk-VMJPFQNE.js +0 -261
  132. package/dist/claude-launcher-J5M44PFS.js +0 -115
  133. package/dist/host-key-U4B5QETR.js +0 -8
  134. package/dist/init-XG47KCI4.js +0 -53
  135. package/dist/loop-TPUYUMSY.js +0 -77
  136. package/dist/machine-TRHBT5NE.js +0 -48
  137. package/dist/pending-532ABZYC.js +0 -8
  138. package/dist/relay-L76HDX72.js +0 -46
  139. package/dist/{chunk-YVZJ6GKF.js → chunk-DIB2WLBL.js} +1 -1
  140. package/dist/{chunk-GOSFARMS.js → chunk-F35VMF5M.js} +1 -1
  141. package/dist/{chunk-DSF2FWAZ.js → chunk-IZSVXRDR.js} +1 -1
  142. /package/skill-pack/{template/build → maintained/outerlayer-build}/references/agent-briefs.md +0 -0
  143. /package/skill-pack/{template/build → maintained/outerlayer-build}/references/criteria-judge.md +0 -0
  144. /package/skill-pack/{template/build → maintained/outerlayer-build}/references/review-briefs.md +0 -0
  145. /package/skill-pack/{template/build → maintained/outerlayer-build}/scripts/criterion-citations.mjs +0 -0
  146. /package/skill-pack/{template/build → maintained/outerlayer-build}/scripts/record-criteria.mjs +0 -0
@@ -1,33 +1,32 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
- import { LEASE_SECONDS, releaseLease, isUnfinished, setStage, jobEnv, runHook, describeHookEnd, provisionMicroVm, deliverCommitHooks, provisionProcess, provisionEnvironment, readProvisionOut, resolveHostBuild, workdirProblem, listenOnBroker, hostEnvironment, hostCommandEnvironment, execArgv, microVmCommandLine, runCommand, execCommandLine, startSender, startHostServices, finishJob, isGroupAlive, outcomeFromExitCode, isTimedOut, readCpuUsec, isIdle, stopOrphanedMicroVms, unfinishedJobs, listAdoptableJobs, retryFailedReleases, recoverLostJobs, hookLimitsFor, AttemptLogBook, needsRenew, renewLease, RENEW_INTERVAL_SECONDS, ALREADY_RELEASED } from './chunk-B6SCN5NY.js';
4
- import { writePendingUpdate } from './chunk-MTP2QEVO.js';
5
- import { readHousekeepingRecord, writeHousekeepingRecord } from './chunk-S5PCA3SN.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, listAdoptableJobs, retryFailedReleases, recoverLostJobs, hookLimitsFor, AttemptLogBook, needsRenew, renewLease, RENEW_INTERVAL_SECONDS, listTrackedJobs, restartSender, ALREADY_RELEASED } from './chunk-FGW2774C.js';
6
4
  import { decideInstalledSwitch } from './chunk-3JVYMD4G.js';
7
- import { fetchVersionMeta, npmVersion, npmSupportsProvenance, fetchTarballDigest, fetchProvenance, installVersion, auditSignatures } from './chunk-YETTWTAY.js';
8
- import { writeJobFile, readJobFile, readJobFileChecked } from './chunk-SADUQSO4.js';
9
5
  import { compareVersions, satisfiesRange } from './chunk-I2UOAV7X.js';
10
- import { RUNNER_PROTOCOL } from './chunk-BWSUPRDK.js';
11
- import { currentSkillCommand, formerSkillProblem, placeholderQueues, placeholderProblem } from './chunk-GEMQLBMW.js';
6
+ import { fetchVersionMeta, npmVersion, npmSupportsProvenance, fetchTarballDigest, fetchProvenance, installVersion, auditSignatures } from './chunk-YETTWTAY.js';
7
+ import { RUNNER_PROTOCOL } from './chunk-TPKYTORP.js';
8
+ import { writePendingUpdate } from './chunk-KPBQYEND.js';
9
+ import { readHousekeepingRecord, writeHousekeepingRecord } from './chunk-2J5JGLW5.js';
10
+ import { currentSkillCommand, formerSkillProblem, placeholderQueues, placeholderProblem } from './chunk-WMHYIBJW.js';
12
11
  import { freeBytes, formatBytes } from './chunk-YYYRBYWG.js';
13
- import { writeMarker, readMarker, currentVersion, removeMarker, entryIn, versionDir, startedThroughCurrent, switchCurrent, cleanVersions, isInLayout, stagingDir } from './chunk-AMFWYESY.js';
12
+ import { writeMarker, readMarker, currentVersion, removeMarker, entryIn, versionDir, startedThroughCurrent, switchCurrent, cleanVersions, isInLayout, stagingDir } from './chunk-4NM3I3VV.js';
14
13
  import { RepoFilter } from './chunk-TBT347UY.js';
15
- import { provisionContainer, lifecycleSteps, lifecycleFailureReason, prepareBranch, containerCommandLine, measureDisk, setUpRunnerCgroup, parseCpuList, ensureTunnel, keepGhConfigCurrent, createContainerRuntime } from './chunk-3ZR7Y23Y.js';
16
- import { spawnRun } from './chunk-VMJPFQNE.js';
17
- import { builderName, runnerLabelValue, ensureFailureCount, builderSpec, ensureBuilder, BUILD_LABEL, RUNNER_LABEL } from './chunk-3UZTIRLF.js';
18
- import { describeCallFailure, retrySend, shortClaim, relativeToRunner, formatDuration, BuildTokens, classifyRefusal, refusalCode, createRunnerLog, formatClock } from './chunk-XZ4H4X5F.js';
19
- import { checkFit } from './chunk-QEFKA2QD.js';
20
- import { isSignatureRefusal, WorkCommandError, getJson, postJson } from './chunk-FBTGMSQF.js';
21
- import { isBuiltinContainer, isBuiltinVm, isBuiltinProcess, loadRunnerConfig, checkHooks, builtinKind, DEFAULT_UPDATE_MAX_WAIT_HOURS } from './chunk-P53VEAOF.js';
14
+ import { checkFit } from './chunk-I66MQDXH.js';
15
+ import { LEASE_SECONDS, provisionContainer, leaseExpiresAtMs, deliverContainerHooks, lifecycleSteps, lifecycleFailureReason, prepareBranch, containerCommandExec, measureDisk, setUpRunnerCgroup, createContainerRuntime, parseCpuList, ensureTunnel, keepGhConfigCurrent, filesAreCopied } from './chunk-VX74ND77.js';
16
+ import { writeJobFile, ensureRunnerIdentity, readJobFile, builderName, runnerLabelValue, ensureFailureCount, builderSpec, ensureBuilder, spawnRun, BUILD_LABEL, RUNNER_LABEL, readJobFileChecked } from './chunk-WERZMIKA.js';
17
+ import { describeCallFailure, retrySend, shortClaim, relativeToRunner, formatDuration, BuildTokens, classifyRefusal, refusalCode, createRunnerLog, formatClock } from './chunk-C3OVGKVJ.js';
18
+ import { isSignatureRefusal, WorkCommandError, getJson, postJson } from './chunk-2HT2XGJX.js';
19
+ import { isBuiltinContainer, isBuiltinVm, isBuiltinProcess, loadRunnerConfig, checkHooks, builtinKind, DEFAULT_UPDATE_MAX_WAIT_HOURS } from './chunk-2CMJLYV6.js';
22
20
  import { exceedsDisk } from './chunk-7RQ73DLO.js';
23
- import { claimPidFile, readPidFile, releasePidFile } from './chunk-ECODQ7II.js';
21
+ import { claimPidFile, readPidFile, releasePidFile } from './chunk-ZFLOGMUC.js';
24
22
  import { startTimeOf, isSameProcess } from './chunk-JBJTC3UU.js';
25
23
  import { resolveVersion } from './chunk-RA3O54FC.js';
26
- import { loadHostKey, ensureHostKey, signerFor } from './chunk-M2ZWWTWZ.js';
27
- import { RUNNER_DIR_MODE, runnerDir, buildDir, attemptStamp, jobDir, jobsDir, pidFilePath, spoolDir, RUNNER_FILE_MODE, rootfsDir } from './chunk-N2PXGQRL.js';
28
- import { parseClaimIdentity, parseClaimedRepositories, repositoriesOf, primaryOf, sharedUserEnvironment, JOB_MOUNT, claudeCredential } from './chunk-BQXFBI7T.js';
24
+ import { loadHostKey, ensureHostKey, signerFor } from './chunk-KDHYLBRR.js';
25
+ import { RUNNER_DIR_MODE, runnerDir, buildDir, attemptStamp, jobDir, RUNNER_FILE_MODE, jobsDir, pidFilePath, spoolDir, rootfsDir } from './chunk-KIGSGZBB.js';
26
+ import { parseClaimIdentity, parseClaimedRepositories, parseUnreachable, repositoriesOf, primaryOf, sharedUserEnvironment, JOB_MOUNT, claudeCredential } from './chunk-V7MQKOZY.js';
27
+ import { COMMAND_LOST_REASON, CONTAINER_STOPPED_REASON } from './chunk-GISNHSYL.js';
29
28
  import { randomBytes } from 'crypto';
30
- import { mkdirSync, existsSync, chmodSync, statSync, readdirSync, writeFileSync, readFileSync, rmSync, renameSync, closeSync, openSync, constants, lstatSync } from 'fs';
29
+ import { mkdirSync, existsSync, chmodSync, writeFileSync, renameSync, statSync, readdirSync, readFileSync, rmSync, closeSync, openSync, constants, lstatSync } from 'fs';
31
30
  import { join, dirname } from 'path';
32
31
  import { totalmem, constants as constants$1 } from 'os';
33
32
 
@@ -85,6 +84,9 @@ var HEARTBEAT_TIMEOUT_MS = 1e4;
85
84
  var ERROR_CODE_MAX = 100;
86
85
  var ERROR_MESSAGE_MAX = 500;
87
86
  var BLOCKED_REASON_MAX = 500;
87
+ var TAKES_PATTERNS_MAX = 50;
88
+ var TAKES_PATTERN_CHARS_MAX = 200;
89
+ var WAITING_ITEMS_MAX = 200;
88
90
  var UNREACHABLE_MESSAGE = "the request did not reach the platform";
89
91
  function noPlatformMessage(status) {
90
92
  return `the request got HTTP ${status} with no error message from the platform`;
@@ -112,6 +114,35 @@ function workRequestFromFailure(err) {
112
114
  function workRequestFromRetryFailure(outcome) {
113
115
  return shapeWorkRequestFailure(outcome.code, outcome.status, outcome.message);
114
116
  }
117
+ function stopsOf(data) {
118
+ const raw = data?.stops;
119
+ if (!Array.isArray(raw)) return [];
120
+ const stops = [];
121
+ for (const entry of raw) {
122
+ const stop = entry;
123
+ if (stop === null || typeof stop !== "object" || typeof stop.claimId !== "string") continue;
124
+ stops.push({
125
+ claimId: stop.claimId,
126
+ requestedByName: typeof stop.requestedByName === "string" ? stop.requestedByName : "",
127
+ note: typeof stop.note === "string" && stop.note !== "" ? stop.note : null
128
+ });
129
+ }
130
+ return stops;
131
+ }
132
+ function takesBody(takes) {
133
+ let truncated = false;
134
+ const list = (patterns) => {
135
+ const all = (patterns ?? []).filter((pattern) => typeof pattern === "string");
136
+ if (all.length > TAKES_PATTERNS_MAX) truncated = true;
137
+ return all.slice(0, TAKES_PATTERNS_MAX).map((pattern) => {
138
+ if (pattern.length > TAKES_PATTERN_CHARS_MAX) truncated = true;
139
+ return cutForGateway(pattern, TAKES_PATTERN_CHARS_MAX);
140
+ }).filter((pattern) => pattern !== "");
141
+ };
142
+ const include = list(takes.repos.include);
143
+ const exclude = list(takes.repos.exclude);
144
+ return { queues: takes.queues, repos: { include, exclude }, truncated };
145
+ }
115
146
  async function sendHeartbeat(args, fetchImpl) {
116
147
  const body = {
117
148
  host: args.host,
@@ -132,7 +163,10 @@ async function sendHeartbeat(args, fetchImpl) {
132
163
  // Free disk on the runner's file system, in bytes. Sent only while held, so a host with room sends nothing about its disk.
133
164
  lowDisk: args.lowDisk === void 0 ? void 0 : { free: args.lowDisk.free, needed: args.lowDisk.needed },
134
165
  // Why the runner claims nothing. Sent only while it holds, so a runner that takes work says nothing about it.
135
- blocked: args.blocked === void 0 ? void 0 : { reason: cutForGateway(args.blocked.reason, BLOCKED_REASON_MAX) }
166
+ blocked: args.blocked === void 0 ? void 0 : { reason: cutForGateway(args.blocked.reason, BLOCKED_REASON_MAX) },
167
+ state: args.state,
168
+ waitingItems: args.waitingItems === void 0 ? void 0 : args.waitingItems.slice(0, WAITING_ITEMS_MAX),
169
+ takes: args.takes === void 0 ? void 0 : takesBody(args.takes)
136
170
  };
137
171
  try {
138
172
  const response = await postJson(
@@ -146,11 +180,22 @@ async function sendHeartbeat(args, fetchImpl) {
146
180
  await signerFor(args.configPath)
147
181
  );
148
182
  const runnerVersion = response.data?.runnerVersion;
149
- return typeof runnerVersion === "string" ? { ok: true, runnerVersion } : { ok: true };
183
+ const stops = stopsOf(response.data);
184
+ return typeof runnerVersion === "string" ? { ok: true, runnerVersion, stops } : { ok: true, stops };
150
185
  } catch (err) {
151
186
  return { ok: false, reason: describeCallFailure(err) };
152
187
  }
153
188
  }
189
+
190
+ // src/runner/host-state.ts
191
+ function hostState(flags) {
192
+ if (flags.draining) return "draining";
193
+ if (flags.restarting) return "restarting";
194
+ if (flags.holdingForUpdate) return "holding-for-update";
195
+ if (flags.blocked) return "blocked";
196
+ if (flags.lowDisk) return "low-disk";
197
+ return "taking-work";
198
+ }
154
199
  var OUTCOMES = ["updated", "rolled_back", "verification_failed", "not_published", "retrying"];
155
200
  function lastUpdatePath(configPath) {
156
201
  return join(runnerDir(configPath), "last-update.json");
@@ -930,6 +975,7 @@ var QUEUE_ORDER = ["amend", "implement"];
930
975
  var TRACKER_REPOSITORY_HOSTS = {
931
976
  github: "github.com"
932
977
  };
978
+ var PROBE_INTERVAL_MS = 3e3;
933
979
  var HOUSEKEEPING_INTERVAL_MS = 36e5;
934
980
  function logOf(state) {
935
981
  state.deps.log ??= createRunnerLog(state.deps.now ?? (() => /* @__PURE__ */ new Date()));
@@ -1042,6 +1088,7 @@ async function sendTickHeartbeat(state, workRequest) {
1042
1088
  let next = workRequest;
1043
1089
  for (; ; ) {
1044
1090
  reportRepeating(state, "the heartbeat's previous call", void 0, "line");
1091
+ const waitingItems = state.draining === true ? runningItems(state) : [];
1045
1092
  const result = await sendHeartbeat(
1046
1093
  {
1047
1094
  configPath: state.config.configPath,
@@ -1056,12 +1103,25 @@ async function sendTickHeartbeat(state, workRequest) {
1056
1103
  workRequest: next,
1057
1104
  lastUpdate: readLastUpdate(state.config.configPath),
1058
1105
  lowDisk: state.lowDisk,
1059
- blocked: state.blocked
1106
+ blocked: state.blocked,
1107
+ state: hostState({
1108
+ draining: state.draining === true,
1109
+ restarting: state.restarting === true,
1110
+ holdingForUpdate: state.holdingForUpdate === true,
1111
+ blocked: state.blocked !== void 0,
1112
+ lowDisk: state.lowDisk !== void 0
1113
+ }),
1114
+ // Left out unless a draining runner has builds to wait on, so the gateway stores null.
1115
+ waitingItems: waitingItems.length > 0 ? waitingItems : void 0,
1116
+ takes: { queues: state.config.queues, repos: { include: state.config.repos?.include, exclude: state.config.repos?.exclude } }
1060
1117
  },
1061
1118
  state.deps.fetchImpl
1062
1119
  );
1063
1120
  reportRepeating(state, "the heartbeat", result.ok ? void 0 : `heartbeat failed: ${result.reason}`);
1064
- if (result.ok) considerNamedVersion(state, result.runnerVersion);
1121
+ if (result.ok) {
1122
+ considerNamedVersion(state, result.runnerVersion);
1123
+ applyStops(state, result.stops);
1124
+ }
1065
1125
  next = state.queuedWorkRequest;
1066
1126
  state.queuedWorkRequest = void 0;
1067
1127
  if (next === void 0) break;
@@ -1070,6 +1130,27 @@ async function sendTickHeartbeat(state, workRequest) {
1070
1130
  state.heartbeating = false;
1071
1131
  }
1072
1132
  }
1133
+ function applyStops(state, stops) {
1134
+ for (const stop of stops) {
1135
+ const held = [...state.jobs.values()].find((entry) => entry.job.claimId === stop.claimId);
1136
+ const active = held !== void 0 && !held.ending ? held : void 0;
1137
+ if (active === void 0) {
1138
+ reportRepeating(
1139
+ state,
1140
+ `stop of claim ${stop.claimId}`,
1141
+ held === void 0 ? `ignoring a stop for claim ${stop.claimId}: this runner holds no build under that claim` : `#${held.job.item} a stop was asked for; the build is already ending`,
1142
+ "line"
1143
+ );
1144
+ continue;
1145
+ }
1146
+ const who = `stopped by ${stop.requestedByName === "" ? "someone" : stop.requestedByName} through OuterLayer`;
1147
+ const reason = stop.note === null ? who : `${who}: ${stop.note}`;
1148
+ logOf(state).line(`#${active.job.item} ${reason} \xB7 ending the build`);
1149
+ void endJob(state, active, "stopped", { reason }).catch((err) => {
1150
+ logOf(state).error(`#${active.job.item} could not be stopped: ${describeCallFailure(err)}`);
1151
+ });
1152
+ }
1153
+ }
1073
1154
  function ownVersionOf(deps) {
1074
1155
  return (deps.cliVersion ?? resolveVersion()).split(" ")[0] ?? "";
1075
1156
  }
@@ -1388,7 +1469,7 @@ async function housekeepingPass(state) {
1388
1469
  state.housekeepingRunning = true;
1389
1470
  try {
1390
1471
  state.deps.housekeeping ??= {};
1391
- state.deps.housekeeping.docker ??= createHousekeepingDocker({ runnerId: runnerLabelValue(state.config.configPath), builder: builderName(state.config.configPath) });
1472
+ state.deps.housekeeping.docker ??= createHousekeepingDocker({ run: state.deps.housekeeping.run, runnerId: runnerLabelValue(state.config.configPath), builder: builderName(state.config.configPath) });
1392
1473
  const result = await runHousekeeping({
1393
1474
  configPath: state.config.configPath,
1394
1475
  keepDays: config.jobsKeepDays,
@@ -1405,9 +1486,17 @@ async function housekeepingPass(state) {
1405
1486
  }
1406
1487
  }
1407
1488
  async function runTick(state) {
1489
+ state.restarting = false;
1490
+ state.holdingForUpdate = false;
1408
1491
  reloadConfig(state);
1409
- if (restartOntoInstalled(state)) return;
1410
- if (await updateWhenIdle(state)) return;
1492
+ if (restartOntoInstalled(state)) {
1493
+ state.restarting = true;
1494
+ return;
1495
+ }
1496
+ if (await updateWhenIdle(state)) {
1497
+ state.holdingForUpdate = true;
1498
+ return;
1499
+ }
1411
1500
  if (holdWhenBlocked(state)) return;
1412
1501
  if (holdOffForDisk(state)) return;
1413
1502
  const log = logOf(state);
@@ -1460,10 +1549,10 @@ async function runTick(state) {
1460
1549
  );
1461
1550
  }
1462
1551
  var CLAIM_REFUSAL_CODES = new Set(
1463
- "pull_request_from_fork pull_request_on_default_branch pull_request_ambiguous pull_request_repository_not_connected branch_claimed_elsewhere".split(" ")
1552
+ "pull_request_from_fork pull_request_on_default_branch pull_request_ambiguous pull_request_repository_not_connected repository_not_reachable branch_claimed_elsewhere".split(" ")
1464
1553
  );
1465
1554
  async function tryClaimAndStart(state, item, queue) {
1466
- const { postJson: postJson2 } = await import('./work-cmd-5I3J2XQQ.js');
1555
+ const { postJson: postJson2 } = await import('./work-cmd-L6WMPAA2.js');
1467
1556
  const log = logOf(state);
1468
1557
  const hostKey = await loadHostKey(state.config.configPath);
1469
1558
  const claim = await retrySend(
@@ -1567,6 +1656,9 @@ async function tryClaimAndStart(state, item, queue) {
1567
1656
  return { kind: "unreadable_repositories" };
1568
1657
  }
1569
1658
  reportRepeating(state, "the repositories", void 0);
1659
+ for (const entry of parseUnreachable(data.unreachable)) {
1660
+ log.line(`#${item.number}: ${entry.repository} is left out of this build because its GitHub App installation is suspended`);
1661
+ }
1570
1662
  try {
1571
1663
  await startJob(state, item, queue, claimId, itemKey, branch, identity, repositories);
1572
1664
  } catch (err) {
@@ -1697,7 +1789,8 @@ async function startJob(state, item, queue, claimId, itemKey, branch, identity,
1697
1789
  aborted: () => active.ending,
1698
1790
  deadlineMs: startedAt.getTime() + state.config.timeLimitMinutes * 6e4,
1699
1791
  fetchImpl: state.deps.fetchImpl,
1700
- now: state.deps.now
1792
+ now: state.deps.now,
1793
+ leaseExpiryMs: () => leaseExpiresAtMs(active.job)
1701
1794
  }).catch((err) => ({ ok: false, reason: `provision failed: ${describeCallFailure(err)}`, aborted: false }));
1702
1795
  const provisionTook = formatDuration(nowOf(state).getTime() - provisionStartedMs);
1703
1796
  if (active.ending) return;
@@ -1710,6 +1803,11 @@ async function startJob(state, item, queue, claimId, itemKey, branch, identity,
1710
1803
  active.job.container = provisioned.state;
1711
1804
  active.job.environment = provisioned.environment;
1712
1805
  installCommitHooks(active.job, log, tag);
1806
+ const hooksDelivered = await deliverContainerHooks(runtimeOf(state), provisioned.state, dirPath);
1807
+ if (!hooksDelivered.ok) {
1808
+ await endOnProvisionFailure(state, active, tokens, provisionTook, hooksDelivered.reason);
1809
+ return;
1810
+ }
1713
1811
  } else if (vmMode) {
1714
1812
  const provisioned = await provisionMicroVm({
1715
1813
  config: state.config,
@@ -1941,14 +2039,16 @@ async function startJob(state, item, queue, claimId, itemKey, branch, identity,
1941
2039
  const composed = microVmCommandLine({ job, cmdline, runtime, config: state.config, cli: join(runtime.cliBuild().distDir, "index.js") });
1942
2040
  running = runCommand(composed.line, { ...composed.env, OUTERLAYER_JOB_DIR: dirPath }, dirPath, join(dirPath, "command.log"), state.deps.spawnFn);
1943
2041
  } else if (job.container !== void 0) {
1944
- const composed = containerCommandLine({ job, cmdline, runtime: runtimeOf(state), config: state.config });
1945
- running = runCommand(
1946
- composed.line,
1947
- { ...composed.env, OUTERLAYER_JOB_DIR: dirPath },
1948
- dirPath,
1949
- join(dirPath, "command.log"),
1950
- state.deps.spawnFn
1951
- );
2042
+ const runtime = runtimeOf(state);
2043
+ const exec = containerCommandExec({ job, cmdline, runtime, config: state.config });
2044
+ job.container.tracking = "container";
2045
+ job.container.commandLogBase = commandLogOffset(dirPath, 0);
2046
+ writeJobFile(jobFilePath, job);
2047
+ const started = await runtime.host.execDetached(exec);
2048
+ if (started.status !== 0) {
2049
+ const said = started.stderr.trim().split("\n")[0] ?? "";
2050
+ throw new Error(`docker exec exited ${started.status}${said === "" ? "" : `: ${said}`}`);
2051
+ }
1952
2052
  } else {
1953
2053
  const build = job.build;
1954
2054
  const commandEnv = hostCommandEnvironment(jobEnv(state.config, item.number, itemKey, dirPath, process.env, branch, identity?.author), build, state.config, identity?.hooksPath);
@@ -1959,12 +2059,13 @@ async function startJob(state, item, queue, claimId, itemKey, branch, identity,
1959
2059
  await endJob(state, active, "failed", { stage: "command" });
1960
2060
  return;
1961
2061
  }
1962
- active.cmd = running;
1963
- job = { ...job, stage: "command", pid: running.pid, start: running.start ?? void 0 };
2062
+ active.cmd = running ?? null;
2063
+ job = { ...job, stage: "command", ...running === void 0 ? {} : { pid: running.pid, start: running.start ?? void 0 } };
1964
2064
  active.job = job;
1965
2065
  writeJobFile(jobFilePath, job);
1966
2066
  const commandStartedMs = nowOf(state).getTime();
1967
- log.line(`${tag} command started \xB7 pid ${running.pid}`);
2067
+ active.commandStartedMs = commandStartedMs;
2068
+ log.line(running === void 0 ? `${tag} command started \xB7 in container ${job.container.container}` : `${tag} command started \xB7 pid ${running.pid}`);
1968
2069
  const sender = await startSender({
1969
2070
  config: state.config,
1970
2071
  job,
@@ -1976,6 +2077,10 @@ async function startJob(state, item, queue, claimId, itemKey, branch, identity,
1976
2077
  active.job = job;
1977
2078
  writeJobFile(jobFilePath, job);
1978
2079
  }
2080
+ if (running === void 0) {
2081
+ void probeBuild(state, active);
2082
+ return;
2083
+ }
1979
2084
  running.onExit.then(({ code, signal, error }) => {
1980
2085
  if (active.ending) return;
1981
2086
  if (error !== void 0) {
@@ -2122,12 +2227,84 @@ async function renewLeaseIfDue(state, active, nowMs) {
2122
2227
  active.job.lastRenewAt = new Date(nowMs).toISOString();
2123
2228
  writeJobFile(join(active.dirPath, "job.json"), active.job);
2124
2229
  log.line(`${tag} lease renewed \xB7 expires ${formatClock(new Date(nowMs + LEASE_SECONDS * 1e3), new Date(nowMs))}`);
2230
+ if (active.job.container !== void 0) state.containerRuntime?.leases.get(active.job.container.buildId)?.publish();
2125
2231
  } else {
2126
2232
  log.error(`${tag} renew did not land \xB7 retrying in ${RENEW_INTERVAL_SECONDS / 60}m`);
2127
2233
  }
2128
2234
  active.lastRenewAtMs = nowMs;
2129
2235
  return true;
2130
2236
  }
2237
+ var COMMAND_START_GRACE_MS = 3e4;
2238
+ async function endStoppedBuild(state, active, where) {
2239
+ const log = logOf(state);
2240
+ const tag = `#${active.job.item}`;
2241
+ const read = where === "stopped" ? await readStoppedBuild(state, active.dirPath, active.job) : { code: void 0, readable: true };
2242
+ if (active.ending) return;
2243
+ active.job.container.stopped = true;
2244
+ if (read.code !== void 0) {
2245
+ recordCommandExit(state, active, read.code);
2246
+ return;
2247
+ }
2248
+ log.line(
2249
+ `${tag} the build's container ${where === "missing" ? "is gone" : "stopped"} and its volume ${read.readable ? "holds no exit code" : "could not be read"} \xB7 ending job as interrupted`
2250
+ );
2251
+ void endJob(state, active, "interrupted", { stage: "command", reason: COMMAND_LOST_REASON });
2252
+ }
2253
+ async function probeBuild(state, active) {
2254
+ const container = active.job.container;
2255
+ if (container === void 0 || !isContainerTracked(active.job) || active.ending || active.probing) return;
2256
+ active.probing = true;
2257
+ const log = logOf(state);
2258
+ const tag = `#${active.job.item}`;
2259
+ try {
2260
+ const runtime = runtimeOf(state);
2261
+ for (; ; ) {
2262
+ const offset = commandLogOffset(active.dirPath, container.commandLogBase);
2263
+ const probe = await probeCommand(runtime, container, offset);
2264
+ if (active.ending) return;
2265
+ if (probe === void 0) {
2266
+ const where = await runtime.host.state(container.container);
2267
+ if (where === "stopped" || where === "missing") {
2268
+ await endStoppedBuild(state, active, where);
2269
+ } else if (active.probeFailing !== true) {
2270
+ active.probeFailing = true;
2271
+ log.error(`${tag} the build's command could not be read from its container (${where}) \xB7 trying again`);
2272
+ }
2273
+ return;
2274
+ }
2275
+ active.probeFailing = false;
2276
+ appendCommandLog(active.dirPath, probe.bytes);
2277
+ if (probe.bytes.length > 0 && offset + probe.bytes.length < probe.size) continue;
2278
+ const starting = active.commandStartedMs !== void 0 && nowOf(state).getTime() - active.commandStartedMs < COMMAND_START_GRACE_MS;
2279
+ const verdict = starting && probe.exitCode === void 0 && !probe.alive ? { kind: "running" } : judgeProbe(probe, active.commandMissed === true);
2280
+ active.commandMissed = verdict.kind === "recheck";
2281
+ if (verdict.kind === "exited") {
2282
+ recordCommandExit(state, active, verdict.code);
2283
+ } else if (verdict.kind === "lost") {
2284
+ log.line(`${tag} command gone, no exit code recorded \xB7 ending job as interrupted`);
2285
+ void endJob(state, active, "interrupted", { stage: "command", reason: COMMAND_LOST_REASON });
2286
+ }
2287
+ return;
2288
+ }
2289
+ } catch (err) {
2290
+ log.error(`${tag} reading the build's command failed: ${describeCallFailure(err)}`);
2291
+ } finally {
2292
+ active.probing = false;
2293
+ }
2294
+ }
2295
+ function recordCommandExit(state, active, code) {
2296
+ const log = logOf(state);
2297
+ const tag = `#${active.job.item}`;
2298
+ const file = join(active.dirPath, EXIT_CODE_FILE);
2299
+ writeFileSync(`${file}.partial`, `${code}
2300
+ `, { mode: RUNNER_FILE_MODE });
2301
+ renameSync(`${file}.partial`, file);
2302
+ const took = active.commandStartedMs === void 0 ? "" : ` (${formatDuration(nowOf(state).getTime() - active.commandStartedMs)})`;
2303
+ log.line(`${tag} command exited ${code}${took}`);
2304
+ active.job.exitCode = code;
2305
+ active.job.commandEndReason = jobNotEndedByRunnerReason({ code, signal: null }, active.job);
2306
+ void endJob(state, active, code === 0 ? "ok" : "failed");
2307
+ }
2131
2308
  async function watchTick(state, active, nowMs, logMtimeMs) {
2132
2309
  if (active.ending) {
2133
2310
  if (active.job.stage === "checks" || active.job.stage === "judge") await renewLeaseIfDue(state, active, nowMs);
@@ -2136,6 +2313,7 @@ async function watchTick(state, active, nowMs, logMtimeMs) {
2136
2313
  const log = logOf(state);
2137
2314
  const tag = `#${active.job.item}`;
2138
2315
  const pid = active.job.pid;
2316
+ const started = pid !== void 0 || isContainerTracked(active.job);
2139
2317
  if (pid !== void 0 && !isGroupAlive(pid)) {
2140
2318
  if (active.cmd === null) {
2141
2319
  const recorded = outcomeFromExitCode(active.dirPath);
@@ -2162,16 +2340,22 @@ async function watchTick(state, active, nowMs, logMtimeMs) {
2162
2340
  }
2163
2341
  if (refusal?.kind === "failed") {
2164
2342
  log.error(`${tag} a token request was refused: ${refusal.reason} \xB7 ending job as failed`);
2165
- await endJob(state, active, "failed", { stage: pid === void 0 ? "provision" : "command", reason: refusal.reason });
2343
+ await endJob(state, active, "failed", { stage: started ? "command" : "provision", reason: refusal.reason });
2166
2344
  return;
2167
2345
  }
2168
2346
  }
2347
+ const unreachable = active.job.container === void 0 ? void 0 : runtimeOf(state).relayFatal.get(active.job.container.buildId);
2348
+ if (unreachable !== void 0) {
2349
+ log.error(`${tag} ${unreachable} \xB7 ending job as failed`);
2350
+ await endJob(state, active, "failed", { stage: started ? "command" : "provision", reason: unreachable });
2351
+ return;
2352
+ }
2169
2353
  if (isTimedOut(new Date(active.job.startedAt).getTime(), nowMs, state.config.timeLimitMinutes)) {
2170
2354
  log.line(`${tag} past the ${state.config.timeLimitMinutes}m time limit \xB7 ending job as timed_out`);
2171
2355
  await endJob(state, active, "timed_out");
2172
2356
  return;
2173
2357
  }
2174
- if (pid === void 0) return;
2358
+ if (!started) return;
2175
2359
  if (active.job.container !== void 0) {
2176
2360
  const limit = state.config.build?.disk ?? "40g";
2177
2361
  const used = await measureDisk(runtimeOf(state), active.job.container);
@@ -2219,6 +2403,117 @@ async function probeCredentials(config, fetchImpl = fetch) {
2219
2403
  return { ok: false, fatal: false, reason: describeCallFailure(err) };
2220
2404
  }
2221
2405
  }
2406
+ async function rejoinContainerBuild(state, adopted) {
2407
+ const { job, dirPath } = adopted;
2408
+ const config = state.config;
2409
+ const container = job.container;
2410
+ const cores = container.cpuset === void 0 ? void 0 : parseCpuList(container.cpuset);
2411
+ if (cores !== void 0) runtimeOf(state).cpus.hold(container.buildId, cores);
2412
+ const tokens = buildTokensFor(state, adopted, job.item);
2413
+ runtimeOf(state).credentials.set(dirPath, tokens);
2414
+ await ensureTunnel(runtimeOf(state), container, dirPath, config, job.itemKey, job.branch, job.repositories, (line) => logOf(state).line(`#${job.item} ${line}`), () => leaseExpiresAtMs(adopted.job));
2415
+ try {
2416
+ await tokens.token("read");
2417
+ keepGhConfigCurrent(
2418
+ tokens,
2419
+ container.buildDir,
2420
+ (err) => logOf(state).error(`#${job.item} the read token could not be written for gh: ${describeCallFailure(err)}`),
2421
+ filesAreCopied(container) ? {
2422
+ runtime: runtimeOf(state),
2423
+ state: container,
2424
+ secrets: () => tokens.issuedTokens(),
2425
+ onFailure: (reason) => logOf(state).error(`#${job.item} ${reason}`)
2426
+ } : void 0
2427
+ );
2428
+ } catch (err) {
2429
+ logOf(state).error(`#${job.item} could not give the adopted build a read token: ${describeCallFailure(err)}`);
2430
+ }
2431
+ }
2432
+ async function adoptTrackedBuilds(state, jobsDirPath) {
2433
+ const log = logOf(state);
2434
+ const attemptLogs = attemptLogsOf(state);
2435
+ const runtime = runtimeOf(state);
2436
+ for (const { dirPath, job, known } of listTrackedJobs(jobsDirPath)) {
2437
+ if (!known) continue;
2438
+ const container = job.container;
2439
+ const where = await runtime.host.state(container.container).catch(() => "unknown");
2440
+ if (where === "stopped" || where === "missing") {
2441
+ await finishTrackedWhileAway(state, dirPath, job, where);
2442
+ continue;
2443
+ }
2444
+ const adopted = { job, dirPath, lastRenewAtMs: 0, cmd: null, cpuSample: null, ending: false };
2445
+ state.jobs.set(dirPath, adopted);
2446
+ attemptLogs.open(job.item, dirPath);
2447
+ await rejoinContainerBuild(state, adopted);
2448
+ if (!await renewLeaseIfDue(state, adopted, nowOf(state).getTime())) continue;
2449
+ const sender = await restartSender({ config: state.config, job: adopted.job, jobDir: dirPath, deps: { spawnFn: state.deps.spawnFn, runtime, log, sleep: state.deps.sleep } });
2450
+ adopted.job = sender === void 0 ? { ...adopted.job, sender: void 0 } : { ...adopted.job, sender };
2451
+ writeJobFile(join(dirPath, "job.json"), adopted.job);
2452
+ const line = `adopted job ${job.queue} #${job.item} (claim ${shortClaim(job.claimId)}) \xB7 command tracked in container ${container.container}`;
2453
+ log.line(line);
2454
+ attemptLogs.note(job.item, line);
2455
+ void probeBuild(state, adopted);
2456
+ }
2457
+ }
2458
+ async function readStoppedBuild(state, dirPath, job) {
2459
+ const runtime = runtimeOf(state);
2460
+ const container = job.container;
2461
+ runtime.leases.get(container.buildId)?.stop();
2462
+ try {
2463
+ for (let attempt = 1; attempt <= STOPPED_READ_ATTEMPTS; attempt++) {
2464
+ const started = await runtime.host.start(container.container);
2465
+ if (started.status === 0) {
2466
+ const code = await readVolumeExitCode(runtime, dirPath, container);
2467
+ if (code !== "unreadable") {
2468
+ await runtime.host.stop(container.container);
2469
+ return { code, readable: true };
2470
+ }
2471
+ await runtime.host.stop(container.container).catch(() => void 0);
2472
+ }
2473
+ if (attempt < STOPPED_READ_ATTEMPTS) await (state.deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))))(STOPPED_READ_RETRY_MS);
2474
+ }
2475
+ logOf(state).error(`#${job.item} the stopped build's volume could not be read after ${STOPPED_READ_ATTEMPTS} tries`);
2476
+ return { code: void 0, readable: false };
2477
+ } catch (err) {
2478
+ logOf(state).error(`#${job.item} the stopped build's volume could not be read: ${describeCallFailure(err)}`);
2479
+ return { code: void 0, readable: false };
2480
+ }
2481
+ }
2482
+ var STOPPED_READ_ATTEMPTS = 3;
2483
+ var STOPPED_READ_RETRY_MS = 1e3;
2484
+ async function readVolumeExitCode(runtime, dirPath, container) {
2485
+ for (; ; ) {
2486
+ const offset = commandLogOffset(dirPath, container.commandLogBase);
2487
+ const probe = await probeCommand(runtime, container, offset);
2488
+ if (probe === void 0) return "unreadable";
2489
+ appendCommandLog(dirPath, probe.bytes);
2490
+ if (probe.bytes.length > 0 && offset + probe.bytes.length < probe.size) continue;
2491
+ return probe.exitCode;
2492
+ }
2493
+ }
2494
+ async function finishTrackedWhileAway(state, dirPath, job, where) {
2495
+ const log = logOf(state);
2496
+ const container = job.container;
2497
+ const read = where === "stopped" ? await readStoppedBuild(state, dirPath, job) : { code: void 0, readable: true };
2498
+ const code = read.code;
2499
+ container.stopped = true;
2500
+ let outcome = "interrupted";
2501
+ let ending = { reason: CONTAINER_STOPPED_REASON };
2502
+ if (code !== void 0) {
2503
+ const file = join(dirPath, EXIT_CODE_FILE);
2504
+ writeFileSync(`${file}.partial`, `${code}
2505
+ `, { mode: RUNNER_FILE_MODE });
2506
+ renameSync(`${file}.partial`, file);
2507
+ job.exitCode = code;
2508
+ job.commandEndReason = jobNotEndedByRunnerReason({ code, signal: null }, job);
2509
+ outcome = code === 0 ? "ok" : "failed";
2510
+ ending = void 0;
2511
+ }
2512
+ const finished = await finishJob(job, dirPath, outcome, state.config, finishDepsOf(state), ending);
2513
+ log.line(
2514
+ `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
+ );
2516
+ }
2222
2517
  async function start(configPath, deps = { fetchImpl: fetch }) {
2223
2518
  const log = deps.log ??= createRunnerLog(deps.now ?? (() => /* @__PURE__ */ new Date()));
2224
2519
  const update = deps.update;
@@ -2226,7 +2521,7 @@ async function start(configPath, deps = { fetchImpl: fetch }) {
2226
2521
  const outcome = await onStartUnderMarker({
2227
2522
  configPath,
2228
2523
  own: ownVersionOf(deps),
2229
- check: update.check ? () => update.check(configPath) : async () => (await import('./check-W2JGXN3B.js')).runRunnerCheck(configPath, { fetchImpl: deps.fetchImpl }),
2524
+ check: update.check ? () => update.check(configPath) : async () => (await import('./check-3YHMJQ3P.js')).runRunnerCheck(configPath, { fetchImpl: deps.fetchImpl }),
2230
2525
  now: () => (deps.now ?? (() => /* @__PURE__ */ new Date()))().getTime(),
2231
2526
  log
2232
2527
  });
@@ -2239,6 +2534,12 @@ async function start(configPath, deps = { fetchImpl: fetch }) {
2239
2534
  const hookProblem = checkHooks(config);
2240
2535
  if (hookProblem) throw new RunnerStartError(hookProblem);
2241
2536
  await ensureHostKey(configPath);
2537
+ if (builtinKind(config.hooks.provision) === "container" || builtinKind(config.hooks.provision) === "vm") {
2538
+ await ensureRunnerIdentity(configPath, {
2539
+ docker: deps.runnerIdentity ?? (deps.container?.host === void 0 ? void 0 : { holdsObjects: async () => false }),
2540
+ log: (line) => log.error(line)
2541
+ }).catch((err) => log.error(`the runner's Docker identity could not be stored: ${describeCallFailure(err)} \xB7 using the names its folder's path gives`));
2542
+ }
2242
2543
  const probe = await probeCredentials(config, deps.fetchImpl);
2243
2544
  if (!probe.ok && probe.fatal) throw new RunnerStartError(probe.reason);
2244
2545
  if (!probe.ok) log.error(`the gateway did not answer at start: ${probe.reason} \xB7 starting anyway`);
@@ -2269,23 +2570,7 @@ async function start(configPath, deps = { fetchImpl: fetch }) {
2269
2570
  const adopted = { job, dirPath, lastRenewAtMs: nowMs(), cmd: null, cpuSample: null, ending: false };
2270
2571
  state.jobs.set(dirPath, adopted);
2271
2572
  attemptLogs.open(job.item, dirPath);
2272
- if (job.container !== void 0) {
2273
- const cores = job.container.cpuset === void 0 ? void 0 : parseCpuList(job.container.cpuset);
2274
- if (cores !== void 0) runtimeOf(state).cpus.hold(job.container.buildId, cores);
2275
- const tokens = buildTokensFor(state, adopted, job.item);
2276
- runtimeOf(state).credentials.set(dirPath, tokens);
2277
- await ensureTunnel(runtimeOf(state), job.container, dirPath, config, job.itemKey, job.branch, job.repositories);
2278
- try {
2279
- await tokens.token("read");
2280
- keepGhConfigCurrent(
2281
- tokens,
2282
- job.container.buildDir,
2283
- (err) => logOf(state).error(`#${job.item} the read token could not be written for gh: ${describeCallFailure(err)}`)
2284
- );
2285
- } catch (err) {
2286
- logOf(state).error(`#${job.item} could not give the adopted build a read token: ${describeCallFailure(err)}`);
2287
- }
2288
- }
2573
+ if (job.container !== void 0) await rejoinContainerBuild(state, adopted);
2289
2574
  if (job.build !== void 0 && !runtimeOf(state).tunnels.has(job.build.buildId)) {
2290
2575
  await startHostServices({ runtime: runtimeOf(state), config, build: job.build, jobDir: dirPath, itemKey: job.itemKey, process: job.build.workDir !== void 0 }).catch((err) => {
2291
2576
  logOf(state).error(`the tunnel of #${job.item} could not start again: ${describeCallFailure(err)}`);
@@ -2295,6 +2580,7 @@ async function start(configPath, deps = { fetchImpl: fetch }) {
2295
2580
  log.line(adoptedLine);
2296
2581
  attemptLogs.note(job.item, adoptedLine);
2297
2582
  }
2583
+ await adoptTrackedBuilds(state, jobsDir(configPath));
2298
2584
  for (const { job, released } of await retryFailedReleases(jobsDir(configPath), config, {
2299
2585
  fetchImpl: deps.fetchImpl,
2300
2586
  now: deps.now,
@@ -2312,12 +2598,11 @@ async function start(configPath, deps = { fetchImpl: fetch }) {
2312
2598
  }
2313
2599
  let intakeClosed = false;
2314
2600
  let closed = false;
2315
- let draining = false;
2316
2601
  let stopping = false;
2317
2602
  let exited = false;
2318
2603
  let interrupts = 0;
2319
2604
  state.pollTimerSeconds = state.config.pollSeconds;
2320
- const pollTimer = setInterval(() => void tick(state), state.pollTimerSeconds * 1e3);
2605
+ let pollTimer = setInterval(() => void tick(state), state.pollTimerSeconds * 1e3);
2321
2606
  const hooksKind = builtinKind(config.hooks.provision);
2322
2607
  const housekeepingTimer = hooksKind === "container" || hooksKind === "vm" ? (() => {
2323
2608
  const everyMs = deps.housekeeping?.intervalMs ?? HOUSEKEEPING_INTERVAL_MS;
@@ -2337,6 +2622,9 @@ async function start(configPath, deps = { fetchImpl: fetch }) {
2337
2622
  });
2338
2623
  }
2339
2624
  }, deps.watchIntervalMs ?? 3e4);
2625
+ const probeTimer = setInterval(() => {
2626
+ for (const active of state.jobs.values()) void probeBuild(state, active);
2627
+ }, deps.probeIntervalMs ?? PROBE_INTERVAL_MS);
2340
2628
  const onSigterm = () => void drain();
2341
2629
  const onSigusr1 = () => void stopNow();
2342
2630
  const onSigint = () => {
@@ -2357,6 +2645,7 @@ async function start(configPath, deps = { fetchImpl: fetch }) {
2357
2645
  if (intakeClosed) return;
2358
2646
  intakeClosed = true;
2359
2647
  clearInterval(pollTimer);
2648
+ pollTimer = setInterval(() => void sendTickHeartbeat(state, void 0), (state.pollTimerSeconds ?? state.config.pollSeconds) * 1e3);
2360
2649
  clearInterval(housekeepingTimer);
2361
2650
  };
2362
2651
  const close = () => {
@@ -2364,18 +2653,21 @@ async function start(configPath, deps = { fetchImpl: fetch }) {
2364
2653
  detachSignals();
2365
2654
  if (closed) return;
2366
2655
  closed = true;
2656
+ clearInterval(pollTimer);
2367
2657
  clearInterval(watchTimer);
2658
+ clearInterval(probeTimer);
2368
2659
  };
2369
2660
  const finish2 = () => {
2370
2661
  if (exited) return;
2371
2662
  exited = true;
2372
2663
  close();
2664
+ for (const relay of state.containerRuntime?.relays.values() ?? []) void relay.close();
2373
2665
  releasePidFile(pidFilePath(configPath), self);
2374
2666
  log.line("runner exited");
2375
2667
  };
2376
2668
  const drain = async (onWaiting) => {
2377
- if (draining) return;
2378
- draining = true;
2669
+ if (state.draining === true) return;
2670
+ state.draining = true;
2379
2671
  stopIntake();
2380
2672
  let passes = 0;
2381
2673
  while (state.jobs.size > 0 && !exited) {
@@ -2397,7 +2689,9 @@ async function start(configPath, deps = { fetchImpl: fetch }) {
2397
2689
  if (stopping) return;
2398
2690
  stopping = true;
2399
2691
  stopIntake();
2692
+ clearInterval(pollTimer);
2400
2693
  clearInterval(watchTimer);
2694
+ clearInterval(probeTimer);
2401
2695
  log.line(`stopping: ending ${state.jobs.size} running job(s) now`);
2402
2696
  await Promise.all([...state.jobs.values()].map((active) => endJob(state, active, "stopped")));
2403
2697
  finish2();
@@ -2483,4 +2777,4 @@ function runnerProcessState(configPath) {
2483
2777
  return isSameProcess(entry.pid, entry.start) ? { state: "running", pid: entry.pid } : { state: "stale", pid: entry.pid };
2484
2778
  }
2485
2779
 
2486
- export { RunnerStartError, listJobFiles, probeCredentials, requestStop, runnerProcessState, start, tick, watchTick };
2780
+ export { RunnerStartError, listJobFiles, probeBuild, probeCredentials, requestStop, runnerProcessState, start, tick, watchTick };