@outerlayer/cli 0.17.0 → 0.19.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 (160) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +36 -2
  3. package/dist/agent-setup-TPR4BBFV.js +27 -0
  4. package/dist/{artifact-link-cmd-2F47LT7F.js → artifact-link-cmd-W7XF5BFB.js} +4 -3
  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-C4QNTYMS.js} +5 -6
  8. package/dist/{check-5XE7XMC4.js → check-UWQAL2MN.js} +56 -52
  9. package/dist/{chunk-R7PF5OOU.js → chunk-3CWUOFV6.js} +21 -14
  10. package/dist/{chunk-4NPZVMJU.js → chunk-3UZTIRLF.js} +60 -4
  11. package/dist/chunk-452TM7J4.js +502 -0
  12. package/dist/{chunk-NNRTPG7X.js → chunk-5GHGAF6P.js} +4 -4
  13. package/dist/{chunk-WVUIRTWA.js → chunk-5MYFOW5V.js} +196 -37
  14. package/dist/{chunk-HGECYFEI.js → chunk-5TN7GLYX.js} +138 -109
  15. package/dist/{chunk-VQYBXPI7.js → chunk-6D7GT22T.js} +5 -5
  16. package/dist/{chunk-G5NPRL2K.js → chunk-6U3MV7TZ.js} +687 -76
  17. package/dist/{chunk-42ZI52JW.js → chunk-6X4SQ5MY.js} +4 -3
  18. package/dist/{chunk-MAK5LYRF.js → chunk-7KSSBQTS.js} +278 -535
  19. package/dist/{chunk-NWQTTSBB.js → chunk-7SJPQWBM.js} +41 -16
  20. package/dist/{chunk-J5M7BSJQ.js → chunk-7VUK5RFS.js} +23 -4
  21. package/dist/{chunk-OZGHL5Y5.js → chunk-7YP7T6GV.js} +2 -2
  22. package/dist/{chunk-TI7B7UEO.js → chunk-AMFWYESY.js} +1 -1
  23. package/dist/{chunk-YRYMNBGQ.js → chunk-ANPNT6PM.js} +2 -1
  24. package/dist/{chunk-4KSQKQXE.js → chunk-BM63HB2M.js} +20 -16
  25. package/dist/{chunk-4Q7OMKAT.js → chunk-BQXFBI7T.js} +81 -5
  26. package/dist/{chunk-ADJRGVZC.js → chunk-BWSUPRDK.js} +1 -1
  27. package/dist/{chunk-3NSNIIBE.js → chunk-CG36IONK.js} +18 -17
  28. package/dist/chunk-CJJDJ7AT.js +56 -0
  29. package/dist/chunk-CRB3POP4.js +34 -0
  30. package/dist/{chunk-LWSJQJC6.js → chunk-DADWEUH3.js} +7 -7
  31. package/dist/{chunk-KG5ECZGJ.js → chunk-DJ3ZUSC3.js} +11 -11
  32. package/dist/{chunk-F47JBIAW.js → chunk-DSF2FWAZ.js} +2 -1
  33. package/dist/{chunk-N5FOE5PS.js → chunk-ECODQ7II.js} +4 -33
  34. package/dist/{chunk-HHJDN6AU.js → chunk-ESVW4MJY.js} +1 -1
  35. package/dist/{chunk-YO4PSLVB.js → chunk-FVMYKL6L.js} +1 -1
  36. package/dist/chunk-G2YNJIGF.js +44 -0
  37. package/dist/{chunk-WUYTWNMA.js → chunk-G3IJ3N6F.js} +5 -3
  38. package/dist/{chunk-QKFB4K7X.js → chunk-GGOMT6KF.js} +1 -1
  39. package/dist/{chunk-N6LURUEF.js → chunk-GOSFARMS.js} +1 -1
  40. package/dist/{chunk-S3NYLI2P.js → chunk-HPJNLETG.js} +93 -38
  41. package/dist/{chunk-IP7BIBXJ.js → chunk-IF2OSI5T.js} +35 -1
  42. package/dist/{chunk-C5Y5ZJJZ.js → chunk-IXYZUUYQ.js} +5 -4
  43. package/dist/chunk-JBJTC3UU.js +35 -0
  44. package/dist/chunk-KYS3TQAG.js +296 -0
  45. package/dist/chunk-LLM3I5YU.js +89 -0
  46. package/dist/{chunk-YU3VKZ5F.js → chunk-LRG3ZMZD.js} +6 -7
  47. package/dist/{chunk-V7NIVOFD.js → chunk-M2ZWWTWZ.js} +1 -1
  48. package/dist/{chunk-ERI5GBNI.js → chunk-MLAAT4S6.js} +3 -3
  49. package/dist/{chunk-O765JR4W.js → chunk-MTP2QEVO.js} +1 -1
  50. package/dist/{chunk-ZM2IMMYN.js → chunk-N2PXGQRL.js} +9 -1
  51. package/dist/{chunk-BWQ66M6N.js → chunk-NFIR3GEE.js} +98 -24
  52. package/dist/{chunk-POJMEEJJ.js → chunk-NNLA7LLQ.js} +1 -1
  53. package/dist/{chunk-PLMTLGYH.js → chunk-NTRCK4NH.js} +5 -4
  54. package/dist/{chunk-22RMZBGW.js → chunk-P53VEAOF.js} +1 -1
  55. package/dist/{chunk-RLYZLUQ7.js → chunk-QEFKA2QD.js} +2 -2
  56. package/dist/{chunk-JXYVA45C.js → chunk-QNLX477Q.js} +1200 -322
  57. package/dist/{chunk-UGP76IZ5.js → chunk-S5PCA3SN.js} +1 -1
  58. package/dist/{chunk-RMWLBUCJ.js → chunk-SADUQSO4.js} +1 -1
  59. package/dist/{chunk-GQD22KKN.js → chunk-T2Q7ISPE.js} +2 -1
  60. package/dist/chunk-TUSYOL2O.js +113 -0
  61. package/dist/{chunk-NBGDBTO2.js → chunk-TWF3RICI.js} +9 -8
  62. package/dist/{chunk-ONKJUM6R.js → chunk-UVG35MWF.js} +1 -1
  63. package/dist/{chunk-KMDLFI4P.js → chunk-VMJPFQNE.js} +1 -1
  64. package/dist/chunk-VRRGENS4.js +144 -0
  65. package/dist/{chunk-PTW6W5NR.js → chunk-VYAL7PUE.js} +1 -1
  66. package/dist/{chunk-OHVJA253.js → chunk-WFSUUUAN.js} +47 -30
  67. package/dist/{chunk-RSNSZ2LJ.js → chunk-X5H2WYMB.js} +1 -1
  68. package/dist/{chunk-HYZRY4KZ.js → chunk-XNGMQ27R.js} +16 -6
  69. package/dist/chunk-XSZD4AZ6.js +490 -0
  70. package/dist/chunk-YQR6E6OV.js +109 -0
  71. package/dist/{chunk-XCK2YAY5.js → chunk-YVZJ6GKF.js} +3 -2
  72. package/dist/{chunk-2MNPHL23.js → chunk-ZHAUSEEN.js} +38 -15
  73. package/dist/claude-launcher-FPPHZPMR.js +115 -0
  74. package/dist/{cli-A76AXELQ.js → cli-HROW5T7X.js} +567 -285
  75. package/dist/{command-R2QCVX55.js → command-OEAV7BVX.js} +42 -40
  76. package/dist/{config-N3C6DFQA.js → config-CLSGABD5.js} +2 -3
  77. package/dist/{connect-cmd-JBKAQNSF.js → connect-cmd-UNYKGDAL.js} +10 -9
  78. package/dist/context-cleanup-HSVYDGMI.js +9 -0
  79. package/dist/control-plane-launch-NYVDJESK.js +216 -0
  80. package/dist/{dist-LKDWQWZ7.js → dist-WO7NOJNG.js} +2 -2
  81. package/dist/{docker-4J77Q2BF.js → docker-BFU3AQGY.js} +7 -8
  82. package/dist/{doctor-4TAIARIG.js → doctor-23BBJWBX.js} +48 -42
  83. package/dist/{emit-artifact-cmd-MPZVR56N.js → emit-artifact-cmd-O6SCM7TV.js} +27 -24
  84. package/dist/emit-cmd-7SP3I6UZ.js +14 -0
  85. package/dist/{emit-criteria-cmd-EFGYI23V.js → emit-criteria-cmd-LQNERZRV.js} +17 -16
  86. package/dist/{emit-finding-cmd-5GF3V4LR.js → emit-finding-cmd-AJPKHIOV.js} +18 -17
  87. package/dist/{emit-result-cmd-I4OADKMD.js → emit-result-cmd-RUICOKWQ.js} +22 -19
  88. package/dist/emit-step-cmd-347XNGPQ.js +26 -0
  89. package/dist/{exec-client-CQV746UF.js → exec-client-7UMESIE2.js} +3 -4
  90. package/dist/governing-source-V5EEGPTA.js +34 -0
  91. package/dist/guest-init-KPBQOFE5.js +9 -0
  92. package/dist/{hook-fast-R4FVSZ2H.js → hook-fast-YAZG5MDX.js} +5 -2
  93. package/dist/host-key-U4B5QETR.js +8 -0
  94. package/dist/{import-capture-cmd-EQ35EARA.js → import-capture-cmd-KKFFOGWP.js} +7 -5
  95. package/dist/{import-ruler-cmd-BGAZXCQM.js → import-ruler-cmd-5P6JOXQN.js} +5 -3
  96. package/dist/index.js +8 -4
  97. package/dist/{init-PWU5YVL7.js → init-LWHDMZQG.js} +40 -38
  98. package/dist/init-MOINY2DD.js +53 -0
  99. package/dist/{init-cmd-XAVO7HYZ.js → init-cmd-HSMRID6A.js} +75 -30
  100. package/dist/{init-update-CVCWD6VS.js → init-update-LWX5MPPR.js} +29 -29
  101. package/dist/{install-HDBIPHTX.js → install-4SEFIIW2.js} +4 -5
  102. package/dist/{install-cmd-5HYMXLEW.js → install-cmd-QDOTR4OM.js} +5 -4
  103. package/dist/{login-browser-QZCS3WW6.js → login-browser-DX35MARZ.js} +5 -4
  104. package/dist/{logout-cmd-QAUFDJ6J.js → logout-cmd-5V5MMBSY.js} +3 -2
  105. package/dist/{logs-H55NWW4H.js → logs-AMLKC3VP.js} +3 -1
  106. package/dist/loop-SYYPVFYW.js +77 -0
  107. package/dist/machine-6X63G2KH.js +48 -0
  108. package/dist/{mcp-install-cmd-FDTPQQS6.js → mcp-install-cmd-GGWB3JJ2.js} +3 -2
  109. package/dist/{mcp-serve-cmd-3KVYEUXG.js → mcp-serve-cmd-N3UTY6EE.js} +11 -9
  110. package/dist/paths-7XP63OAX.js +7 -0
  111. package/dist/pending-532ABZYC.js +8 -0
  112. package/dist/{pidfile-UZRH774M.js → pidfile-3DM4OBKO.js} +4 -2
  113. package/dist/{record-C43U4PKS.js → record-3WP6V2K5.js} +3 -2
  114. package/dist/{runner-env-EN7YE7QX.js → runner-env-FRZFFHDF.js} +5 -6
  115. package/dist/{runner-key-6SLBI7PP.js → runner-key-276SJU5R.js} +40 -38
  116. package/dist/session-sender-GGIY5OAX.js +36 -0
  117. package/dist/{pending-MXLSRYMO.js → shell-function-NRD4KVUK.js} +1 -2
  118. package/dist/spec-cmd-SIZBRN7D.js +115 -0
  119. package/dist/{starter-pack-KF66HGZR.js → starter-pack-6LQVLBIA.js} +7 -5
  120. package/dist/{status-7VNQN5PW.js → status-NAK6DYSF.js} +46 -42
  121. package/dist/{statusline-fast-SVTMBMD7.js → statusline-fast-KN3BX67H.js} +2 -1
  122. package/dist/sync-cmd-G7GF4UTW.js +35 -0
  123. package/dist/{watch-23HSAEI6.js → watch-EVM5GSMP.js} +22 -15
  124. package/dist/{work-claim-cmd-LKL3GHFQ.js → work-claim-cmd-3J3QW56U.js} +26 -29
  125. package/dist/work-cmd-5EPPD33Y.js +24 -0
  126. package/dist/{work-comment-cmd-AWHP56M2.js → work-comment-cmd-2ACOZQAE.js} +16 -15
  127. package/dist/{work-launch-R5HTAP6X.js → work-launch-LEQ2MPM3.js} +3 -1
  128. package/dist/work-logs-cmd-QDD2NMJL.js +60 -0
  129. package/dist/{work-open-pr-cmd-IOM4AVQQ.js → work-open-pr-cmd-XNCEI4ZM.js} +18 -17
  130. package/dist/work-pr-cmd-H4OQR26N.js +25 -0
  131. package/package.json +3 -2
  132. package/skill-pack/maintained/outerlayer-amend/SKILL.md +4 -2
  133. package/skill-pack/maintained/outerlayer-cli/SKILL.md +9 -2
  134. package/skill-pack/maintained/outerlayer-cli/references/find-out-why-a-build-ended.md +76 -0
  135. package/skill-pack/shipped.json +18 -2
  136. package/skill-pack/template/build/SKILL.md +23 -10
  137. package/skill-pack/template/build/references/agent-briefs.md +3 -1
  138. package/skill-pack/template/build/references/criteria-judge.md +27 -0
  139. package/skill-pack/template/build/references/release.md +22 -29
  140. package/skill-pack/template/spec/SKILL.md +35 -10
  141. package/skill-pack/validators/acceptance-criteria.yaml +25 -26
  142. package/dist/agent-setup-T624CWPD.js +0 -27
  143. package/dist/chunk-6Y5RUTHO.js +0 -39
  144. package/dist/chunk-BYT5O5RV.js +0 -61
  145. package/dist/chunk-WTE3MQML.js +0 -133
  146. package/dist/chunk-XCCLFVXM.js +0 -401
  147. package/dist/chunk-YZ5F4PIS.js +0 -797
  148. package/dist/context-adopt-NFHCLVUB.js +0 -108
  149. package/dist/context-materialize-IV2TUUFO.js +0 -22
  150. package/dist/emit-cmd-IJ3GVRFD.js +0 -12
  151. package/dist/guest-init-LJP5GPZF.js +0 -9
  152. package/dist/host-key-YMLXY6QB.js +0 -7
  153. package/dist/init-6XPDRDTC.js +0 -51
  154. package/dist/loop-FK3LPMMZ.js +0 -73
  155. package/dist/machine-SQXEBAM2.js +0 -46
  156. package/dist/paths-OYKMVYJP.js +0 -6
  157. package/dist/session-sender-FO3LCREV.js +0 -29
  158. package/dist/sync-cmd-GVPPFZ3G.js +0 -28
  159. package/dist/work-cmd-Z2WXLZPH.js +0 -23
  160. package/dist/work-pr-cmd-HNEUIEN7.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 { senderPidfile } from './chunk-NNRTPG7X.js';
