@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,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
- export { RUNNER_DIR_MODE, RUNNER_FILE_MODE, attemptStamp, jobDir, jobsDir, pidFilePath, runnerDir } from './chunk-JJP7YLMN.js';
3
+ export { currentCliBuild } from './chunk-OGFGZQA3.js';
4
4
  import './chunk-Z5GEFMRW.js';
5
5
 
6
6
  createRequire(import.meta.url);
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
3
+ export { RunnerConfigError, builtinKind, checkHooks, defaultRunnerConfigPath, hasRunnerBlock, isBuiltinContainer, isBuiltinProcess, isBuiltinVm, loadRunnerConfig, resolveBuild, runnerVmOf } from './chunk-5Y3QQRUH.js';
4
+ import './chunk-F6GFZXZ3.js';
5
+ import './chunk-AUFG23AA.js';
6
+ import './chunk-VNVZDWO3.js';
7
+ import './chunk-Z5GEFMRW.js';
8
+
9
+ createRequire(import.meta.url);
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
3
+ export { promptOnTerminal, realConnectDeps, runConnect, runConnectCommand } from './chunk-2E54Z3P5.js';
4
+ import './chunk-3TT7RQYB.js';
5
+ import './chunk-6Y63SXFO.js';
6
+ import './chunk-G7FSV7JX.js';
7
+ import './chunk-USO2DKBD.js';
8
+ import './chunk-N6LURUEF.js';
9
+ import './chunk-YRYMNBGQ.js';
10
+ import './chunk-BOLTI6LR.js';
11
+ import './chunk-EABW6AJQ.js';
12
+ import './chunk-BJ3KTMDH.js';
13
+ import './chunk-VNVZDWO3.js';
14
+ import './chunk-Z5GEFMRW.js';
15
+
16
+ createRequire(import.meta.url);
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
3
+ import { defaultGitRunner, hasSymlinkComponent } from './chunk-XDDW4FRS.js';
4
+ import { writeContextManifest, writePinIfChanged, touchLastCheck, clearDegraded, writeSourceCache, SOURCE_CACHE_TTL_SUCCESS_MS, writeBuildHold } from './chunk-WIOZAJ2W.js';
5
+ import './chunk-WO2BXCTQ.js';
6
+ import './chunk-EMY4I27X.js';
7
+ import './chunk-ZJSNLVCE.js';
8
+ import './chunk-BCLJSQCV.js';
9
+ import './chunk-BOLTI6LR.js';
10
+ import './chunk-EABW6AJQ.js';
11
+ import './chunk-RA3O54FC.js';
12
+ import './chunk-M5POMKKW.js';
13
+ import './chunk-ZM2IMMYN.js';
14
+ import { ensureContextExcludes, installPreCommitGuardInGitDir } from './chunk-XCCLFVXM.js';
15
+ import './chunk-BJ3KTMDH.js';
16
+ import './chunk-VNVZDWO3.js';
17
+ import './chunk-Z5GEFMRW.js';
18
+ import { lstatSync, readFileSync, mkdirSync, writeFileSync, chmodSync } from 'fs';
19
+ import { join, dirname } from 'path';
20
+
21
+ createRequire(import.meta.url);
22
+ var CONFLICT_MESSAGE = "this repository tracks AGENTS.md, .claude or .outerlayer in git, which would shadow the control plane's context: remove them in a commit";
23
+ var refused = (code, message) => ({ status: "refused", code, message });
24
+ function readSummary(stagedDir) {
25
+ try {
26
+ const parsed = JSON.parse(readFileSync(join(stagedDir, "staged.json"), "utf8"));
27
+ if (parsed.schemaVersion === 1 && typeof parsed.sha === "string" && typeof parsed.repository === "string" && typeof parsed.ref === "string" && Array.isArray(parsed.files) && parsed.files.every((f) => typeof f === "string")) {
28
+ return parsed;
29
+ }
30
+ } catch {
31
+ }
32
+ return void 0;
33
+ }
34
+ function isContextPath(rel) {
35
+ if (rel.split("/").some((segment) => segment === ".." || segment === "")) return false;
36
+ return rel === "AGENTS.md" || rel.startsWith(".claude/") || rel.startsWith(".outerlayer/");
37
+ }
38
+ function adoptStagedContext(input) {
39
+ const git = input.git ?? defaultGitRunner;
40
+ const now = input.now ?? Date.now;
41
+ const summary = readSummary(input.stagedDir);
42
+ if (summary === void 0) return refused("context_adoption_failed", `${join(input.stagedDir, "staged.json")} is missing or malformed`);
43
+ const run = (args) => {
44
+ const result = git(args, { cwd: input.cwd, timeoutMs: 5e3 });
45
+ return result.ok ? result.stdout.toString("utf8").trim() : void 0;
46
+ };
47
+ const repoRoot = run(["rev-parse", "--show-toplevel"]);
48
+ const gitCommonDir = run(["rev-parse", "--path-format=absolute", "--git-common-dir"]);
49
+ if (repoRoot === void 0 || gitCommonDir === void 0) return refused("context_adoption_failed", `${input.cwd} is not a git checkout`);
50
+ const tracked = git(["ls-files", "-z", "--", "AGENTS.md", ".claude", ".outerlayer"], { cwd: repoRoot, timeoutMs: 5e3 });
51
+ if (!tracked.ok) return refused("context_adoption_failed", "git could not list the checkout's tracked files");
52
+ if (tracked.stdout.length > 0) return refused("context_adoption_conflict", CONFLICT_MESSAGE);
53
+ const tree = join(input.stagedDir, "tree");
54
+ const copies = [];
55
+ for (const rel of summary.files) {
56
+ if (!isContextPath(rel)) return refused("context_adoption_failed", `the staged context lists ${rel}, which is not a context path`);
57
+ let content;
58
+ let executable;
59
+ try {
60
+ const source = join(tree, rel);
61
+ const stat = lstatSync(source);
62
+ if (!stat.isFile()) return refused("context_adoption_failed", `the staged ${rel} is not a regular file`);
63
+ executable = (stat.mode & 73) !== 0;
64
+ content = readFileSync(source);
65
+ } catch {
66
+ return refused("context_adoption_failed", `the staged ${rel} could not be read`);
67
+ }
68
+ if (hasSymlinkComponent(repoRoot, rel)) return refused("context_adoption_failed", `${rel} in the checkout passes through a symlink, so it is not written`);
69
+ copies.push({ rel, content, mode: executable ? 493 : 420 });
70
+ }
71
+ for (const { rel, content, mode } of copies) {
72
+ const destination = join(repoRoot, rel);
73
+ mkdirSync(dirname(destination), { recursive: true });
74
+ writeFileSync(destination, content);
75
+ chmodSync(destination, mode);
76
+ }
77
+ const at = new Date(now()).toISOString();
78
+ writeContextManifest(gitCommonDir, { schemaVersion: 1, sha: summary.sha, files: summary.files, materializedAt: at });
79
+ ensureContextExcludes(gitCommonDir);
80
+ const guard = installPreCommitGuardInGitDir(gitCommonDir);
81
+ writePinIfChanged(repoRoot, summary.sha);
82
+ touchLastCheck(gitCommonDir, now());
83
+ clearDegraded(gitCommonDir);
84
+ writeSourceCache(gitCommonDir, { checkedAt: at, status: "governed", repository: summary.repository, ref: summary.ref, ttlMs: SOURCE_CACHE_TTL_SUCCESS_MS });
85
+ writeBuildHold(gitCommonDir, { sha: summary.sha, repository: summary.repository, ref: summary.ref, at });
86
+ const guardMissing = !guard.adopted;
87
+ return {
88
+ status: "adopted",
89
+ sha: summary.sha,
90
+ files: copies.length,
91
+ ...guardMissing ? { message: "the commit guard is NOT installed: a pre-commit hook this build did not write occupies its slot" } : {}
92
+ };
93
+ }
94
+ function runContextAdopt(stagedDir, cwd, deps = {}) {
95
+ let outcome;
96
+ try {
97
+ outcome = adoptStagedContext({ stagedDir, cwd, ...deps });
98
+ } catch (err) {
99
+ outcome = refused("context_adoption_failed", err instanceof Error ? err.message : String(err));
100
+ }
101
+ return { exitCode: outcome.status === "adopted" ? 0 : 1, stdout: `${JSON.stringify(outcome)}
102
+ ` };
103
+ }
104
+
105
+ export { adoptStagedContext, runContextAdopt };
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
3
+ export { defaultGitRunner, hasSymlinkComponent, materializeContext, parseOwnerRepo, parseRemoteHost, resolveGitCommonDir } from './chunk-XDDW4FRS.js';
4
+ import './chunk-WIOZAJ2W.js';
5
+ import './chunk-WO2BXCTQ.js';
6
+ import './chunk-EMY4I27X.js';
7
+ import './chunk-ZJSNLVCE.js';
8
+ import './chunk-BCLJSQCV.js';
9
+ import './chunk-BOLTI6LR.js';
10
+ import './chunk-EABW6AJQ.js';
11
+ import './chunk-RA3O54FC.js';
12
+ import './chunk-M5POMKKW.js';
13
+ import './chunk-ZM2IMMYN.js';
14
+ import './chunk-XCCLFVXM.js';
15
+ import './chunk-BJ3KTMDH.js';
16
+ import './chunk-VNVZDWO3.js';
17
+ import './chunk-Z5GEFMRW.js';
18
+
19
+ createRequire(import.meta.url);
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
3
+ export { CaptureDaemon, PRICE_TABLE_SOURCE, SECRET_PATTERNS, SOURCE_ADAPTERS, SUPPORTED_VERSIONS, StatuslineAggregator, WARNING_CODES, claudeCodeAdapter, codexAdapter, codexSessionIdFromPath, compareVersions, completePrefix, computeStatuslineState, costOfUsage, cursorAdapter, cursorChatIdFromPath, defaultCodexSessionsDir, defaultCursorChatsDir, defaultCursorProjectsDir, defaultProjectsDir, defaultRawDir, enrichSessionRepo, findCodexRollouts, findCursorChats, findTranscripts, isNewerThanSupported, isPriceKnown, normalizeErrorSignature, normalizeRemote, parseCodexRollout, parseCursorChat, parseTranscript, readStatuslineState, readSyncWatermarkMs, remoteLookupFailed, resolvePrice, resolveRepoIdentity, rootChildIds, scanAll, scrubDeep, scrubText, sessionIdFromPath, setCustomScrubbers, setLocalIdentity, setRepoRoot, statuslineStatePath, tailTranscript, workspacesByHash, writeStatuslineState } from './chunk-G7FSV7JX.js';
4
+ import './chunk-Z5GEFMRW.js';
5
+
6
+ createRequire(import.meta.url);
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
3
+ export { childEnvironment, createDockerHost, spawnRun } from './chunk-774HEQL6.js';
4
+ import './chunk-QOZKPLVJ.js';
5
+ import './chunk-Z5GEFMRW.js';
6
+
7
+ createRequire(import.meta.url);
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
3
+ import { launchAgentPath } from './chunk-LF6MLJCY.js';
4
+ import './chunk-Z5GEFMRW.js';
5
+ import { existsSync } from 'fs';
6
+
7
+ createRequire(import.meta.url);
8
+ function runnerVmCheck(configPath, env) {
9
+ const name = "Runner VM";
10
+ const { instance } = env;
11
+ if (env.lima.locate() === void 0) {
12
+ return { name, status: "fail", detail: `limactl is not installed, so ${instance} cannot be read`, fix: "brew install lima" };
13
+ }
14
+ const status = env.lima.status(instance);
15
+ if (status === void 0) {
16
+ return {
17
+ name,
18
+ status: "fail",
19
+ detail: `${instance} does not exist, so the runner is not running`,
20
+ fix: `run outerlayer login, remove the runnerVm key from ${configPath}, and run outerlayer runner init --vm`
21
+ };
22
+ }
23
+ if (status === "Stopped") {
24
+ return { name, status: "fail", detail: `${instance} is stopped, so the runner is not running`, fix: `limactl start ${instance}` };
25
+ }
26
+ if (status !== "Running") return { name, status: "warn", detail: `${instance} is ${status}`, fix: `limactl list ${instance}` };
27
+ const agent = launchAgentPath(env.home);
28
+ if (!existsSync(agent)) {
29
+ return {
30
+ name,
31
+ status: "warn",
32
+ detail: `${instance} is running, but nothing starts it at login`,
33
+ fix: `${agent} is missing: restore it, or run limactl start ${instance} after each login`
34
+ };
35
+ }
36
+ return {
37
+ name,
38
+ status: "pass",
39
+ detail: `${instance} is running, and the runner lives inside it (limactl shell ${instance} outerlayer runner status); it starts at login`
40
+ };
41
+ }
42
+
43
+ export { runnerVmCheck };
@@ -0,0 +1,426 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
3
+ import { needsClaudeCredential, claudeCredentialCheck, missingPermissionOf, missingPermissionFix } from './chunk-7YIOOFVJ.js';
4
+ import { RepoFilter } from './chunk-TBT347UY.js';
5
+ import './chunk-CBZB6XY6.js';
6
+ import { detectMachine, supportsContainerBuilds, describeMachineDocker, reachDockerStep, supportsMicroVmBuilds, microVmArchSupported, microVmAssetsPinned } from './chunk-L2DBRKRA.js';
7
+ import { MIN_ENGINE_MAJOR } from './chunk-7A6RVXPZ.js';
8
+ import { CLAUDE_CODE_VERSION } from './chunk-ZNA27WEV.js';
9
+ import './chunk-N5FOE5PS.js';
10
+ import './chunk-OGFGZQA3.js';
11
+ import { resolveDevcontainerCli } from './chunk-774HEQL6.js';
12
+ import { locateRecipe, assertBuildable, readRecipe, RecipeError } from './chunk-QOZKPLVJ.js';
13
+ import './chunk-W4SQCOZU.js';
14
+ import './chunk-B2G7JHHB.js';
15
+ import { getJson, WorkCommandError, signatureRefusalMessage } from './chunk-5DLBAXY5.js';
16
+ import './chunk-BFESLKPP.js';
17
+ import './chunk-6Y63SXFO.js';
18
+ import { compareVersions } from './chunk-G7FSV7JX.js';
19
+ import { builtinKind } from './chunk-5Y3QQRUH.js';
20
+ import { parseSize } from './chunk-F6GFZXZ3.js';
21
+ import './chunk-AUFG23AA.js';
22
+ import './chunk-WZBHBH3J.js';
23
+ import { defaultGitRunner } from './chunk-XDDW4FRS.js';
24
+ import './chunk-WIOZAJ2W.js';
25
+ import './chunk-WO2BXCTQ.js';
26
+ import './chunk-EMY4I27X.js';
27
+ import './chunk-ZJSNLVCE.js';
28
+ import { defaultFetch } from './chunk-BCLJSQCV.js';
29
+ import './chunk-BOLTI6LR.js';
30
+ import './chunk-EABW6AJQ.js';
31
+ import './chunk-RA3O54FC.js';
32
+ import { loadHostKey } from './chunk-M5POMKKW.js';
33
+ import { runnerDir, hostKeyPath } from './chunk-ZM2IMMYN.js';
34
+ import './chunk-XCCLFVXM.js';
35
+ import './chunk-BJ3KTMDH.js';
36
+ import './chunk-VNVZDWO3.js';
37
+ import './chunk-Z5GEFMRW.js';
38
+ import { spawnSync } from 'child_process';
39
+ import { existsSync, readdirSync, statSync } from 'fs';
40
+ import { totalmem, tmpdir } from 'os';
41
+ import { join } from 'path';
42
+
43
+ createRequire(import.meta.url);
44
+
45
+ // src/runner/microvm/doctor.ts
46
+ var GIB = 1024 ** 3;
47
+ var gib = (bytes) => `${Number((bytes / GIB).toFixed(1))} GiB`;
48
+ function kvmCheck(machine) {
49
+ const name = "KVM";
50
+ const kvm = machine.kvm;
51
+ if (kvm?.state === "usable") return { name, status: "pass", detail: "this user can open /dev/kvm, so each build runs in a microVM of its own" };
52
+ if (kvm?.state === "no-permission") {
53
+ const group = kvm.group ?? "kvm";
54
+ return {
55
+ name,
56
+ status: "fail",
57
+ detail: "/dev/kvm exists but this user cannot read and write it, so every build fails at provision",
58
+ fix: `sudo usermod -aG ${group} $USER, then log in again so the runner's service starts with the group; or set both hooks to builtin:container`
59
+ };
60
+ }
61
+ return {
62
+ name,
63
+ status: "fail",
64
+ detail: "this machine has no /dev/kvm, so builtin:vm cannot start a microVM and every build fails at provision",
65
+ fix: "run the runner on a host with KVM (bare metal, or a cloud instance that allows nested virtualization), or set both hooks to builtin:container"
66
+ };
67
+ }
68
+ function memoryCheck(input) {
69
+ const name = "MicroVM memory";
70
+ const perBuild = parseSize(input.buildMemory);
71
+ if (perBuild === void 0) {
72
+ return {
73
+ name,
74
+ status: "warn",
75
+ detail: `runner.build.memory is ${JSON.stringify(input.buildMemory)}, which is not a size, so the memory the microVMs need cannot be compared with this host's`,
76
+ fix: 'set runner.build.memory to a size such as "8g"'
77
+ };
78
+ }
79
+ const needed = input.concurrency * perBuild;
80
+ if (needed <= input.hostMemory) {
81
+ return { name, status: "pass", detail: `${input.concurrency} concurrent build(s) of ${gib(perBuild)} need up to ${gib(needed)}, and this host has ${gib(input.hostMemory)}` };
82
+ }
83
+ return {
84
+ name,
85
+ status: "warn",
86
+ detail: `runner.concurrency ${input.concurrency} times runner.build.memory ${input.buildMemory} is ${gib(needed)}, more than this host's ${gib(input.hostMemory)}; memory a build touches stays resident until its microVM stops`,
87
+ fix: "lower runner.concurrency or runner.build.memory, or run the runner on a host with more memory"
88
+ };
89
+ }
90
+
91
+ // src/runner/image/doctor.ts
92
+ var LATEST_URL = "https://registry.npmjs.org/@anthropic-ai/claude-code/latest";
93
+ var REGISTRY_TIMEOUT_MS = 5e3;
94
+ var directories = (dir) => readdirSync(dir).sort().filter((entry) => statSync(join(dir, entry)).isDirectory());
95
+ function devcontainerCliRunnable(resolveCli = resolveDevcontainerCli) {
96
+ try {
97
+ resolveCli();
98
+ return true;
99
+ } catch {
100
+ return false;
101
+ }
102
+ }
103
+ function recipeCheck(reposDir, owner, name) {
104
+ const checkout = join(reposDir, owner, name);
105
+ const label = `Image recipe: ${owner}/${name}`;
106
+ try {
107
+ const location = locateRecipe(checkout);
108
+ if (location.kind === "default") {
109
+ return {
110
+ name: label,
111
+ status: "warn",
112
+ detail: "no devcontainer file, so builds use the default image",
113
+ fix: "add .devcontainer/devcontainer.json to the repository's default branch"
114
+ };
115
+ }
116
+ assertBuildable(checkout, location, readRecipe(checkout, location));
117
+ return { name: label, status: "pass", detail: `built from ${location.path}` };
118
+ } catch (err) {
119
+ if (err instanceof RecipeError && err.code === "recipe_needs_docker") {
120
+ return {
121
+ name: label,
122
+ status: "warn",
123
+ detail: "the recipe uses Docker Compose (dockerComposeFile), and container builds cannot use it",
124
+ fix: "describe the build with image or build in .devcontainer/devcontainer.json"
125
+ };
126
+ }
127
+ if (err instanceof RecipeError && err.code === "recipe_ambiguous") {
128
+ return {
129
+ name: label,
130
+ status: "warn",
131
+ detail: "only named configurations exist, and the runner cannot choose one",
132
+ fix: "add .devcontainer/devcontainer.json to the repository's default branch"
133
+ };
134
+ }
135
+ const reason = err instanceof Error ? err.message : String(err);
136
+ return { name: label, status: "warn", detail: `the recipe cannot be built: ${reason}`, fix: "fix .devcontainer/devcontainer.json on the default branch" };
137
+ }
138
+ }
139
+ async function pinCheck(fetchImpl) {
140
+ const name = "Claude Code pin";
141
+ const unknown = (why) => ({
142
+ name,
143
+ status: "warn",
144
+ detail: `could not read the latest release: ${why}`,
145
+ fix: "check the host's network access to registry.npmjs.org"
146
+ });
147
+ let latest;
148
+ try {
149
+ const response = await fetchImpl(LATEST_URL, {
150
+ headers: { accept: "application/json" },
151
+ signal: AbortSignal.timeout(REGISTRY_TIMEOUT_MS)
152
+ });
153
+ if (!response.ok) return unknown(`the registry answered ${response.status}`);
154
+ latest = (await response.json()).version;
155
+ } catch (err) {
156
+ return unknown(err instanceof Error ? err.message : String(err));
157
+ }
158
+ if (typeof latest !== "string") return unknown("the registry's answer named no version");
159
+ if (compareVersions(latest, CLAUDE_CODE_VERSION) > 0) {
160
+ return {
161
+ name,
162
+ status: "warn",
163
+ detail: `images pin Claude Code ${CLAUDE_CODE_VERSION}, behind the latest release ${latest}`,
164
+ fix: "update the outerlayer CLI; the pin moves with each release"
165
+ };
166
+ }
167
+ return { name, status: "pass", detail: `images pin Claude Code ${CLAUDE_CODE_VERSION}; latest is ${latest}` };
168
+ }
169
+ function isolationCheck(machine, hooks) {
170
+ const name = "Build isolation";
171
+ const kind = builtinKind(hooks.provision);
172
+ const can = supportsContainerBuilds(machine);
173
+ const has = describeMachineDocker(machine);
174
+ if (kind === "container") {
175
+ if (can) {
176
+ const where = machine.platform === "wsl2" ? " inside WSL2" : "";
177
+ return { name, status: "pass", detail: `builtin:container: builds run in containers (isolation: container) on Docker Engine ${machine.docker.version}${where}` };
178
+ }
179
+ return {
180
+ name,
181
+ status: "fail",
182
+ detail: `builtin:container needs Docker Engine ${MIN_ENGINE_MAJOR} or later on Linux or WSL2, and this machine has ${has}, so every build fails at provision`,
183
+ fix: `${reachDockerStep(machine) ?? `install Docker Engine${machine.platform === "windows" ? " inside WSL2" : ""} and run the runner where it is`}, or set both hooks to builtin:process to run builds as processes (isolation: shared-user)`
184
+ };
185
+ }
186
+ if (kind === "vm") {
187
+ if (supportsMicroVmBuilds(machine)) {
188
+ return { name, status: "pass", detail: `builtin:vm: each build runs in a Firecracker microVM of its own (isolation: vm), with Docker Engine ${machine.docker.version} building the image` };
189
+ }
190
+ if (machine.platform === "linux" && machine.kvm?.state === "usable" && can && microVmArchSupported(machine) && !microVmAssetsPinned(machine)) {
191
+ return {
192
+ name,
193
+ status: "fail",
194
+ detail: "builtin:vm cannot run yet: this CLI release has no published guest kernel and helper, so every build would fail their hash check at provision",
195
+ fix: "set both hooks to builtin:container to run builds in containers (isolation: container), or upgrade to a CLI release that publishes the guest assets"
196
+ };
197
+ }
198
+ return {
199
+ name,
200
+ status: "fail",
201
+ detail: `builtin:vm needs Linux (not WSL2) where this user can use /dev/kvm, Docker Engine ${MIN_ENGINE_MAJOR} or later, and an x86_64 CPU; this machine has ${has}${machine.kvm?.state === "usable" ? "" : " and no KVM this user can use"}, so every build fails at provision`,
202
+ fix: `${machine.kvm?.state === "usable" ? reachDockerStep(machine) ?? `install Docker Engine ${MIN_ENGINE_MAJOR} or later` : "see the KVM check"}, or set both hooks to builtin:container to run builds in containers (isolation: container)`
203
+ };
204
+ }
205
+ if (kind === "process") {
206
+ if (machine.platform === "darwin") {
207
+ return {
208
+ name,
209
+ status: "warn",
210
+ detail: "builtin:process: builds are not isolated: they run as processes under this user (isolation: shared-user), and macOS cannot run container builds itself",
211
+ fix: "run outerlayer runner init --vm to run the runner in a Linux VM, where builds run in containers (isolation: container)"
212
+ };
213
+ }
214
+ if (can) {
215
+ return {
216
+ name,
217
+ status: "warn",
218
+ detail: "builtin:process: builds run as processes under this user (isolation: shared-user), though this machine has Docker Engine and could isolate them",
219
+ fix: "set runner.hooks.provision and runner.hooks.cleanup to builtin:container"
220
+ };
221
+ }
222
+ const reach = reachDockerStep(machine);
223
+ const fix = reach !== void 0 ? `isolated builds need Docker Engine ${MIN_ENGINE_MAJOR} or later: ${reach}, then set both hooks to builtin:container` : machine.platform === "windows" ? `install Docker Engine inside WSL2 (version ${MIN_ENGINE_MAJOR} or later), run the runner from a WSL2 shell, and set both hooks to builtin:container; Docker Desktop cannot isolate builds` : machine.docker.kind === "desktop" ? `isolated builds need Docker Engine ${MIN_ENGINE_MAJOR} or later, not Docker Desktop: install it and set both hooks to builtin:container` : `isolated builds need Docker Engine ${MIN_ENGINE_MAJOR} or later on Linux or WSL2: install it and set both hooks to builtin:container`;
224
+ return {
225
+ name,
226
+ status: "warn",
227
+ detail: `builtin:process: builds run as processes under this user (isolation: shared-user); this machine has ${has}`,
228
+ fix
229
+ };
230
+ }
231
+ return {
232
+ name,
233
+ status: "pass",
234
+ detail: "your own hooks: each attempt records the isolation your provision hook reports in provision.out, and shared-user when it reports none"
235
+ };
236
+ }
237
+ async function hostKeyBindingCheck(configPath, gateway, fetchImpl) {
238
+ const name = "Host key binding";
239
+ let hostKey;
240
+ try {
241
+ hostKey = await loadHostKey(configPath);
242
+ } catch (err) {
243
+ return {
244
+ name,
245
+ status: "fail",
246
+ detail: err instanceof Error ? err.message : String(err),
247
+ fix: `${hostKeyPath(configPath)} is damaged; restore it from a backup, or delete it so the runner makes a new host key (a member then clears the old binding on the API keys page)`
248
+ };
249
+ }
250
+ if (hostKey === void 0) {
251
+ return {
252
+ name,
253
+ status: "warn",
254
+ detail: "this runner has no host key yet",
255
+ fix: "run outerlayer runner init, or start the runner: it makes the key on first use"
256
+ };
257
+ }
258
+ try {
259
+ const { data } = await getJson(gateway.url, "/v1/runner/host-key", gateway.apiKey, gateway.appId, fetchImpl, hostKey.signer);
260
+ const binding = data;
261
+ if (binding.bound !== true) {
262
+ return {
263
+ name,
264
+ status: "pass",
265
+ detail: `not yet bound: the runner key binds to this host's key (${hostKey.fingerprint}) on its first claim`
266
+ };
267
+ }
268
+ if (binding.fingerprint === hostKey.fingerprint) {
269
+ return { name, status: "pass", detail: `the runner key is bound to this host (${hostKey.fingerprint})` };
270
+ }
271
+ return boundElsewhere(name);
272
+ } catch (err) {
273
+ if (err instanceof WorkCommandError) {
274
+ if (err.code === "runner_key_signature_invalid") return boundElsewhere(name);
275
+ const refusal = signatureRefusalMessage(err.code);
276
+ if (refusal !== void 0) {
277
+ return { name, status: "fail", detail: `the gateway refused this host's signature (${err.code})`, fix: refusal };
278
+ }
279
+ const permission = missingPermissionOf(err);
280
+ if (permission !== void 0) {
281
+ return {
282
+ name,
283
+ status: "fail",
284
+ detail: `the runner key lacks ${permission}, which a runner needs to claim work`,
285
+ fix: missingPermissionFix(permission)
286
+ };
287
+ }
288
+ if (err.status === 403) {
289
+ return {
290
+ name,
291
+ status: "fail",
292
+ detail: `the gateway refused the runner key (403)`,
293
+ fix: "the gateway did not name a missing permission: check what the key may do on the factory's API keys page, and that it belongs to this factory"
294
+ };
295
+ }
296
+ if (err.status === 401) {
297
+ return {
298
+ name,
299
+ status: "fail",
300
+ detail: `the gateway refused the runner key (401)`,
301
+ fix: "the gateway does not accept this key: create a new one on the factory's API keys page and run outerlayer login"
302
+ };
303
+ }
304
+ }
305
+ const reason = err instanceof Error ? err.message : String(err);
306
+ return { name, status: "warn", detail: `could not read the binding: ${reason}`, fix: "check the host's network access to the gateway" };
307
+ }
308
+ }
309
+ function boundElsewhere(name) {
310
+ return {
311
+ name,
312
+ status: "fail",
313
+ detail: "the runner key is bound to another host's key, so the gateway refuses every request from this host",
314
+ fix: "a member clears the binding on the factory's API keys page (Clear host key); this host's next claim then binds its own key"
315
+ };
316
+ }
317
+ var CONTROL_PLANE_CHECK = "Control plane access";
318
+ async function controlPlaneAccessCheck(env, fetchImpl) {
319
+ const name = CONTROL_PLANE_CHECK;
320
+ const git = env.git ?? defaultGitRunner;
321
+ const include = env.repos?.include ?? [];
322
+ const hostKey = await loadHostKey(env.configPath).catch(() => void 0);
323
+ const signer = hostKey?.signer;
324
+ const failures = [];
325
+ const warnings = [];
326
+ const readable = [];
327
+ const repositories = [];
328
+ let listFactory = include.length === 0;
329
+ for (const entry of include) {
330
+ if (entry.includes("*")) {
331
+ listFactory = true;
332
+ continue;
333
+ }
334
+ const parts = entry.split("/");
335
+ const [host, owner, repo] = parts;
336
+ if (parts.length !== 3 || !host || !owner || !repo) {
337
+ warnings.push(`${entry} is not a single repository, so its control plane cannot be checked`);
338
+ continue;
339
+ }
340
+ repositories.push({ host, repository: `${owner}/${repo}` });
341
+ }
342
+ if (listFactory) {
343
+ try {
344
+ const { data } = await getJson(env.gateway.url, "/v1/repositories/access", env.gateway.apiKey, env.gateway.appId, fetchImpl, signer);
345
+ const filter = new RepoFilter(env.repos);
346
+ if (!Array.isArray(data)) {
347
+ warnings.push("the gateway's answer listed no repositories");
348
+ } else {
349
+ for (const row of data) {
350
+ if (typeof row.repository !== "string") continue;
351
+ const known = repositories.some((r) => r.host === "github.com" && r.repository === row.repository);
352
+ if (!known && filter.allows(`github.com/${row.repository}`)) repositories.push({ host: "github.com", repository: row.repository });
353
+ }
354
+ }
355
+ } catch (err) {
356
+ warnings.push(`could not list the factory's repositories: ${err instanceof Error ? err.message : String(err)}`);
357
+ }
358
+ }
359
+ for (const { host, repository } of repositories) {
360
+ let sources;
361
+ try {
362
+ const { data } = await getJson(env.gateway.url, `/v1/context/source?repo=${encodeURIComponent(repository)}`, env.gateway.apiKey, env.gateway.appId, fetchImpl, signer);
363
+ sources = data.sources ?? [];
364
+ } catch (err) {
365
+ warnings.push(`could not ask the gateway which repository governs ${repository}: ${err instanceof Error ? err.message : String(err)}`);
366
+ continue;
367
+ }
368
+ for (const source of sources) {
369
+ const url = env.controlPlaneUrl?.(source.repository, host) ?? `https://${host}/${source.repository}.git`;
370
+ const listed = git(["ls-remote", url, source.ref], { cwd: tmpdir(), timeoutMs: 15e3 });
371
+ if (listed.ok) {
372
+ readable.push(source.repository);
373
+ } else {
374
+ failures.push({
375
+ detail: `this host's git login cannot read ${source.repository}, the control plane named for ${repository}, so container builds of ${repository} fail at provision`,
376
+ fix: `give the git login this host uses (not the GitHub App) read access to ${source.repository}, then run outerlayer doctor again; builds fetch it over HTTPS`
377
+ });
378
+ }
379
+ }
380
+ }
381
+ if (failures.length > 0) {
382
+ return { name, status: "fail", detail: failures.map((f) => f.detail).join("; "), fix: [...new Set(failures.map((f) => f.fix))].join("; ") };
383
+ }
384
+ if (warnings.length > 0) {
385
+ return { name, status: "warn", detail: warnings.join("; "), fix: "list each repository in repos.include as host/owner/name, and check the host's network access to the gateway" };
386
+ }
387
+ return {
388
+ name,
389
+ status: "pass",
390
+ detail: readable.length > 0 ? `this host's git login reads ${[...new Set(readable)].join(", ")}` : repositories.length === 0 ? "the factory has no repositories this runner builds" : `no control plane governs ${repositories.length === 1 ? "the repository" : "any of the repositories"} ${listFactory ? "connected to the factory" : "in repos.include"}`
391
+ };
392
+ }
393
+ async function runnerHostChecks(env) {
394
+ const dockerAvailable = env.dockerAvailable ?? (() => spawnSync("docker", ["info"], { stdio: "ignore" }).status === 0);
395
+ const devcontainerRunnable = env.devcontainerRunnable ?? (() => devcontainerCliRunnable());
396
+ const reposDir = join(runnerDir(env.configPath), "repos");
397
+ const recipes = existsSync(reposDir) ? directories(reposDir).flatMap(
398
+ (owner) => directories(join(reposDir, owner)).filter((name) => existsSync(join(reposDir, owner, name, ".git"))).map((name) => recipeCheck(reposDir, owner, name))
399
+ ) : [];
400
+ return [
401
+ dockerAvailable() ? { name: "Docker Engine", status: "pass", detail: "reachable" } : {
402
+ name: "Docker Engine",
403
+ status: "warn",
404
+ detail: "docker info failed, so `outerlayer runner image` cannot build",
405
+ fix: "start Docker, or give this user access to its socket"
406
+ },
407
+ ...env.hooks === void 0 ? [] : [isolationCheck(env.machine ?? detectMachine(), env.hooks)],
408
+ ...env.hooks !== void 0 && builtinKind(env.hooks.provision) === "vm" ? [
409
+ kvmCheck(env.machine ?? detectMachine()),
410
+ ...env.runner === void 0 ? [] : [memoryCheck({ concurrency: env.runner.concurrency, buildMemory: env.runner.buildMemory, hostMemory: env.hostMemory ?? totalmem() })]
411
+ ] : [],
412
+ ...env.hooks !== void 0 && needsClaudeCredential(env.hooks) ? [claudeCredentialCheck(env.env ?? process.env, { shell: true })] : [],
413
+ ...env.gateway === void 0 ? [] : [await hostKeyBindingCheck(env.configPath, env.gateway, env.fetchImpl ?? defaultFetch)],
414
+ ...env.gateway === void 0 || env.hooks === void 0 || !["container", "vm"].includes(builtinKind(env.hooks.provision)) ? [] : [await controlPlaneAccessCheck({ ...env, gateway: env.gateway }, env.fetchImpl ?? defaultFetch)],
415
+ devcontainerRunnable() ? { name: "Devcontainer CLI", status: "pass", detail: "installed beside the outerlayer CLI" } : {
416
+ name: "Devcontainer CLI",
417
+ status: "fail",
418
+ detail: "the devcontainer CLI the outerlayer CLI depends on is not installed",
419
+ fix: "run yarn install in the checkout this CLI was built from"
420
+ },
421
+ ...recipes,
422
+ await pinCheck(env.fetchImpl ?? defaultFetch)
423
+ ];
424
+ }
425
+
426
+ export { devcontainerCliRunnable, runnerHostChecks };