@outerlayer/cli 0.2.0 → 0.4.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 (141) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/README.md +226 -59
  3. package/dist/agent-setup-IFH3FB5X.js +197 -0
  4. package/dist/build-TZUUTRGH.js +9 -0
  5. package/dist/build-info.json +1 -1
  6. package/dist/check-WQP2CZDW.js +94 -0
  7. package/dist/chunk-2E54Z3P5.js +232 -0
  8. package/dist/chunk-3TT7RQYB.js +55 -0
  9. package/dist/{chunk-U32VSRLO.js → chunk-3YPKXE5L.js} +90 -632
  10. package/dist/{chunk-Q6CL5THG.js → chunk-5DLBAXY5.js} +208 -61
  11. package/dist/{chunk-R5KBGVII.js → chunk-5QRP3MVS.js} +30 -14
  12. package/dist/chunk-5Y3QQRUH.js +300 -0
  13. package/dist/chunk-6Y63SXFO.js +383 -0
  14. package/dist/chunk-774HEQL6.js +178 -0
  15. package/dist/chunk-7A6RVXPZ.js +2233 -0
  16. package/dist/chunk-7YIOOFVJ.js +71 -0
  17. package/dist/chunk-ABIWDUMS.js +104 -0
  18. package/dist/chunk-AUFG23AA.js +534 -0
  19. package/dist/chunk-B2G7JHHB.js +42 -0
  20. package/dist/chunk-BCJNHMZT.js +34 -0
  21. package/dist/chunk-BCLJSQCV.js +56 -0
  22. package/dist/chunk-BFESLKPP.js +61 -0
  23. package/dist/chunk-BJ3KTMDH.js +63 -0
  24. package/dist/chunk-BKO6JEZI.js +47 -0
  25. package/dist/chunk-BOLTI6LR.js +37 -0
  26. package/dist/chunk-CBPPJSAR.js +89 -0
  27. package/dist/chunk-CBZB6XY6.js +102 -0
  28. package/dist/{chunk-65WXAANR.js → chunk-DVDEBNQJ.js} +17 -2
  29. package/dist/{work-pr-cmd-AZQPWH4H.js → chunk-DVYHCMB2.js} +9 -26
  30. package/dist/{chunk-TFUIDMOB.js → chunk-EABW6AJQ.js} +12 -1
  31. package/dist/{chunk-KFYJV2ZG.js → chunk-EMY4I27X.js} +1 -1
  32. package/dist/{chunk-NTTPJV35.js → chunk-F47JBIAW.js} +3 -1
  33. package/dist/chunk-F6GFZXZ3.js +60 -0
  34. package/dist/{chunk-YOOSBOKS.js → chunk-F7CU5ABH.js} +1 -1
  35. package/dist/chunk-FAJSS2WN.js +1357 -0
  36. package/dist/chunk-FS2VYGZT.js +1764 -0
  37. package/dist/chunk-FV2CCGXK.js +9 -0
  38. package/dist/{chunk-4TNZMO7V.js → chunk-G7FSV7JX.js} +3452 -751
  39. package/dist/{emit-cmd-TWSYYEZI.js → chunk-H5NCNVDA.js} +68 -17
  40. package/dist/chunk-HE2D4EY5.js +83 -0
  41. package/dist/chunk-L2DBRKRA.js +94 -0
  42. package/dist/chunk-LF6MLJCY.js +38 -0
  43. package/dist/chunk-M5POMKKW.js +1051 -0
  44. package/dist/{mcp-install-cmd-FDQH6SEN.js → chunk-MQ3IPHIZ.js} +35 -18
  45. package/dist/{chunk-D77LS3UI.js → chunk-N5FOE5PS.js} +33 -4
  46. package/dist/chunk-N6LURUEF.js +64 -0
  47. package/dist/chunk-NYC54VBD.js +1084 -0
  48. package/dist/chunk-OGFGZQA3.js +23 -0
  49. package/dist/chunk-QOZKPLVJ.js +226 -0
  50. package/dist/chunk-RA3O54FC.js +30 -0
  51. package/dist/chunk-TBT347UY.js +41 -0
  52. package/dist/{chunk-NXLLURA4.js → chunk-UFFXNXLQ.js} +115 -71
  53. package/dist/chunk-USO2DKBD.js +421 -0
  54. package/dist/chunk-W4SQCOZU.js +206 -0
  55. package/dist/{chunk-A3WLZX2F.js → chunk-WIOZAJ2W.js} +15 -2
  56. package/dist/chunk-WO2BXCTQ.js +83 -0
  57. package/dist/{chunk-I3ETLSNE.js → chunk-XCCLFVXM.js} +120 -11
  58. package/dist/{context-materialize-6WKT3RBQ.js → chunk-XDDW4FRS.js} +134 -26
  59. package/dist/chunk-YYYXRJUW.js +101 -0
  60. package/dist/chunk-ZM2IMMYN.js +76 -0
  61. package/dist/chunk-ZMYPLFG3.js +71 -0
  62. package/dist/chunk-ZNA27WEV.js +470 -0
  63. package/dist/{cli-G7DILYJY.js → cli-NHUXABYH.js} +590 -876
  64. package/dist/{paths-D2VGWWFI.js → cli-build-K56DK4DS.js} +1 -1
  65. package/dist/config-XVJYZ4IQ.js +9 -0
  66. package/dist/connect-cmd-OOG4TPU7.js +16 -0
  67. package/dist/context-adopt-UMA4O5NY.js +105 -0
  68. package/dist/context-materialize-G2FUFC72.js +19 -0
  69. package/dist/dist-RW4UPPC2.js +6 -0
  70. package/dist/docker-NEGDLU6D.js +7 -0
  71. package/dist/doctor-53F2PYY7.js +43 -0
  72. package/dist/doctor-UFVL4PZY.js +426 -0
  73. package/dist/{emit-artifact-cmd-UQVT3OKW.js → emit-artifact-cmd-KTYW2TN2.js} +24 -20
  74. package/dist/emit-cmd-DCERAU27.js +9 -0
  75. package/dist/emit-criteria-cmd-VAUUSAYD.js +166 -0
  76. package/dist/{emit-finding-cmd-FDOU2OPD.js → emit-finding-cmd-UR4ID45Z.js} +34 -31
  77. package/dist/{emit-result-cmd-H2T4C2K3.js → emit-result-cmd-FDTUKGKE.js} +34 -62
  78. package/dist/exec-client-4XGXV3XN.js +8 -0
  79. package/dist/guest-init-GENRHNP7.js +8 -0
  80. package/dist/hook-fast-EYENPK6F.js +9 -0
  81. package/dist/{hook-wrap-fast-KXYNX3AD.js → hook-wrap-fast-NPLHJXFK.js} +2 -2
  82. package/dist/host-key-KDYZ5ECC.js +7 -0
  83. package/dist/import-capture-cmd-AVZ4VIVJ.js +68 -0
  84. package/dist/{import-ruler-cmd-7LH2QOMG.js → import-ruler-cmd-GUHL6IY5.js} +1 -1
  85. package/dist/index.js +4 -4
  86. package/dist/init-DXBCOAJK.js +36 -0
  87. package/dist/init-YEXK35CF.js +221 -0
  88. package/dist/init-cmd-NHR7PRSQ.js +135 -0
  89. package/dist/install-cmd-7WXOIFDO.js +55 -0
  90. package/dist/lima-XN65D7GN.js +55 -0
  91. package/dist/login-browser-7XGSV7MA.js +10 -0
  92. package/dist/{config-POF7DEQW.js → logout-cmd-QAUFDJ6J.js} +3 -1
  93. package/dist/{logs-TRNPQM42.js → logs-7RYUH5A6.js} +1 -1
  94. package/dist/loop-VZOLL4WQ.js +43 -0
  95. package/dist/machine-WSG52J75.js +35 -0
  96. package/dist/mcp-install-cmd-RLK4SO3N.js +9 -0
  97. package/dist/{mcp-serve-cmd-57EZZOTL.js → mcp-serve-cmd-JPTP5FMS.js} +20 -9
  98. package/dist/paths-OYKMVYJP.js +6 -0
  99. package/dist/{pidfile-PTW76F56.js → pidfile-UZRH774M.js} +2 -3
  100. package/dist/real-deps-SU24ZA2K.js +21 -0
  101. package/dist/relay-L76HDX72.js +46 -0
  102. package/dist/settings-J2652U5N.js +7 -0
  103. package/dist/starter-pack-LIZYMKYQ.js +10 -0
  104. package/dist/{status-I27IST4L.js → status-2UZKKNB7.js} +30 -13
  105. package/dist/{statusline-fast-3C5OXHDD.js → statusline-fast-SVTMBMD7.js} +3 -2
  106. package/dist/sync-cmd-VPPYCNNM.js +28 -0
  107. package/dist/version-BWM6VLDI.js +6 -0
  108. package/dist/{watch-3DTPJETH.js → watch-5C4ZBBGO.js} +19 -8
  109. package/dist/{work-claim-cmd-5AUNCLS2.js → work-claim-cmd-LSO2B2EX.js} +33 -20
  110. package/dist/work-cmd-UOQO2AD4.js +17 -0
  111. package/dist/work-comment-cmd-WF3EOLAE.js +106 -0
  112. package/dist/{work-launch-LT663PB3.js → work-launch-YI4CJDAP.js} +1 -1
  113. package/dist/work-open-pr-cmd-4SSQZMYG.js +156 -0
  114. package/dist/work-pr-cmd-6AN6RRQJ.js +16 -0
  115. package/package.json +13 -3
  116. package/skill-pack/maintained/amend/SKILL.md +104 -0
  117. package/skill-pack/maintained/emitting-evidence/SKILL.md +99 -0
  118. package/skill-pack/maintained/emitting-evidence/references/agents-snippet.md +20 -0
  119. package/skill-pack/maintained/outerlayer/SKILL.md +55 -0
  120. package/skill-pack/maintained/reporting-findings/SKILL.md +148 -0
  121. package/skill-pack/template/build/SKILL.md +193 -0
  122. package/skill-pack/template/build/references/agent-briefs.md +243 -0
  123. package/skill-pack/template/build/references/criteria-judge.md +91 -0
  124. package/skill-pack/template/build/references/evidence.md +42 -0
  125. package/skill-pack/template/build/references/release.md +74 -0
  126. package/skill-pack/template/build/references/review-briefs.md +275 -0
  127. package/skill-pack/template/build/references/review-loop.md +158 -0
  128. package/skill-pack/template/build/scripts/record-criteria.mjs +235 -0
  129. package/skill-pack/template/spec/SKILL.md +84 -0
  130. package/skill-pack/template/writing-specs/SKILL.md +134 -0
  131. package/dist/chunk-DCNOXRMV.js +0 -589
  132. package/dist/chunk-JJP7YLMN.js +0 -25
  133. package/dist/chunk-OZ7C3XUE.js +0 -34
  134. package/dist/chunk-WQ6VGRGZ.js +0 -150
  135. package/dist/hook-fast-J5LCDHUJ.js +0 -8
  136. package/dist/import-capture-cmd-EUMBGIV3.js +0 -176
  137. package/dist/init-PTBITAUO.js +0 -103
  138. package/dist/login-cmd-IRX6LZT7.js +0 -62
  139. package/dist/loop-ASZRX3CZ.js +0 -648
  140. package/dist/sync-cmd-BBAUZ5JD.js +0 -17
  141. package/dist/work-cmd-2P4BVX47.js +0 -18