5
- import { DOCKERD_LOG } from './chunk-HYZRY4KZ.js';
6
- import { installScrubContext } from './chunk-NWQTTSBB.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-MAK5LYRF.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-S3NYLI2P.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-5GHGAF6P.js';
5
+ import { DOCKERD_LOG, HELPER_PATH, HELPER_RECORD } from './chunk-XNGMQ27R.js';
6
+ import { installScrubContext } from './chunk-7SJPQWBM.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-7KSSBQTS.js';
8
+ import { childEnvironment } from './chunk-VMJPFQNE.js';
9
+ import { HASH_ARGV, parseProgramHash, JUDGE_PROGRAM, judgeCommand, MAX_REPORT_BYTES, JUDGE_MAX_TURNS, JUDGE_DIR } from './chunk-3UZTIRLF.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, logStamp, createRunnerLog, formatDuration, retrySend, formatClock } from './chunk-XSZD4AZ6.js';
13
+ import { WorkCommandError, postJson, isSignatureRefusal, getJson } from './chunk-HPJNLETG.js';
14
+ import { planArtifactUpload } from './chunk-G3IJ3N6F.js';
15
+ import { isBuiltinContainer, isBuiltinProcess, isBuiltinVm, DEFAULT_CHECK_TIME_LIMIT_MINUTES } from './chunk-P53VEAOF.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-QNLX477Q.js';
21
+ import { resolvePackRoot } from './chunk-NNLA7LLQ.js';
22
+ import { parsePolicy } from './chunk-WFSUUUAN.js';
22
23
  import { readCloudConfig } from './chunk-EABW6AJQ.js';
