@outerlayer/cli 0.16.1 → 0.18.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 (153) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +26 -11
  3. package/dist/agent-setup-RBTEACYU.js +28 -0
  4. package/dist/artifact-link-cmd-AVM2EKTV.js +25 -0
  5. package/dist/{build-BG2YENGK.js → build-TQ6X5PYN.js} +5 -4
  6. package/dist/build-info.json +1 -1
  7. package/dist/{builder-2D3WSOOY.js → builder-DUI2ZH2H.js} +5 -6
  8. package/dist/{check-CNTMSGOG.js → check-5C4YDZYV.js} +53 -50
  9. package/dist/{chunk-R7PF5OOU.js → chunk-3CWUOFV6.js} +21 -14
  10. package/dist/{chunk-R4ZQQ7U2.js → chunk-3OFZM4X6.js} +551 -72
  11. package/dist/{chunk-2MNPHL23.js → chunk-47TTLEDX.js} +6 -5
  12. package/dist/{chunk-PLMTLGYH.js → chunk-52NC2FIU.js} +4 -3
  13. package/dist/chunk-6CHIYEZZ.js +564 -0
  14. package/dist/{chunk-TI7B7UEO.js → chunk-AMFWYESY.js} +1 -1
  15. package/dist/{chunk-YRYMNBGQ.js → chunk-ANPNT6PM.js} +2 -1
  16. package/dist/{chunk-JJEGKEFV.js → chunk-B3UKB46N.js} +6 -6
  17. package/dist/{chunk-JXYVA45C.js → chunk-BMYGAGBJ.js} +1050 -304
  18. package/dist/{chunk-4Q7OMKAT.js → chunk-BQXFBI7T.js} +81 -5
  19. package/dist/chunk-CRB3POP4.js +34 -0
  20. package/dist/{chunk-F47JBIAW.js → chunk-DSF2FWAZ.js} +2 -1
  21. package/dist/{chunk-N5FOE5PS.js → chunk-ECODQ7II.js} +4 -33
  22. package/dist/{chunk-NBGDBTO2.js → chunk-ESC6UR76.js} +6 -5
  23. package/dist/{chunk-HHJDN6AU.js → chunk-ESVW4MJY.js} +1 -1
  24. package/dist/{chunk-42ZI52JW.js → chunk-F4XSVQJ4.js} +4 -3
  25. package/dist/{chunk-FWX735W7.js → chunk-FUB7LRUE.js} +7 -7
  26. package/dist/chunk-G2YNJIGF.js +44 -0
  27. package/dist/{chunk-QKFB4K7X.js → chunk-GGOMT6KF.js} +1 -1
  28. package/dist/{chunk-CA3XMYFN.js → chunk-GOBOJMOJ.js} +6 -6
  29. package/dist/{chunk-N6LURUEF.js → chunk-GOSFARMS.js} +1 -1
  30. package/dist/chunk-JBJTC3UU.js +35 -0
  31. package/dist/{chunk-U3HXXRTY.js → chunk-JF42URPU.js} +16 -15
  32. package/dist/{chunk-ADJRGVZC.js → chunk-KDVIU23F.js} +1 -1
  33. package/dist/{chunk-6QRMLNH3.js → chunk-KK4VBOH6.js} +93 -37
  34. package/dist/{chunk-KMDLFI4P.js → chunk-L2USZT6T.js} +1 -1
  35. package/dist/{chunk-WTE3MQML.js → chunk-LXAY4A2G.js} +3 -12
  36. package/dist/{chunk-V7NIVOFD.js → chunk-M2ZWWTWZ.js} +1 -1
  37. package/dist/{chunk-ERI5GBNI.js → chunk-MLAAT4S6.js} +3 -3
  38. package/dist/{chunk-POJMEEJJ.js → chunk-MNTSH4CL.js} +1 -1
  39. package/dist/{chunk-WUYTWNMA.js → chunk-MQJSXHV7.js} +1 -1
  40. package/dist/{chunk-O765JR4W.js → chunk-MTP2QEVO.js} +1 -1
  41. package/dist/{chunk-ZM2IMMYN.js → chunk-N2PXGQRL.js} +9 -1
  42. package/dist/{chunk-J5M7BSJQ.js → chunk-N3CUDFCW.js} +9 -3
  43. package/dist/{chunk-4KSQKQXE.js → chunk-NACDBXUG.js} +19 -15
  44. package/dist/{chunk-RSPXEVDX.js → chunk-NL4XPC7W.js} +1 -1
  45. package/dist/{chunk-EHAAPRWY.js → chunk-QQM2UJ6T.js} +31 -26
  46. package/dist/{chunk-5FEK6PNE.js → chunk-RAZTWYZN.js} +10 -9
  47. package/dist/{chunk-4NPZVMJU.js → chunk-S26M3YBJ.js} +60 -4
  48. package/dist/{chunk-UGP76IZ5.js → chunk-S5PCA3SN.js} +1 -1
  49. package/dist/{chunk-YDSMROTB.js → chunk-S76AYB4M.js} +25 -20
  50. package/dist/{chunk-RMWLBUCJ.js → chunk-SADUQSO4.js} +1 -1
  51. package/dist/{chunk-OZGHL5Y5.js → chunk-SWCTGCSC.js} +2 -2
  52. package/dist/{chunk-GQD22KKN.js → chunk-T2Q7ISPE.js} +2 -1
  53. package/dist/{chunk-C5Y5ZJJZ.js → chunk-T4Y6SCG3.js} +5 -4
  54. package/dist/{chunk-6JNURGOA.js → chunk-ULSIJGLS.js} +45 -13
  55. package/dist/{chunk-ONRCEE6D.js → chunk-UYOYTKE2.js} +4 -4
  56. package/dist/{chunk-PTW6W5NR.js → chunk-VYAL7PUE.js} +1 -1
  57. package/dist/{chunk-JEHRUE3F.js → chunk-W5VPCONO.js} +166 -196
  58. package/dist/{chunk-NPQWKGUA.js → chunk-WBT5X3NH.js} +5 -4
  59. package/dist/{chunk-RLYZLUQ7.js → chunk-WGAUY5LE.js} +2 -2
  60. package/dist/{chunk-RSNSZ2LJ.js → chunk-X5H2WYMB.js} +1 -1
  61. package/dist/{chunk-OHVJA253.js → chunk-X5N6KC6V.js} +47 -29
  62. package/dist/{chunk-22RMZBGW.js → chunk-XGP3VYOG.js} +1 -1
  63. package/dist/{chunk-HYZRY4KZ.js → chunk-XNGMQ27R.js} +16 -6
  64. package/dist/{chunk-W3EKCZSQ.js → chunk-Y5GE76XC.js} +180 -53
  65. package/dist/{chunk-EWORLJMO.js → chunk-Y5MXET4M.js} +277 -526
  66. package/dist/{chunk-XCK2YAY5.js → chunk-YVZJ6GKF.js} +3 -2
  67. package/dist/{chunk-YO4PSLVB.js → chunk-ZRL6HKBY.js} +1 -1
  68. package/dist/claude-launcher-YVXI76NX.js +102 -0
  69. package/dist/{cli-AEWH62UE.js → cli-C77H54BV.js} +489 -174
  70. package/dist/{command-HHPBR6CS.js → command-H5CILO4S.js} +40 -37
  71. package/dist/{config-N3C6DFQA.js → config-FXDG4NPJ.js} +2 -3
  72. package/dist/connect-cmd-ZW5FV3ZW.js +17 -0
  73. package/dist/{context-adopt-VMMGFRUZ.js → context-adopt-O5OFXMKI.js} +13 -12
  74. package/dist/context-materialize-Y5IJ4MQT.js +23 -0
  75. package/dist/{dist-LKDWQWZ7.js → dist-QTX4RSCL.js} +2 -2
  76. package/dist/{docker-4J77Q2BF.js → docker-RC3PRX3Q.js} +7 -8
  77. package/dist/{doctor-N6Y7HKK3.js → doctor-2NQQML6P.js} +47 -39
  78. package/dist/{emit-artifact-cmd-H5MH3AOP.js → emit-artifact-cmd-7MP63KXU.js} +25 -22
  79. package/dist/emit-cmd-PPN4QQF2.js +14 -0
  80. package/dist/{emit-criteria-cmd-HBP6XS5C.js → emit-criteria-cmd-3RXPDLNK.js} +16 -14
  81. package/dist/{emit-finding-cmd-ZXDRL6PG.js → emit-finding-cmd-OL7JB6H3.js} +18 -16
  82. package/dist/{emit-result-cmd-RFKALS5Z.js → emit-result-cmd-UZVMOCF4.js} +20 -17
  83. package/dist/{exec-client-CQV746UF.js → exec-client-7UMESIE2.js} +3 -4
  84. package/dist/governing-source-BUU4IABS.js +35 -0
  85. package/dist/guest-init-KPBQOFE5.js +9 -0
  86. package/dist/hidden-prompt-UXBFMQC5.js +68 -0
  87. package/dist/{hook-fast-D7HV4IXG.js → hook-fast-W5MKHMJT.js} +4 -1
  88. package/dist/host-key-U4B5QETR.js +8 -0
  89. package/dist/{import-capture-cmd-EQ35EARA.js → import-capture-cmd-ZO6KGHI2.js} +7 -5
  90. package/dist/{import-ruler-cmd-BGAZXCQM.js → import-ruler-cmd-KJMWL5V6.js} +5 -3
  91. package/dist/index.js +8 -4
  92. package/dist/init-VWXIZEAN.js +54 -0
  93. package/dist/{init-FBCA4DRA.js → init-WNXXYMO7.js} +80 -53
  94. package/dist/{init-cmd-CAG572UE.js → init-cmd-WS6TIRYT.js} +28 -24
  95. package/dist/{init-update-VDFF4K3B.js → init-update-E3J6TVIN.js} +27 -26
  96. package/dist/{install-HDBIPHTX.js → install-CBWIEF3R.js} +4 -5
  97. package/dist/{install-cmd-5HYMXLEW.js → install-cmd-QDOTR4OM.js} +5 -4
  98. package/dist/{login-browser-QZCS3WW6.js → login-browser-O63QZN7D.js} +4 -3
  99. package/dist/{logout-cmd-QAUFDJ6J.js → logout-cmd-5V5MMBSY.js} +3 -2
  100. package/dist/{logs-H55NWW4H.js → logs-AMLKC3VP.js} +3 -1
  101. package/dist/loop-UGI2BJOT.js +76 -0
  102. package/dist/machine-JOW4RNZ7.js +49 -0
  103. package/dist/{mcp-install-cmd-FDTPQQS6.js → mcp-install-cmd-GGWB3JJ2.js} +3 -2
  104. package/dist/{mcp-serve-cmd-3KVYEUXG.js → mcp-serve-cmd-DOTY5FKY.js} +10 -8
  105. package/dist/paths-7XP63OAX.js +7 -0
  106. package/dist/pending-532ABZYC.js +8 -0
  107. package/dist/{pidfile-UZRH774M.js → pidfile-3DM4OBKO.js} +4 -2
  108. package/dist/{record-C43U4PKS.js → record-3WP6V2K5.js} +3 -2
  109. package/dist/{runner-env-EN7YE7QX.js → runner-env-4NB2GA7O.js} +5 -6
  110. package/dist/runner-key-TC3NFWFV.js +177 -0
  111. package/dist/{session-sender-24L5EW25.js → session-sender-NNVPSPY7.js} +17 -14
  112. package/dist/spec-cmd-OWVTBB4J.js +116 -0
  113. package/dist/{starter-pack-KF66HGZR.js → starter-pack-OPWUDOWX.js} +7 -5
  114. package/dist/{status-XHSGEYO5.js → status-TKCLLKFG.js} +42 -39
  115. package/dist/{statusline-fast-SVTMBMD7.js → statusline-fast-KN3BX67H.js} +2 -1
  116. package/dist/{sync-cmd-5G5EIEHD.js → sync-cmd-Q6VYNPWG.js} +16 -13
  117. package/dist/{watch-UTOXO4SE.js → watch-ARAGADTF.js} +16 -13
  118. package/dist/{work-claim-cmd-RKHYFE5Y.js → work-claim-cmd-34FERLYX.js} +25 -27
  119. package/dist/work-cmd-7S5B2H2P.js +25 -0
  120. package/dist/{work-comment-cmd-A55CUSYE.js → work-comment-cmd-SZJSPM5S.js} +15 -13
  121. package/dist/{work-launch-R5HTAP6X.js → work-launch-LEQ2MPM3.js} +3 -1
  122. package/dist/{work-open-pr-cmd-E6O53IJD.js → work-open-pr-cmd-RPXKCBWS.js} +17 -15
  123. package/dist/work-pr-cmd-5LGANL32.js +26 -0
  124. package/package.json +3 -2
  125. package/skill-pack/maintained/outerlayer-amend/SKILL.md +6 -2
  126. package/skill-pack/maintained/outerlayer-cli/SKILL.md +2 -0
  127. package/skill-pack/maintained/outerlayer-evidence/SKILL.md +4 -2
  128. package/skill-pack/maintained/outerlayer-findings/SKILL.md +2 -0
  129. package/skill-pack/shipped.json +27 -5
  130. package/skill-pack/template/build/SKILL.md +25 -10
  131. package/skill-pack/template/build/references/agent-briefs.md +3 -1
  132. package/skill-pack/template/build/references/criteria-judge.md +27 -0
  133. package/skill-pack/template/build/references/release.md +27 -34
  134. package/skill-pack/template/build/scripts/criterion-citations.mjs +8 -3
  135. package/skill-pack/template/build/scripts/record-criteria.mjs +8 -2
  136. package/skill-pack/template/spec/SKILL.md +37 -10
  137. package/skill-pack/validators/acceptance-criteria.yaml +25 -25
  138. package/skill-pack/validators/code-review.yaml +6 -5
  139. package/dist/agent-setup-K3ABU5NL.js +0 -27
  140. package/dist/chunk-BYT5O5RV.js +0 -61
  141. package/dist/connect-cmd-JUAW6MU4.js +0 -16
  142. package/dist/context-materialize-2WXTS4CW.js +0 -22
  143. package/dist/emit-cmd-IJ3GVRFD.js +0 -12
  144. package/dist/guest-init-LJP5GPZF.js +0 -9
  145. package/dist/host-key-YMLXY6QB.js +0 -7
  146. package/dist/init-MYX7YMIH.js +0 -51
  147. package/dist/loop-75J4YXLZ.js +0 -73
  148. package/dist/machine-A6ZCTUGS.js +0 -46
  149. package/dist/paths-OYKMVYJP.js +0 -6
  150. package/dist/pending-MXLSRYMO.js +0 -7
  151. package/dist/runner-key-W25ISHI3.js +0 -155
  152. package/dist/work-cmd-DJQJEZVB.js +0 -23
  153. package/dist/work-pr-cmd-L77K32WK.js +0 -24
