@outerlayer/cli 0.10.2 → 0.11.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 (44) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/build-info.json +1 -1
  3. package/dist/{builder-EVO6F2NO.js → builder-PP436VGZ.js} +2 -2
  4. package/dist/{check-NQYQHITZ.js → check-YDXOLVJ5.js} +16 -15
  5. package/dist/{chunk-VVIRHTH4.js → chunk-2GBCTEN4.js} +1 -1
  6. package/dist/{chunk-ERHJETKP.js → chunk-3YFCWYBU.js} +128 -30
  7. package/dist/{chunk-M6G7TJVF.js → chunk-4Q7OMKAT.js} +44 -4
  8. package/dist/{chunk-MC6J453K.js → chunk-7C4T5BGN.js} +2 -2
  9. package/dist/{chunk-472KC6JV.js → chunk-BEZSECPA.js} +1 -1
  10. package/dist/{chunk-KZ5M4QOB.js → chunk-BHOBM3IZ.js} +32 -14
  11. package/dist/{chunk-JSUUXA5W.js → chunk-CJGQQZWB.js} +1 -1
  12. package/dist/{chunk-BREEX2Z7.js → chunk-E5DC4VZL.js} +115 -58
  13. package/dist/{chunk-R2UYH4QV.js → chunk-EOWWTFUU.js} +3 -3
  14. package/dist/{chunk-PDTSUOVT.js → chunk-FI5UYJMQ.js} +1 -1
  15. package/dist/{chunk-V42JY4F6.js → chunk-HYZRY4KZ.js} +2 -2
  16. package/dist/chunk-O765JR4W.js +35 -0
  17. package/dist/{chunk-6CXYORUS.js → chunk-QKFB4K7X.js} +1 -1
  18. package/dist/{chunk-ESZJ5QIC.js → chunk-R7BJYFD2.js} +4 -4
  19. package/dist/{chunk-S6IUUNCD.js → chunk-RSNSZ2LJ.js} +1 -1
  20. package/dist/{chunk-NTLGWCWK.js → chunk-TFIOXPZT.js} +1 -1
  21. package/dist/{chunk-M2PBLRB7.js → chunk-XR3BJN33.js} +23 -3
  22. package/dist/{cli-ZCWMJCEK.js → cli-RQZXTOEI.js} +39 -35
  23. package/dist/{command-LNEFKIVC.js → command-U6UGIBVT.js} +9 -9
  24. package/dist/config-MKBX2ZQD.js +11 -0
  25. package/dist/{docker-GM526LGX.js → docker-WVRRMK6R.js} +3 -3
  26. package/dist/{doctor-QSV4DYT2.js → doctor-ZF4PYWVN.js} +10 -10
  27. package/dist/{exec-client-QTUO6TZT.js → exec-client-CQV746UF.js} +3 -3
  28. package/dist/{guest-init-4UW4HM6N.js → guest-init-LJP5GPZF.js} +3 -3
  29. package/dist/index.js +1 -1
  30. package/dist/{init-MJ2PO2VQ.js → init-M5IBAGVE.js} +9 -9
  31. package/dist/{init-OKCDXEOG.js → init-PUY2BUHH.js} +8 -8
  32. package/dist/{init-cmd-5TEM2QNH.js → init-cmd-NPNR23UL.js} +1 -1
  33. package/dist/{install-PYNCWSTV.js → install-HKOVISUP.js} +3 -3
  34. package/dist/{loop-DBF3UJAD.js → loop-3FNROLW7.js} +16 -15
  35. package/dist/{machine-TBJKZ6TN.js → machine-RWDDJRWB.js} +7 -7
  36. package/dist/pending-MXLSRYMO.js +7 -0
  37. package/dist/{status-KPDVZMJ6.js → status-2OW5AKWL.js} +17 -9
  38. package/dist/{work-claim-cmd-DYD6II56.js → work-claim-cmd-LXXSNEGF.js} +3 -3
  39. package/dist/{work-open-pr-cmd-N2RBDKZ4.js → work-open-pr-cmd-YZAENDP6.js} +5 -4
  40. package/package.json +1 -1
  41. package/skill-pack/maintained/amend/SKILL.md +10 -1
  42. package/skill-pack/maintained/outerlayer/SKILL.md +19 -3
  43. package/skill-pack/maintained/outerlayer/references/open-pull-request.md +11 -0
  44. package/dist/config-X45X7AS4.js +0 -11
@@ -3,17 +3,17 @@ import { createRequire } from 'node:module';
3
3
  import { readDefaultBranchHead, cloneRepository, buildRepositoryImage } from './chunk-3FMPZPJQ.js';
4
4
  import { startTimeOf, isSameProcess } from './chunk-N5FOE5PS.js';
5
5
  import { currentCliBuild } from './chunk-OGFGZQA3.js';
6
- import { createDockerHost, childEnvironment, spawnRun } from './chunk-VVIRHTH4.js';
6
+ import { createDockerHost, childEnvironment, spawnRun } from './chunk-2GBCTEN4.js';
7
7
  import { RecipeError } from './chunk-QOZKPLVJ.js';