23
- 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';
26
24
  import { homeFromEnv } from './chunk-VNVZDWO3.js';
25
+ import { resolveVersion } from './chunk-RA3O54FC.js';
26
+ import { signerFor } from './chunk-M2ZWWTWZ.js';
27
+ import { runnerDir, buildDir, vsockPath, RUNNER_FILE_MODE } from './chunk-N2PXGQRL.js';
28
+ 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';
27
29
  import { spawn, spawnSync } from 'child_process';
28
30
  import { randomBytes, createHash } from 'crypto';
29
- import { mkdirSync, chmodSync, writeFileSync, appendFileSync, statSync, accessSync, constants, openSync, existsSync, readFileSync, closeSync, rmSync, readdirSync, lstatSync, realpathSync } from 'fs';
31
+ import { mkdirSync, chmodSync, writeFileSync, appendFileSync, statSync, accessSync, constants, openSync, existsSync, readFileSync, closeSync, rmSync, readdirSync, lstatSync, fstatSync, readSync, 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;
@@ -875,11 +989,123 @@ async function settleVerdict(deps) {
875
989
  await deps.sleep(everyMs);
876
990
  }
877
991
  }
992
+ var RUNNER_LOG_FILE = "runner.log";
993
+ var RUNNER_LOG_KEEP_BYTES = 65536;
994
+ function isTailedStage(stage) {
995
+ const tailed = ["provision", "sync", "cleanup", "report"];
996
+ return tailed.includes(stage);
997
+ }
998
+ var READ_LIMIT_BYTES = 1048576;
999
+ var ITEM_LINE = /^#(\d+)\b/;
1000
+ var AttemptLogBook = class {
1001
+ constructor(now = () => /* @__PURE__ */ new Date()) {
1002
+ this.now = now;
1003
+ this.dirs = /* @__PURE__ */ new Map();
1004
+ this.lastByKey = /* @__PURE__ */ new Map();
1005
+ }
1006
+ /** Starts keeping lines for `item` in `jobDirPath`. Idempotent. */
1007
+ open(item, jobDirPath) {
1008
+ this.dirs.set(Number(item), jobDirPath);
1009
+ }
1010
+ /** Stops keeping lines for `item`; its file stays on disk until housekeeping removes the job directory. */
1011
+ close(item) {
1012
+ const number = Number(item);
1013
+ this.dirs.delete(number);
1014
+ for (const key of [...this.lastByKey.keys()]) {
1015
+ if (key.startsWith(`${number}:`)) this.lastByKey.delete(key);
1016
+ }
1017
+ }
1018
+ /** The log with every line it writes for an open item also kept in that item's file. */
1019
+ wrap(log) {
1020
+ return {
1021
+ line: (text2) => {
1022
+ log.line(text2);
1023
+ this.keepItemLine(text2);
1024
+ },
1025
+ error: (text2) => {
1026
+ log.error(text2);
1027
+ this.keepItemLine(text2);
1028
+ }
1029
+ };
1030
+ }
1031
+ /** One line for `item` that does not itself start with its number. */
1032
+ note(item, text2) {
1033
+ this.append(Number(item), text2);
1034
+ }
1035
+ /** One line, written only when `text` differs from the last one written under `key` for the item. */
1036
+ noteChanged(item, key, text2) {
1037
+ const number = Number(item);
1038
+ const slot = `${number}:${key}`;
1039
+ if (this.lastByKey.get(slot) === text2) return;
1040
+ this.lastByKey.set(slot, text2);
1041
+ this.append(number, text2);
1042
+ }
1043
+ keepItemLine(text2) {
1044
+ const match = ITEM_LINE.exec(text2);
1045
+ if (match !== null) this.append(Number(match[1]), text2);
1046
+ }
1047
+ append(item, text2) {
1048
+ const dir = this.dirs.get(item);
1049
+ if (dir === void 0) return;
1050
+ try {
1051
+ appendFileSync(join(dir, RUNNER_LOG_FILE), `${logStamp(this.now())} ${text2.replace(/[\r\n]+/g, " ")}
1052
+ `, { mode: 384 });
1053
+ } catch {
1054
+ }
1055
+ }
1056
+ };
1057
+ function readEnd(path, maxBytes) {
1058
+ let fd;
1059
+ try {
1060
+ fd = openSync(path, "r");
1061
+ const size = fstatSync(fd).size;
1062
+ const length = Math.min(size, maxBytes);
1063
+ const buffer = Buffer.alloc(length);
1064
+ readSync(fd, buffer, 0, length, size - length);
1065
+ return buffer.toString("utf8");
1066
+ } catch {
1067
+ return void 0;
1068
+ } finally {
1069
+ if (fd !== void 0) closeSync(fd);
1070
+ }
1071
+ }
1072
+ function lastLines(text2, count) {
1073
+ const lines = text2.split("\n");
1074
+ if (lines.at(-1) === "") lines.pop();
1075
+ return lines.slice(-200);
1076
+ }
1077
+ function capRunnerLog(text2) {
1078
+ const bytes = Buffer.from(text2, "utf8");
1079
+ if (bytes.length <= RUNNER_LOG_KEEP_BYTES) return text2;
1080
+ let start = bytes.length - RUNNER_LOG_KEEP_BYTES;
1081
+ if (bytes[start - 1] !== 10) {
1082
+ const newline = bytes.indexOf(10, start);
1083
+ if (newline + 1 < bytes.length) {
1084
+ start = newline + 1;
1085
+ } else {
1086
+ while ((bytes[start] & 192) === 128) start++;
1087
+ }
1088
+ }
1089
+ return `[runner log truncated: the first ${start} bytes were cut]
1090
+ ${bytes.subarray(start).toString("utf8")}`;
1091
+ }
1092
+ function buildReleaseLog(jobDirPath, endStage, scrub) {
1093
+ const parts = [];
1094
+ const own = readEnd(join(jobDirPath, RUNNER_LOG_FILE), READ_LIMIT_BYTES);
1095
+ if (own !== void 0 && own.trim() !== "") parts.push(own.endsWith("\n") ? own.slice(0, -1) : own);
1096
+ if (isTailedStage(endStage)) {
1097
+ const file = `${endStage}.log`;
1098
+ const stage = readEnd(join(jobDirPath, file), READ_LIMIT_BYTES);
1099
+ const tail = stage === void 0 ? [] : lastLines(stage);
1100
+ if (tail.length > 0) parts.push(`--- the last ${tail.length} lines of ${file} ---`, ...tail);
1101
+ }
1102
+ if (parts.length === 0) return void 0;
1103
+ return capRunnerLog(scrub(parts.join("\n")) + "\n");
1104
+ }
878
1105
  var ENDED = "ended before provision finished";
879
1106
  async function provisionProcess(input) {
880
- const { config, runtime, item, jobDir } = input;
1107
+ const { config, runtime, item, repository, jobDir } = input;
881
1108
  if (claudeCredential(runtime.hostEnv) === void 0) return { ok: false, reason: AGENT_CREDENTIAL_MISSING };
882
- const repository = item.specRepository;
883
1109
  if (repository === void 0 || repository === null || repository === "") {
884
1110
  return { ok: false, reason: "the work item names no repository to clone" };
885
1111
  }
@@ -921,6 +1147,9 @@ var MAX_VALIDATOR_FILES = 20;
921
1147
  var VALIDATORS_DIR = ".outerlayer/validators";
922
1148
  var POLICY_FILE = ".outerlayer/policy.yaml";
923
1149
  var LIMIT_KEY_TAIL = " (runner.checks.timeLimitMinutes)";
1150
+ function isJudgeCheck(check) {
1151
+ return "judge" in check;
1152
+ }
924
1153
  function lastLine(text2) {
925
1154
  return text2.split(/\r\n|\r|\n/).map((line) => line.trim()).filter((line) => line !== "").at(-1);
926
1155
  }
@@ -956,17 +1185,24 @@ function checksIn(root, log, tag) {
956
1185
  const policy = readInside(root, POLICY_FILE);
957
1186
  const loaded = parsePolicy(policy === void 0 ? null : { path: POLICY_FILE, content: policy }, validatorFilesIn(root));
958
1187
  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 }));
