@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
@@ -0,0 +1,490 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
3
+ import { WorkCommandError, postJson } from './chunk-HPJNLETG.js';
4
+ import { composeDelivery } from './chunk-LLM3I5YU.js';
5
+ import { defaultGitRunner, materializeContext, isStagingWriteFault, lookupGoverningSource } from './chunk-452TM7J4.js';
6
+ import { signingFetchFor } from './chunk-FVMYKL6L.js';
7
+ import { SHIM_SCRIPT } from './chunk-G2YNJIGF.js';
8
+ import { readDegraded, readContextManifest, readSourceCache } from './chunk-YQR6E6OV.js';
9
+ import { signerFor } from './chunk-M2ZWWTWZ.js';
10
+ import { SHIM_DIR, stagedContextDir, contextHomeDir, deliveryDir } from './chunk-N2PXGQRL.js';
11
+ import { fstatSync, mkdirSync, writeFileSync, chmodSync, rmSync, lstatSync } from 'fs';
12
+ import { relative, join, dirname } from 'path';
13
+
14
+ createRequire(import.meta.url);
15
+ function logStamp(at) {
16
+ return at.toISOString().replace(/\.\d{3}Z$/, "Z");
17
+ }
18
+ function sameDestination(stat = fstatSync) {
19
+ try {
20
+ const out = stat(1);
21
+ const err = stat(2);
22
+ return out.dev === err.dev && out.ino === err.ino;
23
+ } catch {
24
+ return false;
25
+ }
26
+ }
27
+ function createRunnerLog(now = () => /* @__PURE__ */ new Date(), io = { stdout: process.stdout, stderr: process.stderr, joined: () => sameDestination() }) {
28
+ const write = (stream, text) => {
29
+ stream.write(`${logStamp(now())} ${text}
30
+ `);
31
+ };
32
+ return {
33
+ line: (text) => write(io.stdout, text),
34
+ error: (text) => {
35
+ write(io.stdout, text);
36
+ if (!io.joined()) write(io.stderr, text);
37
+ }
38
+ };
39
+ }
40
+ function formatDuration(ms) {
41
+ const total = Math.max(0, Math.round(ms / 1e3));
42
+ const hours = Math.floor(total / 3600);
43
+ const minutes = Math.floor(total % 3600 / 60);
44
+ const seconds = total % 60;
45
+ if (hours > 0) return `${hours}h ${minutes}m`;
46
+ if (minutes > 0) return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
47
+ return `${seconds}s`;
48
+ }
49
+ function formatClock(at, today) {
50
+ const hhmm = `${String(at.getUTCHours()).padStart(2, "0")}:${String(at.getUTCMinutes()).padStart(2, "0")} UTC`;
51
+ const sameDay = at.getUTCFullYear() === today.getUTCFullYear() && at.getUTCMonth() === today.getUTCMonth() && at.getUTCDate() === today.getUTCDate();
52
+ if (sameDay) return hhmm;
53
+ const month = at.toLocaleString("en-US", { month: "short", timeZone: "UTC" });
54
+ return `${month} ${at.getUTCDate()} ${hhmm}`;
55
+ }
56
+ function shortClaim(claimId) {
57
+ return claimId.length <= 8 ? claimId : `${claimId.slice(0, 8)}\u2026`;
58
+ }
59
+ function relativeToRunner(runnerDirPath, path) {
60
+ const rel = relative(runnerDirPath, path);
61
+ return rel === "" || rel.startsWith("..") ? path : rel;
62
+ }
63
+ function describeCallFailure(err) {
64
+ if (err instanceof Error) {
65
+ const status = err.status;
66
+ return status === void 0 ? err.message : `${err.message} (${status})`;
67
+ }
68
+ return String(err);
69
+ }
70
+
71
+ // src/runner/retry.ts
72
+ var RETRY_WINDOW_MS = 12e4;
73
+ var FIRST_BACKOFF_MS = 1e3;
74
+ var MAX_BACKOFF_MS = 15e3;
75
+ function isRetryable(err) {
76
+ if (err instanceof WorkCommandError) return err.retryable;
77
+ return true;
78
+ }
79
+ async function retrySend(send, deps = {}) {
80
+ const now = deps.now ?? (() => /* @__PURE__ */ new Date());
81
+ const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
82
+ const windowMs = deps.windowMs ?? RETRY_WINDOW_MS;
83
+ const startedMs = now().getTime();
84
+ let attempts = 0;
85
+ let backoffMs = FIRST_BACKOFF_MS;
86
+ for (; ; ) {
87
+ attempts++;
88
+ try {
89
+ return { ok: true, value: await send(), attempts };
90
+ } catch (err) {
91
+ const reason = describeCallFailure(err);
92
+ const code = err instanceof WorkCommandError ? err.code : void 0;
93
+ const status = err instanceof WorkCommandError ? err.status : void 0;
94
+ const message = err instanceof WorkCommandError ? err.message : void 0;
95
+ if (!isRetryable(err)) return { ok: false, attempts, reason, retryable: false, code, status, message };
96
+ const elapsedMs = now().getTime() - startedMs;
97
+ if (elapsedMs + backoffMs >= windowMs) return { ok: false, attempts, reason, retryable: true, code, status, message };
98
+ await sleep(backoffMs);
99
+ backoffMs = Math.min(backoffMs * 2, MAX_BACKOFF_MS);
100
+ }
101
+ }
102
+ }
103
+
104
+ // src/runner/build-tokens.ts
105
+ var ISSUE_RETRY_WINDOW_MS = 3e4;
106
+ var REVOKE_TIMEOUT_MS = 15e3;
107
+ function isDue(expiresAtMs, nowMs) {
108
+ return nowMs >= expiresAtMs - 10 * 6e4;
109
+ }
110
+ function classifyRefusal(err) {
111
+ if (!(err instanceof WorkCommandError)) return { kind: "retry" };
112
+ if (err.status === 401 || err.code === "claim_expired" || err.code === "claim_not_held") return { kind: "lease_lost" };
113
+ if (err.code === "github_unavailable") return { kind: "retry" };
114
+ if (err.code === "github_request_failed") return { kind: "failed", reason: refusalCode(err) };
115
+ if (err.retryable && err.code !== "repository_tokens_unavailable") return { kind: "retry" };
116
+ return { kind: "failed", reason: refusalCode(err) };
117
+ }
118
+ function refusalCode(err) {
119
+ if (!(err instanceof WorkCommandError)) return "unavailable";
120
+ return err.code ?? `http_${err.status ?? "unknown"}`;
121
+ }
122
+ function keyOf(kind, repository) {
123
+ return repository === void 0 ? kind : `${kind}:${repository}`;
124
+ }
125
+ var BuildTokens = class {
126
+ constructor(options) {
127
+ this.options = options;
128
+ this.held = /* @__PURE__ */ new Map();
129
+ this.asking = /* @__PURE__ */ new Map();
130
+ this.issued = [];
131
+ this.usedKinds = /* @__PURE__ */ new Set();
132
+ this.revoked = false;
133
+ this.controlPlaneRetry = false;
134
+ }
135
+ now() {
136
+ return (this.options.now ?? (() => /* @__PURE__ */ new Date()))();
137
+ }
138
+ /** `repository` as the holder keys it: `undefined` for the primary, which is the one a request names no repository for. */
139
+ secondary(repository) {
140
+ if (repository === void 0) return void 0;
141
+ const primary = this.options.primary;
142
+ return primary !== void 0 && primary.toLowerCase() === repository.toLowerCase() ? void 0 : repository;
143
+ }
144
+ /**
145
+ * The token of `kind` for `repository`, or for the primary when none is
146
+ * named, asking the gateway for a new one when the build holds none or its
147
+ * token is within ten minutes of expiry.
148
+ */
149
+ token(kind, repository) {
150
+ const other = this.secondary(repository);
151
+ const key = keyOf(kind, other);
152
+ const held = this.held.get(key);
153
+ if (held !== void 0 && !isDue(held.expiresAtMs, this.now().getTime())) return Promise.resolve(held.token);
154
+ let asking = this.asking.get(key);
155
+ if (asking === void 0) {
156
+ asking = this.issueOrKeep(kind, other).finally(() => this.asking.delete(key));
157
+ this.asking.set(key, asking);
158
+ }
159
+ return asking;
160
+ }
161
+ /** A due token that has not yet expired is kept when the gateway cannot issue a new one. */
162
+ async issueOrKeep(kind, repository) {
163
+ try {
164
+ return await this.issue(kind, repository);
165
+ } catch (err) {
166
+ const held = this.held.get(keyOf(kind, repository));
167
+ if (held !== void 0 && this.now().getTime() < held.expiresAtMs && classifyRefusal(err).kind === "retry") return held.token;
168
+ throw err;
169
+ }
170
+ }
171
+ /** Asks the gateway for a token with `body`, retrying a failure about the moment. */
172
+ ask(body) {
173
+ const { config, item, fetchImpl } = this.options;
174
+ return retrySend(
175
+ async () => {
176
+ try {
177
+ return await postJson(
178
+ config.url,
179
+ `/v1/work-items/${item}/claim/tokens`,
180
+ config.apiKey,
181
+ config.appId,
182
+ body,
183
+ fetchImpl,
184
+ void 0,
185
+ await signerFor(config.configPath)
186
+ );
187
+ } catch (err) {
188
+ if (err instanceof WorkCommandError && (err.code === "repository_tokens_unavailable" || err.code === "github_request_failed")) {
189
+ throw new WorkCommandError(err.message, false, err.status, err.code);
190
+ }
191
+ throw err;
192
+ }
193
+ },
194
+ { now: this.options.now, sleep: this.options.sleep, windowMs: ISSUE_RETRY_WINDOW_MS }
195
+ );
196
+ }
197
+ /**
198
+ * A read-only token for the repository that governs the item's, which the
199
+ * runner uses on the host to fetch its context. It is revoked with the
200
+ * others when the attempt ends. It is not held or renewed: nothing uses it
201
+ * after the fetch. A refusal is thrown with the gateway's code. A lost
202
+ * lease is kept for `refusal()`, and a failure about the moment for
203
+ * `controlPlaneUnavailable()`, as the read token's are.
204
+ */
205
+ async controlPlane() {
206
+ const asked = await this.ask({ access: "read", target: "control_plane" });
207
+ if (!asked.ok) {
208
+ const err = new WorkCommandError(asked.message ?? asked.reason, asked.retryable, asked.status, asked.code);
209
+ const refusal = classifyRefusal(err);
210
+ if (refusal.kind === "lease_lost") this.failure = refusal;
211
+ if (refusal.kind === "retry") this.controlPlaneRetry = true;
212
+ throw err;
213
+ }
214
+ const { token, expiresAt, repository } = asked.value.data;
215
+ if (typeof token !== "string" || token === "" || typeof expiresAt !== "string" || typeof repository !== "string" || repository === "") {
216
+ throw new WorkCommandError("the gateway's token answer carried no token, expiry or repository", false, void 0, "malformed_token_response");
217
+ }
218
+ if (this.revoked) {
219
+ await this.revoke([token]);
220
+ throw new WorkCommandError("the build's tokens were already revoked", false, void 0, "tokens_revoked");
221
+ }
222
+ this.issued.push(token);
223
+ return { token, expiresAt, repository };
224
+ }
225
+ async issue(kind, repository) {
226
+ const asked = await this.ask({ access: kind, ...repository === void 0 ? {} : { repository } });
227
+ let err;
228
+ if (!asked.ok) {
229
+ err = new WorkCommandError(asked.message ?? asked.reason, asked.retryable, asked.status, asked.code);
230
+ } else {
231
+ const { token, expiresAt } = asked.value.data;
232
+ const expiresAtMs = typeof expiresAt === "string" ? Date.parse(expiresAt) : Number.NaN;
233
+ if (typeof token === "string" && token !== "" && Number.isFinite(expiresAtMs)) {
234
+ if (this.revoked) {
235
+ await this.revoke([token]);
236
+ throw new WorkCommandError("the build's tokens were already revoked", false, void 0, "tokens_revoked");
237
+ }
238
+ this.held.set(keyOf(kind, repository), { token, expiresAtMs });
239
+ this.issued.push(token);
240
+ if (kind === "read" && repository === void 0) this.readSink?.(token);
241
+ return token;
242
+ }
243
+ err = new WorkCommandError("the gateway's token answer carried no token or no expiry", false, void 0, "malformed_token_response");
244
+ }
245
+ const refusal = classifyRefusal(err);
246
+ if (refusal.kind !== "retry") this.failure = refusal;
247
+ throw err;
248
+ }
249
+ /** Asks for a new read token when the build holds one within ten minutes of expiry. A refusal is kept for `refusal()`, not thrown. */
250
+ async renewReadIfDue() {
251
+ const held = this.held.get("read");
252
+ if (held === void 0 || !isDue(held.expiresAtMs, this.now().getTime())) return;
253
+ await this.token("read").catch(() => void 0);
254
+ }
255
+ /** Calls `sink` with the read token now, and again with each one that replaces it. */
256
+ onRead(sink) {
257
+ this.readSink = sink;
258
+ const held = this.held.get("read");
259
+ if (held !== void 0) sink(held.token);
260
+ }
261
+ markUsed(kind) {
262
+ if (this.usedKinds.has(kind)) return;
263
+ this.usedKinds.add(kind);
264
+ this.options.onUsed?.(this.used());
265
+ }
266
+ /** The kinds of token the build has used, read before push. */
267
+ used() {
268
+ return ["read", "push"].filter((kind) => this.usedKinds.has(kind));
269
+ }
270
+ /** The last refusal that ends the build, if a renewal or a push's token request met one. */
271
+ refusal() {
272
+ return this.failure;
273
+ }
274
+ /** Whether a control plane token request failed for a reason about the moment, which a later attempt may not meet. */
275
+ controlPlaneUnavailable() {
276
+ return this.controlPlaneRetry;
277
+ }
278
+ /** Revokes every token this holder was issued, the superseded ones too, at GitHub with the token itself. A failure is not an error: the token lapses within the hour. */
279
+ async revokeAll() {
280
+ this.revoked = true;
281
+ const tokens = this.issued.splice(0);
282
+ this.held.clear();
283
+ await this.revoke(tokens);
284
+ }
285
+ async revoke(tokens) {
286
+ const apiUrl = this.options.apiUrl ?? "https://api.github.com";
287
+ await Promise.allSettled(
288
+ tokens.map(
289
+ (token) => this.options.fetchImpl(`${apiUrl}/installation/token`, {
290
+ method: "DELETE",
291
+ headers: {
292
+ authorization: `Bearer ${token}`,
293
+ accept: "application/vnd.github+json",
294
+ "x-github-api-version": "2022-11-28",
295
+ "user-agent": "outerlayer-runner"
296
+ },
297
+ signal: AbortSignal.timeout(REVOKE_TIMEOUT_MS)
298
+ })
299
+ )
300
+ );
301
+ }
302
+ };
303
+
304
+ // src/runner/control-plane-delivery.ts
305
+ function shimScript() {
306
+ return SHIM_SCRIPT;
307
+ }
308
+ function commandWithShim(job, cmdline) {
309
+ return job.environment?.controlPlane === void 0 ? cmdline : `PATH=${SHIM_DIR}:"$PATH"; export PATH; ${cmdline}`;
310
+ }
311
+
312
+ // src/runner/container/context.ts
313
+ var TokenRefused = class extends Error {
314
+ constructor(code, detail) {
315
+ super(code);
316
+ this.code = code;
317
+ this.detail = detail;
318
+ }
319
+ };
320
+ var NoControlPlane = class extends Error {
321
+ };
322
+ var GatewayConflict = class extends Error {
323
+ };
324
+ var unavailable = (why) => ({ kind: "refused", reason: `context_unavailable: ${why}` });
325
+ var DESIGNATION_LOOKUP = { timeoutMs: 1e4, attempts: 2 };
326
+ function lookupFailureReason(failure, repository) {
327
+ switch (failure?.kind) {
328
+ case "timeout":
329
+ return `the gateway did not answer which repository governs ${repository} within ${failure.timeoutMs / 1e3} seconds, twice`;
330
+ case "status":
331
+ return `the gateway answered ${failure.status} when asked which repository governs ${repository}`;
332
+ case "network":
333
+ return `the request asking which repository governs ${repository} failed: ${failure.message}`;
334
+ default:
335
+ return `the gateway did not say which repository governs ${repository}`;
336
+ }
337
+ }
338
+ function controlPlaneTokenFailure(err, repository) {
339
+ const code = refusalCode(err);
340
+ const detail = err instanceof WorkCommandError && err.code === void 0 && err.status === void 0 ? describeCallFailure(err) : void 0;
341
+ return detail === void 0 ? `context_unavailable: the gateway refused a token for the control plane named for ${repository} (${code})` : `context_unavailable: the token request for the control plane named for ${repository} failed: ${detail}`;
342
+ }
343
+ function makeReadable(tree, dir, files) {
344
+ chmodSync(dir, 493);
345
+ chmodSync(tree, 493);
346
+ const directories = /* @__PURE__ */ new Set();
347
+ for (const rel of files) {
348
+ for (let current = dirname(join(tree, rel)); current !== tree; current = dirname(current)) directories.add(current);
349
+ chmodSync(join(tree, rel), (lstatSync(join(tree, rel)).mode & 73) !== 0 ? 493 : 420);
350
+ }
351
+ for (const directory of directories) chmodSync(directory, 493);
352
+ }
353
+ async function stageGovernedContext(input) {
354
+ const git = input.git ?? defaultGitRunner;
355
+ const dir = stagedContextDir(input.buildDir);
356
+ const tree = join(dir, "tree");
357
+ const finish = (outcome) => {
358
+ removeStagedContext(input.buildDir);
359
+ return outcome;
360
+ };
361
+ try {
362
+ mkdirSync(tree, { recursive: true });
363
+ for (const args of [
364
+ ["init", "-q", "--initial-branch=main", tree],
365
+ ["-C", tree, "remote", "add", "origin", `https://github.com/${input.repository}.git`]
366
+ ]) {
367
+ const done = git(args, { cwd: dir, timeoutMs: 1e4 });
368
+ if (!done.ok) return finish(unavailable(`the staging directory could not be prepared: ${done.stderr.trim().split("\n")[0] ?? ""}`));
369
+ }
370
+ const { url, apiKey, appId, configPath } = input.config;
371
+ let tokenRepository;
372
+ const result = await materializeContext({
373
+ cwd: tree,
374
+ sessionId: `build-${input.buildId}`,
375
+ home: contextHomeDir(configPath),
376
+ cloud: { url, apiKey, appId },
377
+ fetchImpl: signingFetchFor(input.fetchImpl, configPath, apiKey),
378
+ designationLookup: DESIGNATION_LOOKUP,
379
+ ...input.source === void 0 ? {} : { pinnedSource: input.source },
380
+ git,
381
+ now: input.now,
382
+ resolveControlPlaneUrl: (repository) => input.controlPlaneUrl(repository),
383
+ controlPlaneAccess: input.controlPlaneToken === void 0 ? void 0 : async (candidates) => {
384
+ const access = await controlPlaneAccess(input.controlPlaneToken, candidates, input.repository, input.log, input.tag);
385
+ tokenRepository = access?.repository;
386
+ return access;
387
+ }
388
+ });
389
+ switch (result.status) {
390
+ case "materialized": {
391
+ const manifest = readContextManifest(join(tree, ".git"));
392
+ const source = readSourceCache(join(tree, ".git"));
393
+ if (result.sha === null || manifest === void 0 || source?.status !== "governed" || source.repository === void 0 || source.ref === void 0) {
394
+ return finish(unavailable(`the context of ${input.repository} was materialized without a record of it`));
395
+ }
396
+ const composed = composeDelivery({ tree, dir: deliveryDir(source.repository, result.sha) });
397
+ writeFileSync(
398
+ join(dir, "staged.json"),
399
+ JSON.stringify({ schemaVersion: 1, sha: result.sha, repository: source.repository, ref: source.ref, files: manifest.files }) + "\n"
400
+ );
401
+ makeReadable(tree, dir, [.../* @__PURE__ */ new Set([...manifest.files, ...composed])]);
402
+ chmodSync(join(dir, "staged.json"), 420);
403
+ return { kind: "staged", dir, sha: result.sha, repository: source.repository, ref: source.ref, files: manifest.files };
404
+ }
405
+ case "self":
406
+ if (result.designation === "answered") return finish({ kind: "none" });
407
+ if (result.authRejected === true) {
408
+ return finish(unavailable(`the gateway rejected the runner's API key (401/403) when asked which repository governs ${input.repository}`));
409
+ }
410
+ if (result.message !== void 0) return finish(unavailable(result.message));
411
+ return finish(unavailable(lookupFailureReason(result.lookupFailure, input.repository)));
412
+ case "unreachable":
413
+ case "no-access": {
414
+ const plane = isStagingWriteFault(result.message) ? void 0 : readDegraded(join(tree, ".git"))?.repository;
415
+ if (plane !== void 0) {
416
+ const credential = tokenRepository !== void 0 && tokenRepository.toLowerCase() === plane.toLowerCase() ? "the token the gateway issued" : "the host's git login";
417
+ return finish(unavailable(`${credential} cannot read ${plane}, the control plane named for ${input.repository}`));
418
+ }
419
+ return finish(unavailable(result.message ?? `the context of ${input.repository} could not be materialized (${result.status})`));
420
+ }
421
+ default:
422
+ return finish(unavailable(result.message ?? `the context of ${input.repository} could not be materialized (${result.status})`));
423
+ }
424
+ } catch (err) {
425
+ if (err instanceof NoControlPlane) return finish({ kind: "none" });
426
+ if (err instanceof GatewayConflict) return finish({ kind: "refused", reason: `context_conflict: ${err.message}` });
427
+ if (err instanceof TokenRefused) {
428
+ return finish(
429
+ unavailable(
430
+ err.detail === void 0 ? `the gateway refused a token for the control plane named for ${input.repository} (${err.code})` : `the token request for the control plane named for ${input.repository} failed: ${err.detail}`
431
+ )
432
+ );
433
+ }
434
+ return finish(unavailable(`the context of ${input.repository} could not be staged: ${err instanceof Error ? err.message : String(err)}`));
435
+ }
436
+ }
437
+ async function controlPlaneAccess(ask, candidates, repository, log, tag) {
438
+ let answer;
439
+ try {
440
+ answer = await ask();
441
+ } catch (err) {
442
+ const code = refusalCode(err);
443
+ if (code === "repository_tokens_unavailable") return void 0;
444
+ if (code === "no_control_plane") throw new NoControlPlane(code);
445
+ if (code === "context_conflict") throw new GatewayConflict(err instanceof Error ? err.message : code);
446
+ throw new TokenRefused(code, err instanceof WorkCommandError && err.code === void 0 && err.status === void 0 ? describeCallFailure(err) : void 0);
447
+ }
448
+ const named = candidates.some((candidate) => candidate.repository.toLowerCase() === answer.repository.toLowerCase());
449
+ if (!named) {
450
+ log?.line(
451
+ `${tag === void 0 ? "" : `${tag} `}the gateway issued a control plane token for ${answer.repository}, which is none of the repositories named for ${repository} (${candidates.map((candidate) => candidate.repository).join(", ")}) \xB7 fetching with the host's git login`
452
+ );
453
+ return void 0;
454
+ }
455
+ return answer;
456
+ }
457
+ function removeStagedContext(buildDir) {
458
+ rmSync(stagedContextDir(buildDir), { recursive: true, force: true });
459
+ }
460
+
461
+ // src/runner/governing-source.ts
462
+ var unavailable2 = (why) => ({ ok: false, reason: `context_unavailable: ${why}` });
463
+ function pinnedSourceOf(source) {
464
+ return source.kind === "self" ? null : { repository: source.repository, ref: source.ref, sha: source.sha };
465
+ }
466
+ async function resolveGoverningSource(input) {
467
+ const { url, apiKey, appId, configPath } = input.config;
468
+ const lookup = await lookupGoverningSource({ url, apiKey, appId }, input.repository, signingFetchFor(input.fetchImpl, configPath, apiKey), DESIGNATION_LOOKUP);
469
+ switch (lookup.kind) {
470
+ case "rejected":
471
+ return unavailable2(`the gateway rejected the runner's API key (401/403) when asked which repository governs ${input.repository}`);
472
+ case "failed":
473
+ return unavailable2(lookupFailureReason(lookup.failure, input.repository));
474
+ case "answered":
475
+ if (lookup.source === null) return { ok: true, source: { kind: "self" } };
476
+ if (lookup.source === void 0) {
477
+ return unavailable2("the OuterLayer this machine talks to is too old to name the control plane \u2014 ask whoever runs it to upgrade");
478
+ }
479
+ return { ok: true, source: { kind: "governed", ...lookup.source } };
480
+ }
481
+ }
482
+ async function imageTargetFor(input) {
483
+ if (input.config === void 0) return { ok: true, repository: input.repository };
484
+ const resolved = await resolveGoverningSource({ config: input.config, repository: input.repository, fetchImpl: input.fetchImpl });
485
+ if (!resolved.ok) return resolved;
486
+ if (resolved.source.kind === "self") return { ok: true, repository: input.repository };
487
+ return { ok: true, repository: resolved.source.repository, pinned: { commit: resolved.source.sha, ref: resolved.source.ref } };
488
+ }
489
+
490
+ export { BuildTokens, classifyRefusal, commandWithShim, controlPlaneTokenFailure, createRunnerLog, describeCallFailure, formatClock, formatDuration, imageTargetFor, logStamp, pinnedSourceOf, refusalCode, relativeToRunner, removeStagedContext, resolveGoverningSource, retrySend, shimScript, shortClaim, stageGovernedContext };
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
3
+ import { unlinkSync, readFileSync, mkdirSync, writeFileSync, existsSync } from 'fs';
4
+ import { isAbsolute, join, dirname } from 'path';
5
+
6
+ createRequire(import.meta.url);
7
+ function stateDir(gitCommonDir) {
8
+ return join(gitCommonDir, "outerlayer");
9
+ }
10
+ function readJson(path) {
11
+ try {
12
+ return JSON.parse(readFileSync(path, "utf8"));
13
+ } catch {
14
+ return void 0;
15
+ }
16
+ }
17
+ function writeJson(path, value) {
18
+ mkdirSync(dirname(path), { recursive: true });
19
+ writeFileSync(path, JSON.stringify(value, null, 2) + "\n");
20
+ }
21
+ function manifestPath(gitCommonDir) {
22
+ return join(stateDir(gitCommonDir), "materialized.json");
23
+ }
24
+ function readContextManifest(gitCommonDir) {
25
+ const parsed = readJson(manifestPath(gitCommonDir));
26
+ if (parsed && typeof parsed.sha === "string" && Array.isArray(parsed.files) && parsed.files.every((f) => typeof f === "string") && typeof parsed.materializedAt === "string") {
27
+ return parsed;
28
+ }
29
+ return void 0;
30
+ }
31
+ function writeContextManifest(gitCommonDir, manifest) {
32
+ writeJson(manifestPath(gitCommonDir), manifest);
33
+ }
34
+ function removeContextManifest(gitCommonDir) {
35
+ try {
36
+ unlinkSync(manifestPath(gitCommonDir));
37
+ } catch {
38
+ }
39
+ }
40
+ function degradedPath(gitCommonDir) {
41
+ return join(stateDir(gitCommonDir), "degraded.json");
42
+ }
43
+ function readDegraded(gitCommonDir) {
44
+ const parsed = readJson(degradedPath(gitCommonDir));
45
+ if (parsed && typeof parsed.kind === "string" && typeof parsed.message === "string") return parsed;
46
+ return void 0;
47
+ }
48
+ function writeDegraded(gitCommonDir, state) {
49
+ writeJson(degradedPath(gitCommonDir), state);
50
+ }
51
+ function clearDegraded(gitCommonDir) {
52
+ try {
53
+ unlinkSync(degradedPath(gitCommonDir));
54
+ } catch {
55
+ }
56
+ }
57
+ var SOURCE_CACHE_TTL_SUCCESS_MS = 24 * 60 * 60 * 1e3;
58
+ var SOURCE_CACHE_TTL_RETRY_MS = 15 * 60 * 1e3;
59
+ function sourceCachePath(gitCommonDir) {
60
+ return join(stateDir(gitCommonDir), "source.json");
61
+ }
62
+ function readSourceCache(gitCommonDir) {
63
+ const parsed = readJson(sourceCachePath(gitCommonDir));
64
+ if (parsed && (parsed.status === "self" || parsed.status === "governed") && typeof parsed.checkedAt === "string" && typeof parsed.ttlMs === "number") {
65
+ return parsed;
66
+ }
67
+ return void 0;
68
+ }
69
+ function writeSourceCache(gitCommonDir, cache) {
70
+ writeJson(sourceCachePath(gitCommonDir), cache);
71
+ }
72
+ function isSourceCacheFresh(cache, now) {
73
+ return now - Date.parse(cache.checkedAt) < cache.ttlMs;
74
+ }
75
+ function deliveryRecordPath(gitCommonDir) {
76
+ return join(stateDir(gitCommonDir), "delivery.json");
77
+ }
78
+ function readDeliveryRecord(gitCommonDir) {
79
+ const parsed = readJson(deliveryRecordPath(gitCommonDir));
80
+ if (typeof parsed?.dir === "string" && isAbsolute(parsed.dir) && typeof parsed.repository === "string" && typeof parsed.sha === "string") {
81
+ return {
82
+ ...typeof parsed.schemaVersion === "number" ? { schemaVersion: parsed.schemaVersion } : {},
83
+ repository: parsed.repository,
84
+ ...typeof parsed.ref === "string" ? { ref: parsed.ref } : {},
85
+ sha: parsed.sha,
86
+ dir: parsed.dir,
87
+ ...parsed.launcher === true ? { launcher: true } : {}
88
+ };
89
+ }
90
+ return void 0;
91
+ }
92
+ function removeLauncherDeliveryRecord(gitCommonDir) {
93
+ if (readDeliveryRecord(gitCommonDir)?.launcher !== true) return;
94
+ try {
95
+ unlinkSync(deliveryRecordPath(gitCommonDir));
96
+ } catch {
97
+ }
98
+ }
99
+ function deliveryRecordExists(gitCommonDir) {
100
+ return existsSync(deliveryRecordPath(gitCommonDir));
101
+ }
102
+ function writeDeliveryRecord(gitCommonDir, record) {
103
+ writeJson(deliveryRecordPath(gitCommonDir), record);
104
+ }
105
+ function cloneIsGoverned(gitCommonDir) {
106
+ return readSourceCache(gitCommonDir)?.status === "governed" || deliveryRecordExists(gitCommonDir);
107
+ }
108
+
109
+ export { SOURCE_CACHE_TTL_RETRY_MS, SOURCE_CACHE_TTL_SUCCESS_MS, clearDegraded, cloneIsGoverned, isSourceCacheFresh, readContextManifest, readDegraded, readDeliveryRecord, readSourceCache, removeContextManifest, removeLauncherDeliveryRecord, writeContextManifest, writeDegraded, writeDeliveryRecord, writeSourceCache };
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
- import { C } from './chunk-YRYMNBGQ.js';
3
+ import { C } from './chunk-ANPNT6PM.js';
4
4
  import { readCloudConfig } from './chunk-EABW6AJQ.js';
5
+ import { cliInvocation } from './chunk-CRB3POP4.js';
5
6
  import { existsSync, writeFileSync, readFileSync, mkdirSync } from 'fs';
6
7
  import { join, dirname } from 'path';
7
8
 
@@ -12,7 +13,7 @@ function defaultMcpUrl(home) {
12
13
  const origin = readCloudConfig(home).url;
13
14
  if (!origin) {
14
15
  throw new McpInstallError(
15
- "no gateway URL: pass --url <your gateway>, or run `outerlayer login` first."
16
+ `no gateway URL: pass --url <your gateway>, or run \`${cliInvocation()} login\` first.`
16
17
  );
17
18
  }
18
19
  return `${origin.replace(/\/+$/, "")}/v1/mcp`;