8
- import { builderSpec, runnerLabelValue, RUNNER_LABEL, BUILD_LABEL, prepareRunArgs, containerRunArgs, containerExecArgs } from './chunk-JSUUXA5W.js';
8
+ import { builderSpec, runnerLabelValue, RUNNER_LABEL, BUILD_LABEL, prepareRunArgs, containerRunArgs, containerExecArgs } from './chunk-CJGQQZWB.js';
9
9
  import { CLAUDE_CODE_VERSION, firecrackerArch, MICROVM_ARCHES, DEFAULT_BASE_IMAGE, GUEST_HELPER_SHA256, GUEST_ASSETS_BASE_URL, GUEST_KERNEL_SHA256, FIRECRACKER_BINARY_SHA256, FIRECRACKER_SHA256, FIRECRACKER_VERSION, GUEST_ASSETS_VERSION } from './chunk-CX22S5BA.js';
10
- import { execCapture } from './chunk-6CXYORUS.js';
11
- import { GUEST_CID } from './chunk-S6IUUNCD.js';
10
+ import { execCapture } from './chunk-QKFB4K7X.js';
11
+ import { GUEST_CID } from './chunk-RSNSZ2LJ.js';
12
12
  import { WorkCommandError, postJson } from './chunk-DTDZXDQ3.js';
13
13
  import { setCustomScrubbers, setLocalIdentity, setRepoRoot, parseTranscript, scrubDeep, scrubText } from './chunk-IU4NZROR.js';
14
14
  import { hostAllowed } from './chunk-UXYLWVTQ.js';
15
15
  import { parseDuKilobytes } from './chunk-7RQ73DLO.js';
16
- import { RELAY_PORT, CHECKOUT_DIR, JOB_MOUNT, claudeCredential, buildEnvironment, startBuildServices, checkEnvironment, AGENT_CREDENTIAL_MISSING } from './chunk-M6G7TJVF.js';
16
+ import { repositoriesOf, primaryOf, RELAY_PORT, CHECKOUT_DIR, JOB_MOUNT, claudeCredential, buildEnvironment, startBuildServices, checkEnvironment, AGENT_CREDENTIAL_MISSING, checkoutPath } from './chunk-4Q7OMKAT.js';
17
17
  import { defaultGitRunner, materializeContext } from './chunk-RWQWPZB4.js';
18
18
  import { readContextManifest, readSourceCache } from './chunk-BMX3L7CR.js';
19
19
  import { orderSessions, extractFacts, sessionsToTimeline } from './chunk-TB7EP24C.js';
@@ -133,6 +133,9 @@ function refusalCode(err) {
133
133
  if (!(err instanceof WorkCommandError)) return "unavailable";
134
134
  return err.code ?? `http_${err.status ?? "unknown"}`;
135
135
  }