1188
+ const hostChecks = loaded.customs.filter((def) => def.runWhere === "host" && def.level !== "off" && def.declaresEmit !== null);
1189
+ const checks = [];
1190
+ for (const def of hostChecks) {
1191
+ if (def.judge !== void 0) checks.push({ name: def.declaresEmit, judge: { brief: def.judge.brief, text: readInside(root, def.judge.brief) } });
1192
+ else if (def.command !== void 0) checks.push({ name: def.declaresEmit, command: def.command });
1193
+ }
1194
+ return checks;
961
1195
  }
962
1196
  async function readDefinitions(input, repository, scratch, token, tokens) {
963
1197
  const { config, job, deps } = input;
964
1198
  const tag = `#${job.item}`;
965
1199
  const stage = deps.stage ?? stageGovernedContext;
966
1200
  const controlPlaneUrl = (controlPlane) => deps.runtime.remoteFor?.(controlPlane) ?? `${deps.runtime.github.gitUrl}/${controlPlane}.git`;
1201
+ const governing = (job.container ?? job.microvm)?.governing;
967
1202
  const staged = await stage({
968
1203
  config,
969
1204
  repository,
1205
+ ...governing === void 0 ? {} : { source: { repository: governing.repository, ref: governing.ref, sha: governing.commit } },
970
1206
  buildId: `checks-${randomBytes(4).toString("hex")}`,
971
1207
  buildDir: scratch,
972
1208
  fetchImpl: deps.fetchImpl,
@@ -1004,6 +1240,33 @@ function noCleanEnvironment(job) {
1004
1240
  const ran = job.build?.workDir !== void 0 ? "builtin:process" : "a provision hook of the host's own";
1005
1241
  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
1242
  }
1243
+ function makeRecorder(input) {
1244
+ const { config, job, deps } = input;
1245
+ const tag = `#${job.item}`;
1246
+ const now = deps.now ?? (() => /* @__PURE__ */ new Date());
1247
+ let signer;
1248
+ return async (name, result, sentence, opts = {}) => {
1249
+ signer ??= await signerFor(config.configPath);
1250
+ const keyTail = opts.keyTail ?? "";
1251
+ const body = sentence === void 0 ? void 0 : cutReasonLine(`${scrubReason(sentence, config) ?? "check failed"}${keyTail}`);
1252
+ const clientEmitId = `host-${createHash("sha256").update(`${job.claimId}:${job.startedAt}:${name}${opts.idTag === void 0 ? "" : `:${opts.idTag}`}`).digest("hex").slice(0, 40)}`;
1253
+ const sent = await retrySend(
1254
+ () => postJson(
1255
+ config.url,
1256
+ "/v1/emitted-results",
1257
+ config.apiKey,
1258
+ config.appId,
1259
+ { 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) } } },
1260
+ deps.fetchImpl,
1261
+ void 0,
1262
+ signer
1263
+ ),
1264
+ { now: deps.now }
1265
+ );
1266
+ if (sent.ok) deps.log.line(`${tag} check ${name} ${result}${body === void 0 ? "" : `: ${body}`}`);
1267
+ else deps.log.error(`${tag} check ${name} ${result} could not be recorded: ${sent.reason}`);
1268
+ };
1269
+ }
1007
1270
  async function runHostChecks(input) {
1008
1271
  const { config, job, jobDir, deps } = input;
1009
1272
  const { runtime, log } = deps;
@@ -1035,34 +1298,15 @@ async function runHostChecks(input) {
1035
1298
  rmSync(scratch, { recursive: true, force: true });
1036
1299
  return;
1037
1300
  }
1038
- const checks = definitions.checks;
1301
+ const checks = definitions.checks.filter((check) => !isJudgeCheck(check));
1039
1302
  if (checks.length === 0) {
1040
1303
  rmSync(scratch, { recursive: true, force: true });
1041
1304
  return;
1042
1305
  }
1043
1306
  log.line(`${tag} checks started (${checks.map((check) => check.name).join(", ")})`);
1044
1307
  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
- };
1308
+ const recorder = makeRecorder({ config, job, deps });
1309
+ const record = (check, result, sentence, keyTail = "") => recorder(check.name, result, sentence, { keyTail });
1066
1310
  const failAll = async (reason, keyTail = "") => {
1067
1311
  log.error(`${tag} checks failed: ${reason}${keyTail}`);
1068
1312
  for (const check of checks) await record(check, "fail", reason, keyTail);
@@ -1070,12 +1314,36 @@ async function runHostChecks(input) {
1070
1314
  try {
1071
1315
  if (job.container === void 0 && job.microvm === void 0) return await failAll(noCleanEnvironment(job));
1072
1316
  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}`);
1317
+ if (job.branch === void 0) return await failAll("the build pushed no branch: the claim names none");
1318
+ const claimed = repositoriesOf({ repositories: job.repositories, repository, branch: job.branch });
1319
+ if (claimed.length === 0) return await failAll("the claim names no repository to check out");
1320
+ const remoteOf = (name) => runtime.remoteFor?.(name) ?? `${runtime.github.gitUrl}/${name}.git`;
1321
+ const remote = remoteOf(repository);
1322
+ const pushed = /* @__PURE__ */ new Map();
1323
+ const unpushed = [];
1324
+ let primaryHead;
1325
+ for (const entry of claimed) {
1326
+ let entryToken = readToken;
1327
+ if (!entry.primary) {
1328
+ try {
1329
+ entryToken = await tokens.token("read", entry.repository);
1330
+ } catch (err) {
1331
+ return await failAll(`no read token for ${entry.repository}: ${describeCallFailure(err)}`);
1332
+ }
1333
+ }
1334
+ const read = await (deps.readBranch ?? readBranchHead)({ remote: remoteOf(entry.repository), dir: scratch, branch: entry.branch, token: entryToken });
1335
+ if (read.kind === "unreadable") return await failAll(`the branch ${entry.branch} could not be read${entry.primary ? "" : ` on ${entry.repository}`}: ${read.reason}`);
1336
+ if (read.kind === "absent") {
1337
+ unpushed.push(entry);
1338
+ continue;
1339
+ }
1340
+ pushed.set(entry.repository, entry.branch);
1341
+ if (entry.primary) primaryHead = read.sha;
1342
+ }
1343
+ if (pushed.size === 0) {
1344
+ return await failAll(`the build pushed no branch: ${unpushed.map((entry) => `${entry.branch} does not exist on ${entry.repository}`).join("; ")}`);
1345
+ }
1346
+ const repositoriesToCheckOut = claimed;
1079
1347
  const limitMinutes = config.checks?.timeLimitMinutes ?? DEFAULT_CHECK_TIME_LIMIT_MINUTES;
1080
1348
  const minutes = `${limitMinutes} ${limitMinutes === 1 ? "minute" : "minutes"}`;
1081
1349
  const environmentLog = join(jobDir, "checks", "environment.log");
@@ -1130,20 +1398,33 @@ async function runHostChecks(input) {
1130
1398
  workdir: "/build/work",
1131
1399
  failed: (status) => `configuring git failed (exit ${status})`
1132
1400
  },
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
- }
1401
+ ...repositoriesToCheckOut.flatMap((entry) => {
1402
+ const branch = pushed.get(entry.repository);
1403
+ const path = entry.primary ? CHECKOUT_DIR : repositoryPath(entry.repository);
1404
+ const clone = {
1405
+ argv: ["git", "clone", "--filter=blob:none", "--single-branch", ...branch === void 0 ? [] : ["--branch", branch], `https://github.com/${entry.repository}.git`, path],
1406
+ workdir: "/build/work",
1407
+ failed: (status) => `the clone of ${branch ?? `the default branch of ${entry.repository}`} failed (exit ${status})`
1408
+ };
1409
+ return entry.primary && repositoriesToCheckOut.length > 1 ? [
1410
+ clone,
1411
+ {
1412
+ argv: ["sh", "-c", 'mkdir -p "$(dirname "$1")" && ln -s "$2" "$1"', "link", repositoryPath(entry.repository), CHECKOUT_DIR],
1413
+ workdir: "/build/work",
1414
+ failed: (status) => `linking ${entry.repository} at ${repositoryPath(entry.repository)} failed (exit ${status})`
1415
+ }
1416
+ ] : [clone];
1417
+ })
1138
1418
  ];