@@ -1,33 +1,36 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
- import { writeJobFile, readJobFile } from './chunk-RMWLBUCJ.js';
4
- import { DOCKERD_LOG } from './chunk-HYZRY4KZ.js';
5
- import { senderPidfile } from './chunk-ONRCEE6D.js';
6
- import { installScrubContext } from './chunk-U3HXXRTY.js';
7
- import { AssetError, describeCallFailure, obtainImage, ensureRootfs, gitRemoteFor, startServicesFor, createBuildDisk, machineConfig, refusalCode, resolveAgentUser, AgentUserError, runBuildSteps, removeStagedContext, stageGovernedContext, shellQuote, createRunnerLog, createContainerRuntime, stopContainer, cleanupContainer, outOfMemoryReason, formatDuration, retrySend, formatClock, containerSenderCommand, syncInContainer, copyTranscriptsFromContainer, GH_CONFIG_YML, ghHostsYml, ADDED_USER, isLiteralAddress, signalInContainer, deleteBuildDisk, startCheckContainer, lifecycleSteps, lifecycleFailureReason, writeEvalInput, EVAL_INPUT_NAME, copyTranscripts, senderSignalScript } from './chunk-EWORLJMO.js';
8
- import { childEnvironment } from './chunk-KMDLFI4P.js';
9
- import { waitForAgent, vmExecArgs } from './chunk-QKFB4K7X.js';
10
- import { socketPathProblem, vsockLinks, linkVsockSockets } from './chunk-RSNSZ2LJ.js';
11
- import { WorkCommandError, postJson, isSignatureRefusal, getJson } from './chunk-6QRMLNH3.js';
12
- import { isBuiltinContainer, isBuiltinProcess, isBuiltinVm, DEFAULT_CHECK_TIME_LIMIT_MINUTES } from './chunk-22RMZBGW.js';
13
- import { readBranchHead, diffBetween, fetchCommit } from './chunk-R7PF5OOU.js';
14
- import { isSameProcess, startTimeOf } from './chunk-N5FOE5PS.js';
3
+ import { writeJobFile, readJobFile } from './chunk-SADUQSO4.js';
4
+ import { senderPidfile } from './chunk-UYOYTKE2.js';
5
+ import { DOCKERD_LOG, HELPER_PATH, HELPER_RECORD } from './chunk-XNGMQ27R.js';
6
+ import { installScrubContext } from './chunk-JF42URPU.js';
7
+ import { AssetError, obtainImage, ensureRootfs, gitRemoteFor, startServicesFor, createBuildDisk, machineConfig, resolveAgentUser, AgentUserError, runBuildSteps, shellQuote, createContainerRuntime, stopContainer, cleanupContainer, outOfMemoryReason, containerSenderCommand, syncInContainer, copyTranscriptsFromContainer, GH_CONFIG_YML, ghHostsYml, ADDED_USER, isLiteralAddress, signalInContainer, deleteBuildDisk, startCheckContainer, lifecycleSteps, lifecycleFailureReason, writeEvalInput, EVAL_INPUT_NAME, copyTranscripts, senderSignalScript, containerJudgeEnvironment, copyJudgeTree } from './chunk-Y5MXET4M.js';
8
+ import { childEnvironment } from './chunk-L2USZT6T.js';
9
+ import { HASH_ARGV, parseProgramHash, JUDGE_PROGRAM, judgeCommand, MAX_REPORT_BYTES, JUDGE_MAX_TURNS, JUDGE_DIR } from './chunk-S26M3YBJ.js';
10
+ import { waitForAgent, vmExecArgs } from './chunk-GGOMT6KF.js';
11
+ import { socketPathProblem, vsockLinks, linkVsockSockets } from './chunk-X5H2WYMB.js';
12
+ import { describeCallFailure, resolveGoverningSource, refusalCode, removeStagedContext, stageGovernedContext, pinnedSourceOf, commandWithShim, createRunnerLog, formatDuration, retrySend, formatClock } from './chunk-6CHIYEZZ.js';
13
+ import { WorkCommandError, postJson, isSignatureRefusal, getJson } from './chunk-KK4VBOH6.js';
14
+ import { planArtifactUpload } from './chunk-MQJSXHV7.js';
15
+ import { isBuiltinContainer, isBuiltinProcess, isBuiltinVm, DEFAULT_CHECK_TIME_LIMIT_MINUTES } from './chunk-XGP3VYOG.js';
15
16
  import { parseSize } from './chunk-7RQ73DLO.js';