136
+ function keyOf(kind, repository) {
137
+ return repository === void 0 ? kind : `${kind}:${repository}`;
138
+ }
136
139
  var BuildTokens = class {
137
140
  constructor(options) {
138
141
  this.options = options;
@@ -146,23 +149,35 @@ var BuildTokens = class {
146
149
  now() {
147
150
  return (this.options.now ?? (() => /* @__PURE__ */ new Date()))();
148
151
  }
149
- /** The token of `kind`, asking the gateway for a new one when the build holds none or its token is within ten minutes of expiry. */
150
- token(kind) {
151
- const held = this.held.get(kind);
152
+ /** `repository` as the holder keys it: `undefined` for the primary, which is the one a request names no repository for. */
153
+ secondary(repository) {
154
+ if (repository === void 0) return void 0;
155
+ const primary = this.options.primary;
156
+ return primary !== void 0 && primary.toLowerCase() === repository.toLowerCase() ? void 0 : repository;
157
+ }
158
+ /**
159
+ * The token of `kind` for `repository`, or for the primary when none is
160
+ * named, asking the gateway for a new one when the build holds none or its
161
+ * token is within ten minutes of expiry.
162
+ */
163
+ token(kind, repository) {
164
+ const other = this.secondary(repository);
165
+ const key = keyOf(kind, other);
166
+ const held = this.held.get(key);
152
167
  if (held !== void 0 && !isDue(held.expiresAtMs, this.now().getTime())) return Promise.resolve(held.token);
153
- let asking = this.asking.get(kind);
168
+ let asking = this.asking.get(key);
154
169
  if (asking === void 0) {
155
- asking = this.issueOrKeep(kind).finally(() => this.asking.delete(kind));
156
- this.asking.set(kind, asking);
170
+ asking = this.issueOrKeep(kind, other).finally(() => this.asking.delete(key));
171
+ this.asking.set(key, asking);
157
172
  }
158
173
  return asking;
159
174
  }
160
175
  /** A due token that has not yet expired is kept when the gateway cannot issue a new one. */
161
- async issueOrKeep(kind) {
176
+ async issueOrKeep(kind, repository) {
162
177
  try {
163
- return await this.issue(kind);
178
+ return await this.issue(kind, repository);
164
179
  } catch (err) {
165
- const held = this.held.get(kind);
180
+ const held = this.held.get(keyOf(kind, repository));
166
181
  if (held !== void 0 && this.now().getTime() < held.expiresAtMs && classifyRefusal(err).kind === "retry") return held.token;
167
182
  throw err;
168
183
  }
@@ -221,8 +236,8 @@ var BuildTokens = class {
221
236
  this.issued.push(token);
222
237
  return { token, expiresAt, repository };
223
238
  }
224
- async issue(kind) {
225
- const asked = await this.ask({ access: kind });
239
+ async issue(kind, repository) {
240
+ const asked = await this.ask({ access: kind, ...repository === void 0 ? {} : { repository } });
226
241
  let err;
227
242
  if (!asked.ok) {
228
243
  err = new WorkCommandError(asked.message ?? asked.reason, asked.retryable, asked.status, asked.code);
@@ -234,9 +249,9 @@ var BuildTokens = class {
234
249
  await this.revoke([token]);
235
250
  throw new WorkCommandError("the build's tokens were already revoked", false, void 0, "tokens_revoked");
236
251
  }
237
- this.held.set(kind, { token, expiresAtMs });
252
+ this.held.set(keyOf(kind, repository), { token, expiresAtMs });
238
253
  this.issued.push(token);
239
- if (kind === "read") this.readSink?.(token);
254
+ if (kind === "read" && repository === void 0) this.readSink?.(token);
240
255
  return token;
241
256
  }
242
257
  err = new WorkCommandError("the gateway's token answer carried no token or no expiry", false, void 0, "malformed_token_response");
@@ -935,31 +950,45 @@ function adoptionFailureReason(result) {
935
950
  }
936
951
  return `context_adoption_failed: adopting the control plane's context failed (exit ${result.status})${firstLine3(result.stderr) === "" ? "" : `: ${firstLine3(result.stderr)}`}`;
937
952
  }
938
- async function runBuildSteps(input) {
939
- const { exec, log, tag, repository } = input;
940
- let startCommit = "";
941
- const rewrite = await exec(["git", "config", "--global", `url.http://127.0.0.1:${RELAY_PORT}/github.com/.insteadOf`, "https://github.com/"], "/build/work");
942
- if (rewrite.status !== 0) return { ok: false, reason: `configuring the build's git remote failed (exit ${rewrite.status})` };
943
- const clone = await exec(["git", "clone", "--filter=blob:none", "--single-branch", `https://github.com/${repository}.git`, CHECKOUT_DIR], "/build/work");
953
+ async function checkOut(input, entry) {
954
+ const { exec, log, tag } = input;
955
+ const path = checkoutPath(entry);
956
+ const clone = await exec(["git", "clone", "--filter=blob:none", "--single-branch", `https://github.com/${entry.repository}.git`, path], "/build/work");
944
957
  if (clone.status !== 0) {
945
958
  const detail = gitErrorLine(clone.stderr);
946
- return { ok: false, reason: `the clone of ${repository} failed (exit ${clone.status})${detail === "" ? "" : `: ${detail}`}` };
959
+ return { ok: false, reason: `the clone of ${entry.repository} failed (exit ${clone.status})${detail === "" ? "" : `: ${detail}`}` };
947
960
  }
948
961
  try {
949
962
  const prepared = await prepareBranch({
950
- workdir: CHECKOUT_DIR,
951
- branch: input.branch,
963
+ workdir: path,
964
+ branch: entry.branch,
952
965
  git: async (cwd, args) => {
953
966
  const ran = await exec(["git", ...args], cwd, { quiet: true });
954
967
  return { status: ran.status, stdout: ran.stdout.trim(), stderr: ran.stderr.trim() };
955
968
  }
956
969
  });
957
- startCommit = prepared.startCommit;
958
- log.line(`${tag} on ${prepared.branch} at ${prepared.head.slice(0, 8)}`);
970
+ log.line(`${tag} ${entry.primary ? "" : `${entry.repository} `}on ${prepared.branch} at ${prepared.head.slice(0, 8)}`);
971
+ return { ok: true, startCommit: prepared.startCommit };
959
972
  } catch (err) {
960
- if (err instanceof BranchPrepareError) return { ok: false, reason: err.message };
973
+ if (err instanceof BranchPrepareError) return { ok: false, reason: entry.primary ? err.message : `${entry.repository}: ${err.message}` };
961
974
  throw err;
962
975
  }
976
+ }
977
+ async function runBuildSteps(input) {
978
+ const { exec, log, tag, repository } = input;
979
+ let startCommit = "";
980
+ const repositories = repositoriesOf({ repositories: input.repositories, repository, branch: input.branch });
981
+ const primary = primaryOf(repositories) ?? { repository, branch: input.branch, primary: true };
982
+ const rewrite = await exec(["git", "config", "--global", `url.http://127.0.0.1:${RELAY_PORT}/github.com/.insteadOf`, "https://github.com/"], "/build/work");
983
+ if (rewrite.status !== 0) return { ok: false, reason: `configuring the build's git remote failed (exit ${rewrite.status})` };
984
+ const first = await checkOut(input, { ...primary, primary: true });
985
+ if (!first.ok) return first;
986
+ startCommit = first.startCommit;
987
+ for (const entry of repositories.filter((candidate) => candidate.repository !== primary.repository)) {
988
+ if (input.aborted()) return aborted();
989
+ const checked = await checkOut(input, { ...entry, primary: false });
990
+ if (!checked.ok) return checked;
991
+ }
963
992
  for (const step of lifecycleSteps(input.lifecycle)) {
964
993
  if (input.aborted()) return aborted();
965
994
  log.line(`${tag} lifecycle ${step.name} (${step.source})`);
@@ -1639,6 +1668,7 @@ function readBody(req, limit) {
1639
1668
  }
1640
1669
  function createGitRemote(options) {
1641
1670
  const gitUrl = options.gitUrl ?? "https://github.com";
1671
+ const claimed = options.repositories ?? (options.repository === void 0 || options.branch === void 0 || options.mirrorDir === void 0 ? [] : [{ repository: options.repository, branch: options.branch, mirrorDir: options.mirrorDir }]);
1642
1672
  const maxLfsBodyBytes = 1024 * 1024;
1643
1673
  const hopByHop = /* @__PURE__ */ new Set(["connection", "keep-alive", "transfer-encoding", "upgrade", "proxy-authenticate", "proxy-authorization", "te", "trailer", "set-cookie"]);
1644
1674
  const log = (line) => options.log?.(line);
@@ -1685,9 +1715,9 @@ function createGitRemote(options) {
1685
1715
  input.feed(upstream);
1686
1716
  });
1687
1717
  }
1688
- async function tokenOrRefuse(res, kind) {
1718
+ async function tokenOrRefuse(res, kind, repository) {
1689
1719
  try {
1690
- return await options.tokens.token(kind);
1720
+ return await options.tokens.token(kind, repository);
1691
1721
  } catch (err) {
1692
1722
  log(`could not get a ${kind} token: ${refusalCode(err)}`);
1693
1723
  plain(res, 502, `the runner could not get a ${kind} token: ${refusalCode(err)}
@@ -1695,7 +1725,7 @@ function createGitRemote(options) {
1695
1725
  return void 0;
1696
1726
  }
1697
1727
  }
1698
- async function push(req, res, route) {
1728
+ async function push(req, res, route, target) {
1699
1729
  const encoding = req.headers["content-encoding"];
1700
1730
  const gzip = encoding === "gzip";
1701
1731
  const { held, section } = await readPrefix(req, gzip);
@@ -1720,8 +1750,8 @@ function createGitRemote(options) {
1720
1750
  let bodyPath;
1721
1751
  try {
1722
1752
  let fastForward;
1723
- if (needsFastForwardCheck(commands, options.branch)) {
1724
- const read = await tokenOrRefuse(res, "read");
1753
+ if (needsFastForwardCheck(commands, target.branch)) {
1754
+ const read = await tokenOrRefuse(res, "read", target.repository);
1725
1755
  if (read === void 0) return void await drain(req);
1726
1756
  mkdirSync(options.spoolDir, { recursive: true, mode: 448 });
1727
1757
  bodyPath = join(options.spoolDir, randomBytes(8).toString("hex"));
@@ -1742,21 +1772,21 @@ function createGitRemote(options) {
1742
1772
  if (size > cap) return void await refuseInBand(`the push is larger than the ${cap} bytes the runner holds to check it`);
1743
1773
  try {
1744
1774
  fastForward = await isFastForward({
1745
- mirrorDir: options.mirrorDir,
1775
+ mirrorDir: target.mirrorDir,
1746
1776
  remoteUrl: `${gitUrl}/${route.owner}/${route.name}.git`,
1747
1777
  token: read,
1748
- branch: options.branch,
1778
+ branch: target.branch,
1749
1779
  bodyPath,
1750
1780
  gzip
1751
1781
  });
1752
1782
  } catch (err) {
1753
1783
  log(`fast-forward check could not run: ${err instanceof Error ? err.message : String(err)}`);
1754
- return void await refuseInBand(`the runner could not check whether this push fast-forwards ${options.branch}`);
1784
+ return void await refuseInBand(`the runner could not check whether this push fast-forwards ${target.branch}`);
1755
1785
  }
1756
1786
  }
1757
- const verdict = judgePush({ commands, branch: options.branch, fastForward });
1787
+ const verdict = judgePush({ commands, branch: target.branch, fastForward });
1758
1788
  if (!verdict.allowed) return void await refuseInBand(verdict.message);
1759
- const token = await tokenOrRefuse(res, "push");
1789
+ const token = await tokenOrRefuse(res, "push", target.repository);
1760
1790
  if (token === void 0) return void await drain(req);
1761
1791
  options.tokens.markUsed("push");
1762
1792
  const headers = upstreamHeaders(req.headers, basicAuthorization(token));
@@ -1793,8 +1823,15 @@ function createGitRemote(options) {
1793
1823
  req.resume();
1794
1824
  return plain(res, 403, "this remote is read-only: a check cannot push\n");
1795
1825
  }
1796
- const own = isOwnRepository(options.repository, route.owner, route.name);
1797
- if (own && route.kind === "receive-pack") return push(req, res, route);
1826
+ const target = claimed.find((entry) => isOwnRepository(entry.repository, route.owner, route.name));
1827
+ const pushing = route.kind === "receive-pack" || route.kind === "info-refs" && route.service === "git-receive-pack";
1828
+ if (target === void 0 && pushing) {
1829
+ log(`push refused: ${route.owner}/${route.name} is not a repository of this build's claim`);
1830
+ req.resume();
1831
+ return plain(res, 403, `the runner refused this push: ${route.owner}/${route.name} is not a repository of this build's claim
1832
+ `);
1833
+ }
1834
+ if (target !== void 0 && route.kind === "receive-pack") return push(req, res, route, target);
1798
1835
  let kind = route.kind === "info-refs" && route.service === "git-receive-pack" ? "push" : "read";
1799
1836
  let body;
1800
1837
  if (route.kind === "lfs-batch") {
@@ -1804,8 +1841,8 @@ function createGitRemote(options) {
1804
1841
  if (options.readOnly === true && kind === "push") return plain(res, 403, "this remote is read-only: a check cannot upload\n");
1805
1842
  }
1806
1843
  let authorization;
1807
- if (own) {
1808
- const token = await tokenOrRefuse(res, kind);
1844
+ if (target !== void 0) {
1845
+ const token = await tokenOrRefuse(res, kind, target.repository);
1809
1846
  if (token === void 0) return void await drain(req);
1810
1847
  authorization = basicAuthorization(token);
1811
1848
  if (kind === "read" || route.kind === "lfs-batch") options.tokens.markUsed(kind);
@@ -2268,14 +2305,14 @@ function keepGhConfigCurrent(tokens, buildDir2, onError) {
2268
2305
  });
2269
2306
  }
2270
2307
  function gitRemoteFor(runtime, input) {
2271
- const [owner = "", name = ""] = input.repository.split("/");
2272
2308
  const runnerDirPath = runnerDirOfBuild(input.buildDir);
2273
2309
  return createGitRemote({
2274
- repository: input.repository,
2275
- branch: input.branch,
2310
+ repositories: input.repositories.map((entry) => {
2311
+ const [owner = "", name = ""] = entry.repository.split("/");
2312
+ return { repository: entry.repository, branch: entry.branch, mirrorDir: mirrorDir(runnerDirPath, owner, name) };
2313
+ }),
2276
2314
  tokens: input.tokens,
2277
2315
  gitUrl: runtime.github.gitUrl,
2278
- mirrorDir: mirrorDir(runnerDirPath, owner, name),
2279
2316
  spoolDir: spoolDir(runnerDirPath),
2280
2317
  onWorkflowRefused: () => {
2281
2318
  input.tokens.reason = WORKFLOW_PERMISSION_REASON;
@@ -2349,10 +2386,10 @@ async function provisionContainer(input) {
2349
2386
  const logPath = join(jobDir, "provision.log");
2350
2387
  const fail = (reason) => ({ ok: false, reason });
2351
2388
  if (claudeCredential(runtime.hostEnv) === void 0) return fail(AGENT_CREDENTIAL_MISSING);
2352
- const repository = item.specRepository;
2353
- if (repository === void 0 || repository === null || repository === "") {
2354
- return fail("the work item names no repository to clone");
2355
- }
2389
+ const repositories = repositoriesOf({ repositories: input.repositories, repository: item.specRepository, branch: input.branch });
2390
+ const primary = primaryOf(repositories);
2391
+ if (primary === void 0) return fail("the work item names no repository to clone");
2392
+ const repository = primary.repository;
2356
2393
  const engine = await host.version();
2357
2394
  if (engine === null) return fail("Docker is not available: `docker version` failed");
2358
2395
  if (!(Number.parseInt(engine, 10) >= MIN_ENGINE_MAJOR)) {
@@ -2385,6 +2422,7 @@ async function provisionContainer(input) {
2385
2422
  item: item.number,
2386
2423
  itemKey,
2387
2424
  repository,
2425
+ repositories,
2388
2426
  hostEnv: runtime.hostEnv,
2389
2427
  containerEnv: report.apply.containerEnv,
2390
2428
  builtIns: true
@@ -2421,11 +2459,19 @@ async function provisionContainer(input) {
2421
2459
  err instanceof WorkCommandError ? `the build's read token could not be had: ${refusalCode(err)}` : `the build's gh configuration could not be written: ${describeCallFailure(err)}`
2422
2460
  );
2423
2461
  }
2462
+ for (const entry of repositories) {
2463
+ if (entry.repository === primary.repository) continue;
2464
+ try {
2465
+ await tokens.token("read", entry.repository);
2466
+ } catch (err) {
2467
+ return bail(`the build's read token for ${entry.repository} could not be had: ${refusalCode(err)}`);
2468
+ }
2469
+ }
2424
2470
  keepGhConfigCurrent(tokens, buildDir2, (err) => log.error(`${tag} the read token could not be written for gh: ${describeCallFailure(err)}`));
2425
2471
  tokens.markUsed("read");
2426
2472
  environment.credentials = tokens.used();
2427
2473
  try {
2428
- const gitRemote = gitRemoteFor(runtime, { repository, branch: input.branch, tokens, buildDir: buildDir2, log: (line) => log.line(`${tag} ${line}`) });
2474
+ const gitRemote = gitRemoteFor(runtime, { repositories, tokens, buildDir: buildDir2, log: (line) => log.line(`${tag} ${line}`) });
2429
2475
  runtime.tunnels.set(buildId, await startServicesFor(runtime, config, buildDir2, jobDir, itemKey, true, gitRemote));
2430
2476
  } catch (err) {
2431
2477
  return bail(`the tunnel could not start: ${describeCallFailure(err)}`);
@@ -2476,7 +2522,8 @@ async function provisionContainer(input) {
2476
2522
  const exec = (argv, workdir, opts) => host.exec({ container: name, user: agentUser, workdir, env, argv }, opts?.quiet === true ? void 0 : { logPath });
2477
2523
  const steps = await runBuildSteps({
2478
2524
  repository,
2479
- branch: input.branch,
2525
+ branch: primary.branch,
2526
+ repositories,
2480
2527
  lifecycle: report.apply.lifecycle,
2481
2528
  exec,
2482
2529
  log,
@@ -2519,6 +2566,7 @@ function containerCommandLine(input) {
2519
2566
  itemKey: input.job.itemKey,
2520
2567
  branch: input.job.branch,
2521
2568
  repository: state.repository,
2569
+ repositories: input.job.repositories,
2522
2570
  hostEnv: input.runtime.hostEnv,
2523
2571
  containerEnv: state.containerEnv ?? {},
2524
2572
  builtIns: state.builtIns === true,
@@ -2544,6 +2592,7 @@ function containerSenderCommand(input) {
2544
2592
  itemKey: input.job.itemKey,
2545
2593
  branch: input.job.branch,
2546
2594
  repository: state.repository,
2595
+ repositories: input.job.repositories,
2547
2596
  hostEnv: input.runtime.hostEnv,
2548
2597
  containerEnv: state.containerEnv ?? {},
2549
2598
  builtIns: state.builtIns === true,
@@ -2597,17 +2646,18 @@ async function startServicesFor(runtime, config, buildDir2, jobDir, itemKey, bui
2597
2646
  destinationOptions: runtime.destinationOptions
2598
2647
  });
2599
2648
  }
2600
- async function ensureTunnel(runtime, state, jobDir, config, itemKey, branch) {
2649
+ async function ensureTunnel(runtime, state, jobDir, config, itemKey, branch, repositories) {
2601
2650
  if (runtime.tunnels.has(state.buildId)) return;
2602
2651
  const tokens = runtime.credentials.get(jobDir);
2603
- const gitRemote = tokens === void 0 || state.repository === void 0 || branch === void 0 ? void 0 : gitRemoteFor(runtime, { repository: state.repository, branch, tokens, buildDir: state.buildDir });
2652
+ const served = repositoriesOf({ repositories, repository: state.repository, branch });
2653
+ const gitRemote = tokens === void 0 || served.length === 0 ? void 0 : gitRemoteFor(runtime, { repositories: served, tokens, buildDir: state.buildDir });
2604
2654
  runtime.tunnels.set(state.buildId, await startServicesFor(runtime, config, state.buildDir, jobDir, itemKey, state.builtIns === true, gitRemote));
2605
2655
  }
2606
2656
  async function syncInContainer(input) {
2607
2657
  const { runtime, job } = input;
2608
2658
  const state = job.container;
2609
2659
  try {
2610
- await ensureTunnel(runtime, state, input.jobDir, input.config, job.itemKey, job.branch);
2660
+ await ensureTunnel(runtime, state, input.jobDir, input.config, job.itemKey, job.branch, job.repositories);
2611
2661
  if (state.stopped) {
2612
2662
  await runtime.host.start(state.container);
2613
2663
  state.stopped = false;
@@ -2619,6 +2669,7 @@ async function syncInContainer(input) {
2619
2669
  itemKey: job.itemKey,
2620
2670
  branch: job.branch,
2621
2671
  repository: state.repository,
2672
+ repositories: job.repositories,
2622
2673
  hostEnv: runtime.hostEnv,
2623
2674
  containerEnv: state.containerEnv ?? {},
2624
2675
  builtIns: state.builtIns === true,
@@ -2711,7 +2762,13 @@ async function startCheckContainer(input) {
2711
2762
  mkdirSync(buildDir2, { recursive: true });
2712
2763
  chmodSync(buildDir2, 493);
2713
2764
  try {
2714
- const gitRemote = gitRemoteFor(runtime, { repository, branch: job.branch, tokens, buildDir: buildDir2, log: (line) => log.line(`${tag} ${line}`), readOnly: true });
2765
+ const gitRemote = gitRemoteFor(runtime, {
2766
+ repositories: repositoriesOf({ repositories: job.repositories, repository, branch: job.branch }),
2767
+ tokens,
2768
+ buildDir: buildDir2,
2769
+ log: (line) => log.line(`${tag} ${line}`),
2770
+ readOnly: true
2771
+ });
2715
2772
  runtime.tunnels.set(buildId, await startServicesFor(runtime, config, buildDir2, checksDir, void 0, false, gitRemote));
2716
2773
  } catch (err) {
2717
2774
  return fail(`the tunnel could not start: ${describeCallFailure(err)}`);
@@ -1004,7 +1004,7 @@ function doctorExitCode(checks) {
1004
1004
 
1005
1005
  // src/doctor-report.ts
1006
1006
  async function gatherDoctorChecks() {
1007
- const { defaultRunnerConfigPath, hasRunnerBlock, loadRunnerConfig, runnerVmOf } = await import('./config-X45X7AS4.js');
1007
+ const { defaultRunnerConfigPath, hasRunnerBlock, loadRunnerConfig, runnerVmOf } = await import('./config-MKBX2ZQD.js');
1008
1008
  const configPath = defaultRunnerConfigPath();
1009
1009
  const checks = runDoctor({ runnerHost: hasRunnerBlock(configPath) });
1010
1010
  const credential = await credentialChecks();
@@ -1016,7 +1016,7 @@ async function gatherDoctorChecks() {
1016
1016
  const { runnerVmCheck } = await import('./doctor-53F2PYY7.js');
1017
1017
  checks.push(runnerVmCheck(configPath, { lima: realLima(), home: homeFromEnv(), instance: runnerVm.instance }));
1018
1018
  } else if (hasRunnerBlock(configPath)) {
1019
- const { runnerHostChecks } = await import('./doctor-QSV4DYT2.js');
1019
+ const { runnerHostChecks } = await import('./doctor-ZF4PYWVN.js');
1020
1020
  const hooks = (() => {
1021
1021
  try {
1022
1022
  return loadRunnerConfig(configPath).hooks;
@@ -1051,7 +1051,7 @@ async function gatherDoctorChecks() {
1051
1051
  const { existsSync: existsSync2 } = await import('fs');
1052
1052
  if (process.platform !== "linux" || !existsSync2("/run/systemd/system")) return void 0;
1053
1053
  const { inspectService, realInspectProbes } = await import('./inspect-6UYAAXH7.js');
1054
- const { DEFAULT_UNIT_NAME } = await import('./install-PYNCWSTV.js');
1054
+ const { DEFAULT_UNIT_NAME } = await import('./install-HKOVISUP.js');
1055
1055
  const { dirname: dirname2 } = await import('path');
1056
1056
  return inspectService(DEFAULT_UNIT_NAME, dirname2(configPath), realInspectProbes());
1057
1057
  })();
@@ -4,6 +4,6 @@ import { createRequire } from 'node:module';
4
4
  createRequire(import.meta.url);
5
5
 
6
6
  // src/runner/protocol.ts
7
- var RUNNER_PROTOCOL = 15;
7
+ var RUNNER_PROTOCOL = 16;
8
8
 
9
9
  export { RUNNER_PROTOCOL };
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
- import { EXEC_PORT } from './chunk-S6IUUNCD.js';
4
- import { RELAY_PORT } from './chunk-M6G7TJVF.js';
3
+ import { EXEC_PORT } from './chunk-RSNSZ2LJ.js';
4
+ import { RELAY_PORT } from './chunk-4Q7OMKAT.js';
5
5
  import { spawn, spawnSync } from 'child_process';
6
6
  import { writeFileSync, chmodSync, mkdirSync } from 'fs';
7
7
 
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
3
+ import { RUNNER_DIR_MODE, RUNNER_FILE_MODE } from './chunk-ZM2IMMYN.js';
4
+ import { readFileSync, existsSync, rmSync, mkdirSync, writeFileSync, renameSync } from 'fs';
5
+ import { join } from 'path';
6
+
7
+ createRequire(import.meta.url);
8
+ var FILE = "update-pending.json";
9
+ function readPendingUpdate(runnerDirPath) {
10
+ try {
11
+ const raw = JSON.parse(readFileSync(join(runnerDirPath, FILE), "utf8"));
12
+ if (typeof raw.version !== "string" || typeof raw.since !== "string") return void 0;
13
+ const record = { version: raw.version, since: raw.since };
14
+ const holding = raw.holding;
15
+ if (holding && Array.isArray(holding.builds) && typeof holding.since === "string") {
16
+ record.holding = { builds: holding.builds.filter((n) => typeof n === "number"), since: holding.since };
17
+ }
18
+ return record;
19
+ } catch {
20
+ return void 0;
21
+ }
22
+ }
23
+ function writePendingUpdate(runnerDirPath, record) {
24
+ const path = join(runnerDirPath, FILE);
25
+ if (record === void 0) {
26
+ if (existsSync(path)) rmSync(path);
27
+ return;
28
+ }
29
+ mkdirSync(runnerDirPath, { recursive: true, mode: RUNNER_DIR_MODE });
30
+ const temp = `${path}.partial-${process.pid}`;
31
+ writeFileSync(temp, JSON.stringify(record, null, 2), { mode: RUNNER_FILE_MODE });
32
+ renameSync(temp, path);
33
+ }
34
+
35
+ export { readPendingUpdate, writePendingUpdate };
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
- import { EXEC_PORT } from './chunk-S6IUUNCD.js';
3
+ import { EXEC_PORT } from './chunk-RSNSZ2LJ.js';
4
4
  import { createConnection } from 'net';
5
5
 
6
6
  createRequire(import.meta.url);
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
- import { isPlaceholderCommand } from './chunk-MC6J453K.js';
4
- import { describeCallFailure } from './chunk-BREEX2Z7.js';
3
+ import { isPlaceholderCommand } from './chunk-7C4T5BGN.js';
4
+ import { describeCallFailure } from './chunk-E5DC4VZL.js';
5
5
  import { WorkCommandError, getJson } from './chunk-DTDZXDQ3.js';
6
6
  import { sanitizeTerminal } from './chunk-VDI5POCC.js';
7
- import { builtinKind } from './chunk-M2PBLRB7.js';
8
- import { CLAUDE_CREDENTIAL_VARIABLES, claudeCredential } from './chunk-M6G7TJVF.js';
7
+ import { builtinKind } from './chunk-XR3BJN33.js';
8
+ import { CLAUDE_CREDENTIAL_VARIABLES, claudeCredential } from './chunk-4Q7OMKAT.js';
9
9
  import { signerFor } from './chunk-V7NIVOFD.js';
10
10
 
11
11
  createRequire(import.meta.url);
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
- import { RELAY_PORT, GATEWAY_SOCKET, GATEWAY_PORT, CLAUDE_SOCKET, CLAUDE_PORT, DESTINATION_BASE_PORT } from './chunk-M6G7TJVF.js';
3
+ import { RELAY_PORT, GATEWAY_SOCKET, GATEWAY_PORT, CLAUDE_SOCKET, CLAUDE_PORT, DESTINATION_BASE_PORT } from './chunk-4Q7OMKAT.js';
4
4
  import { symlinkSync, unlinkSync } from 'fs';
5
5
  import { join } from 'path';
6
6
 
@@ -2,7 +2,7 @@
2
2
  import { createRequire } from 'node:module';
3
3
  import { runnerUnit, UNIT_MARKER } from './chunk-FBTE2A2X.js';
4
4
  import { checkFit } from './chunk-JKIWMRFH.js';
5
- import { builtinKind } from './chunk-M2PBLRB7.js';
5
+ import { builtinKind } from './chunk-XR3BJN33.js';
6
6
  import { runnerDir } from './chunk-ZM2IMMYN.js';
7
7
  import { dirname } from 'path';
8
8
  import { spawnSync } from 'child_process';
@@ -2,7 +2,7 @@
2
2
  import { createRequire } from 'node:module';
3
3
  import { MAX_ALLOW_HOSTS, allowHostProblem } from './chunk-UXYLWVTQ.js';
4
4
  import { parseSize } from './chunk-7RQ73DLO.js';
5
- import { isRefusedInConfig, resolveDestinations, isReservedVariable } from './chunk-M6G7TJVF.js';
5
+ import { isRefusedInConfig, resolveDestinations, isReservedVariable } from './chunk-4Q7OMKAT.js';
6
6
  import { homeFromEnv } from './chunk-VNVZDWO3.js';
7
7
  import { existsSync, statSync, accessSync, constants, readFileSync } from 'fs';
8
8
  import { hostname } from 'os';
@@ -86,8 +86,11 @@ var RUNNER_KEYS = [
86
86
  "build",
87
87
  "housekeeping",
88
88
  "checks",
89
- "autoUpdate"
89
+ "autoUpdate",
90
+ "update"
90
91
  ];
92
+ var UPDATE_KEYS = ["maxWaitHours"];
93
+ var DEFAULT_UPDATE_MAX_WAIT_HOURS = 6;
91
94
  function runnerNumbers() {
92
95
  return [
93
96
  { key: "concurrency", min: 1 },
@@ -205,6 +208,7 @@ function loadRunnerConfig(configPath) {
205
208
  const build = resolveBuild(configPath, runner.build);
206
209
  const housekeeping = resolveHousekeeping(configPath, runner.housekeeping, build.disk);
207
210
  const checks = resolveChecks(configPath, runner.checks);
211
+ const update = resolveUpdate(configPath, runner.update);
208
212
  if (config.scrub?.literals !== void 0 && !Array.isArray(config.scrub.literals)) {
209
213
  fail(configPath, "scrub.literals", "must be a list of strings");
210
214
  }
@@ -233,6 +237,7 @@ function loadRunnerConfig(configPath) {
233
237
  build,
234
238
  housekeeping,
235
239
  checks,
240
+ update,
236
241
  // A host with executables of its own for provision and cleanup chose
237
242
  // its own environment, and an update could change what its hooks run
238
243
  // under; it opts in. The built-in hooks are the runner's own. Provision
@@ -300,6 +305,21 @@ function resolveChecks(configPath, block) {
300
305
  }
301
306
  return { timeLimitMinutes: limit };
302
307
  }
308
+ function resolveUpdate(configPath, block) {
309
+ const given = block ?? {};
310
+ if (typeof given !== "object" || Array.isArray(given)) fail(configPath, "runner.update", "must be an object");
311
+ const keys = given;
312
+ for (const key of Object.keys(keys)) {
313
+ if (UPDATE_KEYS.includes(key)) continue;
314
+ const near = nearestKey(key, UPDATE_KEYS);
315
+ fail(configPath, `runner.update.${key}`, near ? `is not a key this block accepts; did you mean ${near}?` : "is not a key this block accepts");
316
+ }
317
+ const hours = keys.maxWaitHours === void 0 ? DEFAULT_UPDATE_MAX_WAIT_HOURS : keys.maxWaitHours;
318
+ if (typeof hours !== "number" || !Number.isFinite(hours) || hours < 0) {
319
+ fail(configPath, "runner.update.maxWaitHours", "must be a number of hours, 0 or more");
320
+ }
321
+ return { maxWaitHours: hours };
322
+ }
303
323
  function wholeBytes(bytes) {
304
324
  if (bytes === void 0) return void 0;
305
325
  const whole = Math.floor(bytes);
@@ -359,4 +379,4 @@ function resolveBuild(configPath, block) {
359
379
  };
360
380
  }
361
381
 
362
- export { DEFAULT_CHECK_TIME_LIMIT_MINUTES, RunnerConfigError, builtinKind, checkHooks, defaultRunnerConfigPath, hasRunnerBlock, isBuiltinContainer, isBuiltinProcess, isBuiltinVm, loadRunnerConfig, resolveBuild, resolveHousekeeping, runnerVmOf };
382
+ export { DEFAULT_CHECK_TIME_LIMIT_MINUTES, DEFAULT_UPDATE_MAX_WAIT_HOURS, RunnerConfigError, builtinKind, checkHooks, defaultRunnerConfigPath, hasRunnerBlock, isBuiltinContainer, isBuiltinProcess, isBuiltinVm, loadRunnerConfig, resolveBuild, resolveHousekeeping, runnerVmOf };