1139
1419
  for (const step of steps) {
1140
1420
  if (abandoned) return "the check environment setup ran out of time";
1141
1421
  const ran = await made.exec(step.argv, step.workdir, { logPath: environmentLog });
1142
1422
  if (ran.status !== 0) return `the check environment could not be prepared: ${step.failed(ran.status)}`;
1143
1423
  }
1424
+ const lifecycleDir = lifecycleWorkdir(repositoriesToCheckOut, (job.container ?? job.microvm)?.governing?.repository);
1144
1425
  for (const step of lifecycleSteps((job.container ?? job.microvm)?.lifecycle ?? {})) {
1145
1426
  log.line(`${tag} checks lifecycle ${step.name} (${step.source})`);
1146
- const ran = await made.exec(step.argv, CHECKOUT_DIR, { logPath: environmentLog });
1427
+ const ran = await made.exec(step.argv, lifecycleDir, { logPath: environmentLog });
1147
1428
  if (ran.status !== 0) return `the check environment could not be prepared: ${lifecycleFailureReason(step, ran.status)}`;
1148
1429
  }
1149
1430
  return made;
@@ -1172,7 +1453,7 @@ async function runHostChecks(input) {
1172
1453
  scratch,
1173
1454
  remote,
1174
1455
  readToken,
1175
- branchHead: read.sha,
1456
+ branchHead: primaryHead ?? (job.container ?? job.microvm)?.startCommit ?? "",
1176
1457
  transcripts,
1177
1458
  scrub: transcriptScrub(config, [config.apiKey, job.itemKey, readToken]),
1178
1459
  readWorkItem: () => readWorkItem(config, job.item, deps.fetchImpl),
@@ -1239,6 +1520,296 @@ async function runHostChecks(input) {
1239
1520
  }
1240
1521
  }
1241
1522
 