@@ -1,589 +0,0 @@
1
- #!/usr/bin/env node
2
- import { createRequire } from 'node:module';
3
- import { startTimeOf, isSameProcess } from './chunk-OZ7C3XUE.js';
4
- import { RUNNER_DIR_MODE, RUNNER_FILE_MODE } from './chunk-JJP7YLMN.js';
5
- import { postJson, WorkCommandError } from './chunk-Q6CL5THG.js';
6
- import { relative, dirname, delimiter, join } from 'path';
7
- import { existsSync, readFileSync, mkdirSync, writeFileSync, openSync, statSync, closeSync, readdirSync } from 'fs';
8
- import { spawn } from 'child_process';
9
-
10
- createRequire(import.meta.url);
11
- function logStamp(at) {
12
- return at.toISOString().replace(/\.\d{3}Z$/, "Z");
13
- }
14
- function createRunnerLog(now = () => /* @__PURE__ */ new Date()) {
15
- const write = (stream, text) => {
16
- stream.write(`${logStamp(now())} ${text}
17
- `);
18
- };
19
- return {
20
- line: (text) => write(process.stdout, text),
21
- error: (text) => {
22
- write(process.stdout, text);
23
- write(process.stderr, text);
24
- }
25
- };
26
- }
27
- function formatDuration(ms) {
28
- const total = Math.max(0, Math.round(ms / 1e3));
29
- const hours = Math.floor(total / 3600);
30
- const minutes = Math.floor(total % 3600 / 60);
31
- const seconds = total % 60;
32
- if (hours > 0) return `${hours}h ${minutes}m`;
33
- if (minutes > 0) return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
34
- return `${seconds}s`;
35
- }
36
- function formatClock(at, today) {
37
- const hhmm = `${String(at.getUTCHours()).padStart(2, "0")}:${String(at.getUTCMinutes()).padStart(2, "0")} UTC`;
38
- const sameDay = at.getUTCFullYear() === today.getUTCFullYear() && at.getUTCMonth() === today.getUTCMonth() && at.getUTCDate() === today.getUTCDate();
39
- if (sameDay) return hhmm;
40
- const month = at.toLocaleString("en-US", { month: "short", timeZone: "UTC" });
41
- return `${month} ${at.getUTCDate()} ${hhmm}`;
42
- }
43
- function shortClaim(claimId) {
44
- return claimId.length <= 8 ? claimId : `${claimId.slice(0, 8)}\u2026`;
45
- }
46
- function relativeToRunner(runnerDirPath, path) {
47
- const rel = relative(runnerDirPath, path);
48
- return rel === "" || rel.startsWith("..") ? path : rel;
49
- }
50
- function describeCallFailure(err) {
51
- if (err instanceof Error) {
52
- const status = err.status;
53
- return status === void 0 ? err.message : `${err.message} (${status})`;
54
- }
55
- return String(err);
56
- }
57
-
58
- // src/runner/retry.ts
59
- var RETRY_WINDOW_MS = 12e4;
60
- var FIRST_BACKOFF_MS = 1e3;
61
- var MAX_BACKOFF_MS = 15e3;
62
- function isRetryable(err) {
63
- if (err instanceof WorkCommandError) return err.retryable;
64
- return true;
65
- }
66
- async function retrySend(send, deps = {}) {
67
- const now = deps.now ?? (() => /* @__PURE__ */ new Date());
68
- const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
69
- const windowMs = deps.windowMs ?? RETRY_WINDOW_MS;
70
- const startedMs = now().getTime();
71
- let attempts = 0;
72
- let backoffMs = FIRST_BACKOFF_MS;
73
- for (; ; ) {
74
- attempts++;
75
- try {
76
- return { ok: true, value: await send(), attempts };
77
- } catch (err) {
78
- const reason = describeCallFailure(err);
79
- if (!isRetryable(err)) return { ok: false, attempts, reason, retryable: false };
80
- const elapsedMs = now().getTime() - startedMs;
81
- if (elapsedMs + backoffMs >= windowMs) return { ok: false, attempts, reason, retryable: true };
82
- await sleep(backoffMs);
83
- backoffMs = Math.min(backoffMs * 2, MAX_BACKOFF_MS);
84
- }
85
- }
86
- }
87
- function readJobFile(path) {
88
- if (!existsSync(path)) return null;
89
- try {
90
- return JSON.parse(readFileSync(path, "utf8"));
91
- } catch {
92
- return null;
93
- }
94
- }
95
- function writeJobFile(path, job) {
96
- mkdirSync(dirname(path), { recursive: true, mode: RUNNER_DIR_MODE });
97
- writeFileSync(path, JSON.stringify(job, null, 2), { mode: RUNNER_FILE_MODE });
98
- }
99
- var LEASE_SECONDS = 900;
100
- var RENEW_INTERVAL_SECONDS = 300;
101
- var IDLE_CPU_USEC_PER_MINUTE_FLOOR = 2e6;
102
- function jobEnv(config, item, jobDirPath, baseEnv = process.env) {
103
- const passthrough = {};
104
- for (const [key, value] of Object.entries(baseEnv)) {
105
- if (value === void 0) continue;
106
- if (key.startsWith("OUTERLAYER_")) continue;
107
- passthrough[key] = value;
108
- }
109
- const path = cliFirstPath(passthrough.PATH);
110
- return {
111
- ...passthrough,
112
- ...path === void 0 ? {} : { PATH: path },
113
- OUTERLAYER_URL: config.url,
114
- OUTERLAYER_API_KEY: config.apiKey,
115
- OUTERLAYER_APP_ID: config.appId,
116
- OUTERLAYER_WORK: String(item),
117
- OUTERLAYER_JOB_DIR: jobDirPath
118
- };
119
- }
120
- function cliFirstPath(currentPath) {
121
- const entry = process.argv[1];
122
- const cliDir = entry === void 0 || entry === "" ? void 0 : dirname(entry);
123
- if (cliDir === void 0) return currentPath;
124
- const parts = (currentPath ?? "").split(delimiter).filter((p) => p !== "");
125
- if (parts[0] === cliDir) return currentPath;
126
- return [cliDir, ...parts.filter((p) => p !== cliDir)].join(delimiter);
127
- }
128
- var HOOK_ENDED_EXIT_CODE = 124;
129
- function runHook(hookPath, env, cwd, logPath, spawnFn = spawn, limits) {
130
- return new Promise((resolve, reject) => {
131
- let fd;
132
- try {
133
- fd = openSync(logPath, "a", RUNNER_FILE_MODE);
134
- } catch (err) {
135
- reject(err instanceof Error ? err : new Error(String(err)));
136
- return;
137
- }
138
- let timer;
139
- let endedBy;
140
- const settle = () => {
141
- if (timer) clearInterval(timer);
142
- try {
143
- closeSync(fd);
144
- } catch {
145
- }
146
- };
147
- const child = spawnFn(hookPath, [], { env, cwd, detached: true, stdio: ["ignore", fd, fd] });
148
- if (limits !== void 0 && child.pid !== void 0) {
149
- const pid = child.pid;
150
- const readClock = limits.now ?? (() => /* @__PURE__ */ new Date());
151
- let lastActivityAtMs = readClock().getTime();
152
- let lastSeen = readLogState(logPath);
153
- timer = setInterval(() => {
154
- const nowMs = readClock().getTime();
155
- const seen = readLogState(logPath);
156
- if (seen.size !== lastSeen.size || seen.mtimeMs !== lastSeen.mtimeMs) {
157
- lastSeen = seen;
158
- lastActivityAtMs = nowMs;
159
- }
160
- const reason = nowMs >= limits.endBy ? "time limit" : nowMs - lastActivityAtMs >= limits.idleMs ? "idle" : void 0;
161
- if (reason === void 0) return;
162
- endedBy = reason;
163
- if (timer) clearInterval(timer);
164
- void stopProcessGroup(pid);
165
- }, limits.pollMs ?? 1e3);
166
- timer.unref?.();
167
- }
168
- child.on("error", (err) => {
169
- settle();
170
- reject(hookSpawnError(hookPath, err));
171
- });
172
- child.on("exit", (code) => {
173
- settle();
174
- resolve(endedBy === void 0 ? { code: code ?? 1 } : { code: HOOK_ENDED_EXIT_CODE, endedBy });
175
- });
176
- });
177
- }
178
- function hookSpawnError(hookPath, err) {
179
- const code = err?.code;
180
- if (code === "ENOENT") {
181
- return new Error(
182
- `${hookPath}: no such executable. A hook is run directly, not through a shell, so the path must name an executable file with no arguments and no variables to expand.`
183
- );
184
- }
185
- if (code === "EACCES") {
186
- return new Error(`${hookPath}: not executable. Give the file the execute bit, or name one that has it.`);
187
- }
188
- return err instanceof Error ? err : new Error(String(err));
189
- }
190
- function workdirProblem(workdir) {
191
- let stats;
192
- try {
193
- stats = statSync(workdir);
194
- } catch {
195
- return "does not exist";
196
- }
197
- return stats.isDirectory() ? void 0 : "is not a directory";
198
- }
199
- function readProvisionOut(jobDirPath) {
200
- const path = join(jobDirPath, "provision.out");
201
- if (!existsSync(path)) return { ok: false };
202
- let text;
203
- try {
204
- text = readFileSync(path, "utf8");
205
- } catch {
206
- return { ok: false };
207
- }
208
- const kv = {};
209
- for (const line of text.split("\n")) {
210
- const eq = line.indexOf("=");
211
- if (eq === -1) continue;
212
- kv[line.slice(0, eq).trim()] = line.slice(eq + 1).trim();
213
- }
214
- if (!kv.workdir) return { ok: false };
215
- return kv.cgroup ? { ok: true, workdir: kv.workdir, cgroup: kv.cgroup } : { ok: true, workdir: kv.workdir };
216
- }
217
- var EXIT_CODE_FILE = "exit-code";
218
- function commandWithExitCodeTrailer(cmdline) {
219
- return [
220
- "__ol_record_status() {",
221
- " __ol_status=$?",
222
- ' if [ -n "$OUTERLAYER_JOB_DIR" ]; then',
223
- ` echo "$__ol_status" > "$OUTERLAYER_JOB_DIR/${EXIT_CODE_FILE}.partial" 2>/dev/null &&`,
224
- ` mv -f "$OUTERLAYER_JOB_DIR/${EXIT_CODE_FILE}.partial" "$OUTERLAYER_JOB_DIR/${EXIT_CODE_FILE}" 2>/dev/null`,
225
- " fi",
226
- ' return "$__ol_status"',
227
- "}",
228
- "trap __ol_record_status EXIT",
229
- cmdline
230
- ].join("\n");
231
- }
232
- function readExitCode(jobDirPath) {
233
- let text;
234
- try {
235
- text = readFileSync(join(jobDirPath, EXIT_CODE_FILE), "utf8");
236
- } catch {
237
- return void 0;
238
- }
239
- const trimmed = text.trim();
240
- return /^\d+$/.test(trimmed) ? Number(trimmed) : void 0;
241
- }
242
- function outcomeFromExitCode(jobDirPath) {
243
- const code = readExitCode(jobDirPath);
244
- if (code === void 0) return void 0;
245
- return { outcome: code === 0 ? "ok" : "failed", code };
246
- }
247
- function runCommand(cmdline, env, cwd, logPath, spawnFn = spawn) {
248
- const fd = openSync(logPath, "a", RUNNER_FILE_MODE);
249
- const child = spawnFn("/bin/sh", ["-c", commandWithExitCodeTrailer(cmdline)], {
250
- env,
251
- cwd,
252
- detached: true,
253
- stdio: ["ignore", fd, fd]
254
- });
255
- const pid = child.pid;
256
- if (pid === void 0) {
257
- try {
258
- closeSync(fd);
259
- } catch {
260
- }
261
- throw new Error("command failed to spawn \u2014 no pid");
262
- }
263
- const onExit = new Promise((resolve) => {
264
- const close = () => {
265
- try {
266
- closeSync(fd);
267
- } catch {
268
- }
269
- };
270
- child.on("exit", (code, signal) => {
271
- close();
272
- resolve({ code, signal });
273
- });
274
- child.on("error", (err) => {
275
- close();
276
- resolve({ code: null, signal: null, error: err instanceof Error ? err : new Error(String(err)) });
277
- });
278
- });
279
- return { pid, start: startTimeOf(pid), onExit };
280
- }
281
- function isGroupAlive(pid) {
282
- try {
283
- process.kill(-pid, 0);
284
- return true;
285
- } catch {
286
- return false;
287
- }
288
- }
289
- async function stopProcessGroup(pid, opts = {}) {
290
- const graceMs = opts.graceMs ?? 15e3;
291
- const pollMs = opts.pollMs ?? 200;
292
- try {
293
- process.kill(-pid, "SIGTERM");
294
- } catch {
295
- return;
296
- }
297
- const deadline = Date.now() + graceMs;
298
- while (isGroupAlive(pid) && Date.now() < deadline) {
299
- await new Promise((resolve) => setTimeout(resolve, pollMs));
300
- }
301
- if (isGroupAlive(pid)) {
302
- try {
303
- process.kill(-pid, "SIGKILL");
304
- } catch {
305
- }
306
- const killDeadline = Date.now() + 2e3;
307
- while (isGroupAlive(pid) && Date.now() < killDeadline) {
308
- await new Promise((resolve) => setTimeout(resolve, pollMs));
309
- }
310
- }
311
- }
312
- function isTimedOut(startedAtMs, nowMs, timeLimitMinutes) {
313
- return nowMs - startedAtMs > timeLimitMinutes * 6e4;
314
- }
315
- function needsRenew(lastRenewAtMs, nowMs, intervalSeconds = RENEW_INTERVAL_SECONDS) {
316
- return nowMs - lastRenewAtMs >= intervalSeconds * 1e3;
317
- }
318
- function isIdle(logMtimeMs, nowMs, idleLimitMinutes, cpuUsecPerMinuteOverWindow) {
319
- const windowMs = nowMs - logMtimeMs;
320
- if (windowMs <= idleLimitMinutes * 6e4) return false;
321
- if (cpuUsecPerMinuteOverWindow === null) return true;
322
- return cpuUsecPerMinuteOverWindow < IDLE_CPU_USEC_PER_MINUTE_FLOOR;
323
- }
324
- function readCpuUsec(cgroupPath) {
325
- try {
326
- const text = readFileSync(join(cgroupPath, "cpu.stat"), "utf8");
327
- const match = /^usage_usec (\d+)$/m.exec(text);
328
- return match ? Number(match[1]) : null;
329
- } catch {
330
- return null;
331
- }
332
- }
333
- async function renewLease(config, item, fetchImpl) {
334
- try {
335
- await postJson(
336
- config.url,
337
- `/v1/work-items/${item}/claim/renew`,
338
- config.apiKey,
339
- config.appId,
340
- { host: config.host, seconds: LEASE_SECONDS },
341
- fetchImpl
342
- );
343
- return "renewed";
344
- } catch (err) {
345
- if (err instanceof WorkCommandError && !err.retryable) return "lease_lost";
346
- return "retry_later";
347
- }
348
- }
349
- var ALREADY_RELEASED = "already released";
350
- async function releaseLease(config, item, outcome, fetchImpl) {
351
- const { data } = await postJson(
352
- config.url,
353
- `/v1/work-items/${item}/claim/release`,
354
- config.apiKey,
355
- config.appId,
356
- outcome === void 0 ? { host: config.host } : { host: config.host, outcome },
357
- fetchImpl
358
- );
359
- return {
360
- releasedAt: typeof data.releasedAt === "string" ? data.releasedAt : void 0,
361
- outcome: data.outcome === void 0 ? void 0 : data.outcome
362
- };
363
- }
364
- var RELEASE_SKEW_MS = 6e4;
365
- function wasAlreadyReleased(result, sentOutcome, sentAtMs) {
366
- const stored = result.outcome ?? void 0;
367
- if (stored !== void 0 && stored !== sentOutcome) return true;
368
- if (result.releasedAt === void 0) return false;
369
- const releasedAtMs = Date.parse(result.releasedAt);
370
- return Number.isFinite(releasedAtMs) && releasedAtMs < sentAtMs - RELEASE_SKEW_MS;
371
- }
372
- function runSync(config, item, workdir, jobDirPath, spawnFn = spawn) {
373
- return new Promise((resolve) => {
374
- const env = jobEnv(config, item, jobDirPath);
375
- let child;
376
- try {
377
- child = spawnFn(process.execPath, [process.argv[1] ?? "outerlayer", "sync"], {
378
- cwd: workdir,
379
- env,
380
- stdio: "ignore"
381
- });
382
- } catch (err) {
383
- resolve({ ok: false, reason: describeCallFailure(err) });
384
- return;
385
- }
386
- child.on("exit", (code) => {
387
- resolve(code === 0 || code === null ? { ok: true } : { ok: false, reason: `exit ${code}` });
388
- });
389
- child.on("error", (err) => {
390
- resolve({ ok: false, reason: describeCallFailure(err) });
391
- });
392
- });
393
- }
394
- function leaseExpiry(job, fallback) {
395
- const base = Date.parse(job.lastRenewAt ?? job.startedAt);
396
- return new Date((Number.isFinite(base) ? base : fallback.getTime()) + LEASE_SECONDS * 1e3);
397
- }
398
- function setStage(job, jobDirPath, stage) {
399
- job.stage = stage;
400
- writeJobFile(join(jobDirPath, "job.json"), job);
401
- }
402
- async function finishJob(job, jobDirPath, outcome, config, deps) {
403
- const spawnFn = deps.spawnFn ?? spawn;
404
- const now = deps.now ?? (() => /* @__PURE__ */ new Date());
405
- const log = deps.log ?? createRunnerLog(now);
406
- const jobFilePath = join(jobDirPath, "job.json");
407
- const logPath = join(jobDirPath, "command.log");
408
- const tag = `#${job.item}`;
409
- if (job.pid !== void 0 && isGroupAlive(job.pid)) {
410
- await stopProcessGroup(job.pid);
411
- }
412
- const hookLimits = () => hookLimitsFor(config, now().getTime(), deps.now);
413
- if (!job.cleanupRan) {
414
- if (job.workdir) {
415
- setStage(job, jobDirPath, "sync");
416
- if (workdirProblem(job.workdir) !== void 0) {
417
- log.line(`${tag} sync skipped: workdir ${job.workdir} is gone`);
418
- } else {
419
- log.line(`${tag} sync started`);
420
- const startedMs = now().getTime();
421
- const sync = await runSync(config, job.item, job.workdir, jobDirPath, spawnFn);
422
- const took = formatDuration(now().getTime() - startedMs);
423
- if (sync.ok) log.line(`${tag} sync ok (${took})`);
424
- else log.error(`${tag} sync failed: ${sync.reason ?? "no reason given"} \xB7 continuing`);
425
- }
426
- }
427
- setStage(job, jobDirPath, "cleanup");
428
- log.line(`${tag} cleanup started`);
429
- const cleanupStartedMs = now().getTime();
430
- const cleanupEnv = {
431
- ...jobEnv(config, job.item, jobDirPath),
432
- OUTERLAYER_OUTCOME: outcome ?? "",
433
- OUTERLAYER_LOG: logPath
434
- };
435
- const cleanup = await runHook(
436
- config.hooks.cleanup,
437
- cleanupEnv,
438
- jobDirPath,
439
- join(jobDirPath, "cleanup.log"),
440
- spawnFn,
441
- hookLimits()
442
- ).catch((err) => {
443
- log.error(`${tag} cleanup failed: ${describeCallFailure(err)} \xB7 continuing`);
444
- return { code: -1 };
445
- });
446
- const cleanupTook = formatDuration(now().getTime() - cleanupStartedMs);
447
- if (cleanup.endedBy !== void 0) {
448
- log.error(`${tag} ${describeHookEnd("cleanup", cleanup.endedBy, config)} \xB7 continuing`);
449
- } else if (cleanup.code === 0) {
450
- log.line(`${tag} cleanup ok (${cleanupTook})`);
451
- } else if (cleanup.code > 0) {
452
- log.error(`${tag} cleanup exited ${cleanup.code} (${cleanupTook}) \xB7 continuing`);
453
- }
454
- job.cleanupRan = true;
455
- writeJobFile(jobFilePath, job);
456
- }
457
- if (config.hooks.report) {
458
- setStage(job, jobDirPath, "report");
459
- log.line(`${tag} report started \xB7 phase end`);
460
- const reportStartedMs = now().getTime();
461
- const reportEnv = {
462
- ...jobEnv(config, job.item, jobDirPath),
463
- OUTERLAYER_PHASE: "end",
464
- OUTERLAYER_OUTCOME: outcome ?? "",
465
- OUTERLAYER_LOG: logPath
466
- };
467
- const report = await runHook(
468
- config.hooks.report,
469
- reportEnv,
470
- jobDirPath,
471
- join(jobDirPath, "report.log"),
472
- spawnFn,
473
- hookLimits()
474
- ).catch((err) => {
475
- log.error(`${tag} report failed: ${describeCallFailure(err)} \xB7 continuing`);
476
- return { code: -1 };
477
- });
478
- const reportTook = formatDuration(now().getTime() - reportStartedMs);
479
- if (report.endedBy !== void 0) {
480
- log.error(`${tag} ${describeHookEnd("report", report.endedBy, config)} \xB7 continuing`);
481
- } else if (report.code === 0) {
482
- log.line(`${tag} report ok (${reportTook})`);
483
- } else if (report.code > 0) {
484
- log.error(`${tag} report exited ${report.code} (${reportTook}) \xB7 continuing`);
485
- }
486
- }
487
- const sentAtMs = now().getTime();
488
- const release = await retrySend(() => releaseLease(config, job.item, outcome, deps.fetchImpl), {
489
- now: deps.now,
490
- sleep: deps.sleep
491
- });
492
- const alreadyReleased = release.ok && wasAlreadyReleased(release.value, outcome, sentAtMs);
493
- const endedAt = now();
494
- job.outcome = outcome;
495
- job.endedAt = endedAt.toISOString();
496
- job.releaseFailed = release.ok ? alreadyReleased ? ALREADY_RELEASED : void 0 : release.reason;
497
- writeJobFile(jobFilePath, job);
498
- const total = formatDuration(endedAt.getTime() - new Date(job.startedAt).getTime());
499
- if (alreadyReleased) {
500
- log.line(
501
- `${tag} lease was already released (outcome ${release.ok ? release.value.outcome ?? "none" : "unknown"}) \xB7 this attempt ended as ${outcome ?? "not attempted"}`
502
- );
503
- } else if (release.ok) {
504
- log.line(
505
- outcome === void 0 ? `${tag} released \xB7 not attempted (provision exit 75) \xB7 ${total}` : `${tag} released \xB7 outcome ${outcome} \xB7 ${total}`
506
- );
507
- } else {
508
- log.error(
509
- `${tag} release failed after ${release.attempts} attempt(s): ${release.reason}; the lease expires at ${formatClock(leaseExpiry(job, endedAt), endedAt)}`
510
- );
511
- }
512
- return job;
513
- }
514
- function readLogState(logPath) {
515
- try {
516
- const stats = statSync(logPath);
517
- return { size: stats.size, mtimeMs: stats.mtimeMs };
518
- } catch {
519
- return { size: -1, mtimeMs: -1 };
520
- }
521
- }
522
- function describeHookEnd(stage, endedBy, config) {
523
- return endedBy === "idle" ? `${stage} idle for ${config.idleLimitMinutes}m, ended` : `${stage} past the ${config.timeLimitMinutes}m time limit, ended`;
524
- }
525
- function hookLimitsFor(config, fromMs, now) {
526
- return {
527
- idleMs: config.idleLimitMinutes * 6e4,
528
- endBy: fromMs + config.timeLimitMinutes * 6e4,
529
- now
530
- };
531
- }
532
- async function retryFailedReleases(jobsDirPath, config, deps) {
533
- if (!existsSync(jobsDirPath)) return [];
534
- const results = [];
535
- for (const entry of readdirSync(jobsDirPath, { withFileTypes: true })) {
536
- if (!entry.isDirectory()) continue;
537
- const dirPath = join(jobsDirPath, entry.name);
538
- const jobFilePath = join(dirPath, "job.json");
539
- const job = readJobFile(jobFilePath);
540
- if (!job || job.releaseFailed === void 0 || job.releaseFailed === ALREADY_RELEASED || isUnfinished(job)) {
541
- continue;
542
- }
543
- const release = await retrySend(() => releaseLease(config, job.item, job.outcome, deps.fetchImpl), {
544
- now: deps.now,
545
- sleep: deps.sleep
546
- });
547
- if (release.ok || !release.retryable) {
548
- job.releaseFailed = void 0;
549
- writeJobFile(jobFilePath, job);
550
- } else {
551
- job.releaseFailed = release.reason;
552
- writeJobFile(jobFilePath, job);
553
- }
554
- results.push({ job, released: release.ok });
555
- }
556
- return results;
557
- }
558
- function isUnfinished(job) {
559
- return job.outcome === void 0 && job.endedAt === void 0;
560
- }
561
- function unfinishedJobs(jobsDirPath) {
562
- if (!existsSync(jobsDirPath)) return [];
563
- const found = [];
564
- for (const entry of readdirSync(jobsDirPath, { withFileTypes: true })) {
565
- if (!entry.isDirectory()) continue;
566
- const dirPath = join(jobsDirPath, entry.name);
567
- const job = readJobFile(join(dirPath, "job.json"));
568
- if (job && isUnfinished(job)) found.push({ dirPath, job });
569
- }
570
- return found;
571
- }
572
- function listAdoptableJobs(jobsDirPath) {
573
- return unfinishedJobs(jobsDirPath).filter(
574
- ({ job }) => job.pid !== void 0 && job.start !== void 0 && isSameProcess(job.pid, job.start)
575
- );
576
- }
577
- async function recoverLostJobs(jobsDirPath, config, deps) {
578
- const recovered = [];
579
- for (const { dirPath, job } of unfinishedJobs(jobsDirPath)) {
580
- const alive = job.pid !== void 0 && job.start !== void 0 && isSameProcess(job.pid, job.start);
581
- if (alive) continue;
582
- const recorded = outcomeFromExitCode(dirPath);
583
- if (recorded !== void 0) job.exitCode = recorded.code;
584
- recovered.push(await finishJob(job, dirPath, recorded?.outcome ?? "interrupted", config, deps));
585
- }
586
- return recovered;
587
- }
588
-
589
- export { LEASE_SECONDS, RENEW_INTERVAL_SECONDS, createRunnerLog, describeCallFailure, describeHookEnd, finishJob, formatClock, formatDuration, hookLimitsFor, isGroupAlive, isIdle, isTimedOut, isUnfinished, jobEnv, listAdoptableJobs, needsRenew, outcomeFromExitCode, readCpuUsec, readJobFile, readProvisionOut, recoverLostJobs, relativeToRunner, renewLease, retryFailedReleases, retrySend, runCommand, runHook, setStage, shortClaim, workdirProblem, writeJobFile };
@@ -1,25 +0,0 @@
1
- #!/usr/bin/env node
2
- import { createRequire } from 'node:module';
3
- import { join, dirname } from 'path';
4
-
5
- createRequire(import.meta.url);
6
- var RUNNER_DIR_MODE = 448;
7
- var RUNNER_FILE_MODE = 384;
8
- function runnerDir(configPath) {
9
- return join(dirname(configPath), "runner");
10
- }
11
- function pidFilePath(configPath) {
12
- return join(runnerDir(configPath), "runner.pid");
13
- }
14
- function jobsDir(configPath) {
15
- return join(runnerDir(configPath), "jobs");
16
- }
17
- function jobDir(configPath, queue, item, claimId, attempt) {
18
- const suffix = attempt === void 0 ? "" : `-${attempt}`;
19
- return join(jobsDir(configPath), `${queue}-${item}-${claimId}${suffix}`);
20
- }
21
- function attemptStamp(at) {
22
- return at.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
23
- }
24
-
25
- export { RUNNER_DIR_MODE, RUNNER_FILE_MODE, attemptStamp, jobDir, jobsDir, pidFilePath, runnerDir };
@@ -1,34 +0,0 @@
1
- #!/usr/bin/env node
2
- import { createRequire } from 'node:module';
3
- import { readFileSync } from 'fs';
4
- import { execFileSync } from 'child_process';
5
-
6
- createRequire(import.meta.url);
7
- function startTimeOf(pid) {
8
- if (process.platform === "linux") return startTimeOfLinux(pid);
9
- return startTimeOfPs(pid);
10
- }
11
- function startTimeOfLinux(pid) {
12
- try {
13
- const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
14
- const afterComm = stat.slice(stat.lastIndexOf(")") + 1).trim();
15
- const fields = afterComm.split(/\s+/);
16
- const starttime = fields[19];
17
- return starttime ?? null;
18
- } catch {
19
- return null;
20
- }
21
- }
22
- function startTimeOfPs(pid) {
23
- try {
24
- const out = execFileSync("ps", ["-o", "lstart=", "-p", String(pid)], { encoding: "utf8" }).trim();
25
- return out.length > 0 ? out : null;
26
- } catch {
27
- return null;
28
- }
29
- }
30
- function isSameProcess(pid, start) {
31
- return startTimeOf(pid) === start;
32
- }
33
-
34
- export { isSameProcess, startTimeOf };