16
- import { claudeCredential, repositoriesOf, primaryOf, buildEnvironment, JOB_MOUNT, CHECKOUT_DIR, parseIsolation, proxyEnvironment, containerLocalAddress, destinationEnvironment, CLAUDE_CREDENTIAL_VARIABLES, builtInEnvironment, startBuildServices, BUILT_IN_NAMES, AGENT_CREDENTIAL_MISSING, sharedUserEnvironment, headerVariables, withTunnelRecord, RELAY_PORT, checkEnvironment, EVAL_INPUT_DIR } from './chunk-4Q7OMKAT.js';
17
+ import { readBranchHead, fetchCommit, diffBetween } from './chunk-3CWUOFV6.js';
17
18
  import { CLAUDE_CODE_VERSION } from './chunk-CX22S5BA.js';
18
- import { planArtifactUpload } from './chunk-WUYTWNMA.js';
19
- import { scrubText } from './chunk-JXYVA45C.js';
20
- import { withHooksPath, authorEnvironment, commitIdentityRecord } from './chunk-BYT5O5RV.js';
21
- import { parsePolicy } from './chunk-OHVJA253.js';
19
+ import { isSameProcess, startTimeOf } from './chunk-JBJTC3UU.js';
20
+ import { scrubText } from './chunk-BMYGAGBJ.js';
21
+ import { resolvePackRoot } from './chunk-MNTSH4CL.js';
22
+ import { parsePolicy } from './chunk-X5N6KC6V.js';
22
23
  import { readCloudConfig } from './chunk-EABW6AJQ.js';
23
24
  import { resolveVersion } from './chunk-RA3O54FC.js';
24
- import { signerFor } from './chunk-V7NIVOFD.js';
25
- import { runnerDir, buildDir, vsockPath, RUNNER_FILE_MODE } from './chunk-ZM2IMMYN.js';
25
+ import { signerFor } from './chunk-M2ZWWTWZ.js';
26
+ import { runnerDir, buildDir, vsockPath, RUNNER_FILE_MODE } from './chunk-N2PXGQRL.js';
27
+ import { claudeCredential, repositoriesOf, primaryOf, buildEnvironment, JOB_MOUNT, CHECKOUT_DIR, parseIsolation, proxyEnvironment, withProxyWarningHidden, containerLocalAddress, destinationEnvironment, CLAUDE_CREDENTIAL_VARIABLES, builtInEnvironment, withHooksPath, startBuildServices, BUILT_IN_NAMES, AGENT_CREDENTIAL_MISSING, sharedUserEnvironment, headerVariables, authorEnvironment, withTunnelRecord, commitIdentityRecord, RELAY_PORT, repositoryPath, lifecycleWorkdir, checkEnvironment, EVAL_INPUT_DIR } from './chunk-BQXFBI7T.js';
26
28
  import { homeFromEnv } from './chunk-VNVZDWO3.js';
27
29
  import { spawn, spawnSync } from 'child_process';
28
30
  import { randomBytes, createHash } from 'crypto';
29
31
  import { mkdirSync, chmodSync, writeFileSync, appendFileSync, statSync, accessSync, constants, openSync, existsSync, readFileSync, closeSync, rmSync, readdirSync, lstatSync, realpathSync } from 'fs';
30
32
  import { join, isAbsolute, dirname, delimiter, sep } from 'path';
33
+ import { pathToFileURL } from 'url';
31
34
 
32
35
  createRequire(import.meta.url);
33
36
  var DEFAULT_MEMORY = "8g";
@@ -103,11 +106,16 @@ async function provisionMicroVm(input) {
103
106
  } catch (err) {
104
107
  return fail(err instanceof AssetError ? err.message : `the microVM's assets could not be had: ${describeCallFailure(err)}`);
105
108
  }