1523
+ // src/runner/judge-verdict.ts
1524
+ function mechanicalFailures(criteria, citations) {
1525
+ return criteria.filter((criterion) => criterion.proof === null && (citations[criterion.id]?.length ?? 0) === 0).map((criterion) => criterion.id);
1526
+ }
1527
+ function firstLine2(text2) {
1528
+ return text2.trim().split(/\r\n|\r|\n/)[0]?.trim() ?? "";
1529
+ }
1530
+ function parseOutput(stdout) {
1531
+ try {
1532
+ const parsed = JSON.parse(stdout);
1533
+ if (typeof parsed !== "object" || parsed === null) return void 0;
1534
+ const { subtype, result } = parsed;
1535
+ return { subtype: typeof subtype === "string" ? subtype : void 0, result: typeof result === "string" ? result : "" };
1536
+ } catch {
1537
+ return void 0;
1538
+ }
1539
+ }
1540
+ var RESULT_LINE = /^result:\s*(pass|fail)\s*$/i;
1541
+ var UNMET_LINE = /^unmet:\s*(\S+)\s*$/i;
1542
+ function decideVerdict(input) {
1543
+ const { run, criteria } = input;
1544
+ if (run.kind === "timeout") return { result: "fail", reason: `the judge ran out of time after ${input.minutes}`, limitKey: true };
1545
+ const output = parseOutput(run.stdout);
1546
+ if (output === void 0) {
1547
+ const said = firstLine2(run.stderr);
1548
+ return { result: "fail", reason: `the judge exited ${run.status} and printed no result${said === "" ? "" : `: ${said}`}` };
1549
+ }
1550
+ 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` };
1551
+ const lines = output.result.split(/\r\n|\r|\n/).map((line) => line.trim()).filter((line) => line !== "");
1552
+ const verdict = RESULT_LINE.exec(lines.at(-1) ?? "")?.[1]?.toLowerCase();
1553
+ if (verdict === void 0) return { result: "fail", reason: "the judge gave no `result:` line" };
1554
+ const named = new Set(lines.flatMap((line) => UNMET_LINE.exec(line)?.[1] ?? []));
1555
+ const mechanical = new Set(mechanicalFailures(criteria, input.citations));
1556
+ const unmet = criteria.map((criterion) => criterion.id).filter((id) => named.has(id) || mechanical.has(id));
1557
+ if (verdict === "pass" && unmet.length === 0) return { result: "pass" };
1558
+ const ruled = unmet.filter((id) => mechanical.has(id));
1559
+ const rule = ruled.length === 0 ? "" : `: the mechanical rule found no proof annotation and no citing test for ${ruled.join(", ")}`;
1560
+ if (unmet.length === 0) return { result: "fail", reason: "the judge failed the change and named no criterion" };
1561
+ return { result: "fail", reason: `${unmet.length} of ${criteria.length} criteria not satisfied, first ${unmet[0]}${rule}` };
1562
+ }
1563
+
1564
+ // src/runner/judge.ts
1565
+ var LIMIT_KEY_TAIL2 = " (runner.checks.timeLimitMinutes)";
1566
+ var MAX_DIFF_BYTES = 4 * 1024 * 1024;
1567
+ var MAX_TEST_FILES = 2e4;
1568
+ var MAX_TEST_FILE_BYTES = 1024 * 1024;
1569
+ var NO_CLAUDE = "the image has no Claude Code for the judge";
1570
+ var REPORT_CAPTION = "The host's criteria judge, each criterion judged against the test that cites it";
1571
+ async function loadCitationFinder() {
1572
+ return await import(pathToFileURL(join(resolvePackRoot(), "template", "build", "scripts", "criterion-citations.mjs")).href);
1573
+ }
1574
+ function testFilesIn(root, isTest, limits = { files: MAX_TEST_FILES, bytes: MAX_TEST_FILE_BYTES }) {
1575
+ const files = [];
1576
+ const walk = (dir, prefix) => {
1577
+ for (const name of readdirSync(dir).sort()) {
1578
+ if (files.length >= limits.files) return;
1579
+ if (prefix === "" && name === ".git") continue;
1580
+ const path = join(dir, name);
1581
+ const rel = prefix === "" ? name : `${prefix}/${name}`;
1582
+ const stat = lstatSync(path);
1583
+ if (stat.isDirectory()) walk(path, rel);
1584
+ else if (stat.isFile() && isTest.test(rel) && stat.size <= limits.bytes) files.push({ path: rel, contents: readFileSync(path, "utf8") });
1585
+ }
1586
+ };
1587
+ walk(root, "");
1588
+ return files;
1589
+ }
1590
+ async function hostPassedNames(config, item, fetchImpl) {
1591
+ const workItemId = Object(await readWorkItem(config, item, fetchImpl)).workItemId;
1592
+ if (typeof workItemId !== "string") throw new WorkCommandError("the work item read named no id");
1593
+ const signer = await signerFor(config.configPath);
1594
+ const latest = /* @__PURE__ */ new Map();
1595
+ let since = null;
1596
+ do {
1597
+ const query = since === null ? "" : `?since=${encodeURIComponent(since)}`;
1598
+ const page = await getJson(config.url, `/v1/work-items/${encodeURIComponent(workItemId)}/changes${query}`, config.apiKey, config.appId, fetchImpl, signer);
1599
+ for (const entry of Array.isArray(page.data) ? page.data : []) {
1600
+ const { name, result, source } = Object(entry);
1601
+ if (source === "host" && typeof name === "string" && typeof result === "string") latest.set(name, result);
1602
+ }
1603
+ since = page.pagination?.cursor ?? null;
1604
+ } while (since !== null);
1605
+ return [...latest].filter(([, result]) => result === "pass").map(([name]) => name);
1606
+ }
1607
+ function criteriaOf(workItem) {
1608
+ const criteria = Object(Object(workItem).criteria);
1609
+ if (!Array.isArray(criteria?.items)) return void 0;
1610
+ const drafted = criteria.drafted === true;
1611
+ const items = [];
1612
+ for (const entry of criteria.items) {
1613
+ const { id, text: text2, proof } = Object(entry);
1614
+ if (typeof id !== "string" || typeof text2 !== "string") continue;
1615
+ items.push({ id, text: text2, proof: typeof proof === "string" ? proof : null, drafted });
1616
+ }
1617
+ return items.length === 0 ? void 0 : items;
1618
+ }
1619
+ function environmentFor(input) {
1620
+ const { config, job, jobDir, deps } = input;
1621
+ if (deps.judgeEnvironment !== void 0) return deps.judgeEnvironment;
1622
+ if (job.container !== void 0) return containerJudgeEnvironment({ runtime: deps.runtime, config, job, jobDir });
1623
+ if (job.microvm !== void 0) return microVmJudgeEnvironment({ runtime: deps.runtime, job, jobDir });
1624
+ return void 0;
1625
+ }
1626
+ async function runJudge(input) {
1627
+ const { config, job, jobDir, deps } = input;
1628
+ const { runtime, log } = deps;
1629
+ const tag = `#${job.item}`;
1630
+ const now = deps.now ?? (() => /* @__PURE__ */ new Date());
1631
+ const repository = job.repository ?? job.container?.repository;
1632
+ if (repository === void 0) return;
1633
+ const repo = repository;
1634
+ const tokens = runtime.credentials.get(jobDir);
1635
+ let readToken;
1636
+ try {
1637
+ readToken = await tokens?.token("read");
1638
+ } catch (err) {
1639
+ log.error(`${tag} no read token for the judge: ${describeCallFailure(err)}`);
1640
+ }
1641
+ const scratch = join(jobDir, "judge-scratch");
1642
+ rmSync(scratch, { recursive: true, force: true });
1643
+ const definitionsDir = join(scratch, "definitions");
1644
+ mkdirSync(definitionsDir, { recursive: true });
1645
+ let definitions;
1646
+ try {
1647
+ definitions = await readDefinitions(input, repository, definitionsDir, readToken, tokens);
1648
+ } catch (err) {
1649
+ definitions = { ok: false, reason: describeCallFailure(err) };
1650
+ } finally {
1651
+ removeStagedContext(definitionsDir);
1652
+ }
1653
+ if (!definitions.ok) {
1654
+ rmSync(scratch, { recursive: true, force: true });
1655
+ log.line(`${tag} judge not run: the definitions could not be read: ${definitions.reason}`);
1656
+ const passed = await retrySend(() => hostPassedNames(config, job.item, deps.fetchImpl), { now: deps.now });
1657
+ if (!passed.ok) {
1658
+ log.error(`${tag} the item's host results could not be read, so none was failed: ${passed.reason}`);
1659
+ return;
1660
+ }
1661
+ const record2 = makeRecorder({ config, job, deps });
1662
+ for (const name of passed.value) await record2(name, "fail", `the definitions could not be read: ${definitions.reason}`, { idTag: "judge-unread" });
1663
+ return;
1664
+ }
1665
+ const checks = definitions.checks.filter(isJudgeCheck);
1666
+ if (checks.length === 0) {
1667
+ rmSync(scratch, { recursive: true, force: true });
1668
+ return;
1669
+ }
1670
+ log.line(`${tag} judge started (${checks.map((check) => check.name).join(", ")})`);
1671
+ const startedMs = now().getTime();
1672
+ const record = makeRecorder({ config, job, deps });
1673
+ const fail = (check, reason) => {
1674
+ log.error(`${tag} judge failed: ${reason}`);
1675
+ return record(check.name, "fail", reason);
1676
+ };
1677
+ try {
1678
+ for (const check of checks) {
1679
+ try {
1680
+ await judgeOne(check);
1681
+ } catch (err) {
1682
+ await fail(check, `the judge could not be run: ${describeCallFailure(err)}`);
1683
+ }
1684
+ }
1685
+ } finally {
1686
+ rmSync(scratch, { recursive: true, force: true });
1687
+ log.line(`${tag} judge done (${formatDuration(now().getTime() - startedMs)})`);
1688
+ }
1689
+ async function judgeOne(check) {
1690
+ const environment = environmentFor(input);
1691
+ if (environment === void 0) return await fail(check, `${NO_CLAUDE}: this build ran outside a container or microVM`);
1692
+ const expected = (job.container ?? job.microvm)?.judgeProgram;
1693
+ if (expected === void 0) return await fail(check, NO_CLAUDE);
1694
+ if (check.judge.text === void 0) return await fail(check, `the brief ${check.judge.brief} is not in the definitions repository`);
1695
+ if (tokens === void 0) return await fail(check, "the build's repository tokens are no longer held, so the branch cannot be fetched");
1696
+ const credential = claudeCredential(runtime.hostEnv);
1697
+ if (credential === void 0) return await fail(check, "the host holds no Claude credential for the judge");
1698
+ const branch = job.branch;
1699
+ if (branch === void 0) return await fail(check, "the build pushed no branch: the claim names none");
1700
+ const remote = runtime.remoteFor?.(repo) ?? `${runtime.github.gitUrl}/${repo}.git`;
1701
+ try {
1702
+ await environment.stop();
1703
+ } catch (err) {
1704
+ return await fail(check, `the build's processes could not be stopped: ${describeCallFailure(err)}`);
1705
+ }
1706
+ const hashed = await environment.hashProgram();
1707
+ if (!hashed.ok) return await fail(check, `${NO_CLAUDE}: ${hashed.reason}`);
1708
+ if (hashed.sha256 !== expected.sha256) return await fail(check, "the judge program changed during the build");
1709
+ const read = await (deps.readBranch ?? readBranchHead)({ remote, dir: scratch, branch, token: readToken });
1710
+ if (read.kind === "absent") return await fail(check, `the build pushed no branch: ${branch} does not exist on ${repo}`);
1711
+ if (read.kind === "unreadable") return await fail(check, `the branch ${branch} could not be read: ${read.reason}`);
1712
+ const head = read.sha;
1713
+ let criteria;
1714
+ try {
1715
+ criteria = criteriaOf(await readWorkItem(config, job.item, deps.fetchImpl));
1716
+ } catch (err) {
1717
+ return await fail(check, `the item's criteria could not be read: ${describeCallFailure(err)}`);
1718
+ }
1719
+ if (criteria === void 0) return await fail(check, "no criteria are recorded for the item");
1720
+ const branchDir = join(scratch, "branch");
1721
+ const fetched = await (deps.fetchCommit ?? fetchCommit)({ remote, dir: branchDir, commit: head, token: readToken });
1722
+ if (!fetched.ok) return await fail(check, `the build's tests could not be fetched: ${fetched.reason}`);
1723
+ const finder = await loadCitationFinder();
1724
+ const citations = finder.buildCitations({ ids: criteria.map((criterion) => criterion.id), files: testFilesIn(branchDir, finder.TEST_FILE), repository: repo, sha: head });
1725
+ let diff = "";
1726
+ const startCommit = (job.container ?? job.microvm)?.startCommit;
1727
+ if (startCommit !== void 0) {
1728
+ const output = join(scratch, "diff.patch");
1729
+ const made = await (deps.fetchDiff ?? ((request) => diffBetween(request)))({ remote, dir: join(scratch, "diff"), base: startCommit, head, token: readToken, output });
1730
+ if (!made.ok) return await fail(check, `the build's diff could not be read: ${made.reason}`);
1731
+ const bytes = readFileSync(output);
1732
+ diff = bytes.length > MAX_DIFF_BYTES ? `${bytes.subarray(0, MAX_DIFF_BYTES).toString("utf8")}
1733
+ [the diff was cut at ${MAX_DIFF_BYTES} bytes]
1734
+ ` : bytes.toString("utf8");
1735
+ }
1736
+ await environment.prepare({
1737
+ "criteria.json": `${JSON.stringify({ criteria }, null, 2)}
1738
+ `,
1739
+ "citations.json": `${JSON.stringify(citations, null, 2)}
1740
+ `,
1741
+ "diff.patch": diff
1742
+ }, branchDir);
1743
+ const limitMinutes = config.checks?.timeLimitMinutes ?? DEFAULT_CHECK_TIME_LIMIT_MINUTES;
1744
+ const minutes = `${limitMinutes} ${limitMinutes === 1 ? "minute" : "minutes"}`;
1745
+ const clear = async () => {
1746
+ await environment.clear?.().catch((err) => log.error(`${tag} what the judge left in its folder could not be removed: ${describeCallFailure(err)}`));
1747
+ };
1748
+ const running = environment.run(judgeCommand({ brief: check.judge.text, credential }), { logPath: join(jobDir, "judge.log") });
1749
+ running.catch(() => void 0);
1750
+ const timer = timeoutAfter(limitMinutes * 6e4, deps.sleep);
1751
+ let outcome;
1752
+ try {
1753
+ outcome = await Promise.race([running, timer.done]);
1754
+ } catch (err) {
1755
+ await clear();
1756
+ return await fail(check, `the judge could not be run: ${describeCallFailure(err)}`);
1757
+ } finally {
1758
+ timer.cancel();
1759
+ }
1760
+ if (outcome === "timeout") await environment.stop().catch((err) => log.error(`${tag} the judge could not be stopped after its time ran out: ${describeCallFailure(err)}`));
1761
+ const decision = decideVerdict({
1762
+ run: outcome === "timeout" ? { kind: "timeout" } : { kind: "exited", status: outcome.status, stdout: outcome.stdout, stderr: outcome.stderr },
1763
+ criteria,
1764
+ citations,
1765
+ minutes
1766
+ });
1767
+ let report;
1768
+ try {
1769
+ report = await environment.readReport();
1770
+ } catch (err) {
1771
+ log.error(`${tag} the judge's report could not be read: ${describeCallFailure(err)}`);
1772
+ }
1773
+ await clear();
1774
+ const link = report === void 0 ? void 0 : await uploadReport(check, report, { branch, head });
1775
+ await record(check.name, decision.result, decision.reason, { keyTail: decision.limitKey === true ? LIMIT_KEY_TAIL2 : "", link });
1776
+ }
1777
+ async function uploadReport(check, report, at) {
1778
+ const bytes = Buffer.from(report, "utf8");
1779
+ if (bytes.length === 0 || bytes.length > MAX_REPORT_BYTES) {
1780
+ log.error(`${tag} the judge's report was not uploaded: it is ${bytes.length === 0 ? "empty" : `larger than ${MAX_REPORT_BYTES} bytes`}`);
1781
+ return void 0;
1782
+ }
1783
+ const clientArtifactId = `host-judge-${createHash("sha256").update(`${job.claimId}:${job.startedAt}:${check.name}`).digest("hex").slice(0, 40)}`;
1784
+ const request = {
1785
+ schemaVersion: 1,
1786
+ artifact: {
1787
+ clientArtifactId,
1788
+ filename: "criteria-judge-report.html",
1789
+ mediaType: "text/html",
1790
+ bytes: bytes.length,
1791
+ sha256: createHash("sha256").update(bytes).digest("hex"),
1792
+ caption: REPORT_CAPTION,
1793
+ criterionId: check.name,
1794
+ emittedAt: now().toISOString(),
1795
+ repository: repo,
1796
+ gitRepo: repo,
1797
+ gitBranch: at.branch,
1798
+ commitSha: at.head
1799
+ },
1800
+ blob: { data: bytes.toString("base64") }
1801
+ };
1802
+ const signer = await signerFor(config.configPath);
1803
+ const sent = await retrySend(() => postJson(config.url, "/v1/artifacts", config.apiKey, config.appId, request, deps.fetchImpl, void 0, signer), { now: deps.now });
1804
+ if (!sent.ok) {
1805
+ log.error(`${tag} the judge's report could not be uploaded: ${sent.reason}`);
1806
+ return void 0;
1807
+ }
1808
+ const url = sent.value.data?.url;
1809
+ return typeof url === "string" && /^https?:\/\/\S+$/.test(url) ? url : void 0;
1810
+ }
1811
+ }
1812
+
1242
1813
  // src/runner/job.ts
1243
1814
  var LEASE_SECONDS = 900;
1244
1815
  var RENEW_INTERVAL_SECONDS = 300;
@@ -1440,6 +2011,14 @@ function transcriptScrub(config, secrets) {
1440
2011
  function stripUnsendableReasonChars(text2) {
1441
2012
  return text2.replace(/\u0000/g, "").replace(/\p{Cs}/gu, "");
1442
2013
  }
2014
+ function stripTerminalControls(text2) {
2015
+ return text2.replace(/\u001b\[[0-?]*[ -\/]*[@-~]/g, "").replace(/\u001b\][^\u0007\u001b\r\n]*(?:\u0007|\u001b\\)?/g, "").replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g, "");
2016
+ }
2017
+ function releaseLogFor(jobDirPath, job, config) {
2018
+ const home = homeFromEnv();
2019
+ installScrubContext(home, transcriptScrub(config, [job.itemKey, config.apiKey]));
2020
+ return buildReleaseLog(jobDirPath, job.endStage, (text2) => scrubText(stripTerminalControls(stripUnsendableReasonChars(text2))));
2021
+ }
1443
2022
  function readReasonLine(jobDirPath, config, home = homeFromEnv()) {
1444
2023
  let raw;
1445
2024
  try {
@@ -1574,28 +2153,34 @@ async function renewLease(config, item, fetchImpl) {
1574
2153
  }
1575
2154
  var ALREADY_RELEASED = "already released";
1576
2155
  async function releaseLease(config, item, outcome, detail, fetchImpl) {
1577
- const send = async (environment2, sent2) => postJson(
2156
+ const send = async (environment2, sent2, log2) => postJson(
1578
2157
  config.url,
1579
2158
  `/v1/work-items/${item}/claim/release`,
1580
2159
  config.apiKey,
1581
2160
  config.appId,
1582
- { host: config.host, outcome: sent2, stage: detail.stage, exitCode: detail.exitCode, reason: detail.reason, environment: environment2 },
2161
+ { host: config.host, outcome: sent2, stage: detail.stage, exitCode: detail.exitCode, reason: detail.reason, environment: environment2, log: log2 },
1583
2162
  fetchImpl,
1584
2163
  void 0,
1585
2164
  await signerFor(config.configPath)
1586
2165
  );
1587
2166
  let environment = detail.environment;
1588
2167
  let sent = outcome;
2168
+ let log = detail.log;
1589
2169
  let data;
1590
2170
  for (; ; ) {
1591
2171
  try {
1592
- ({ data } = await send(environment, sent));
2172
+ ({ data } = await send(environment, sent, log));
1593
2173
  break;
1594
2174
  } catch (err) {
1595
2175
  if (!(err instanceof WorkCommandError) || err.status !== 400 || err.code !== "invalid_request_body") throw err;
1596
2176
  const namesOutcome = /\boutcome\b/.test(err.message);
1597
2177
  if (sent === "incomplete" && namesOutcome) {
1598
2178
  sent = "ok";
2179
+ } else if (log !== void 0 && /\blog\b/.test(err.message)) {
2180
+ log = void 0;
2181
+ } else if (environment?.controlPlane !== void 0) {
2182
+ const { controlPlane: _omittedPlane, ...withoutPlane } = environment;
2183
+ environment = withoutPlane;
1599
2184
  } else if (environment?.commitIdentity !== void 0) {
1600
2185
  const { commitIdentity: _omitted, ...withoutIdentity } = environment;
1601
2186
  environment = withoutIdentity;
@@ -1689,6 +2274,7 @@ async function finishJob(job, jobDirPath, outcome, config, deps, ending) {
1689
2274
  const jobFilePath = join(jobDirPath, "job.json");
1690
2275
  const logPath = join(jobDirPath, "command.log");
1691
2276
  const tag = `#${job.item}`;
2277
+ deps.attemptLogs?.open(job.item, jobDirPath);
1692
2278
  if (job.sender !== void 0) {
1693
2279
  await stopSender({ job, deps: { spawnFn, runtime: deps.container, log } });
1694
2280
  delete job.sender;
@@ -1706,7 +2292,7 @@ async function finishJob(job, jobDirPath, outcome, config, deps, ending) {
1706
2292
  await stopProcessGroup(job.pid);
1707
2293
  }
1708
2294
  if (outcome !== void 0 && job.endStage === void 0) {
1709
- const TEARDOWN_STAGES = ["checks", "sync", "cleanup", "report"];
2295
+ const TEARDOWN_STAGES = ["judge", "checks", "sync", "cleanup", "report"];
1710
2296
  const staleTeardownStage = job.stage !== void 0 && TEARDOWN_STAGES.includes(job.stage);
1711
2297
  job.endStage = ending?.stage ?? (staleTeardownStage ? job.exitCode !== void 0 ? "command" : void 0 : job.stage);
1712
2298
  job.endExitCode = ending?.exitCode ?? (outcome === "ok" || outcome === "failed" ? job.exitCode : void 0);
@@ -1738,6 +2324,7 @@ async function finishJob(job, jobDirPath, outcome, config, deps, ending) {
1738
2324
  }
1739
2325
  }
1740
2326
  };
2327
+ let buildVmStopLogged = false;
1741
2328
  const endBuildMicroVm = vmBuild === void 0 ? void 0 : async () => {
1742
2329
  if (job.workdir && job.sessionsSynced !== true) {
1743
2330
  await uploadSessions(job.workdir);
@@ -1745,6 +2332,8 @@ async function finishJob(job, jobDirPath, outcome, config, deps, ending) {
1745
2332
  writeJobFile(jobFilePath, job);
1746
2333
  }
1747
2334
  await stopBuildMicroVm(vmBuild.runtime, vmBuild.state, jobDirPath);
2335
+ log.line(`${tag} build microVM stopped`);
2336
+ buildVmStopLogged = true;
1748
2337
  };
1749
2338
  const collectSessions = build !== void 0 ? () => copyTranscriptsFromContainer(build.runtime, build.state) : vmBuild !== void 0 ? () => copyTranscriptsFromMicroVm(vmBuild.runtime, vmBuild.state) : void 0;
1750
2339
  if (job.checkContainer !== void 0 && job.checksRan === true) {
@@ -1765,6 +2354,21 @@ async function finishJob(job, jobDirPath, outcome, config, deps, ending) {
1765
2354
  log.error(`${tag} removing an earlier attempt's check environment failed: ${describeCallFailure(err)} \xB7 continuing`);
1766
2355
  }
1767
2356
  }
2357
+ if (outcome === "ok" && !job.cleanupRan && job.judgeRan !== true) {
2358
+ setStage(job, jobDirPath, "judge");
2359
+ job.judgeRan = true;
2360
+ writeJobFile(jobFilePath, job);
2361
+ try {
2362
+ await runJudge({
2363
+ config,
2364
+ job,
2365
+ jobDir: jobDirPath,
2366
+ deps: { fetchImpl: deps.fetchImpl, runtime: deps.container ?? createContainerRuntime(), log, now: deps.now, endBuildMicroVm, collectSessions, ...deps.hostChecks }
2367
+ });
2368
+ } catch (err) {
2369
+ log.error(`${tag} the host's judge stopped: ${describeCallFailure(err)} \xB7 continuing`);
2370
+ }
2371
+ }
1768
2372
  if (outcome === "ok" && !job.cleanupRan && job.checksRan !== true) {
1769
2373
  setStage(job, jobDirPath, "checks");
1770
2374
  job.checksRan = true;
@@ -1819,6 +2423,7 @@ async function finishJob(job, jobDirPath, outcome, config, deps, ending) {
1819
2423
  cleanedUp = false;
1820
2424
  log.error(`${tag} cleanup failed: ${describeCallFailure(err)} \xB7 continuing`);
1821
2425
  }
2426
+ if (cleanedUp && !buildVmStopLogged) log.line(`${tag} build microVM stopped`);
1822
2427
  if (cleanedUp) log.line(`${tag} cleanup ok (${formatDuration(now().getTime() - cleanupStartedMs)})`);
1823
2428
  } else if (build !== void 0 || !hostMade && isBuiltinContainer(config.hooks.cleanup)) {
1824
2429
  let cleanedUp = true;
@@ -1944,6 +2549,9 @@ async function finishJob(job, jobDirPath, outcome, config, deps, ending) {
1944
2549
  } else if (ending?.reason === void 0) {
1945
2550
  job.reason = (outcome === "ok" ? void 0 : held?.reason) ?? (isolated ? fromHook ?? job.reason : fromHook);
1946
2551
  }
2552
+ if (outcome === "failed" && job.reason === void 0 && job.commandEndReason !== void 0) {
2553
+ job.reason = scrubReason(job.commandEndReason, config);
2554
+ }
1947
2555
  const cliVersion = deps.cliVersion ?? resolveVersion();
1948
2556
  const base = job.environment ?? (job.microvm !== void 0 ? { isolation: "vm", cliVersion } : job.container === void 0 ? sharedUserEnvironment(cliVersion) : { isolation: "container", cliVersion });
1949
2557
  if (isolated || job.build !== void 0) {
@@ -1956,7 +2564,8 @@ async function finishJob(job, jobDirPath, outcome, config, deps, ending) {
1956
2564
  writeJobFile(jobFilePath, job);
1957
2565
  }
1958
2566
  const sentAtMs = now().getTime();
1959
- const detail = outcome === void 0 ? {} : { stage: job.endStage, exitCode: job.endExitCode, reason: job.reason, environment: job.environment };
2567
+ const runnerLog = releaseLogFor(jobDirPath, job, config);
2568
+ const detail = outcome === void 0 ? { log: runnerLog } : { stage: job.endStage, exitCode: job.endExitCode, reason: job.reason, environment: job.environment, log: runnerLog };
1960
2569
  const release = await retrySend(() => releaseLease(config, job.item, outcome, detail, deps.fetchImpl), {
1961
2570
  now: deps.now,
1962
2571
  sleep: deps.sleep
@@ -1967,6 +2576,7 @@ async function finishJob(job, jobDirPath, outcome, config, deps, ending) {
1967
2576
  job.endedAt = endedAt.toISOString();
1968
2577
  job.releaseFailed = release.ok ? alreadyReleased ? ALREADY_RELEASED : void 0 : release.reason;
1969
2578
  writeJobFile(jobFilePath, job);
2579
+ deps.attemptLogs?.close(job.item);
1970
2580
  const total = formatDuration(endedAt.getTime() - new Date(job.startedAt).getTime());
1971
2581
  if (alreadyReleased) {
1972
2582
  log.line(
@@ -2012,7 +2622,8 @@ async function retryFailedReleases(jobsDirPath, config, deps) {
2012
2622
  if (!job || job.releaseFailed === void 0 || job.releaseFailed === ALREADY_RELEASED || isUnfinished(job)) {
2013
2623
  continue;
2014
2624
  }
2015
- const detail = job.outcome === void 0 ? {} : { stage: job.endStage, exitCode: job.endExitCode, reason: job.reason, environment: job.environment };
2625
+ const runnerLog = releaseLogFor(dirPath, job, config);
2626
+ const detail = job.outcome === void 0 ? { log: runnerLog } : { stage: job.endStage, exitCode: job.endExitCode, reason: job.reason, environment: job.environment, log: runnerLog };
2016
2627
  const release = await retrySend(() => releaseLease(config, job.item, job.outcome, detail, deps.fetchImpl), {
2017
2628
  now: deps.now,
2018
2629
  sleep: deps.sleep
@@ -2185,4 +2796,4 @@ async function stopSender(input) {
2185
2796
  }
2186
2797
  }
2187
2798
 
2188
- export { ALREADY_RELEASED, LEASE_SECONDS, RENEW_INTERVAL_SECONDS, deliverCommitHooks, describeHookEnd, execArgv, execCommandLine, finishJob, hookLimitsFor, hostCommandEnvironment, hostEnvironment, isGroupAlive, isIdle, isTimedOut, isUnfinished, jobEnv, listAdoptableJobs, listenOnBroker, microVmCommandLine, needsRenew, outcomeFromExitCode, provisionEnvironment, provisionMicroVm, provisionProcess, readCpuUsec, readProvisionOut, recoverLostJobs, releaseLease, renewLease, resolveHostBuild, retryFailedReleases, runCommand, runHook, setStage, startHostServices, startSender, stopOrphanedMicroVms, unfinishedJobs, workdirProblem };
2799
+ export { ALREADY_RELEASED, AttemptLogBook, LEASE_SECONDS, RENEW_INTERVAL_SECONDS, deliverCommitHooks, describeHookEnd, execArgv, execCommandLine, finishJob, hookLimitsFor, hostCommandEnvironment, hostEnvironment, isGroupAlive, isIdle, isTimedOut, isUnfinished, jobEnv, listAdoptableJobs, listenOnBroker, microVmCommandLine, needsRenew, outcomeFromExitCode, provisionEnvironment, provisionMicroVm, provisionProcess, readCpuUsec, readProvisionOut, recoverLostJobs, releaseLease, renewLease, resolveHostBuild, retryFailedReleases, runCommand, runHook, setStage, startHostServices, startSender, stopOrphanedMicroVms, unfinishedJobs, workdirProblem };