109
+ const governing = await resolveGoverningSource({ config, repository, fetchImpl: input.fetchImpl });
110
+ if (!governing.ok) return fail(governing.reason);
111
+ const governingSource = governing.source;
106
112
  const obtained = await obtainImage({
107
113
  config,
108
114
  runtime,
109
115
  tokens,
110
- repository,
116
+ repository: governingSource.kind === "governed" ? governingSource.repository : repository,
117
+ pinned: governingSource.kind === "governed" ? { commit: governingSource.sha, ref: governingSource.ref } : void 0,
118
+ governedRepository: repository,
111
119
  tag,
112
120
  log,
113
121
  aborted: input.aborted,
@@ -127,9 +135,10 @@ async function provisionMicroVm(input) {
127
135
  containerEnv: report.apply.containerEnv,
128
136
  builtIns: true,
129
137
  lifecycle: report.apply.lifecycle,
138
+ ...governingSource.kind === "governed" ? { governing: { repository: governingSource.repository, ref: governingSource.ref, commit: governingSource.sha } } : {},
130
139
  ...report.docker === true ? { docker: true } : {}
131
140
  };
132
- const env = buildEnvironment({
141
+ const envInput = {
133
142
  config,
134
143
  item: item.number,
135
144
  itemKey,
@@ -138,7 +147,8 @@ async function provisionMicroVm(input) {
138
147
  hostEnv: runtime.hostEnv,
139
148
  containerEnv: report.apply.containerEnv,
140
149
  builtIns: true
141
- });
150
+ };
151
+ const env = buildEnvironment(envInput);
142
152
  if (env.skippedTokenVariables.length > 0) {
143
153
  log.line(`${tag} not passing ${env.skippedTokenVariables.join(", ")} from runner.build.variables into the build: its repository token comes from the runner`);
144
154
  }
@@ -277,17 +287,24 @@ async function provisionMicroVm(input) {
277
287
  }
278
288
  state.agentUser = agentUser;
279
289
  const tools = await asRoot(runtime, state, ["cat", "/opt/outerlayer/tools.json"]);
280
- environment.agentVersion = agentVersionOf(tools.status === 0 ? tools.stdout : "");
290
+ const agentVersion = agentVersionOf(tools.status === 0 ? tools.stdout : "");
291
+ environment.agentVersion = agentVersion;
292
+ const program = await asRoot(runtime, state, [...HASH_ARGV]);
293
+ const hashed = program.status === 0 ? parseProgramHash(program.stdout) : void 0;
294
+ if (hashed !== void 0) state.judgeProgram = { path: JUDGE_PROGRAM, sha256: hashed };
281
295
  input.save(state, environment);
282
296
  if (report.docker === true) {
283
297
  const failure = await waitForDocker(runtime, state, agentUser, input.now ?? (() => /* @__PURE__ */ new Date()));
284
298
  if (failure !== void 0) return bail(failure);
285
299
  }
286
- const request = (argv, workdir) => ({ argv, user: agentUser, cwd: workdir, env: execEnvironment(state.imageEnv, env) });
300
+ const execEnv = buildEnvironment({ ...envInput, imageEnv: state.imageEnv });
301
+ const request = (argv, workdir) => ({ argv, user: agentUser, cwd: workdir, env: execEnvironment(state.imageEnv, execEnv) });
287
302
  const steps = await runBuildSteps({
288
303
  repository,
289
304
  branch: primary.branch,
290
305
  repositories,
306
+ ...governingSource.kind === "governed" ? { governing: { repository: governingSource.repository } } : {},
307
+ agentVersion,
291
308
  lifecycle: report.apply.lifecycle,
292
309
  exec: async (argv, workdir, opts) => {
293
310
  const ran = await vm.exec(state.vsockPath, request(argv, workdir));
@@ -300,6 +317,7 @@ async function provisionMicroVm(input) {
300
317
  stageContext: () => stageGovernedContext({
301
318
  config,
302
319
  repository,
320
+ source: pinnedSourceOf(governingSource),
303
321
  buildId,
304
322
  buildDir: buildDir2,
305
323
  fetchImpl: input.fetchImpl,
@@ -321,6 +339,7 @@ async function provisionMicroVm(input) {
321
339
  });
322
340
  if (!steps.ok) return bail(steps.reason, steps.aborted === true);
323
341
  state.startCommit = steps.startCommit;
342
+ if (steps.controlPlane !== void 0) environment.controlPlane = steps.controlPlane;
324
343
  return { ok: true, workdir: CHECKOUT_DIR, state, environment };
325
344
  }
326
345
  async function waitForDocker(runtime, state, agentUser, now) {
@@ -365,7 +384,8 @@ function vmExecFor(input, argv) {
365
384
  hostEnv: input.runtime.hostEnv,
366
385
  containerEnv: state.containerEnv ?? {},
367
386
  builtIns: state.builtIns === true,
368
- identity: input.job.commitIdentity
387
+ identity: input.job.commitIdentity,
388
+ imageEnv: state.imageEnv
369
389
  });
370
390
  const options = {
371
391
  socket: state.vsockPath,
@@ -378,7 +398,7 @@ function vmExecFor(input, argv) {
378
398
  return { options, env: { ...childEnvironment(input.runtime.hostEnv), ...env.passed } };
379
399
  }
380
400
  function microVmCommandLine(input) {
381
- const { options, env } = vmExecFor(input, ["/bin/sh", "-c", input.cmdline]);
401
+ const { options, env } = vmExecFor(input, ["/bin/sh", "-c", commandWithShim(input.job, input.cmdline)]);
382
402
  return { line: [process.execPath, input.cli, "vm-exec", ...vmExecArgs(options)].map(shellQuote).join(" "), options, env };
383
403
  }
384
404
  function microVmSenderCommand(input) {
@@ -403,7 +423,8 @@ async function syncInMicroVm(input) {
403
423
  hostEnv: runtime.hostEnv,
404
424
  containerEnv: state.containerEnv ?? {},
405
425
  builtIns: state.builtIns === true,
406
- identity: job.commitIdentity
426
+ identity: job.commitIdentity,
427
+ imageEnv: state.imageEnv
407
428
  });
408
429
  const result = await runtime.vm.exec(state.vsockPath, {
409
430
  argv: ["outerlayer", "sync"],
@@ -429,6 +450,98 @@ async function copyTranscriptsFromMicroVm(runtime, state) {
429
450
  if (!isRunning(runtime, state)) return { ok: false, reason: "the build's microVM is gone" };
430
451
  return copyTranscripts((argv) => asRoot(runtime, state, argv));
431
452
  }
453
+ function guestStopScript(paths = { proc: "/proc", record: HELPER_RECORD }) {
454
+ return [
455
+ `record=${shellQuote(paths.record)}`,
456
+ '[ -r "$record" ] || exit 2',
457
+ 'keep=" "',
458
+ 'while read -r kpid kstart; do keep="$keep$kpid:$kstart "; done < "$record"',
459
+ "n=0",
460
+ "while [ $n -lt 50 ]; do",
461
+ ' left=""',
462
+ // The list of /proc entries is made once per pass, so the commands this script runs are not in it.
463
+ ` for p in ${shellQuote(paths.proc)}/[0-9]*; do`,
464
+ " pid=${p##*/}",
465
+ ' [ "$pid" = 1 ] && continue',
466
+ ' [ "$pid" = "$$" ] && continue',
467
+ ' stat=$(cat "$p/stat" 2>/dev/null) || continue',
468
+ // Field 2 of stat is the command name, which can hold spaces, so the fields are counted from its closing ")":
469
+ // $1 is the state, $7 the flags, ${18} the thread count and ${20} the start time.
470
+ ' set -- ${stat##*") "}',
471
+ // 0x200000 is PF_KTHREAD.
472
+ " [ $(($7 & 2097152)) -ne 0 ] && continue",
473
+ ' [ "$1" = Z ] && [ "${18}" -le 1 ] && continue',
474
+ ' case "$keep" in *" $pid:${20} "*) continue ;; esac',
475
+ ' left="$left $pid"',
476
+ " done",
477
+ ' [ -z "$left" ] && exit 0',
478
+ " for pid in $left; do kill -KILL $pid 2>/dev/null; done",
479
+ " n=$((n+1))",
480
+ " sleep 0.2",
481
+ "done",
482
+ "exit 1"
483
+ ].join("\n");
484
+ }
485
+ var GUEST_JUDGE_DIR = JUDGE_DIR;
486
+ function microVmJudgeEnvironment(input) {
487
+ const { runtime, job } = input;
488
+ const state = job.microvm;
489
+ const agentUser = state.agentUser ?? ADDED_USER;
490
+ const needsVm = () => {
491
+ if (state.firecracker !== void 0 && !isRunning(runtime, state)) throw new Error("the build's microVM is gone");
492
+ };
493
+ return {
494
+ stop: async () => {
495
+ needsVm();
496
+ const stopped = await asRoot(runtime, state, ["/bin/sh", "-c", guestStopScript()]);
497
+ if (stopped.status === 2) throw new Error(`the guest has no record of the runner's helper processes at ${HELPER_RECORD}, so nothing was stopped`);
498
+ if (stopped.status !== 0) throw new Error(`processes of the build were still running after they were killed (exit ${stopped.status})`);
499
+ },
500
+ hashProgram: async () => {
501
+ needsVm();
502
+ const ran = await asRoot(runtime, state, [...HASH_ARGV]);
503
+ const sha256 = ran.status === 0 ? parseProgramHash(ran.stdout) : void 0;
504
+ return sha256 === void 0 ? { ok: false, reason: firstLine(ran.stderr) || `the hash could not be taken (exit ${ran.status})` } : { ok: true, sha256 };
505
+ },
506
+ prepare: async (files, tree) => {
507
+ needsVm();
508
+ const staging = join(input.jobDir, "judge-input");
509
+ rmSync(staging, { recursive: true, force: true });
510
+ mkdirSync(staging, { recursive: true });
511
+ try {
512
+ for (const [fileName, text2] of Object.entries(files)) writeFileSync(join(staging, fileName), text2);
513
+ if (tree !== void 0) copyJudgeTree(tree, join(staging, "tree"));
514
+ const dir = GUEST_JUDGE_DIR;
515
+ const script = [
516
+ "set -e",
517
+ `grep -q " ${dir} tmpfs " /proc/mounts || ${HELPER_PATH} mount --target ${dir} --source tmpfs --type tmpfs --options mode=0755`,
518
+ `find ${dir} -mindepth 1 -delete`,
519
+ `tar -x --no-same-owner -C ${dir}`,
520
+ `mkdir -p ${dir}/home/tmp`,
521
+ `chmod -R a+rX ${dir}`,
522
+ `chown -R ${shellQuote(agentUser)} ${dir}/home`,
523
+ `chmod 1777 ${dir}`
524
+ ].join("\n");
525
+ const result = await asRoot(runtime, state, ["/bin/sh", "-c", script], tarOf(staging));
526
+ if (result.status !== 0) throw new Error(`preparing ${dir} in the guest failed (exit ${result.status}): ${firstLine(result.stderr)}`);
527
+ } finally {
528
+ rmSync(staging, { recursive: true, force: true });
529
+ }
530
+ },
531
+ run: async (command, opts) => {
532
+ needsVm();
533
+ const ran = await runtime.vm.exec(state.vsockPath, { argv: command.argv, user: agentUser, cwd: command.cwd, env: command.env });
534
+ appendFileSync(opts.logPath, ran.stdout + ran.stderr);
535
+ return ran;
536
+ },
537
+ readReport: async () => {
538
+ needsVm();
539
+ const file = `${GUEST_JUDGE_DIR}/report.html`;
540
+ const read = await asRoot(runtime, state, ["/bin/sh", "-c", `[ -f ${file} ] && [ ! -L ${file} ] && head -c ${MAX_REPORT_BYTES + 1} ${file}`]);
541
+ return read.status === 0 && Buffer.byteLength(read.stdout) <= MAX_REPORT_BYTES ? read.stdout : void 0;
542
+ }
543
+ };
544
+ }
432
545
  async function startCheckMicroVm(input) {
433
546
  const { config, runtime, job, jobDir, tokens, log } = input;
434
547
  const { vm } = runtime;
@@ -548,7 +661,7 @@ async function startCheckMicroVm(input) {
548
661
  const failed = await boot();
549
662
  if (failed !== void 0) return fail(failed);
550
663
  if (abandoned()) return fail(ABANDONED);
551
- const env = checkEnvironment({ config, item: job.item, repository, hostEnv: runtime.hostEnv, containerEnv: built.containerEnv ?? {} });
664
+ const env = checkEnvironment({ config, item: job.item, repository, hostEnv: runtime.hostEnv, containerEnv: built.containerEnv ?? {}, imageEnv: built.imageEnv });
552
665
  const evalInput = { dir: join(checksDir, EVAL_INPUT_NAME), delivered: false };
553
666
  const deliverEvalInput = async () => {
554
667
  await unpackIntoGuest(runtime, state, evalInput.dir, EVAL_INPUT_DIR, { readOnly: true });
@@ -734,6 +847,7 @@ function hostCommandEnvironment(base, build, config, hooksPath) {
734
847
  Object.assign(env, proxyEnvironment(proxyUrlFor(build), brokerHost));
735
848
  } else if (build.proxyPort !== void 0) {
736
849
  Object.assign(env, proxyEnvironment(`http://127.0.0.1:${build.proxyPort}`));
850
+ env.NODE_OPTIONS = withProxyWarningHidden(env.NODE_OPTIONS);
737
851
  }
738
852
  const ports = build.destinations ?? [];
739
853
  const portOf = (name) => ports.find((entry) => entry.name === name)?.port;
@@ -877,9 +991,8 @@ async function settleVerdict(deps) {
877
991
  }
878
992
  var ENDED = "ended before provision finished";
879
993
  async function provisionProcess(input) {
880
- const { config, runtime, item, jobDir } = input;
994
+ const { config, runtime, item, repository, jobDir } = input;
881
995
  if (claudeCredential(runtime.hostEnv) === void 0) return { ok: false, reason: AGENT_CREDENTIAL_MISSING };
882
- const repository = item.specRepository;
883
996
  if (repository === void 0 || repository === null || repository === "") {
884
997
  return { ok: false, reason: "the work item names no repository to clone" };
885
998
  }
@@ -921,6 +1034,9 @@ var MAX_VALIDATOR_FILES = 20;
921
1034
  var VALIDATORS_DIR = ".outerlayer/validators";
922
1035
  var POLICY_FILE = ".outerlayer/policy.yaml";
923
1036
  var LIMIT_KEY_TAIL = " (runner.checks.timeLimitMinutes)";
1037
+ function isJudgeCheck(check) {
1038
+ return "judge" in check;
1039
+ }
924
1040
  function lastLine(text2) {
925
1041
  return text2.split(/\r\n|\r|\n/).map((line) => line.trim()).filter((line) => line !== "").at(-1);
926
1042
  }
@@ -956,17 +1072,24 @@ function checksIn(root, log, tag) {
956
1072
  const policy = readInside(root, POLICY_FILE);
957
1073
  const loaded = parsePolicy(policy === void 0 ? null : { path: POLICY_FILE, content: policy }, validatorFilesIn(root));
958
1074
  for (const problem of loaded.problems) log.error(`${tag} ${problem.file}: ${problem.problem}`);
959
- const hostChecks = loaded.customs.filter((def) => def.runWhere === "host" && def.level !== "off" && def.declaresEmit !== null && def.command !== void 0);
960
- return hostChecks.map((def) => ({ name: def.declaresEmit, command: def.command }));
1075
+ const hostChecks = loaded.customs.filter((def) => def.runWhere === "host" && def.level !== "off" && def.declaresEmit !== null);
1076
+ const checks = [];
1077
+ for (const def of hostChecks) {
1078
+ if (def.judge !== void 0) checks.push({ name: def.declaresEmit, judge: { brief: def.judge.brief, text: readInside(root, def.judge.brief) } });
1079
+ else if (def.command !== void 0) checks.push({ name: def.declaresEmit, command: def.command });
1080
+ }
1081
+ return checks;
961
1082
  }
962
1083
  async function readDefinitions(input, repository, scratch, token, tokens) {
963
1084
  const { config, job, deps } = input;
964
1085
  const tag = `#${job.item}`;
965
1086
  const stage = deps.stage ?? stageGovernedContext;
966
1087
  const controlPlaneUrl = (controlPlane) => deps.runtime.remoteFor?.(controlPlane) ?? `${deps.runtime.github.gitUrl}/${controlPlane}.git`;
1088
+ const governing = (job.container ?? job.microvm)?.governing;
967
1089
  const staged = await stage({
968
1090
  config,
969
1091
  repository,
1092
+ ...governing === void 0 ? {} : { source: { repository: governing.repository, ref: governing.ref, sha: governing.commit } },
970
1093
  buildId: `checks-${randomBytes(4).toString("hex")}`,
971
1094
  buildDir: scratch,
972
1095
  fetchImpl: deps.fetchImpl,
@@ -1004,6 +1127,33 @@ function noCleanEnvironment(job) {
1004
1127
  const ran = job.build?.workDir !== void 0 ? "builtin:process" : "a provision hook of the host's own";
1005
1128
  return `the host has no clean environment for checks: this build ran under ${ran}, and checks run only in a new container or microVM from a build's image`;
1006
1129
  }
1130
+ function makeRecorder(input) {
1131
+ const { config, job, deps } = input;
1132
+ const tag = `#${job.item}`;
1133
+ const now = deps.now ?? (() => /* @__PURE__ */ new Date());
1134
+ let signer;
1135
+ return async (name, result, sentence, opts = {}) => {
1136
+ signer ??= await signerFor(config.configPath);
1137
+ const keyTail = opts.keyTail ?? "";
1138
+ const body = sentence === void 0 ? void 0 : cutReasonLine(`${scrubReason(sentence, config) ?? "check failed"}${keyTail}`);
1139
+ const clientEmitId = `host-${createHash("sha256").update(`${job.claimId}:${job.startedAt}:${name}${opts.idTag === void 0 ? "" : `:${opts.idTag}`}`).digest("hex").slice(0, 40)}`;
1140
+ const sent = await retrySend(
1141
+ () => postJson(
1142
+ config.url,
1143
+ "/v1/emitted-results",
1144
+ config.apiKey,
1145
+ config.appId,
1146
+ { schemaVersion: 1, emit: { clientEmitId, name, result, ...body === void 0 ? {} : { body }, ...opts.link === void 0 ? {} : { link: opts.link }, emittedAt: now().toISOString(), item: { number: Number(job.item) } } },
1147
+ deps.fetchImpl,
1148
+ void 0,
1149
+ signer
1150
+ ),
1151
+ { now: deps.now }
1152
+ );
1153
+ if (sent.ok) deps.log.line(`${tag} check ${name} ${result}${body === void 0 ? "" : `: ${body}`}`);
1154
+ else deps.log.error(`${tag} check ${name} ${result} could not be recorded: ${sent.reason}`);
1155
+ };
1156
+ }
1007
1157
  async function runHostChecks(input) {
1008
1158
  const { config, job, jobDir, deps } = input;
1009
1159
  const { runtime, log } = deps;
@@ -1035,34 +1185,15 @@ async function runHostChecks(input) {
1035
1185
  rmSync(scratch, { recursive: true, force: true });
1036
1186
  return;
1037
1187
  }
1038
- const checks = definitions.checks;
1188
+ const checks = definitions.checks.filter((check) => !isJudgeCheck(check));
1039
1189
  if (checks.length === 0) {
1040
1190
  rmSync(scratch, { recursive: true, force: true });
1041
1191
  return;
1042
1192
  }
1043
1193
  log.line(`${tag} checks started (${checks.map((check) => check.name).join(", ")})`);
1044
1194
  const startedMs = now().getTime();
1045
- let signer;
1046
- const record = async (check, result, sentence, keyTail = "") => {
1047
- signer ??= await signerFor(config.configPath);
1048
- const body = sentence === void 0 ? void 0 : cutReasonLine(`${scrubReason(sentence, config) ?? "check failed"}${keyTail}`);
1049
- const clientEmitId = `host-${createHash("sha256").update(`${job.claimId}:${job.startedAt}:${check.name}`).digest("hex").slice(0, 40)}`;
1050
- const sent = await retrySend(
1051
- () => postJson(
1052
- config.url,
1053
- "/v1/emitted-results",
1054
- config.apiKey,
1055
- config.appId,
1056
- { schemaVersion: 1, emit: { clientEmitId, name: check.name, result, ...body === void 0 ? {} : { body }, emittedAt: now().toISOString(), item: { number: Number(job.item) } } },
1057
- deps.fetchImpl,
1058
- void 0,
1059
- signer
1060
- ),
1061
- { now: deps.now }
1062
- );
1063
- if (sent.ok) log.line(`${tag} check ${check.name} ${result}${body === void 0 ? "" : `: ${body}`}`);
1064
- else log.error(`${tag} check ${check.name} ${result} could not be recorded: ${sent.reason}`);
1065
- };
1195
+ const recorder = makeRecorder({ config, job, deps });
1196
+ const record = (check, result, sentence, keyTail = "") => recorder(check.name, result, sentence, { keyTail });
1066
1197
  const failAll = async (reason, keyTail = "") => {
1067
1198
  log.error(`${tag} checks failed: ${reason}${keyTail}`);
1068
1199
  for (const check of checks) await record(check, "fail", reason, keyTail);
@@ -1070,12 +1201,36 @@ async function runHostChecks(input) {
1070
1201
  try {
1071
1202
  if (job.container === void 0 && job.microvm === void 0) return await failAll(noCleanEnvironment(job));
1072
1203
  if (tokens === void 0) return await failAll("the build's repository tokens are no longer held, so the branch cannot be fetched");
1073
- const branch = job.branch;
1074
- if (branch === void 0) return await failAll("the build pushed no branch: the claim names none");
1075
- const remote = runtime.remoteFor?.(repository) ?? `${runtime.github.gitUrl}/${repository}.git`;
1076
- const read = await (deps.readBranch ?? readBranchHead)({ remote, dir: scratch, branch, token: readToken });
1077
- if (read.kind === "absent") return await failAll(`the build pushed no branch: ${branch} does not exist on ${repository}`);
1078
- if (read.kind === "unreadable") return await failAll(`the branch ${branch} could not be read: ${read.reason}`);
1204
+ if (job.branch === void 0) return await failAll("the build pushed no branch: the claim names none");
1205
+ const claimed = repositoriesOf({ repositories: job.repositories, repository, branch: job.branch });
1206
+ if (claimed.length === 0) return await failAll("the claim names no repository to check out");
1207
+ const remoteOf = (name) => runtime.remoteFor?.(name) ?? `${runtime.github.gitUrl}/${name}.git`;
1208
+ const remote = remoteOf(repository);
1209
+ const pushed = /* @__PURE__ */ new Map();
1210
+ const unpushed = [];
1211
+ let primaryHead;
1212
+ for (const entry of claimed) {
1213
+ let entryToken = readToken;
1214
+ if (!entry.primary) {
1215
+ try {
1216
+ entryToken = await tokens.token("read", entry.repository);
1217
+ } catch (err) {
1218
+ return await failAll(`no read token for ${entry.repository}: ${describeCallFailure(err)}`);
1219
+ }
1220
+ }
1221
+ const read = await (deps.readBranch ?? readBranchHead)({ remote: remoteOf(entry.repository), dir: scratch, branch: entry.branch, token: entryToken });
1222
+ if (read.kind === "unreadable") return await failAll(`the branch ${entry.branch} could not be read${entry.primary ? "" : ` on ${entry.repository}`}: ${read.reason}`);
1223
+ if (read.kind === "absent") {
1224
+ unpushed.push(entry);
1225
+ continue;
1226
+ }
1227
+ pushed.set(entry.repository, entry.branch);
1228
+ if (entry.primary) primaryHead = read.sha;
1229
+ }
1230
+ if (pushed.size === 0) {
1231
+ return await failAll(`the build pushed no branch: ${unpushed.map((entry) => `${entry.branch} does not exist on ${entry.repository}`).join("; ")}`);
1232
+ }
1233
+ const repositoriesToCheckOut = claimed;
1079
1234
  const limitMinutes = config.checks?.timeLimitMinutes ?? DEFAULT_CHECK_TIME_LIMIT_MINUTES;
1080
1235
  const minutes = `${limitMinutes} ${limitMinutes === 1 ? "minute" : "minutes"}`;
1081
1236
  const environmentLog = join(jobDir, "checks", "environment.log");
@@ -1130,20 +1285,33 @@ async function runHostChecks(input) {
1130
1285
  workdir: "/build/work",
1131
1286
  failed: (status) => `configuring git failed (exit ${status})`
1132
1287
  },
1133
- {
1134
- argv: ["git", "clone", "--filter=blob:none", "--single-branch", "--branch", branch, `https://github.com/${repository}.git`, CHECKOUT_DIR],
1135
- workdir: "/build/work",
1136
- failed: (status) => `the clone of ${branch} failed (exit ${status})`
1137
- }
1288
+ ...repositoriesToCheckOut.flatMap((entry) => {
1289
+ const branch = pushed.get(entry.repository);
1290
+ const path = entry.primary ? CHECKOUT_DIR : repositoryPath(entry.repository);
1291
+ const clone = {
1292
+ argv: ["git", "clone", "--filter=blob:none", "--single-branch", ...branch === void 0 ? [] : ["--branch", branch], `https://github.com/${entry.repository}.git`, path],
1293
+ workdir: "/build/work",
1294
+ failed: (status) => `the clone of ${branch ?? `the default branch of ${entry.repository}`} failed (exit ${status})`
1295
+ };
1296
+ return entry.primary && repositoriesToCheckOut.length > 1 ? [
1297
+ clone,
1298
+ {
1299
+ argv: ["sh", "-c", 'mkdir -p "$(dirname "$1")" && ln -s "$2" "$1"', "link", repositoryPath(entry.repository), CHECKOUT_DIR],
1300
+ workdir: "/build/work",
1301
+ failed: (status) => `linking ${entry.repository} at ${repositoryPath(entry.repository)} failed (exit ${status})`
1302
+ }
1303
+ ] : [clone];
1304
+ })
1138
1305
  ];
1139
1306
  for (const step of steps) {
1140
1307
  if (abandoned) return "the check environment setup ran out of time";
1141
1308
  const ran = await made.exec(step.argv, step.workdir, { logPath: environmentLog });
1142
1309
  if (ran.status !== 0) return `the check environment could not be prepared: ${step.failed(ran.status)}`;
1143
1310
  }
1311
+ const lifecycleDir = lifecycleWorkdir(repositoriesToCheckOut, (job.container ?? job.microvm)?.governing?.repository);
1144
1312
  for (const step of lifecycleSteps((job.container ?? job.microvm)?.lifecycle ?? {})) {
1145
1313
  log.line(`${tag} checks lifecycle ${step.name} (${step.source})`);
1146
- const ran = await made.exec(step.argv, CHECKOUT_DIR, { logPath: environmentLog });
1314
+ const ran = await made.exec(step.argv, lifecycleDir, { logPath: environmentLog });
1147
1315
  if (ran.status !== 0) return `the check environment could not be prepared: ${lifecycleFailureReason(step, ran.status)}`;
1148
1316
  }
1149
1317
  return made;
@@ -1172,7 +1340,7 @@ async function runHostChecks(input) {
1172
1340
  scratch,
1173
1341
  remote,
1174
1342
  readToken,
1175
- branchHead: read.sha,
1343
+ branchHead: primaryHead ?? (job.container ?? job.microvm)?.startCommit ?? "",
1176
1344
  transcripts,
1177
1345
  scrub: transcriptScrub(config, [config.apiKey, job.itemKey, readToken]),
1178
1346
  readWorkItem: () => readWorkItem(config, job.item, deps.fetchImpl),
@@ -1239,6 +1407,296 @@ async function runHostChecks(input) {
1239
1407
  }
1240
1408
  }
1241
1409
 
1410
+ // src/runner/judge-verdict.ts
1411
+ function mechanicalFailures(criteria, citations) {
1412
+ return criteria.filter((criterion) => criterion.proof === null && (citations[criterion.id]?.length ?? 0) === 0).map((criterion) => criterion.id);
1413
+ }
1414
+ function firstLine2(text2) {
1415
+ return text2.trim().split(/\r\n|\r|\n/)[0]?.trim() ?? "";
1416
+ }
1417
+ function parseOutput(stdout) {
1418
+ try {
1419
+ const parsed = JSON.parse(stdout);
1420
+ if (typeof parsed !== "object" || parsed === null) return void 0;
1421
+ const { subtype, result } = parsed;
1422
+ return { subtype: typeof subtype === "string" ? subtype : void 0, result: typeof result === "string" ? result : "" };
1423
+ } catch {
1424
+ return void 0;
1425
+ }
1426
+ }
1427
+ var RESULT_LINE = /^result:\s*(pass|fail)\s*$/i;
1428
+ var UNMET_LINE = /^unmet:\s*(\S+)\s*$/i;
1429
+ function decideVerdict(input) {
1430
+ const { run, criteria } = input;
1431
+ if (run.kind === "timeout") return { result: "fail", reason: `the judge ran out of time after ${input.minutes}`, limitKey: true };
1432
+ const output = parseOutput(run.stdout);
1433
+ if (output === void 0) {
1434
+ const said = firstLine2(run.stderr);
1435
+ return { result: "fail", reason: `the judge exited ${run.status} and printed no result${said === "" ? "" : `: ${said}`}` };
1436
+ }
1437
+ if (output.subtype === "error_max_turns") return { result: "fail", reason: `the judge reached the turn limit of ${JUDGE_MAX_TURNS} turns before it gave a result` };
1438
+ const lines = output.result.split(/\r\n|\r|\n/).map((line) => line.trim()).filter((line) => line !== "");
1439
+ const verdict = RESULT_LINE.exec(lines.at(-1) ?? "")?.[1]?.toLowerCase();
1440
+ if (verdict === void 0) return { result: "fail", reason: "the judge gave no `result:` line" };
1441
+ const named = new Set(lines.flatMap((line) => UNMET_LINE.exec(line)?.[1] ?? []));
1442
+ const mechanical = new Set(mechanicalFailures(criteria, input.citations));
1443
+ const unmet = criteria.map((criterion) => criterion.id).filter((id) => named.has(id) || mechanical.has(id));
1444
+ if (verdict === "pass" && unmet.length === 0) return { result: "pass" };
1445
+ const ruled = unmet.filter((id) => mechanical.has(id));
1446
+ const rule = ruled.length === 0 ? "" : `: the mechanical rule found no proof annotation and no citing test for ${ruled.join(", ")}`;
1447
+ if (unmet.length === 0) return { result: "fail", reason: "the judge failed the change and named no criterion" };
1448
+ return { result: "fail", reason: `${unmet.length} of ${criteria.length} criteria not satisfied, first ${unmet[0]}${rule}` };
1449
+ }
1450
+
1451
+ // src/runner/judge.ts
1452
+ var LIMIT_KEY_TAIL2 = " (runner.checks.timeLimitMinutes)";
1453
+ var MAX_DIFF_BYTES = 4 * 1024 * 1024;
1454
+ var MAX_TEST_FILES = 2e4;
1455
+ var MAX_TEST_FILE_BYTES = 1024 * 1024;
1456
+ var NO_CLAUDE = "the image has no Claude Code for the judge";
1457
+ var REPORT_CAPTION = "The host's criteria judge, each criterion judged against the test that cites it";
1458
+ async function loadCitationFinder() {
1459
+ return await import(pathToFileURL(join(resolvePackRoot(), "template", "build", "scripts", "criterion-citations.mjs")).href);
1460
+ }
1461
+ function testFilesIn(root, isTest, limits = { files: MAX_TEST_FILES, bytes: MAX_TEST_FILE_BYTES }) {
1462
+ const files = [];
1463
+ const walk = (dir, prefix) => {
1464
+ for (const name of readdirSync(dir).sort()) {
1465
+ if (files.length >= limits.files) return;
1466
+ if (prefix === "" && name === ".git") continue;
1467
+ const path = join(dir, name);
1468
+ const rel = prefix === "" ? name : `${prefix}/${name}`;
1469
+ const stat = lstatSync(path);
1470
+ if (stat.isDirectory()) walk(path, rel);
1471
+ else if (stat.isFile() && isTest.test(rel) && stat.size <= limits.bytes) files.push({ path: rel, contents: readFileSync(path, "utf8") });
1472
+ }
1473
+ };
1474
+ walk(root, "");
1475
+ return files;
1476
+ }
1477
+ async function hostPassedNames(config, item, fetchImpl) {
1478
+ const workItemId = Object(await readWorkItem(config, item, fetchImpl)).workItemId;
1479
+ if (typeof workItemId !== "string") throw new WorkCommandError("the work item read named no id");
1480
+ const signer = await signerFor(config.configPath);
1481
+ const latest = /* @__PURE__ */ new Map();
1482
+ let since = null;
1483
+ do {
1484
+ const query = since === null ? "" : `?since=${encodeURIComponent(since)}`;
1485
+ const page = await getJson(config.url, `/v1/work-items/${encodeURIComponent(workItemId)}/changes${query}`, config.apiKey, config.appId, fetchImpl, signer);
1486
+ for (const entry of Array.isArray(page.data) ? page.data : []) {
1487
+ const { name, result, source } = Object(entry);
1488
+ if (source === "host" && typeof name === "string" && typeof result === "string") latest.set(name, result);
1489
+ }
1490
+ since = page.pagination?.cursor ?? null;
1491
+ } while (since !== null);
1492
+ return [...latest].filter(([, result]) => result === "pass").map(([name]) => name);
1493
+ }
1494
+ function criteriaOf(workItem) {
1495
+ const criteria = Object(Object(workItem).criteria);
1496
+ if (!Array.isArray(criteria?.items)) return void 0;
1497
+ const drafted = criteria.drafted === true;
1498
+ const items = [];
1499
+ for (const entry of criteria.items) {
1500
+ const { id, text: text2, proof } = Object(entry);
1501
+ if (typeof id !== "string" || typeof text2 !== "string") continue;
1502
+ items.push({ id, text: text2, proof: typeof proof === "string" ? proof : null, drafted });
1503
+ }
1504
+ return items.length === 0 ? void 0 : items;
1505
+ }
1506
+ function environmentFor(input) {
1507
+ const { config, job, jobDir, deps } = input;
1508
+ if (deps.judgeEnvironment !== void 0) return deps.judgeEnvironment;
1509
+ if (job.container !== void 0) return containerJudgeEnvironment({ runtime: deps.runtime, config, job, jobDir });
1510
+ if (job.microvm !== void 0) return microVmJudgeEnvironment({ runtime: deps.runtime, job, jobDir });
1511
+ return void 0;
1512
+ }
1513
+ async function runJudge(input) {
1514
+ const { config, job, jobDir, deps } = input;
1515
+ const { runtime, log } = deps;
1516
+ const tag = `#${job.item}`;
1517
+ const now = deps.now ?? (() => /* @__PURE__ */ new Date());
1518
+ const repository = job.repository ?? job.container?.repository;
1519
+ if (repository === void 0) return;
1520
+ const repo = repository;
1521
+ const tokens = runtime.credentials.get(jobDir);
1522
+ let readToken;
1523
+ try {
1524
+ readToken = await tokens?.token("read");
1525
+ } catch (err) {
1526
+ log.error(`${tag} no read token for the judge: ${describeCallFailure(err)}`);
1527
+ }
1528
+ const scratch = join(jobDir, "judge-scratch");
1529
+ rmSync(scratch, { recursive: true, force: true });
1530
+ const definitionsDir = join(scratch, "definitions");
1531
+ mkdirSync(definitionsDir, { recursive: true });
1532
+ let definitions;
1533
+ try {
1534
+ definitions = await readDefinitions(input, repository, definitionsDir, readToken, tokens);
1535
+ } catch (err) {
1536
+ definitions = { ok: false, reason: describeCallFailure(err) };
1537
+ } finally {
1538
+ removeStagedContext(definitionsDir);
1539
+ }
1540
+ if (!definitions.ok) {
1541
+ rmSync(scratch, { recursive: true, force: true });
1542
+ log.line(`${tag} judge not run: the definitions could not be read: ${definitions.reason}`);
1543
+ const passed = await retrySend(() => hostPassedNames(config, job.item, deps.fetchImpl), { now: deps.now });
1544
+ if (!passed.ok) {
1545
+ log.error(`${tag} the item's host results could not be read, so none was failed: ${passed.reason}`);
1546
+ return;
1547
+ }
1548
+ const record2 = makeRecorder({ config, job, deps });
1549
+ for (const name of passed.value) await record2(name, "fail", `the definitions could not be read: ${definitions.reason}`, { idTag: "judge-unread" });
1550
+ return;
1551
+ }
1552
+ const checks = definitions.checks.filter(isJudgeCheck);
1553
+ if (checks.length === 0) {
1554
+ rmSync(scratch, { recursive: true, force: true });
1555
+ return;
1556
+ }
1557
+ log.line(`${tag} judge started (${checks.map((check) => check.name).join(", ")})`);
1558
+ const startedMs = now().getTime();
1559
+ const record = makeRecorder({ config, job, deps });
1560
+ const fail = (check, reason) => {
1561
+ log.error(`${tag} judge failed: ${reason}`);
1562
+ return record(check.name, "fail", reason);
1563
+ };
1564
+ try {
1565
+ for (const check of checks) {
1566
+ try {
1567
+ await judgeOne(check);
1568
+ } catch (err) {
1569
+ await fail(check, `the judge could not be run: ${describeCallFailure(err)}`);
1570
+ }
1571
+ }
1572
+ } finally {
1573
+ rmSync(scratch, { recursive: true, force: true });
1574
+ log.line(`${tag} judge done (${formatDuration(now().getTime() - startedMs)})`);
1575
+ }
1576
+ async function judgeOne(check) {
1577
+ const environment = environmentFor(input);
1578
+ if (environment === void 0) return await fail(check, `${NO_CLAUDE}: this build ran outside a container or microVM`);
1579
+ const expected = (job.container ?? job.microvm)?.judgeProgram;
1580
+ if (expected === void 0) return await fail(check, NO_CLAUDE);
1581
+ if (check.judge.text === void 0) return await fail(check, `the brief ${check.judge.brief} is not in the definitions repository`);
1582
+ if (tokens === void 0) return await fail(check, "the build's repository tokens are no longer held, so the branch cannot be fetched");
1583
+ const credential = claudeCredential(runtime.hostEnv);
1584
+ if (credential === void 0) return await fail(check, "the host holds no Claude credential for the judge");
1585
+ const branch = job.branch;
1586
+ if (branch === void 0) return await fail(check, "the build pushed no branch: the claim names none");
1587
+ const remote = runtime.remoteFor?.(repo) ?? `${runtime.github.gitUrl}/${repo}.git`;
1588
+ try {
1589
+ await environment.stop();
1590
+ } catch (err) {
1591
+ return await fail(check, `the build's processes could not be stopped: ${describeCallFailure(err)}`);
1592
+ }
1593
+ const hashed = await environment.hashProgram();
1594
+ if (!hashed.ok) return await fail(check, `${NO_CLAUDE}: ${hashed.reason}`);
1595
+ if (hashed.sha256 !== expected.sha256) return await fail(check, "the judge program changed during the build");
1596
+ const read = await (deps.readBranch ?? readBranchHead)({ remote, dir: scratch, branch, token: readToken });
1597
+ if (read.kind === "absent") return await fail(check, `the build pushed no branch: ${branch} does not exist on ${repo}`);
1598
+ if (read.kind === "unreadable") return await fail(check, `the branch ${branch} could not be read: ${read.reason}`);
1599
+ const head = read.sha;
1600
+ let criteria;
1601
+ try {
1602
+ criteria = criteriaOf(await readWorkItem(config, job.item, deps.fetchImpl));
1603
+ } catch (err) {
1604
+ return await fail(check, `the item's criteria could not be read: ${describeCallFailure(err)}`);
1605
+ }
1606
+ if (criteria === void 0) return await fail(check, "no criteria are recorded for the item");
1607
+ const branchDir = join(scratch, "branch");
1608
+ const fetched = await (deps.fetchCommit ?? fetchCommit)({ remote, dir: branchDir, commit: head, token: readToken });
1609
+ if (!fetched.ok) return await fail(check, `the build's tests could not be fetched: ${fetched.reason}`);
1610
+ const finder = await loadCitationFinder();
1611
+ const citations = finder.buildCitations({ ids: criteria.map((criterion) => criterion.id), files: testFilesIn(branchDir, finder.TEST_FILE), repository: repo, sha: head });
1612
+ let diff = "";
1613
+ const startCommit = (job.container ?? job.microvm)?.startCommit;
1614
+ if (startCommit !== void 0) {
1615
+ const output = join(scratch, "diff.patch");
1616
+ const made = await (deps.fetchDiff ?? ((request) => diffBetween(request)))({ remote, dir: join(scratch, "diff"), base: startCommit, head, token: readToken, output });
1617
+ if (!made.ok) return await fail(check, `the build's diff could not be read: ${made.reason}`);
1618
+ const bytes = readFileSync(output);
1619
+ diff = bytes.length > MAX_DIFF_BYTES ? `${bytes.subarray(0, MAX_DIFF_BYTES).toString("utf8")}
1620
+ [the diff was cut at ${MAX_DIFF_BYTES} bytes]
1621
+ ` : bytes.toString("utf8");
1622
+ }
1623
+ await environment.prepare({
1624
+ "criteria.json": `${JSON.stringify({ criteria }, null, 2)}
1625
+ `,
1626
+ "citations.json": `${JSON.stringify(citations, null, 2)}
1627
+ `,
1628
+ "diff.patch": diff
1629
+ }, branchDir);
1630
+ const limitMinutes = config.checks?.timeLimitMinutes ?? DEFAULT_CHECK_TIME_LIMIT_MINUTES;
1631
+ const minutes = `${limitMinutes} ${limitMinutes === 1 ? "minute" : "minutes"}`;
1632
+ const clear = async () => {
1633
+ await environment.clear?.().catch((err) => log.error(`${tag} what the judge left in its folder could not be removed: ${describeCallFailure(err)}`));
1634
+ };
1635
+ const running = environment.run(judgeCommand({ brief: check.judge.text, credential }), { logPath: join(jobDir, "judge.log") });
1636
+ running.catch(() => void 0);
1637
+ const timer = timeoutAfter(limitMinutes * 6e4, deps.sleep);
1638
+ let outcome;
1639
+ try {
1640
+ outcome = await Promise.race([running, timer.done]);
1641
+ } catch (err) {
1642
+ await clear();
1643
+ return await fail(check, `the judge could not be run: ${describeCallFailure(err)}`);
1644
+ } finally {
1645
+ timer.cancel();
1646
+ }
1647
+ if (outcome === "timeout") await environment.stop().catch((err) => log.error(`${tag} the judge could not be stopped after its time ran out: ${describeCallFailure(err)}`));
1648
+ const decision = decideVerdict({
1649
+ run: outcome === "timeout" ? { kind: "timeout" } : { kind: "exited", status: outcome.status, stdout: outcome.stdout, stderr: outcome.stderr },
1650
+ criteria,
1651
+ citations,
1652
+ minutes
1653
+ });
1654
+ let report;
1655
+ try {
1656
+ report = await environment.readReport();
1657
+ } catch (err) {
1658
+ log.error(`${tag} the judge's report could not be read: ${describeCallFailure(err)}`);
1659
+ }
1660
+ await clear();
1661
+ const link = report === void 0 ? void 0 : await uploadReport(check, report, { branch, head });
1662
+ await record(check.name, decision.result, decision.reason, { keyTail: decision.limitKey === true ? LIMIT_KEY_TAIL2 : "", link });
1663
+ }
1664
+ async function uploadReport(check, report, at) {
1665
+ const bytes = Buffer.from(report, "utf8");
1666
+ if (bytes.length === 0 || bytes.length > MAX_REPORT_BYTES) {
1667
+ log.error(`${tag} the judge's report was not uploaded: it is ${bytes.length === 0 ? "empty" : `larger than ${MAX_REPORT_BYTES} bytes`}`);
1668
+ return void 0;
1669
+ }
1670
+ const clientArtifactId = `host-judge-${createHash("sha256").update(`${job.claimId}:${job.startedAt}:${check.name}`).digest("hex").slice(0, 40)}`;
1671
+ const request = {
1672
+ schemaVersion: 1,
1673
+ artifact: {
1674
+ clientArtifactId,
1675
+ filename: "criteria-judge-report.html",
1676
+ mediaType: "text/html",
1677
+ bytes: bytes.length,
1678
+ sha256: createHash("sha256").update(bytes).digest("hex"),
1679
+ caption: REPORT_CAPTION,
1680
+ criterionId: check.name,
1681
+ emittedAt: now().toISOString(),
1682
+ repository: repo,
1683
+ gitRepo: repo,
1684
+ gitBranch: at.branch,
1685
+ commitSha: at.head
1686
+ },
1687
+ blob: { data: bytes.toString("base64") }
1688
+ };
1689
+ const signer = await signerFor(config.configPath);
1690
+ const sent = await retrySend(() => postJson(config.url, "/v1/artifacts", config.apiKey, config.appId, request, deps.fetchImpl, void 0, signer), { now: deps.now });
1691
+ if (!sent.ok) {
1692
+ log.error(`${tag} the judge's report could not be uploaded: ${sent.reason}`);
1693
+ return void 0;
1694
+ }
1695
+ const url = sent.value.data?.url;
1696
+ return typeof url === "string" && /^https?:\/\/\S+$/.test(url) ? url : void 0;
1697
+ }
1698
+ }
1699
+
1242
1700
  // src/runner/job.ts
1243
1701
  var LEASE_SECONDS = 900;
1244
1702
  var RENEW_INTERVAL_SECONDS = 300;
@@ -1596,6 +2054,9 @@ async function releaseLease(config, item, outcome, detail, fetchImpl) {
1596
2054
  const namesOutcome = /\boutcome\b/.test(err.message);
1597
2055
  if (sent === "incomplete" && namesOutcome) {
1598
2056
  sent = "ok";
2057
+ } else if (environment?.controlPlane !== void 0) {
2058
+ const { controlPlane: _omittedPlane, ...withoutPlane } = environment;
2059
+ environment = withoutPlane;
1599
2060
  } else if (environment?.commitIdentity !== void 0) {
1600
2061
  const { commitIdentity: _omitted, ...withoutIdentity } = environment;
1601
2062
  environment = withoutIdentity;
@@ -1706,7 +2167,7 @@ async function finishJob(job, jobDirPath, outcome, config, deps, ending) {
1706
2167
  await stopProcessGroup(job.pid);
1707
2168
  }
1708
2169
  if (outcome !== void 0 && job.endStage === void 0) {
1709
- const TEARDOWN_STAGES = ["checks", "sync", "cleanup", "report"];
2170
+ const TEARDOWN_STAGES = ["judge", "checks", "sync", "cleanup", "report"];
1710
2171
  const staleTeardownStage = job.stage !== void 0 && TEARDOWN_STAGES.includes(job.stage);
1711
2172
  job.endStage = ending?.stage ?? (staleTeardownStage ? job.exitCode !== void 0 ? "command" : void 0 : job.stage);
1712
2173
  job.endExitCode = ending?.exitCode ?? (outcome === "ok" || outcome === "failed" ? job.exitCode : void 0);
@@ -1738,6 +2199,7 @@ async function finishJob(job, jobDirPath, outcome, config, deps, ending) {
1738
2199
  }
1739
2200
  }
1740
2201
  };
2202
+ let buildVmStopLogged = false;
1741
2203
  const endBuildMicroVm = vmBuild === void 0 ? void 0 : async () => {
1742
2204
  if (job.workdir && job.sessionsSynced !== true) {
1743
2205
  await uploadSessions(job.workdir);
@@ -1745,6 +2207,8 @@ async function finishJob(job, jobDirPath, outcome, config, deps, ending) {
1745
2207
  writeJobFile(jobFilePath, job);
1746
2208
  }
1747
2209
  await stopBuildMicroVm(vmBuild.runtime, vmBuild.state, jobDirPath);
2210
+ log.line(`${tag} build microVM stopped`);
2211
+ buildVmStopLogged = true;
1748
2212
  };
1749
2213
  const collectSessions = build !== void 0 ? () => copyTranscriptsFromContainer(build.runtime, build.state) : vmBuild !== void 0 ? () => copyTranscriptsFromMicroVm(vmBuild.runtime, vmBuild.state) : void 0;
1750
2214
  if (job.checkContainer !== void 0 && job.checksRan === true) {
@@ -1765,6 +2229,21 @@ async function finishJob(job, jobDirPath, outcome, config, deps, ending) {
1765
2229
  log.error(`${tag} removing an earlier attempt's check environment failed: ${describeCallFailure(err)} \xB7 continuing`);
1766
2230
  }
1767
2231
  }
2232
+ if (outcome === "ok" && !job.cleanupRan && job.judgeRan !== true) {
2233
+ setStage(job, jobDirPath, "judge");
2234
+ job.judgeRan = true;
2235
+ writeJobFile(jobFilePath, job);
2236
+ try {
2237
+ await runJudge({
2238
+ config,
2239
+ job,
2240
+ jobDir: jobDirPath,
2241
+ deps: { fetchImpl: deps.fetchImpl, runtime: deps.container ?? createContainerRuntime(), log, now: deps.now, endBuildMicroVm, collectSessions, ...deps.hostChecks }
2242
+ });
2243
+ } catch (err) {
2244
+ log.error(`${tag} the host's judge stopped: ${describeCallFailure(err)} \xB7 continuing`);
2245
+ }
2246
+ }
1768
2247
  if (outcome === "ok" && !job.cleanupRan && job.checksRan !== true) {
1769
2248
  setStage(job, jobDirPath, "checks");
1770
2249
  job.checksRan = true;
@@ -1819,6 +2298,7 @@ async function finishJob(job, jobDirPath, outcome, config, deps, ending) {
1819
2298
  cleanedUp = false;
1820
2299
  log.error(`${tag} cleanup failed: ${describeCallFailure(err)} \xB7 continuing`);
1821
2300
  }
2301
+ if (cleanedUp && !buildVmStopLogged) log.line(`${tag} build microVM stopped`);
1822
2302
  if (cleanedUp) log.line(`${tag} cleanup ok (${formatDuration(now().getTime() - cleanupStartedMs)})`);
1823
2303
  } else if (build !== void 0 || !hostMade && isBuiltinContainer(config.hooks.cleanup)) {
1824
2304
  let cleanedUp = true;
@@ -2074,19 +2554,18 @@ async function startSender(input) {
2074
2554
  const launch = launchFor(config, job, jobDir, deps);
2075
2555
  const fd = openSync(join(jobDir, "sender.log"), "a", RUNNER_FILE_MODE);
2076
2556
  let pid;
2557
+ let exitLogged;
2077
2558
  try {
2078
2559
  const child = spawnFn(launch.command, launch.args, { env: launch.env, cwd: launch.cwd, detached: true, stdio: ["ignore", fd, fd] });
2079
2560
  child.on("error", (err) => deps.log.error(`${tag} session sender failed: ${describeCallFailure(err)}`));
2080
- const exitLogged = new Promise((resolve) => {
2561
+ exitLogged = new Promise((resolve) => {
2081
2562
  child.on("exit", (code, signal) => {
2082
2563
  deps.log.line(`${tag} session sender exited \xB7 ${signal ?? `code ${code}`} \xB7 see sender.log`);
2083
- if (pid !== void 0) exitsLogged.delete(pid);
2084
2564
  resolve();
2085
2565
  });
2086
2566
  });
2087
2567
  child.unref?.();
2088
2568
  pid = child.pid;
2089
- if (pid !== void 0) exitsLogged.set(pid, exitLogged);
2090
2569
  } finally {
2091
2570
  closeSync(fd);
2092
2571
  }
@@ -2094,6 +2573,7 @@ async function startSender(input) {
2094
2573
  deps.log.line(`${tag} session sender did not start: no pid`);
2095
2574
  return void 0;
2096
2575
  }
2576
+ exitsLogged.set(pid, exitLogged);
2097
2577
  deps.log.line(`${tag} session sender started \xB7 pid ${pid}`);
2098
2578
  return { pid, start: startTimeOf(pid) ?? void 0 };
2099
2579
  } catch (err) {
@@ -2148,7 +2628,6 @@ async function signalInGuest(runtime, state, pidfile, signal) {
2148
2628
  }
2149
2629
  async function exitReported(pid) {
2150
2630
  const logged = exitsLogged.get(pid);
2151
- if (logged === void 0) return;
2152
2631
  let timer;
2153
2632
  const bound = new Promise((resolve) => {
2154
2633
  timer = setTimeout(resolve, EXIT_REPORT_WAIT_MS);