@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,21 +1,35 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
- import { pinExists, readPin, readContextManifest, readDegraded, readSourceCache } from './chunk-A3WLZX2F.js';
4
- import './chunk-KFYJV2ZG.js';
5
- import { readStatuslineState, STATE_FRESH_MS } from './chunk-NTTPJV35.js';
6
- import { compareVersions, SUPPORTED_VERSIONS } from './chunk-4TNZMO7V.js';
3
+ import { gatherDoctorChecks, doctorExitCode, summarizeChecks, renderDoctorText, recordWrapped, recordUnwrapped, recordRemoval, readManifest, recordInstall, recordStatusline } from './chunk-NYC54VBD.js';
4
+ import './chunk-3TT7RQYB.js';
5
+ import './chunk-F47JBIAW.js';
6
+ import './chunk-BKO6JEZI.js';
7
+ import './chunk-5DLBAXY5.js';
8
+ import './chunk-BFESLKPP.js';
9
+ import './chunk-6Y63SXFO.js';
10
+ import './chunk-G7FSV7JX.js';
11
+ import './chunk-USO2DKBD.js';
12
+ import './chunk-N6LURUEF.js';
7
13
  import { C, NO_COLOR_FLAG, warnDeprecated, stripNoColor, API_KEY_FLAG_DEPRECATION } from './chunk-YRYMNBGQ.js';
8
14
  import { parseTrackerNumber, trackerNumberError } from './chunk-WZBHBH3J.js';
9
- import { hookFiredMarkerPath, installedCliDistDir, sanitizeForContext, spoolDir, assessSyncHealth, readSyncStatus, SYNC_STALE_AFTER_MS, formatAgo, checkInstalledCliStaleness, workspaceRootOf, refreshCommand } from './chunk-NXLLURA4.js';
10
- import { managedHookScripts, removeJournalHooks, installPreCommitGuardHook, removePreCommitGuardHook, deadHooksState, preparePackageManagerCommand, contextExcludesPresent } from './chunk-I3ETLSNE.js';
11
- import { SettingsParseError, settingsPath, readSettings, listHookCandidates, wrapHooks, writeSettings, unwrapHooks, REGISTERED_EVENTS, removeHooks, removeStatusline, cliBinResolvable, mergeHooks, repointWrappedHooks, AUTO_WRAP_EVENTS, mergeStatusline, isOurStatuslineSlot, statuslineWrappedOriginal, isOurHookCommand, ourCommandBins, wrappedOriginalCommand } from './chunk-R5KBGVII.js';
12
- import { readCloudConfig } from './chunk-TFUIDMOB.js';
15
+ import { settingsPath, readSettings, SettingsParseError, listHookCandidates, wrapHooks, writeSettings, unwrapHooks, REGISTERED_EVENTS, cliCommand, removeHooks, removeStatusline, cliBinResolvable, mergeHooks, repointWrappedHooks, AUTO_WRAP_EVENTS, mergeStatusline } from './chunk-5QRP3MVS.js';
16
+ import { pinExists } from './chunk-WIOZAJ2W.js';
17
+ import './chunk-EMY4I27X.js';
18
+ import './chunk-BCLJSQCV.js';
19
+ import './chunk-BOLTI6LR.js';
20
+ import './chunk-EABW6AJQ.js';
21
+ import { resolveVersion } from './chunk-RA3O54FC.js';
22
+ import './chunk-M5POMKKW.js';
23
+ import './chunk-ZM2IMMYN.js';
24
+ import { hookFiredMarkerPath, installedCliDistDir } from './chunk-UFFXNXLQ.js';
25
+ import { managedHookScripts, removeJournalHooks, installPreCommitGuardHook, removePreCommitGuardHook } from './chunk-XCCLFVXM.js';
26
+ import './chunk-BJ3KTMDH.js';
13
27
  import { homeFromEnv } from './chunk-VNVZDWO3.js';
14
28
  import './chunk-Z5GEFMRW.js';
15
29
  import { EventEmitter } from 'events';
16
30
  import childProcess, { execFileSync } from 'child_process';
17
- import path, { dirname, join, isAbsolute } from 'path';
18
- import fs, { existsSync, unlinkSync, readFileSync, mkdirSync, cpSync, readdirSync, renameSync, statSync, rmSync, writeFileSync, accessSync, constants, realpathSync } from 'fs';
31
+ import path, { isAbsolute, dirname, join } from 'path';
32
+ import fs, { existsSync, unlinkSync, mkdirSync, cpSync, readdirSync, renameSync, statSync, rmSync, readFileSync, writeFileSync, realpathSync } from 'fs';
19
33
  import process2 from 'process';
20
34
  import { stripVTControlCharacters } from 'util';
21
35
  import { fileURLToPath } from 'url';
@@ -3394,6 +3408,19 @@ function samePath(a, b) {
3394
3408
  return false;
3395
3409
  }
3396
3410
  }
3411
+ function sameContent(a, b) {
3412
+ try {
3413
+ const sa = statSync(a);
3414
+ const sb = statSync(b);
3415
+ if (sa.isDirectory() !== sb.isDirectory()) return false;
3416
+ if (!sa.isDirectory()) return sa.size === sb.size && readFileSync(a).equals(readFileSync(b));
3417
+ const na = readdirSync(a).sort();
3418
+ const nb = readdirSync(b).sort();
3419
+ return na.length === nb.length && na.every((n, i) => n === nb[i] && sameContent(join(a, n), join(b, n)));
3420
+ } catch {
3421
+ return false;
3422
+ }
3423
+ }
3397
3424
  function installCliCopy(opts) {
3398
3425
  const rename = opts.rename ?? renameSync;
3399
3426
  const now = opts.now ?? Date.now;
@@ -3408,12 +3435,28 @@ function installCliCopy(opts) {
3408
3435
  cpSync(sourceDist, join(staged, "dist"), { recursive: true });
3409
3436
  const sourcePkg = join(dirname(sourceDist), "package.json");
3410
3437
  if (existsSync(sourcePkg)) cpSync(sourcePkg, join(staged, "package.json"));
3438
+ const sourcePack = join(dirname(sourceDist), "skill-pack");
3439
+ if (existsSync(sourcePack)) cpSync(sourcePack, join(staged, "skill-pack"), { recursive: true });
3411
3440
  const incoming = readdirSync(join(staged, "dist"));
3412
3441
  for (const name of incoming) {
3413
- if (name !== "index.js") rename(join(staged, "dist", name), join(targetDist, name));
3442
+ if (name !== "index.js" && !sameContent(join(staged, "dist", name), join(targetDist, name))) {
3443
+ rename(join(staged, "dist", name), join(targetDist, name));
3444
+ }
3445
+ }
3446
+ if (existsSync(join(staged, "package.json")) && !sameContent(join(staged, "package.json"), join(targetRoot, "package.json"))) rename(join(staged, "package.json"), join(targetRoot, "package.json"));
3447
+ if (existsSync(join(staged, "skill-pack")) && !sameContent(join(staged, "skill-pack"), join(targetRoot, "skill-pack"))) {
3448
+ const livePack = join(targetRoot, "skill-pack");
3449
+ const asidePack = join(staged, "previous-skill-pack");
3450
+ const hadPack = existsSync(livePack);
3451
+ if (hadPack) rename(livePack, asidePack);
3452
+ try {
3453
+ rename(join(staged, "skill-pack"), livePack);
3454
+ } catch (err) {
3455
+ if (hadPack) rename(asidePack, livePack);
3456
+ throw err;
3457
+ }
3414
3458
  }
3415
- if (existsSync(join(staged, "package.json"))) rename(join(staged, "package.json"), join(targetRoot, "package.json"));
3416
- rename(join(staged, "dist", "index.js"), bin);
3459
+ if (!sameContent(join(staged, "dist", "index.js"), bin)) rename(join(staged, "dist", "index.js"), bin);
3417
3460
  const shipped = new Set(incoming);
3418
3461
  for (const name of readdirSync(targetDist)) {
3419
3462
  const path2 = join(targetDist, name);
@@ -3424,96 +3467,6 @@ function installCliCopy(opts) {
3424
3467
  }
3425
3468
  return { bin, copied: true };
3426
3469
  }
3427
- function manifestPath(home = homeFromEnv()) {
3428
- return join(home, ".outerlayer", "hooks-manifest.json");
3429
- }
3430
- function readManifestFile(home) {
3431
- try {
3432
- const parsed = JSON.parse(readFileSync(manifestPath(home), "utf8"));
3433
- if (parsed?.version !== 1) {
3434
- return { manifest: null, newerVersion: typeof parsed?.version === "number" && parsed.version > 1 };
3435
- }
3436
- return {
3437
- manifest: {
3438
- version: 1,
3439
- installs: parsed.installs ?? {},
3440
- wrapped: parsed.wrapped ?? {},
3441
- statusline: parsed.statusline ?? {}
3442
- },
3443
- newerVersion: false
3444
- };
3445
- } catch {
3446
- return { manifest: null, newerVersion: false };
3447
- }
3448
- }
3449
- function readManifest(home = homeFromEnv()) {
3450
- return readManifestFile(home).manifest;
3451
- }
3452
- function writeManifest(home, manifest) {
3453
- const path2 = manifestPath(home);
3454
- try {
3455
- mkdirSync(dirname(path2), { recursive: true });
3456
- const tmp = `${path2}.tmp`;
3457
- writeFileSync(tmp, JSON.stringify(manifest, null, 2) + "\n", { mode: 384 });
3458
- renameSync(tmp, path2);
3459
- } catch {
3460
- }
3461
- }
3462
- function emptyManifest() {
3463
- return { version: 1, installs: {}, wrapped: {}, statusline: {} };
3464
- }
3465
- function updateManifest(home, opts, mutate) {
3466
- const { manifest, newerVersion } = readManifestFile(home);
3467
- if (newerVersion) return;
3468
- if (!manifest && !opts.createIfMissing) return;
3469
- const next = manifest ?? emptyManifest();
3470
- mutate(next);
3471
- writeManifest(home, next);
3472
- }
3473
- function recordInstall(home, settingsPath2, record, now = Date.now) {
3474
- updateManifest(home, { createIfMissing: true }, (manifest) => {
3475
- manifest.installs[settingsPath2] = {
3476
- cliBin: record.cliBin,
3477
- events: [...record.events],
3478
- updatedAt: new Date(now()).toISOString()
3479
- };
3480
- });
3481
- }
3482
- function recordRemoval(home, settingsPath2) {
3483
- updateManifest(home, { createIfMissing: false }, (manifest) => {
3484
- delete manifest.installs[settingsPath2];
3485
- delete manifest.wrapped[settingsPath2];
3486
- delete manifest.statusline[settingsPath2];
3487
- });
3488
- }
3489
- function recordWrapped(home, settingsPath2, wrapped, now = Date.now) {
3490
- if (wrapped.length === 0) return;
3491
- updateManifest(home, { createIfMissing: true }, (manifest) => {
3492
- const existing = manifest.wrapped[settingsPath2] ?? [];
3493
- const at = new Date(now()).toISOString();
3494
- for (const w of wrapped) {
3495
- if (existing.some((e) => e.event === w.event && e.original === w.original)) continue;
3496
- existing.push({ event: w.event, original: w.original, updatedAt: at });
3497
- }
3498
- manifest.wrapped[settingsPath2] = existing;
3499
- });
3500
- }
3501
- function recordUnwrapped(home, settingsPath2) {
3502
- updateManifest(home, { createIfMissing: false }, (manifest) => {
3503
- delete manifest.wrapped[settingsPath2];
3504
- });
3505
- }
3506
- function recordStatusline(home, settingsPath2, outcome, original, now = Date.now) {
3507
- updateManifest(home, { createIfMissing: true }, (manifest) => {
3508
- manifest.statusline[settingsPath2] = {
3509
- outcome,
3510
- ...original !== void 0 ? { original } : {},
3511
- updatedAt: new Date(now()).toISOString()
3512
- };
3513
- });
3514
- }
3515
-
3516
- // src/init.ts
3517
3470
  function runInit(opts) {
3518
3471
  const scope = opts.scope ?? "user";
3519
3472
  const path2 = settingsPath(scope, { home: opts.home, cwd: opts.cwd });
@@ -3633,7 +3586,7 @@ function ensureGitignore(cwd) {
3633
3586
  function orgRolloutSnippet(cliBin) {
3634
3587
  const hooks = {};
3635
3588
  for (const event of REGISTERED_EVENTS) {
3636
- hooks[event] = [{ hooks: [{ type: "command", command: `${cliBin} hook ${event}` }] }];
3589
+ hooks[event] = [{ hooks: [{ type: "command", command: `${cliCommand(cliBin)} hook ${event}` }] }];
3637
3590
  }
3638
3591
  const managed = JSON.stringify({ hooks }, null, 2);
3639
3592
  return [
@@ -3653,577 +3606,6 @@ function orgRolloutSnippet(cliBin) {
3653
3606
  "Each developer still runs `outerlayer login` once to attribute sessions."
3654
3607
  ].join("\n");
3655
3608
  }
3656
- function runDoctor(env = {}) {
3657
- const home = env.home ?? homeFromEnv();
3658
- const now = env.now ?? Date.now;
3659
- const transcripts = scanTranscripts(home);
3660
- return [
3661
- checkClaudeDir(home),
3662
- checkTranscripts(transcripts, now),
3663
- ...checkHooks(home, env.cwd, transcripts, now),
3664
- ...checkDeadHooksState(env.cwd),
3665
- checkSpoolWritable(home),
3666
- checkLastSync(home, now),
3667
- checkDaemon(home),
3668
- checkCleanupPeriod(home),
3669
- checkDiskHeadroom(home),
3670
- checkClaudeVersion(env),
3671
- checkSettingsValid(home, env.cwd),
3672
- ...checkStatusline(home, now, env.cwd),
3673
- ...checkInstalls(env),
3674
- ...checkContextSource(home, env.cwd),
3675
- ...checkInstalledCli(home, env.cwd ?? process.cwd())
3676
- ];
3677
- }
3678
- function checkInstalledCli(home, cwd) {
3679
- const name = "Installed CLI";
3680
- const result = checkInstalledCliStaleness({ home, cwd });
3681
- if (result === null) return [];
3682
- const installedLabel = result.installedId !== null ? sanitizeForContext(result.installedId) : "unknown";
3683
- const workspaceLabel = result.workspaceId !== null ? sanitizeForContext(result.workspaceId) : null;
3684
- const at = result.workspaceDistDir !== null ? ` at ${sanitizeForContext(workspaceRootOf(result.workspaceDistDir))}` : "";
3685
- if (result.stale) {
3686
- return [
3687
- {
3688
- name,
3689
- status: "warn",
3690
- detail: `installed copy (${installedLabel}) is behind the workspace build${at} (${workspaceLabel})`,
3691
- fix: installedCliFix(result.workspaceDistDir, result.workspaceRootTrusted)
3692
- }
3693
- ];
3694
- }
3695
- if (workspaceLabel === null) {
3696
- const detail = result.ambiguousWorkspace ? `installed (${installedLabel}) \u2014 more than one candidate workspace build found nearby; refusing to guess which one to compare against` : `installed (${installedLabel}) \u2014 no workspace build to compare (comparison needs a nearby checkout with a stamped build)`;
3697
- return [{ name, status: "pass", detail }];
3698
- }
3699
- if (result.unorderable) {
3700
- return [
3701
- {
3702
- name,
3703
- status: "pass",
3704
- detail: `installed (${installedLabel}) differs from the workspace build${at} (${workspaceLabel}) \u2014 build order could not be established`
3705
- }
3706
- ];
3707
- }
3708
- if (result.installedAhead) {
3709
- return [{ name, status: "pass", detail: `installed (${installedLabel}) is ahead of the workspace build${at} (${workspaceLabel})` }];
3710
- }
3711
- if (result.installedId !== result.workspaceId) {
3712
- return [{ name, status: "pass", detail: `installed (${installedLabel}) differs from the workspace build${at} (${workspaceLabel})` }];
3713
- }
3714
- return [{ name, status: "pass", detail: `matches the workspace build${at} (${installedLabel})` }];
3715
- }
3716
- function installedCliFix(workspaceDistDir, trusted) {
3717
- const command = refreshCommand(workspaceDistDir, trusted);
3718
- if (command) return `Refresh it: ${command}`;
3719
- const root = sanitizeForContext(workspaceRootOf(workspaceDistDir));
3720
- return `Workspace build found at ${root}. Rebuild it (yarn workspace @outerlayer/cli build) and copy packages/cli/dist to ~/.outerlayer/cli/dist to refresh the installed copy.`;
3721
- }
3722
- function checkStatusline(home, now, cwd) {
3723
- const name = "Status line";
3724
- let installed = false;
3725
- let wrapping;
3726
- let foreign;
3727
- for (const scope of ["user", "project"]) {
3728
- let settings;
3729
- try {
3730
- settings = readSettings(settingsPath(scope, { home, cwd }));
3731
- } catch {
3732
- continue;
3733
- }
3734
- const slot = settings?.statusLine;
3735
- if (!slot || typeof slot !== "object") continue;
3736
- if (isOurStatuslineSlot(slot)) {
3737
- installed = true;
3738
- const original = statuslineWrappedOriginal(slot);
3739
- if (original !== null) wrapping = original;
3740
- break;
3741
- }
3742
- if (typeof slot.command === "string") foreign = slot.command;
3743
- }
3744
- if (!installed) {
3745
- const manifest = readManifest(home);
3746
- const record = ["user", "project"].map((scope) => manifest?.statusline[settingsPath(scope, { home, cwd })]).find((r) => r !== void 0);
3747
- if (record) {
3748
- return [{
3749
- name,
3750
- status: "warn",
3751
- detail: `the OuterLayer segment was installed (recorded ${record.updatedAt}) but the slot no longer holds it \u2014 settings were rewritten or reset${record.original !== void 0 ? `; it wrapped: ${record.original}` : ""}`,
3752
- fix: "Run `outerlayer init` to reinstall the segment."
3753
- }];
3754
- }
3755
- return [
3756
- foreign !== void 0 ? { name, status: "warn", detail: `slot occupied by: ${foreign}`, fix: "Run `outerlayer init` to add the OuterLayer segment (wraps the existing command, never replaces it)." } : { name, status: "warn", detail: "not installed", fix: "Run `outerlayer init`." }
3757
- ];
3758
- }
3759
- const checks = [
3760
- { name, status: "pass", detail: wrapping ? `installed, wrapping: ${wrapping}` : "installed" }
3761
- ];
3762
- const stateName = "Status-line state";
3763
- const state = readStatuslineState(home);
3764
- if (!state) {
3765
- checks.push({ name: stateName, status: "warn", detail: "no state file \u2014 line runs degraded (session cost only)", fix: "Start `outerlayer daemon` (it maintains this file)." });
3766
- return checks;
3767
- }
3768
- const age = now() - Date.parse(state.generatedAt);
3769
- if (!Number.isFinite(age) || age > STATE_FRESH_MS) {
3770
- checks.push({ name: stateName, status: "warn", detail: `state ${formatAgo(age)} \u2014 line runs degraded`, fix: "Restart `outerlayer daemon`." });
3771
- return checks;
3772
- }
3773
- checks.push({ name: stateName, status: "pass", detail: `fresh (${formatAgo(age)})` });
3774
- return checks;
3775
- }
3776
- function checkClaudeDir(home) {
3777
- const dir = join(home, ".claude");
3778
- return existsSync(dir) ? { name: "Claude Code home", status: "pass", detail: dir } : { name: "Claude Code home", status: "fail", detail: `${dir} not found`, fix: "Install Claude Code, or pass --root to point at your transcripts." };
3779
- }
3780
- function scanTranscripts(home) {
3781
- const root = join(home, ".claude", "projects");
3782
- let count = 0;
3783
- let newest = 0;
3784
- if (!existsSync(root)) return { exists: false, count, newest };
3785
- const walk = (dir) => {
3786
- let entries;
3787
- try {
3788
- entries = readdirSync(dir, { withFileTypes: true });
3789
- } catch {
3790
- return;
3791
- }
3792
- for (const entry of entries) {
3793
- const p = join(dir, entry.name);
3794
- if (!entry.isDirectory() && !entry.isSymbolicLink() && !entry.name.endsWith(".jsonl")) continue;
3795
- let st;
3796
- try {
3797
- st = statSync(p);
3798
- } catch {
3799
- continue;
3800
- }
3801
- if (st.isDirectory()) walk(p);
3802
- else if (entry.name.endsWith(".jsonl") && st.size > 0) {
3803
- count += 1;
3804
- if (st.mtimeMs > newest) newest = st.mtimeMs;
3805
- }
3806
- }
3807
- };
3808
- walk(root);
3809
- return { exists: true, count, newest };
3810
- }
3811
- function checkTranscripts(transcripts, now) {
3812
- if (!transcripts.exists) {
3813
- return { name: "Transcripts", status: "warn", detail: "no projects directory yet", fix: "Run a Claude Code session, then re-check." };
3814
- }
3815
- if (transcripts.count === 0) {
3816
- return { name: "Transcripts", status: "warn", detail: "0 transcripts found", fix: "Run a Claude Code session, then run `outerlayer doctor` again." };
3817
- }
3818
- const age = formatAgeH(now() - transcripts.newest);
3819
- return { name: "Transcripts", status: "pass", detail: `${transcripts.count} transcripts, newest ${age}` };
3820
- }
3821
- function formatAgeH(ms) {
3822
- const ageH = ms / 36e5;
3823
- return ageH < 1 ? "<1h ago" : ageH < 48 ? `${Math.round(ageH)}h ago` : `${Math.round(ageH / 24)}d ago`;
3824
- }
3825
- var HOOK_FIRING_GAP_MS = 24 * 60 * 60 * 1e3;
3826
- function newestHookFiring(home) {
3827
- let newest = null;
3828
- for (const event of REGISTERED_EVENTS) {
3829
- try {
3830
- const ms = statSync(hookFiredMarkerPath(event, home)).mtimeMs;
3831
- if (newest === null || ms > newest) newest = ms;
3832
- } catch {
3833
- }
3834
- }
3835
- return newest;
3836
- }
3837
- function checkHooks(home, cwd, transcripts, now) {
3838
- const lastFired = newestHookFiring(home);
3839
- const firingFresh = lastFired !== null && transcripts.count > 0 && transcripts.newest - lastFired <= HOOK_FIRING_GAP_MS;
3840
- const checks = [checkHooksInstalled(home, cwd, firingFresh)];
3841
- const firing = checkHooksFiring(transcripts, lastFired, now);
3842
- if (firing) checks.push(firing);
3843
- const wrapped = checkWrappedHooks(home, cwd);
3844
- if (wrapped) checks.push(wrapped);
3845
- return checks;
3846
- }
3847
- function checkHooksInstalled(home, cwd, firingFresh) {
3848
- const name = "Hooks installed";
3849
- const readablePaths = [];
3850
- for (const scope of ["user", "project"]) {
3851
- const path2 = settingsPath(scope, { home, cwd });
3852
- let settings;
3853
- try {
3854
- settings = readSettings(path2);
3855
- } catch {
3856
- continue;
3857
- }
3858
- readablePaths.push(path2);
3859
- if (!settings?.hooks) continue;
3860
- const installed = REGISTERED_EVENTS.filter(
3861
- (event) => (settings.hooks?.[event] ?? []).some((b) => (b.hooks ?? []).some(isOurHookCommand))
3862
- );
3863
- if (installed.length === REGISTERED_EVENTS.length) {
3864
- const missing = ourCommandBins(settings).filter((bin) => !cliBinResolvable(bin));
3865
- if (missing.length > 0) {
3866
- return {
3867
- name,
3868
- status: "fail",
3869
- detail: `hooks in ${path2} run ${missing.join(", ")}, which no longer exists \u2014 no session is being captured`,
3870
- fix: "Run `npx @outerlayer/cli init` to reinstall."
3871
- };
3872
- }
3873
- return { name, status: "pass", detail: `${installed.join(", ")} (${scope})` };
3874
- }
3875
- if (installed.length > 0) {
3876
- return { name, status: "warn", detail: `only ${installed.join(", ")} installed`, fix: "Run `outerlayer init` to (re)install all lifecycle hooks." };
3877
- }
3878
- }
3879
- const manifest = readManifest(home);
3880
- const recorded = readablePaths.find((p) => manifest?.installs[p]);
3881
- if (recorded) {
3882
- const record = manifest.installs[recorded];
3883
- return {
3884
- name,
3885
- status: "fail",
3886
- detail: `hooks were installed into ${recorded} (recorded ${record.updatedAt}) but the settings file no longer contains them \u2014 it was rewritten or reset`,
3887
- fix: "Run `outerlayer init` to reinstall."
3888
- };
3889
- }
3890
- if (firingFresh) {
3891
- return { name, status: "pass", detail: "not in user/project settings, but hooks are firing \u2014 installed in another scope (managed settings?)" };
3892
- }
3893
- return { name, status: "fail", detail: "no OuterLayer hooks found", fix: "Run `outerlayer init`." };
3894
- }
3895
- function checkWrappedHooks(home, cwd) {
3896
- const manifest = readManifest(home);
3897
- if (!manifest) return null;
3898
- const missing = [];
3899
- for (const scope of ["user", "project"]) {
3900
- const path2 = settingsPath(scope, { home, cwd });
3901
- const records = manifest.wrapped[path2];
3902
- if (!records || records.length === 0) continue;
3903
- let settings;
3904
- try {
3905
- settings = readSettings(path2);
3906
- } catch {
3907
- continue;
3908
- }
3909
- const present = /* @__PURE__ */ new Set();
3910
- const hooks = settings?.hooks ?? {};
3911
- for (const event of Object.keys(hooks)) {
3912
- for (const block of hooks[event] ?? []) {
3913
- for (const h of block.hooks ?? []) {
3914
- const original = wrappedOriginalCommand(h);
3915
- if (original !== null) present.add(`${event}\0${original}`);
3916
- }
3917
- }
3918
- }
3919
- for (const r of records) {
3920
- if (!present.has(`${r.event}\0${r.original}`)) missing.push(`${r.event}: ${r.original}`);
3921
- }
3922
- }
3923
- if (missing.length === 0) return null;
3924
- return {
3925
- name: "Wrapped hooks",
3926
- status: "warn",
3927
- detail: `recorded as wrapped but the settings no longer carry the wrapped form \u2014 ${missing.join("; ")}`,
3928
- fix: "Run `outerlayer hooks wrap` to re-wrap, or `outerlayer hooks unwrap` if the wrap is no longer wanted."
3929
- };
3930
- }
3931
- function checkHooksFiring(transcripts, lastFired, now) {
3932
- const name = "Hooks firing";
3933
- if (transcripts.count === 0) return null;
3934
- if (lastFired === null) {
3935
- return {
3936
- name,
3937
- status: "warn",
3938
- detail: "no firing evidence recorded yet \u2014 every hook run writes a marker, and none exists",
3939
- fix: "Start a Claude Code session, then re-run doctor."
3940
- };
3941
- }
3942
- if (transcripts.newest - lastFired > HOOK_FIRING_GAP_MS) {
3943
- return {
3944
- name,
3945
- status: "fail",
3946
- detail: `sessions as recent as ${formatAgeH(now() - transcripts.newest)}, but no lifecycle hook has fired since ${formatAgeH(now() - lastFired)}`,
3947
- fix: "Hooks are configured but not running \u2014 run `outerlayer init` and check `claude --debug` hook output."
3948
- };
3949
- }
3950
- return { name, status: "pass", detail: `last fired ${formatAgeH(now() - lastFired)}` };
3951
- }
3952
- function checkDeadHooksState(cwd) {
3953
- const dir = cwd ?? process.cwd();
3954
- const state = deadHooksState(dir);
3955
- if (state.hooksDir === void 0) return [];
3956
- const name = "Git hooks directory";
3957
- if (state.unreadableError) {
3958
- return [
3959
- {
3960
- name,
3961
- status: "fail",
3962
- detail: `${sanitizeForContext(state.hooksDir)} could not be read (${state.unreadableError.code ?? state.unreadableError.message}) \u2014 git's hooks status is unknown, not confirmed healthy`,
3963
- fix: "Fix the directory's permissions so it can be listed, then re-run `outerlayer doctor`."
3964
- }
3965
- ];
3966
- }
3967
- if (!state.dead) {
3968
- return [{ name, status: "pass", detail: `hooks present at ${sanitizeForContext(state.hooksDir)}` }];
3969
- }
3970
- const fix = `Run \`${preparePackageManagerCommand(state.repoRoot ?? dir)}\` to (re)generate the hooks directory.`;
3971
- return [
3972
- {
3973
- name,
3974
- status: "fail",
3975
- detail: `${sanitizeForContext(state.hooksDir)} is missing or empty \u2014 core.hooksPath points at a dead directory, so git runs no hooks at all`,
3976
- fix
3977
- }
3978
- ];
3979
- }
3980
- function checkSpoolWritable(home) {
3981
- const dir = spoolDir(home);
3982
- try {
3983
- if (!existsSync(dir)) return { name: "Spool writable", status: "warn", detail: "spool not created yet", fix: "Run `outerlayer init` (creates the spool on first hook)." };
3984
- accessSync(dir, constants.W_OK);
3985
- return { name: "Spool writable", status: "pass", detail: dir };
3986
- } catch {
3987
- return { name: "Spool writable", status: "fail", detail: `${dir} not writable`, fix: "Fix permissions on ~/.outerlayer/spool." };
3988
- }
3989
- }
3990
- function checkLastSync(home, now) {
3991
- const name = "Cloud sync";
3992
- if (!existsSync(join(home, ".outerlayer", "config.json"))) {
3993
- return { name, status: "warn", detail: "not configured \u2014 sessions stay on this machine", fix: "Run `outerlayer sync --url <url> --api-key <key> --app-id <id>` once to connect." };
3994
- }
3995
- const health = assessSyncHealth(readSyncStatus(home), now(), SYNC_STALE_AFTER_MS);
3996
- const detail = [health.headline, ...health.details].join("; ");
3997
- if (health.level === "error") {
3998
- return { name, status: "fail", detail, fix: "Check the destination and API key in ~/.outerlayer/config.json, then run `outerlayer sync` to see the full error." };
3999
- }
4000
- if (health.level === "warn" || health.level === "unknown") {
4001
- return { name, status: "warn", detail, fix: "Run `outerlayer sync` to confirm the connection works." };
4002
- }
4003
- return { name, status: "pass", detail };
4004
- }
4005
- function checkDaemon(home) {
4006
- const pidfile = join(home, ".outerlayer", "daemon.pid");
4007
- const heartbeat = join(home, ".outerlayer", "daemon.heartbeat");
4008
- if (!existsSync(pidfile)) {
4009
- return { name: "Daemon running", status: "warn", detail: "not running", fix: "Start it with `outerlayer daemon` (or a login agent)." };
4010
- }
4011
- try {
4012
- const beatMs = statSync(heartbeat).mtimeMs;
4013
- const staleMin = (Date.now() - beatMs) / 6e4;
4014
- if (staleMin > 30) {
4015
- return { name: "Daemon running", status: "warn", detail: `heartbeat ${Math.round(staleMin)}m stale`, fix: "Restart `outerlayer daemon`." };
4016
- }
4017
- return { name: "Daemon running", status: "pass", detail: "heartbeat fresh" };
4018
- } catch {
4019
- return { name: "Daemon running", status: "warn", detail: "pidfile present, no heartbeat", fix: "Restart `outerlayer daemon`." };
4020
- }
4021
- }
4022
- function checkCleanupPeriod(home) {
4023
- const path2 = join(home, ".claude", "settings.json");
4024
- let settings;
4025
- try {
4026
- settings = readSettings(path2);
4027
- } catch {
4028
- return { name: "Retention (cleanupPeriodDays)", status: "warn", detail: "settings unreadable" };
4029
- }
4030
- const raw = settings?.cleanupPeriodDays;
4031
- const days = typeof raw === "number" ? raw : 30;
4032
- if (days === 0) {
4033
- return {
4034
- name: "Retention (cleanupPeriodDays)",
4035
- status: "fail",
4036
- detail: "set to 0 \u2014 this DISABLES transcript persistence (upstream anthropics/claude-code#23710 and #59248)",
4037
- fix: "Remove cleanupPeriodDays or set it \u226514; run `outerlayer daemon` to mirror before deletion."
4038
- };
4039
- }
4040
- if (days < 14) {
4041
- return { name: "Retention (cleanupPeriodDays)", status: "warn", detail: `${days}d \u2014 transcripts deleted quickly`, fix: "Run `outerlayer daemon` so raws are mirrored before cleanup." };
4042
- }
4043
- return { name: "Retention (cleanupPeriodDays)", status: "pass", detail: `${days}d` };
4044
- }
4045
- function checkDiskHeadroom(home) {
4046
- try {
4047
- accessSync(home, constants.W_OK);
4048
- return { name: "Disk headroom", status: "pass", detail: "home writable (raw mirror capped at 5 GiB, LRU)" };
4049
- } catch {
4050
- return { name: "Disk headroom", status: "fail", detail: "home not writable", fix: "Free space / fix permissions on your home directory." };
4051
- }
4052
- }
4053
- function checkClaudeVersion(env) {
4054
- let version;
4055
- try {
4056
- version = (env.claudeVersion ?? defaultClaudeVersion)();
4057
- } catch {
4058
- return { name: "Claude Code version", status: "warn", detail: "could not run `claude --version`", fix: "Ensure `claude` is on PATH." };
4059
- }
4060
- const parsed = version.match(/\d+\.\d+\.\d+/)?.[0];
4061
- if (!parsed) return { name: "Claude Code version", status: "warn", detail: `unrecognized: ${version}` };
4062
- if (compareVersions(parsed, SUPPORTED_VERSIONS.max) > 0) {
4063
- return { name: "Claude Code version", status: "warn", detail: `${parsed} is newer than validated (${SUPPORTED_VERSIONS.max}) \u2014 parser runs best-effort`, fix: "Update OuterLayer if you see parse warnings." };
4064
- }
4065
- if (compareVersions(parsed, SUPPORTED_VERSIONS.min) < 0) {
4066
- return { name: "Claude Code version", status: "warn", detail: `${parsed} is older than validated (${SUPPORTED_VERSIONS.min})` };
4067
- }
4068
- return { name: "Claude Code version", status: "pass", detail: parsed };
4069
- }
4070
- function checkSettingsValid(home, cwd) {
4071
- for (const scope of ["user", "project"]) {
4072
- const path2 = settingsPath(scope, { home, cwd });
4073
- if (!existsSync(path2)) continue;
4074
- try {
4075
- readSettings(path2);
4076
- } catch (err) {
4077
- return { name: "Settings JSON valid", status: "fail", detail: `${path2} is corrupt`, fix: err instanceof Error ? err.message : "Fix the JSON syntax." };
4078
- }
4079
- }
4080
- return { name: "Settings JSON valid", status: "pass", detail: "all settings files parse" };
4081
- }
4082
- function checkInstalls(env) {
4083
- let installs;
4084
- try {
4085
- installs = (env.claudeInstalls ?? defaultClaudeInstalls)();
4086
- } catch {
4087
- return [];
4088
- }
4089
- if (installs.length > 1) {
4090
- return [{ name: "Claude Code installs", status: "warn", detail: `multiple found: ${installs.join(", ")}`, fix: "Multiple installs can diverge in version; keep one." }];
4091
- }
4092
- return [];
4093
- }
4094
- function defaultClaudeVersion() {
4095
- return execFileSync("claude", ["--version"], { encoding: "utf8", timeout: 5e3 }).trim();
4096
- }
4097
- function defaultClaudeInstalls() {
4098
- try {
4099
- const out = execFileSync(process.platform === "win32" ? "where" : "which", ["-a", "claude"], { encoding: "utf8", timeout: 5e3 });
4100
- return out.split(/\r?\n/).filter((l) => l.trim().length > 0);
4101
- } catch {
4102
- return [];
4103
- }
4104
- }
4105
- function contextDegradedFix(kind) {
4106
- switch (kind) {
4107
- case "unreachable":
4108
- return "Check network/VPN access to the control plane, then run `outerlayer context materialize`.";
4109
- case "no-access":
4110
- return "Check your git credentials (SSH key or token) and org/SSO access to the control plane, then retry.";
4111
- case "designation-unverified":
4112
- return "Check OuterLayer cloud connectivity/config (~/.outerlayer/config.json), then run `outerlayer context materialize`.";
4113
- case "not-governed":
4114
- return "Expected if the control plane's `governs` list changed intentionally; otherwise re-designate the source.";
4115
- case "adoption-conflict":
4116
- return "Remove the committed AGENTS.md/.claude/.outerlayer files in a commit, then run `outerlayer context materialize`.";
4117
- case "conflict":
4118
- return "Revoke the designation from all but one of the claiming factories, then run `outerlayer context materialize`.";
4119
- }
4120
- }
4121
- function checkContextSource(home, cwd) {
4122
- const dir = cwd ?? process.cwd();
4123
- let repoRoot;
4124
- let gitCommonDir;
4125
- try {
4126
- repoRoot = execFileSync("git", ["-C", dir, "rev-parse", "--show-toplevel"], {
4127
- encoding: "utf8",
4128
- stdio: ["ignore", "pipe", "ignore"],
4129
- timeout: 2e3
4130
- }).trim();
4131
- gitCommonDir = execFileSync("git", ["-C", dir, "rev-parse", "--path-format=absolute", "--git-common-dir"], {
4132
- encoding: "utf8",
4133
- stdio: ["ignore", "pipe", "ignore"],
4134
- timeout: 2e3
4135
- }).trim();
4136
- } catch {
4137
- return [];
4138
- }
4139
- if (!repoRoot || !gitCommonDir) return [];
4140
- const pin = readPin(repoRoot);
4141
- const manifest = readContextManifest(gitCommonDir);
4142
- const degraded = readDegraded(gitCommonDir);
4143
- const sourceCache = readSourceCache(gitCommonDir);
4144
- const looksGoverned = pin !== void 0 || sourceCache?.status === "governed";
4145
- if (!pin && !manifest && !degraded && !looksGoverned) return [];
4146
- const checks = [];
4147
- const name = "Context source";
4148
- if (pin && manifest && pin.sha !== manifest.sha) {
4149
- checks.push({
4150
- name,
4151
- status: "warn",
4152
- detail: `pin (${pin.sha.slice(0, 7)}) and manifest (${manifest.sha.slice(0, 7)}) disagree`,
4153
- fix: "Run `outerlayer context materialize` to reconcile."
4154
- });
4155
- } else if (pin ?? manifest) {
4156
- const sha = (pin ?? manifest).sha;
4157
- checks.push({ name, status: degraded ? "warn" : "pass", detail: `materialized at ${sha.slice(0, 7)}` });
4158
- }
4159
- if (degraded) {
4160
- checks.push({
4161
- name: "Context source degradation",
4162
- status: degraded.kind === "adoption-conflict" || degraded.kind === "conflict" ? "fail" : "warn",
4163
- detail: degraded.message,
4164
- fix: contextDegradedFix(degraded.kind)
4165
- });
4166
- }
4167
- if ((pin || manifest) && !contextExcludesPresent(gitCommonDir)) {
4168
- checks.push({
4169
- name: "Context exclude block",
4170
- status: "warn",
4171
- detail: "materialized paths are not excluded in .git/info/exclude",
4172
- fix: "Run `outerlayer context materialize` to repair it."
4173
- });
4174
- }
4175
- if (pin && !readCloudConfig(home).url) {
4176
- checks.push({
4177
- name,
4178
- status: "warn",
4179
- detail: "a pin exists but OuterLayer cloud sync isn't configured \u2014 the designation cannot be confirmed",
4180
- fix: "Run `outerlayer sync --url <url> --api-key <key> --app-id <id>` once to connect."
4181
- });
4182
- }
4183
- if (looksGoverned) {
4184
- let tracked = "";
4185
- try {
4186
- tracked = execFileSync("git", ["-C", repoRoot, "ls-files", "--", "AGENTS.md", ".claude", ".outerlayer"], {
4187
- encoding: "utf8",
4188
- stdio: ["ignore", "pipe", "ignore"],
4189
- timeout: 2e3
4190
- }).trim();
4191
- } catch {
4192
- tracked = "";
4193
- }
4194
- if (tracked) {
4195
- const paths = tracked.split("\n");
4196
- const sample = paths.slice(0, 3).join(", ") + (paths.length > 3 ? ", \u2026" : "");
4197
- checks.push({
4198
- name,
4199
- status: "fail",
4200
- detail: `committed context files shadow the control plane: ${sample}`,
4201
- fix: "Remove the committed AGENTS.md/.claude/.outerlayer files in a commit, then run `outerlayer context materialize`."
4202
- });
4203
- }
4204
- }
4205
- return checks;
4206
- }
4207
- function doctorExitCode(checks) {
4208
- return checks.some((c) => c.status === "fail") ? 1 : 0;
4209
- }
4210
- var FALLBACK_VERSION = "0.1.0";
4211
- function readJson(path2) {
4212
- if (!existsSync(path2)) return null;
4213
- try {
4214
- return JSON.parse(readFileSync(path2, "utf8"));
4215
- } catch {
4216
- return null;
4217
- }
4218
- }
4219
- function resolveVersion(entryPath = fileURLToPath(import.meta.url)) {
4220
- const distDir = dirname(entryPath);
4221
- const pkg = readJson(join(dirname(distDir), "package.json"));
4222
- const version = typeof pkg?.version === "string" ? pkg.version : FALLBACK_VERSION;
4223
- const buildInfo = readJson(join(distDir, "build-info.json"));
4224
- const buildId = typeof buildInfo?.id === "string" ? buildInfo.id : null;
4225
- return buildId ? `${version} (build ${buildId})` : version;
4226
- }
4227
3609
 
4228
3610
  // src/cli.ts
4229
3611
  function resolveCliBin() {
@@ -4234,11 +3616,6 @@ function resolveCliBin() {
4234
3616
  }
4235
3617
  }
4236
3618
  var SUPPORT_TEXT = "\nDocs: https://github.com/outerlayerai/outerlayer#readme\nIssues: https://github.com/outerlayerai/outerlayer/issues\n";
4237
- function icon(status) {
4238
- if (status === "pass") return `${C.green}\u2713${C.reset}`;
4239
- if (status === "warn") return `${C.yellow}!${C.reset}`;
4240
- return `${C.red}\u2717${C.reset}`;
4241
- }
4242
3619
  function fail(message) {
4243
3620
  process.stderr.write(`${C.red}\u2717${C.reset} ${message}
4244
3621
  `);
@@ -4254,7 +3631,18 @@ function apiKeyOption() {
4254
3631
  function warnIfApiKeyFlag(opts) {
4255
3632
  if (opts.apiKey !== void 0) warnDeprecated(API_KEY_FLAG_DEPRECATION);
4256
3633
  }
4257
- function formatInitOutput(result) {
3634
+ async function installStarterPackInRepo() {
3635
+ const { installStarterPack, formatStarterPackOutput } = await import('./starter-pack-LIZYMKYQ.js');
3636
+ const cwd = process.cwd();
3637
+ let repoRoot = cwd;
3638
+ try {
3639
+ repoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || cwd;
3640
+ } catch {
3641
+ }
3642
+ const result = installStarterPack({ repoRoot });
3643
+ return { result, output: formatStarterPackOutput(result) };
3644
+ }
3645
+ function formatInitOutput(result, opts = {}) {
4258
3646
  const lines = [];
4259
3647
  if (result.removed) {
4260
3648
  lines.push(
@@ -4324,15 +3712,12 @@ cannot be disabled.
4324
3712
  `
4325
3713
  );
4326
3714
  lines.push(
4327
- `
3715
+ (opts.next === false ? "\n" : `
4328
3716
  Next: launch a session naming its work item, then sync it:
4329
3717
  ${C.yellow}OUTERLAYER_WORK=42 claude${C.reset}
4330
3718
  ${C.yellow}outerlayer sync${C.reset}
4331
3719
 
4332
- Once you have run outerlayer login, launched sessions sync
4333
- automatically in the background after each agent turn and at the end
4334
- of the session, at most once every five minutes.
4335
- `
3720
+ `) + (opts.connected ? "This repository is connected to a factory, so launched sessions sync\nautomatically in the background after each agent turn and at the end\nof the session, at most once every five minutes.\n" : "Once you have run outerlayer login and outerlayer init in this repository,\nlaunched sessions sync automatically in the background after each agent\nturn and at the end of the session, at most once every five minutes.\n")
4336
3721
  );
4337
3722
  return lines.join("");
4338
3723
  }
@@ -4369,110 +3754,171 @@ function formatGuardInstallOutput(guard) {
4369
3754
  return { lines: [], error: "not inside a git repository." };
4370
3755
  }
4371
3756
  }
3757
+ async function installHooks(opts, resolvedBin, connected, starterTemplate, withNext = true) {
3758
+ const scope = opts.project ? "project" : "user";
3759
+ let cliBin;
3760
+ try {
3761
+ cliBin = initHookBin({ home: homeFromEnv(), runningBin: resolvedBin, remove: opts.remove === true });
3762
+ } catch (err) {
3763
+ const text = `could not copy the CLI to ~/.outerlayer/cli: ${err instanceof Error ? err.message : String(err)}
3764
+ Hooks were not changed. Fix the permissions on ~/.outerlayer, then re-run ${C.yellow}outerlayer init${C.reset}.`;
3765
+ return { ok: false, text, detail: "could not copy the CLI to ~/.outerlayer/cli" };
3766
+ }
3767
+ try {
3768
+ const result = runInit({
3769
+ scope,
3770
+ remove: opts.remove,
3771
+ cliBin,
3772
+ addGitignore: opts.gitignore,
3773
+ wrapHooks: opts.wrapHooks,
3774
+ statusline: opts.statusline
3775
+ });
3776
+ if (result.cliBinUnresolved) {
3777
+ const text = `the resolved CLI path (${cliBin}) does not exist \u2014 refusing to write hook entries pointing at it.
3778
+ Reinstall OuterLayer, then re-run ${C.yellow}outerlayer init${C.reset}.`;
3779
+ return { ok: false, text, detail: "the CLI path does not exist" };
3780
+ }
3781
+ const starter = starterTemplate === void 0 ? null : await installStarterPackInRepo();
3782
+ return {
3783
+ ok: true,
3784
+ text: formatInitOutput(result, { connected: connected === true, next: withNext }) + (starter === null ? "" : `
3785
+ ${starter.output}`),
3786
+ detail: result.changed ? `installed in ${result.path}` : `already installed in ${result.path}`,
3787
+ result: starter === null ? result : { ...result, starterPack: starter.result }
3788
+ };
3789
+ } catch (err) {
3790
+ if (err instanceof SettingsParseError) return { ok: false, text: err.message, detail: err.message };
3791
+ throw err;
3792
+ }
3793
+ }
3794
+ async function realConnectDepsForCwd() {
3795
+ const { realConnectDeps } = await import('./connect-cmd-OOG4TPU7.js');
3796
+ const { resolveRepoIdentity } = await import('./dist-RW4UPPC2.js');
3797
+ return {
3798
+ ...realConnectDeps(),
3799
+ home: homeFromEnv(),
3800
+ cwd: process.cwd(),
3801
+ // Prompts need a person at both ends: stdin to answer on, stderr to ask on.
3802
+ isTerminal: Boolean(process.stdin.isTTY && process.stderr.isTTY),
3803
+ resolveRepository: (cwd) => resolveRepoIdentity(cwd).gitRepo
3804
+ };
3805
+ }
4372
3806
  function invokedCommandToken(argv) {
4373
3807
  return argv.slice(2).find((token) => !token.startsWith("-"));
4374
3808
  }
4375
3809
  function buildProgram(argv = process.argv) {
4376
3810
  const program2 = new Command();
4377
3811
  program2.name("outerlayer").description("OuterLayer \u2014 see what your coding agents actually do").enablePositionalOptions().version(resolveVersion()).option(NO_COLOR_FLAG, "disable color (NO_COLOR=1 does the same)").addHelpText("after", SUPPORT_TEXT);
4378
- program2.command("init").description("Install the Claude Code capture hooks and wrap tool hooks for evidence").option("--user", "install into user settings [default]").option("--project", "install into project settings (./.claude/settings.json)").option("--org", "print a managed-settings.json snippet for MDM/GPO rollout").option("--remove", "uninstall OuterLayer hooks").option("--gitignore", "also add .outerlayer/ to .gitignore (project scope)").option("--no-wrap-hooks", "leave PreToolUse/PostToolUse hooks unwrapped").option("--statusline", "install the status-line segment [default]").option("--no-statusline", "leave the statusLine slot untouched").option("--json", "machine-readable JSON output").addHelpText(
3812
+ program2.command("init").description("Connect this repository, install hooks and agent skills, run doctor").option("--user", "install into user settings [default]").option("--project", "install into project settings (./.claude/settings.json)").option("--org", "print a managed-settings.json snippet for MDM/GPO rollout").option("--remove", "uninstall OuterLayer hooks").option("--local", "install the hooks only: no connect step and no network call").option("--factory <org/factory>", "the factory to connect this repository to (see `outerlayer connect`)").option("--gitignore", "also add .outerlayer/ to .gitignore (project scope)").option("--template <name>", "install the starter skills and a policy file (only: default)").option("--no-wrap-hooks", "leave PreToolUse/PostToolUse hooks unwrapped").option("--statusline", "install the status-line segment [default]").option("--no-statusline", "leave the statusLine slot untouched").option("--json", "machine-readable JSON output").addHelpText(
4379
3813
  "after",
4380
- "\nIdempotent: settings are backed up before any write, and a second run\nchanges nothing. Wrapping adds one spawn per hook firing; undo it with\n`outerlayer hooks unwrap`. An occupied status-line slot is wrapped, never\nreplaced.\n"
4381
- ).action((opts) => {
4382
- let cliBin = resolveCliBin();
3814
+ "\nIn a repository, init runs four steps: connect the repository to a\nfactory (`outerlayer connect`), install the hooks, set up the coding agent,\nand run `outerlayer doctor`. It never signs in and asks nothing. With no\nlogin it skips the connect step and says to run `outerlayer login`. With\n--json it prints one document naming the outcome of each step: done,\nskipped or failed. The exit code is 1 when a step failed.\n\nThe agent setup step installs the starter skills and the `outerlayer` skill\ninto .outerlayer/skills/, writes .outerlayer/config.json for Claude Code when\nthere is none, runs `outerlayer context emit` so the skills appear in\n.claude/skills/, and adds the OuterLayer MCP server to .mcp.json. Nothing you\nwrote is changed: an existing config, policy, edited skill, MCP entry or root\nCLAUDE.md and AGENTS.md is left as it is, and a hand-written file the emit\nwould replace is kept and listed. The files are left uncommitted.\nThe step is skipped outside a git repository and in a repository whose\ncontext comes from a control plane, where it arrives at session start.\n\n--local installs only the hooks: no connect step, no agent setup and no\nnetwork call. Add `--template default` to also write the starter skills and\na policy file into .outerlayer/ without compiling them; `outerlayer context\nemit` does that.\n\nIdempotent: settings are backed up before any write, and a second run\nchanges nothing. Wrapping adds one spawn per hook firing; undo it with\n`outerlayer hooks unwrap`. An occupied status-line slot is wrapped, never\nreplaced.\n\nThree of the starter skills are copied once and are yours to edit. The\nothers are rewritten from this CLI on every `context emit`.\n"
3815
+ ).action(async (opts) => {
3816
+ let starterTemplate;
3817
+ if (opts.template !== void 0) {
3818
+ const { assertKnownTemplate, StarterPackError } = await import('./starter-pack-LIZYMKYQ.js');
3819
+ try {
3820
+ assertKnownTemplate(opts.template);
3821
+ } catch (err) {
3822
+ if (err instanceof StarterPackError) {
3823
+ fail(err.message);
3824
+ return;
3825
+ }
3826
+ throw err;
3827
+ }
3828
+ if (opts.remove || opts.org) {
3829
+ fail("--template installs into a repository, so it cannot be combined with --remove or --org.");
3830
+ return;
3831
+ }
3832
+ starterTemplate = opts.template;
3833
+ }
3834
+ const cliBin = resolveCliBin();
4383
3835
  if (opts.org) {
4384
3836
  process.stdout.write(orgRolloutSnippet(cliBin) + "\n");
4385
3837
  return;
4386
3838
  }
4387
- const scope = opts.project ? "project" : "user";
4388
- try {
4389
- cliBin = initHookBin({ home: homeFromEnv(), runningBin: cliBin, remove: opts.remove === true });
4390
- } catch (err) {
4391
- fail(
4392
- `could not copy the CLI to ~/.outerlayer/cli: ${err instanceof Error ? err.message : String(err)}
4393
- Hooks were not changed. Fix the permissions on ~/.outerlayer, then re-run ${C.yellow}outerlayer init${C.reset}.`
4394
- );
4395
- return;
4396
- }
4397
- try {
4398
- const result = runInit({
4399
- scope,
4400
- remove: opts.remove,
4401
- cliBin,
4402
- addGitignore: opts.gitignore,
4403
- wrapHooks: opts.wrapHooks,
4404
- statusline: opts.statusline
4405
- });
4406
- if (result.cliBinUnresolved) {
4407
- fail(
4408
- `the resolved CLI path (${cliBin}) does not exist \u2014 refusing to write hook entries pointing at it.
4409
- Reinstall OuterLayer, then re-run ${C.yellow}outerlayer init${C.reset}.`
4410
- );
4411
- return;
4412
- }
4413
- if (opts.json) {
4414
- writeJson(result);
4415
- return;
4416
- }
4417
- process.stdout.write(formatInitOutput(result));
4418
- } catch (err) {
4419
- if (err instanceof SettingsParseError) {
4420
- fail(err.message);
3839
+ if (opts.remove || opts.local) {
3840
+ const outcome = await installHooks(opts, cliBin, void 0, starterTemplate);
3841
+ if (!outcome.ok) {
3842
+ fail(outcome.text);
4421
3843
  return;
4422
3844
  }
4423
- throw err;
3845
+ if (opts.json) writeJson(outcome.result);
3846
+ else process.stdout.write(outcome.text);
3847
+ return;
4424
3848
  }
3849
+ const { runInitCommand } = await import('./init-cmd-NHR7PRSQ.js');
3850
+ const code = await runInitCommand(
3851
+ { json: opts.json, factory: opts.factory },
3852
+ {
3853
+ connect: await realConnectDepsForCwd(),
3854
+ stdout: process.stdout,
3855
+ stderr: process.stderr,
3856
+ runHooks: async (hooksOpts) => {
3857
+ const { ok, text, detail } = await installHooks(opts, cliBin, hooksOpts?.connected, void 0, false);
3858
+ return { ok, text, detail };
3859
+ },
3860
+ runAgentSetup: async () => {
3861
+ const { runAgentSetup } = await import('./agent-setup-IFH3FB5X.js');
3862
+ return runAgentSetup({ cwd: process.cwd() });
3863
+ },
3864
+ runDoctorChecks: gatherDoctorChecks
3865
+ }
3866
+ );
3867
+ if (code !== 0) process.exitCode = code;
4425
3868
  });
4426
- program2.command("doctor").description("Diagnose the capture setup; every failing check names its fix").option("--json", "machine-readable JSON output").action((opts) => {
4427
- const checks = runDoctor();
4428
- const fails = checks.filter((c) => c.status === "fail").length;
4429
- const warns = checks.filter((c) => c.status === "warn").length;
4430
- const passed = checks.length - fails - warns;
3869
+ program2.command("connect").description("Choose the factory this repository's work goes to").option("--factory <org/factory>", "the factory to use: <org>/<factory> as in its dashboard address, or a factory id").option("--json", "write one JSON document to stdout; the text goes to stderr").addHelpText(
3870
+ "after",
3871
+ "\nRun it in a repository, after `outerlayer login`. With a login, connect lists\nthe factories you can use and takes the first rule that leaves one answer:\n 1. --factory <org>/<factory>, or a factory id\n 2. the factory this repository is already connected to\n 3. the one factory this repository is linked to\n 4. the one factory you can use\n 5. on a terminal, ask; with no terminal, exit 1 and list each --factory value\n\nIt says what it chose and why. The choice is saved in this repository's git\ndirectory (outerlayer/connection.json), shared by every worktree of the\nclone and never committed. A factory key already belongs to one factory, so\nconnect changes nothing for it. Connect never links a repository.\n\nAfter it saves the choice, connect checks that this repository is linked to\nthe factory. If it is not, connect says so, prints the factory's Work page\nlink where you link it, and exits 2. The choice is saved either way. Run\nconnect again once the repository is linked. With --json, the document has\nlinked (true, false, or null when it could not be checked) and, when false,\nlinkUrl.\n"
3872
+ ).action(async (opts) => {
3873
+ const { runConnectCommand } = await import('./connect-cmd-OOG4TPU7.js');
3874
+ const code = await runConnectCommand({ factory: opts.factory, json: opts.json }, await realConnectDepsForCwd(), {
3875
+ stdout: process.stdout,
3876
+ stderr: process.stderr
3877
+ });
3878
+ if (code !== 0) process.exitCode = code;
3879
+ });
3880
+ program2.command("doctor").description("Diagnose the capture setup; every failing check names its fix").option("--json", "machine-readable JSON output").action(async (opts) => {
3881
+ const checks = await gatherDoctorChecks();
4431
3882
  process.exitCode = doctorExitCode(checks);
4432
3883
  if (opts.json) {
4433
- writeJson({ checks, summary: { passed, warnings: warns, failures: fails } });
3884
+ writeJson({ checks, summary: summarizeChecks(checks) });
4434
3885
  return;
4435
3886
  }
4436
- process.stdout.write("\nOuterLayer doctor\n\n");
4437
- for (const c of checks) {
4438
- process.stdout.write(` ${icon(c.status)} ${c.name}${C.dim} \u2014 ${c.detail}${C.reset}
4439
- `);
4440
- if (c.fix && c.status !== "pass") process.stdout.write(` ${C.dim}\u21B3 ${c.fix}${C.reset}
4441
- `);
4442
- }
4443
- process.stdout.write(`
4444
- ${fails === 0 ? `${C.green}\u2713${C.reset}` : `${C.red}\u2717${C.reset}`} ${passed} passed, ${warns} warnings, ${fails} failures
4445
- `);
3887
+ process.stdout.write(renderDoctorText(checks));
4446
3888
  });
4447
- program2.command("login").description("Save the cloud URL, app id, and API key to ~/.outerlayer/config.json").option("--url <url>", "gateway base URL (required; there is no default)").option("--app-id <id>", "factory id the key is bound to (Settings \u2192 General)").option("--no-input", "never prompt; fail if the key is not on stdin").option("--json", "machine-readable JSON output").addHelpText(
3889
+ program2.command("login").description("Sign in to your account by browser approval, or save a piped factory key").option("--dashboard <address>", "the dashboard to sign in on (default: https://app.outerlayer.ai)").option("--no-browser", "print the approve link without opening a browser").option("--check <id>", "check a login that is still waiting for approval, once").option("--url <url>", "gateway base URL for a piped key (browser login gets it from the dashboard)").option("--app-id <id>", "factory id a piped key is bound to (Settings \u2192 General)").option("--no-input", "never wait on a person; fail if the key is not on stdin").option("--json", "machine-readable JSON output").addHelpText(
4448
3890
  "after",
4449
- '\nThe key is read from stdin when stdin is a pipe, otherwise from a prompt\nwith echo off. It is never a flag and never printed back.\n\n echo "$OUTERLAYER_KEY" | outerlayer login --app-id <factory id>\n\nEvery later command reads the saved file, so none of them needs a key on\nits command line. A saved key is checked for real on the first `sync`.\n'
3891
+ '\nWith no key on stdin, login prints an approve link and a code. A signed-in\nperson approves it on the dashboard. The login is a token for that person\'s\naccount, made for this machine, saved here and never printed. It picks no\nfactory: run `outerlayer connect` for that. `outerlayer logout` revokes it.\n\nWhen stdout is not a terminal, login does not wait. It prints one JSON\ndocument with the link, the code and a `login --check <id>` command, and\nexits 75. Run the check until it stops exiting 75: 0 is signed in, 1 is\ndenied or expired. A dropped connection or a 5xx also exits 75.\n\nA factory key piped on stdin is saved as it always was, never as a flag and\nnever printed back. A factory key always wins over the login, so CI and\nrunners keep acting as their key.\n\n echo "$OUTERLAYER_KEY" | outerlayer login --url <gateway url> --app-id <factory id>\n\nEvery later command reads the saved file, so none of them needs a key on\nits command line. A saved key is checked for real on the first `sync`.\n'
4450
3892
  ).action(async (opts) => {
4451
- const { runLogin, readApiKey, LoginError } = await import('./login-cmd-IRX6LZT7.js');
4452
- try {
4453
- const apiKey = await readApiKey({ noInput: opts.input === false });
4454
- const result = runLogin({ apiKey, url: opts.url, appId: opts.appId });
4455
- if (opts.json) {
4456
- writeJson(result);
4457
- return;
4458
- }
4459
- process.stdout.write(
4460
- `${C.green}\u2713${C.reset} Saved credentials to ${result.path}` + (result.url ? ` for ${result.url}` : "") + (result.appId ? ` (app ${result.appId})` : "") + "\n"
4461
- );
4462
- } catch (err) {
4463
- if (err instanceof LoginError) {
4464
- fail(err.message);
4465
- return;
4466
- }
4467
- throw err;
4468
- }
3893
+ const { runLoginCommand, realLoginDeps } = await import('./login-browser-7XGSV7MA.js');
3894
+ const code = await runLoginCommand(
3895
+ {
3896
+ browser: opts.browser !== false,
3897
+ dashboard: opts.dashboard,
3898
+ json: opts.json,
3899
+ check: opts.check,
3900
+ noInput: opts.input === false,
3901
+ url: opts.url,
3902
+ appId: opts.appId
3903
+ },
3904
+ realLoginDeps()
3905
+ );
3906
+ if (code !== 0) process.exitCode = code;
3907
+ });
3908
+ program2.command("logout").description("Revoke this machine's login and remove it").addHelpText(
3909
+ "after",
3910
+ "\nRevokes the token `outerlayer login` saved, then deletes it from the config\nfile. A factory key and the chosen factory are left alone. When the\ndashboard cannot be reached the local copy is still deleted and the exit\ncode is 1: revoke the token on /profile/security.\n"
3911
+ ).action(async () => {
3912
+ const { runLogoutCommand, realLogoutDeps } = await import('./logout-cmd-QAUFDJ6J.js');
3913
+ const code = await runLogoutCommand(realLogoutDeps());
3914
+ if (code !== 0) process.exitCode = code;
4469
3915
  });
4470
- program2.command("sync").description("Send local coding-agent sessions to OuterLayer cloud").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "app id the key is bound to (or OUTERLAYER_APP_ID / config)").option("--tier <tier>", "capture tier: metrics | redacted | full (default: full)").option("--dry-run", "print what would leave this machine, send nothing").option("--all", "ignore the sync checkpoint and re-send everything").option("--limit <n>", "only the N most recent sessions", (v) => parseInt(v, 10)).option("--json", "machine-readable JSON output").option("--quiet", "no output; exit code only").option("--root <dir>", "Claude Code transcript root (default ~/.claude/projects)").option("--codex-root <dir>", "Codex sessions root (default ~/.codex/sessions)").option("--cursor-root <dir>", "Cursor chats root (default ~/.cursor/chats)").addHelpText(
3916
+ program2.command("sync").description("Send local coding-agent sessions to OuterLayer cloud").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "factory id the key is bound to (or OUTERLAYER_APP_ID / config)").option("--tier <tier>", "capture tier: metrics | redacted | full (default: full)").option("--dry-run", "print what would leave this machine, send nothing").option("--all", "ignore the sync checkpoint and re-send everything").option("--limit <n>", "only the N most recent sessions", (v) => parseInt(v, 10)).option("--json", "machine-readable JSON output").option("--quiet", "no output; exit code only").option("--root <dir>", "Claude Code transcript root (default ~/.claude/projects)").option("--codex-root <dir>", "Codex sessions root (default ~/.codex/sessions)").option("--cursor-root <dir>", "Cursor chats root (default ~/.cursor/chats)").addHelpText(
4471
3917
  "after",
4472
3918
  "\nIncremental: only sessions newer than the last checkpoint ship. The tier\nis applied on this machine before anything leaves it; `--dry-run` prints\nexactly what would. The hook-triggered background sync runs `--quiet`.\nExit 2 means the server rejected at least one session.\n"
4473
3919
  ).action(async (opts) => {
4474
3920
  warnIfApiKeyFlag(opts);
4475
- const { runSync, SyncConfigError, SyncTransportError } = await import('./sync-cmd-BBAUZ5JD.js');
3921
+ const { runSync, SyncConfigError, SyncTransportError } = await import('./sync-cmd-VPPYCNNM.js');
4476
3922
  try {
4477
3923
  const result = await runSync({
4478
3924
  url: opts.url,
@@ -4501,7 +3947,7 @@ ${fails === 0 ? `${C.green}\u2713${C.reset}` : `${C.red}\u2717${C.reset}`} ${pas
4501
3947
  if (invokedCommandToken(argv) === "watch") {
4502
3948
  warnDeprecated("`outerlayer watch` is now `outerlayer daemon`; the old name will be removed in a future release.");
4503
3949
  }
4504
- const { runWatch } = await import('./watch-3DTPJETH.js');
3950
+ const { runWatch } = await import('./watch-5C4ZBBGO.js');
4505
3951
  await runWatch({ once: opts.once });
4506
3952
  });
4507
3953
  program2.command("hook <event>", { hidden: true }).description("capture-hook fast path, installed by `init`").action(() => {
@@ -4510,6 +3956,59 @@ ${fails === 0 ? `${C.green}\u2713${C.reset}` : `${C.red}\u2717${C.reset}`} ${pas
4510
3956
  });
4511
3957
  program2.command("hook-wrap", { hidden: true }).description("runs a hook wrapped by `hooks wrap`, recording exec evidence").option("--id <base64>", "the original hook command, base64-encoded").option("--capture-output", "tee stdout/stderr into a bounded tail").action(() => {
4512
3958
  });
3959
+ program2.command("broker-relay", { hidden: true }).description("relay a build's loopback connections to the runner's tunnel and destinations").requiredOption("--port <port>", "the loopback port to listen on").requiredOption("--socket <path>", "the runner's tunnel socket").option("--forward <port=socket>", "also relay a loopback port to a destination's socket; repeatable", (value, previous) => [...previous, value], []).action(async (opts) => {
3960
+ const { startRelay } = await import('./relay-L76HDX72.js');
3961
+ const port = Number(opts.port);
3962
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
3963
+ fail(`--port must be a port number, not ${opts.port}`);
3964
+ return;
3965
+ }
3966
+ const forwards = [];
3967
+ for (const spec of opts.forward) {
3968
+ const eq = spec.indexOf("=");
3969
+ const forwardPort = Number(spec.slice(0, eq));
3970
+ if (eq === -1 || !Number.isInteger(forwardPort) || forwardPort < 1 || forwardPort > 65535 || eq === spec.length - 1) {
3971
+ fail(`--forward must be <port>=<socket path>, not ${spec}`);
3972
+ return;
3973
+ }
3974
+ forwards.push({ port: forwardPort, socketPath: spec.slice(eq + 1) });
3975
+ }
3976
+ await startRelay({ port, socketPath: opts.socket, forwards });
3977
+ });
3978
+ program2.command("vm-exec", { hidden: true }).description("run a command in a microVM build's guest through its exec agent").requiredOption("--socket <path>", "the Unix socket Firecracker serves the build's vsock device on").option("--user <user>", "the user to run as", "").option("--cwd <dir>", "the working directory", "/").option("--env <NAME=value>", "an environment value the command line carries; repeatable", (value, previous) => [...previous, value], []).option("--pass <NAME>", "an environment variable whose value is read from this process; repeatable", (value, previous) => [...previous, value], []).argument("<argv...>", "the command and its arguments").action(async (argv2, opts) => {
3979
+ const { runVmExec } = await import('./exec-client-4XGXV3XN.js');
3980
+ const env = {};
3981
+ for (const entry of opts.env) {
3982
+ const eq = entry.indexOf("=");
3983
+ if (eq < 1) {
3984
+ fail(`--env must be NAME=value, not ${entry}`);
3985
+ return;
3986
+ }
3987
+ env[entry.slice(0, eq)] = entry.slice(eq + 1);
3988
+ }
3989
+ const abort = new AbortController();
3990
+ for (const signal of ["SIGTERM", "SIGINT", "SIGHUP"]) process.on(signal, () => abort.abort());
3991
+ try {
3992
+ const status = await runVmExec(
3993
+ { socket: opts.socket, user: opts.user, cwd: opts.cwd, env, pass: opts.pass, argv: argv2 },
3994
+ { stdin: process.stdin, stdout: (chunk) => void process.stdout.write(chunk), stderr: (chunk) => void process.stderr.write(chunk), env: process.env, signal: abort.signal }
3995
+ );
3996
+ process.exit(status < 0 ? 255 : status);
3997
+ } catch (err) {
3998
+ process.stderr.write(`vm-exec: ${err instanceof Error ? err.message : String(err)}
3999
+ `);
4000
+ process.exit(255);
4001
+ }
4002
+ });
4003
+ program2.command("vm-init", { hidden: true }).description("lay out a microVM build's guest; the guest helper runs it").action(async () => {
4004
+ const { realGuestHost, runGuestInit } = await import('./guest-init-GENRHNP7.js');
4005
+ const { readFileSync: readFileSync3 } = await import('fs');
4006
+ try {
4007
+ runGuestInit(realGuestHost(), readFileSync3("/proc/cmdline", "utf8"));
4008
+ } catch (err) {
4009
+ fail(`vm-init failed: ${err instanceof Error ? err.message : String(err)}`);
4010
+ }
4011
+ });
4513
4012
  registerEmitCommands(program2);
4514
4013
  registerImportCommands(program2);
4515
4014
  registerHooksCommands(program2);
@@ -4519,13 +4018,17 @@ ${fails === 0 ? `${C.green}\u2713${C.reset}` : `${C.red}\u2717${C.reset}`} ${pas
4519
4018
  registerRunnerCommands(program2);
4520
4019
  return program2;
4521
4020
  }
4021
+ function withoutItemKey(job) {
4022
+ const { itemKey: _itemKey, ...rest } = job;
4023
+ return rest;
4024
+ }
4522
4025
  async function runCli(processArgv) {
4523
4026
  const { argv, noColor } = stripNoColor(processArgv);
4524
4027
  if (noColor) process.env.NO_COLOR = "1";
4525
4028
  await buildProgram(argv).parseAsync(argv);
4526
4029
  }
4527
4030
  async function compileContext(opts) {
4528
- const { runEmit, EmitError } = await import('./emit-cmd-TWSYYEZI.js');
4031
+ const { runEmit, EmitError } = await import('./emit-cmd-DCERAU27.js');
4529
4032
  try {
4530
4033
  const result = runEmit({ cwd: opts.dir, check: opts.check, json: opts.json });
4531
4034
  if (result.exitCode !== 0) process.exitCode = result.exitCode;
@@ -4538,9 +4041,9 @@ async function compileContext(opts) {
4538
4041
  }
4539
4042
  }
4540
4043
  function registerEmitCommands(program2) {
4541
- const emitCmd = program2.command("emit").enablePositionalOptions().description("Record evidence for a work item: a named check result").argument("[name]", "the check name a validator declares (e.g. smoke.pass)").option("--link <url>", "proof link, usually the CI run URL").option("--body <text>", "one sentence recording your judgment").option("--body-file <path>", "read the sentence from a file; - reads standard input").option("--result <pass|fail>", "check outcome (default: pass)").option("--item <number>", "work item to record on directly, by its number in this factory").option("--artifact <id>", "artifact this verdict is on \u2014 only with the artifact-review check name").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "app id the key is bound to (or OUTERLAYER_APP_ID / config)").addOption(new Option("--check", "deprecated: use `context emit --check`").hideHelp()).addOption(new Option("--dir <path>", "deprecated: use `context emit --dir`").hideHelp()).addHelpText(
4044
+ const emitCmd = program2.command("emit").enablePositionalOptions().description("Record evidence for a work item: a named check result").argument("[name]", "the check name a validator declares (e.g. smoke.pass)").option("--link <url>", "proof link, usually the CI run URL").option("--body <text>", "one sentence recording your judgment").option("--body-file <path>", "read the sentence from a file; - reads standard input").option("--result <pass|fail>", "check outcome (default: pass)").option("--item <number>", "work item to record on directly, by its number in this factory").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "factory id the key is bound to (or OUTERLAYER_APP_ID / config)").addOption(new Option("--check", "deprecated: use `context emit --check`").hideHelp()).addOption(new Option("--dir <path>", "deprecated: use `context emit --dir`").hideHelp()).addHelpText(
4542
4045
  "after",
4543
- '\n outerlayer emit smoke.pass --item 412 --link https://ci.example.com/run/42\n outerlayer emit code-review --result fail --item 412 --body "The button should be red."\n outerlayer emit artifact-review --result fail --artifact a1b2c3d4 --body "Wrong state."\n\nRecords that a named check passed or failed, anchored to a work item.\n--item is always required, in or out of a recorded session \u2014 there is no\nsession fallback. A check that ran carries the run URL as proof (--link);\na judgment you are making yourself carries one sentence (--body, or\n--body-file to read it from a file or standard input). A fail needs at\nleast one of the two. Run from inside a session, --item can only name a\nwork item that session was NOT launched for \u2014 a session cannot record a\ncheck on its own item. With nothing to attach it to, the command refuses.\n\nWho recorded a check is decided by your API key, never by what you send:\na key bound to your membership records it as yours, and nothing automated\ncan then clear it.\n\nThe check name `artifact-review` is a different act: a person\'s own pass\nor fail on one artifact, named with --artifact instead of --item, refused\nfrom inside a recorded session \u2014 an artifact verdict is a person\'s act.\n--link is refused in this mode; a fail needs --body / --body-file.\n\nSubcommands: `emit artifact <file>` for screenshots, recordings, reports and\nlogs; `emit finding` / `emit findings <file>` to record what was found wrong\nabout the change or about a rule.\nCompiling .outerlayer/ moved to `outerlayer context emit`.\n'
4046
+ "\n outerlayer emit smoke.pass --item 412 --link https://ci.example.com/run/42\n outerlayer emit code-review --result fail --item 412 --body \"The button should be red.\"\n\nRecords that a named check passed or failed, anchored to a work item.\n--item is always required, in or out of a recorded session \u2014 there is no\nsession fallback. A check that ran carries the run URL as proof (--link);\na judgment you are making yourself carries one sentence (--body, or\n--body-file to read it from a file or standard input). A fail needs at\nleast one of the two. Run from inside a session, the check is stored as\nthe session's; a session cannot record over a person's fail. With\nnothing to attach it to, the command refuses.\n\nWho recorded a check is decided by your API key, never by what you send:\na key bound to your membership records it as yours, and nothing automated\ncan then clear it.\n\nA person's pass or fail on one criterion's proof is recorded on that\ncriterion's thread instead \u2014 see `outerlayer work comment`.\n\nSubcommands: `emit artifact <file>` for screenshots, recordings, reports and\nlogs; `emit finding` / `emit findings <file>` to record a problem in the factory;\n`emit criteria <file>` to record a work item's acceptance criteria.\nCompiling .outerlayer/ moved to `outerlayer context emit`.\n"
4544
4047
  ).action(async (name, opts) => {
4545
4048
  if (name === void 0) {
4546
4049
  warnDeprecated("`outerlayer emit` with no name now lives at `outerlayer context emit`; the bare form will be removed in a future release.");
@@ -4548,7 +4051,7 @@ function registerEmitCommands(program2) {
4548
4051
  return;
4549
4052
  }
4550
4053
  warnIfApiKeyFlag(opts);
4551
- const { runEmitResult, EmitResultError } = await import('./emit-result-cmd-H2T4C2K3.js');
4054
+ const { runEmitResult, EmitResultError } = await import('./emit-result-cmd-FDTUKGKE.js');
4552
4055
  try {
4553
4056
  const result = await runEmitResult({
4554
4057
  name,
@@ -4557,7 +4060,6 @@ function registerEmitCommands(program2) {
4557
4060
  bodyFile: opts.bodyFile,
4558
4061
  result: opts.result,
4559
4062
  item: opts.item,
4560
- artifact: opts.artifact,
4561
4063
  json: opts.json,
4562
4064
  url: opts.url,
4563
4065
  apiKey: opts.apiKey,
@@ -4572,12 +4074,12 @@ function registerEmitCommands(program2) {
4572
4074
  throw err;
4573
4075
  }
4574
4076
  });
4575
- emitCmd.command("artifact <file>").description("Emit a proof artifact: screenshot, recording, report, or log").requiredOption("--caption <text>", "what this artifact shows or proves").option("--for <criterion-id>", "id of the criterion this artifact proves (e.g. LOGIN-01)").option("--replaces <ids>", "artifact id(s) this one corrects; comma-separate 2-20 to retire them all").option("--pr <number>", "pull request number to anchor to").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "app id the key is bound to (or OUTERLAYER_APP_ID / config)").addHelpText(
4077
+ emitCmd.command("artifact <file>").description("Emit a proof artifact: screenshot, recording, report, or log").requiredOption("--caption <text>", "what this artifact shows or proves").option("--for <criterion-id>", "id of the criterion this artifact proves (e.g. LOGIN-01)").option("--replaces <ids>", "artifact id(s) this one corrects; comma-separate 2-20 to retire them all").option("--pr <number>", "pull request number to anchor to").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "factory id the key is bound to (or OUTERLAYER_APP_ID / config)").addHelpText(
4576
4078
  "after",
4577
4079
  '\n outerlayer emit artifact shot.png --caption "the dot row, current dot marked" --for LOGIN-01\n\nInside a recorded Claude Code session the artifact spools locally and ships,\nbound to that session, on the next `outerlayer sync` \u2014 but only if that\nsession was launched with OUTERLAYER_WORK; without it nothing the session\nproduces may leave the machine, and the command refuses rather than queue an\nartifact that can never ship. Otherwise it uploads immediately, anchored to a\npull request (--pr, or CI\'s PR context) or to the current git checkout. With\nnothing to attach it to, the command refuses.\n\n--replaces: an already-uploaded target is marked superseded and hidden\neverywhere, bytes and deep link untouched; a still-queued one is cancelled\nlocally so it never uploads. One correction can retire several duplicates.\n'
4578
4080
  ).action(async (file, opts) => {
4579
4081
  warnIfApiKeyFlag(opts);
4580
- const { runEmitArtifact, EmitArtifactError } = await import('./emit-artifact-cmd-UQVT3OKW.js');
4082
+ const { runEmitArtifact, EmitArtifactError } = await import('./emit-artifact-cmd-KTYW2TN2.js');
4581
4083
  let pr;
4582
4084
  if (opts.pr !== void 0) {
4583
4085
  pr = parseTrackerNumber(opts.pr);
@@ -4606,28 +4108,24 @@ function registerEmitCommands(program2) {
4606
4108
  throw err;
4607
4109
  }
4608
4110
  });
4609
- emitCmd.command("finding").description("Record one finding: what was found wrong about the change or about a rule").requiredOption("--id <id>", "client id \u2014 re-emitting the same id replaces it").requiredOption("--subject <change|context>", "what the finding is about").requiredOption("--title <text>", "one sentence").requiredOption("--file <path>", "the file in the diff, or the rule's own path").option("--line <number>", "line number in --file").option("--severity <blocker|major|minor|nit>", "how serious \u2014 omit for a context finding").requiredOption("--kind <behavior|hygiene|proof>", "what kind of thing this is about").requiredOption("--verdict <confirmed|refuted|unverified>", "what you concluded").option("--fixed", "a confirmed finding whose fix passed this round's check").option("--rule-path <path>", "the rule's own path \u2014 required with --rule-quote & --rule-relation").option("--rule-line <number>", "line number in --rule-path").option("--rule-quote <text>", "the rule's quoted sentence \u2014 required with --rule-path & --rule-relation").option("--rule-relation <broken|wrong|none>", "how this relates to the rule \u2014 required with --rule-path & --rule-quote").requiredOption("--source <implementer|reviewer|refuter|gate>", "which act recorded this").requiredOption("--where <label>", 'where this was recorded, e.g. "review pass 1"').option("--trace-id <id>", "the session's trace id, when there is one").option("--turn <number>", "turn index within the session").option("--round <number>", "review round number").option("--head-sha <sha>", "the commit this batch was recorded against").option("--context-source-sha <sha>", "the context-source commit this batch was recorded against").option("--item <number>", "work item to record on directly, by its number in this factory").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "app id the key is bound to (or OUTERLAYER_APP_ID / config)").addHelpText(
4111
+ emitCmd.command("finding").description("Record one finding: a problem you hit in the factory").requiredOption("--id <id>", "client id \u2014 re-emitting the same id replaces it").option("--category <context|flaky-test|tooling|environment|defect|other>", "what kind of problem this is").requiredOption("--title <text>", "one sentence").requiredOption("--file <path>", "the file the problem is in, or the instruction's own path").option("--line <number>", "line number in --file").option("--rule-path <path>", "the instruction's own path \u2014 required for a context finding").option("--rule-line <number>", "line number in --rule-path").option("--rule-quote <text>", "the instruction's quoted sentence \u2014 required unless --rule-relation is missing").option("--rule-relation <wrong|broken|missing>", "how the instruction fails \u2014 required for a context finding").requiredOption("--where <label>", 'where this was recorded, e.g. "build session"').option("--trace-id <id>", "the session's trace id, when there is one").option("--turn <number>", "turn index within the session").option("--round <number>", "round number, when there is one").addOption(new Option("--subject <value>").hideHelp()).addOption(new Option("--kind <value>").hideHelp()).addOption(new Option("--severity <value>").hideHelp()).addOption(new Option("--verdict <value>").hideHelp()).addOption(new Option("--fixed").hideHelp()).addOption(new Option("--source <value>").hideHelp()).option("--head-sha <sha>", "the commit this batch was recorded against").option("--context-source-sha <sha>", "the context-source commit this batch was recorded against").option("--item <number>", "work item to record on directly, by its number in this factory").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "factory id the key is bound to (or OUTERLAYER_APP_ID / config)").addHelpText(
4610
4112
  "after",
4611
- '\n outerlayer emit finding --id r1-f1 --subject change --title "Off-by-one" \\\n --file src/loop.ts --line 42 --severity major --kind behavior \\\n --verdict confirmed --source reviewer --where "review pass 1"\n\nRecords one finding \u2014 what an agent noticed was wrong about the change, or\nabout a rule it ran on. Validated against the same contract the gateway\nchecks before anything is sent; a subject of context needs --rule-path,\n--rule-quote and --rule-relation together \u2014 all three are required, not\njust --rule-path.\n--item names the work item; without it, inside a recorded session, the\nitem that session was launched for is used automatically \u2014 a session may\nrecord findings on its own item, unlike `outerlayer emit <name>`.\n'
4113
+ '\n outerlayer emit finding --id gate-skip --category tooling \\\n --title "The pre-push gate prints green when a step was skipped" \\\n --file scripts/git/pre-push-checks.mjs --where "build session"\n\nRecords one problem you hit in the factory: an instruction that is wrong or\nmissing (context), a flaky test, a misbehaving script (tooling), a local\nsetup trap (environment), a bug outside your own change (defect), or\nanything else. A problem in your own change is fixed, not recorded.\nA context finding needs --rule-path and --rule-relation, and --rule-quote\nunless the relation is missing.\n--item names the work item; without it, inside a recorded session, the\nitem that session was launched for is used automatically.\n'
4612
4114
  ).action(async (opts) => {
4613
4115
  warnIfApiKeyFlag(opts);
4614
- const { runEmitFinding, EmitFindingError } = await import('./emit-finding-cmd-FDOU2OPD.js');
4116
+ const { runEmitFinding, refuseRemovedFindingFlags, EmitFindingError } = await import('./emit-finding-cmd-UR4ID45Z.js');
4615
4117
  try {
4118
+ refuseRemovedFindingFlags(opts);
4616
4119
  const result = await runEmitFinding({
4617
4120
  id: opts.id,
4618
- subject: opts.subject,
4121
+ category: opts.category,
4619
4122
  title: opts.title,
4620
4123
  file: opts.file,
4621
4124
  line: opts.line,
4622
- severity: opts.severity,
4623
- kind: opts.kind,
4624
- verdict: opts.verdict,
4625
- fixed: opts.fixed,
4626
4125
  rulePath: opts.rulePath,
4627
4126
  ruleLine: opts.ruleLine,
4628
4127
  ruleQuote: opts.ruleQuote,
4629
4128
  ruleRelation: opts.ruleRelation,
4630
- source: opts.source,
4631
4129
  where: opts.where,
4632
4130
  traceId: opts.traceId,
4633
4131
  turn: opts.turn,
@@ -4649,12 +4147,12 @@ function registerEmitCommands(program2) {
4649
4147
  throw err;
4650
4148
  }
4651
4149
  });
4652
- emitCmd.command("findings <file>").description("Record a batch of findings from a FindingBatch JSON file (- for standard input)").option("--item <number>", "work item to record on directly, by its number in this factory").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "app id the key is bound to (or OUTERLAYER_APP_ID / config)").addHelpText(
4150
+ emitCmd.command("findings <file>").description("Record a batch of findings from a FindingBatch JSON file (- for standard input)").option("--item <number>", "work item to record on directly, by its number in this factory").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "factory id the key is bound to (or OUTERLAYER_APP_ID / config)").addHelpText(
4653
4151
  "after",
4654
4152
  "\n outerlayer emit findings review.json --item 412\n\nRecords a whole batch at once \u2014 a FindingBatch JSON file (schemaVersion,\noptional headSha / contextSourceSha, and 1-500 findings), validated against\nthe same contract `emit finding` uses before anything is sent. Re-emitting\nan id already on the item replaces that row rather than adding a second one.\n--item names the work item; without it, inside a recorded session, the item\nthat session was launched for is used automatically.\n"
4655
4153
  ).action(async (file, opts) => {
4656
4154
  warnIfApiKeyFlag(opts);
4657
- const { runEmitFindings, EmitFindingError } = await import('./emit-finding-cmd-FDOU2OPD.js');
4155
+ const { runEmitFindings, EmitFindingError } = await import('./emit-finding-cmd-UR4ID45Z.js');
4658
4156
  try {
4659
4157
  const result = await runEmitFindings({
4660
4158
  file,
@@ -4673,17 +4171,41 @@ function registerEmitCommands(program2) {
4673
4171
  throw err;
4674
4172
  }
4675
4173
  });
4174
+ emitCmd.command("criteria <file>").description("Record a work item's acceptance criteria from a JSON file (- for stdin)").option("--item <number>", "work item to record on directly, by its number in this factory").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "factory id the key is bound to (or OUTERLAYER_APP_ID / config)").addHelpText(
4175
+ "after",
4176
+ '\n outerlayer emit criteria criteria.json --item 412\n\nRecords the item\'s acceptance criteria as one list. The file is JSON:\n { "criteria": [ { "id": "AC-1", "text": "Given ...", "proof": null },\n { "id": "login.redirect", "text": "...", "proof": "screenshot" } ] }\nAn id is 1-64 characters of A-Z a-z 0-9 . _ : - and is the id evidence is bound to with\n`emit artifact --for`. proof is video, screenshot, report, log, file or null (any bound\nartifact proves it). The list holds 1 to 200 criteria, each text at most 2,000\ncharacters, and no id twice. The list is validated before anything is sent.\nOnce an item has a list, only a person can replace it: run the command outside an\nagent session, with your own login. A key bound to no member cannot replace it either.\n--item names the work item; without it, inside a recorded session, the item that\nsession was launched for is used automatically.\n'
4177
+ ).action(async (file, opts) => {
4178
+ warnIfApiKeyFlag(opts);
4179
+ const { runEmitCriteria, EmitCriteriaError } = await import('./emit-criteria-cmd-VAUUSAYD.js');
4180
+ try {
4181
+ const result = await runEmitCriteria({
4182
+ file,
4183
+ item: opts.item,
4184
+ json: opts.json,
4185
+ url: opts.url,
4186
+ apiKey: opts.apiKey,
4187
+ appId: opts.appId
4188
+ });
4189
+ if (result.exitCode !== 0) process.exitCode = result.exitCode;
4190
+ } catch (err) {
4191
+ if (err instanceof EmitCriteriaError) {
4192
+ fail(err.message);
4193
+ return;
4194
+ }
4195
+ throw err;
4196
+ }
4197
+ });
4676
4198
  }
4677
4199
  function registerWorkCommands(program2) {
4678
- const workCmd = program2.command("work").description("Add, remove, or read what the factory is working on (the Floor)");
4679
- workCmd.command("add").description("Record that a source is working on an issue or pull request").requiredOption("--issue <number>", "issue number").option("--repo <owner/name>", "repository (default: the current checkout's remote)").option("--note <text>", "a short note stored with the addition").option("--session-id <id>", "session id to record as-is, skipping detection (used by the SessionStart hook)").option("--retry <count>", "resend after a failure that may pass, with backoff").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "app id the key is bound to (or OUTERLAYER_APP_ID / config)").addHelpText(
4200
+ const workCmd = program2.command("work").description("Start, remove, or read what the factory is working on (the Work page)");
4201
+ workCmd.command("build").description("Start work on an item: a host builds it, or with --local you do").option("--issue <number>", "issue number, or a Linear/Jira key like ENG-123 (adds it to Work first)").option("--tracker <linear|jira>", "tracker for a keyed --issue \u2014 required if more than one is connected").option("--item <number>", "the item's own number inside this factory \u2014 must already exist").option("--local", "you build it in a session you start; no host takes it").option("--repo <owner/name>", "repository \u2014 the checkout's remote for a number, never guessed for a key").option("--note <text>", "a short note stored with the request").option("--session-id <id>", "session id to record as-is, skipping detection").option("--retry <count>", "retries after a gateway answer worth trying again").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "factory id the key is bound to (or OUTERLAYER_APP_ID / config)").addHelpText(
4680
4202
  "after",
4681
- "\n outerlayer work add --issue 42\n\nRecords that a named source \u2014 a recorded session, a CI run, or the API\nkey's bound member \u2014 is working on an issue or pull request. This is\nthe only way an item becomes visible on the Floor. A second add of the\nsame subject by the same source is a no-op.\n"
4203
+ "\n outerlayer work build --issue 42 # a host builds it\n outerlayer work build --item 7 # retry an item already on Work\n outerlayer work build --issue 42 --local # you build it\n outerlayer work build --issue ENG-123 --tracker linear # a Linear issue\n\nWithout --local, records a build request: a host takes the item when it\nhas a free slot. It never links the calling session and never takes a\nlease, even run from inside one.\n\nWith --local, puts the item on Work with no build request, so no host\ntakes it, and prints the line that starts your session on it. That\nsession links itself and holds the item's lease while it works.\n"
4682
4204
  ).action(async (opts) => {
4683
4205
  warnIfApiKeyFlag(opts);
4684
- const { runWorkAdd, WorkCommandError } = await import('./work-cmd-2P4BVX47.js');
4206
+ const { runWorkBuild, WorkCommandError } = await import('./work-cmd-UOQO2AD4.js');
4685
4207
  try {
4686
- await runWorkAdd(opts);
4208
+ await runWorkBuild(opts);
4687
4209
  } catch (err) {
4688
4210
  if (err instanceof WorkCommandError) {
4689
4211
  fail(err.message);
@@ -4692,14 +4214,14 @@ function registerWorkCommands(program2) {
4692
4214
  throw err;
4693
4215
  }
4694
4216
  });
4695
- workCmd.command("link-session").description("Link the calling session to a work item that already exists").argument("<number>", "the item's number inside this factory").option("--session-id <id>", "session id to record as-is, skipping detection (used by the SessionStart hook)").option("--retry <count>", "resend after a failure that may pass, with backoff").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "app id the key is bound to (or OUTERLAYER_APP_ID / config)").addHelpText(
4217
+ workCmd.command("link-session").description("Link the calling session to a work item that already exists").argument("<number>", "the item's number inside this factory").option("--session-id <id>", "session id to record as-is, skipping detection (used by the SessionStart hook)").option("--retry <count>", "resend after a failure that may pass, with backoff").option("--no-lease", "link only \u2014 ask for no session lease (used by a host's own job)").option("--ended", "release this session's lease instead of taking or renewing one").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "factory id the key is bound to (or OUTERLAYER_APP_ID / config)").addHelpText(
4696
4218
  "after",
4697
- "\n outerlayer work link-session 42\n\nNames an item that already exists on the calling session \u2014 it never\ncreates one. Create an item first with `outerlayer work add`. Linking\nthe same session to the same item twice is a no-op.\n"
4219
+ "\n outerlayer work link-session 42\n\nNames an item that already exists on the calling session \u2014 it never\ncreates one. Create one with `outerlayer work build --local`. Linking\nthe same session to the same item twice is a no-op. Also takes or\nrenews that session's own 20-minute lease, unless --no-lease or\n--ended is given, or another worker already holds a live one.\n"
4698
4220
  ).action(async (number, opts) => {
4699
4221
  warnIfApiKeyFlag(opts);
4700
- const { runWorkLinkSession, WorkCommandError } = await import('./work-cmd-2P4BVX47.js');
4222
+ const { runWorkLinkSession, WorkCommandError } = await import('./work-cmd-UOQO2AD4.js');
4701
4223
  try {
4702
- await runWorkLinkSession({ ...opts, number });
4224
+ await runWorkLinkSession({ ...opts, number, noLease: opts.lease === false });
4703
4225
  } catch (err) {
4704
4226
  if (err instanceof WorkCommandError) {
4705
4227
  fail(err.message);
@@ -4708,18 +4230,61 @@ function registerWorkCommands(program2) {
4708
4230
  throw err;
4709
4231
  }
4710
4232
  });
4711
- workCmd.command("remove").description("Withdraw a source's addition \u2014 never deletes the item").option("--item <number>", "the item's number inside this factory").option("--issue <number>", "issue number (default: named by --item instead)").option("--repo <owner/name>", "repository (default: the current checkout's remote)").requiredOption("--reason <text>", "why this source is no longer working on it").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "app id the key is bound to (or OUTERLAYER_APP_ID / config)").addHelpText(
4233
+ workCmd.command("comment").description("Post a comment on a work item's general thread or one of its criterion threads").requiredOption("--item <number>", "the item's number inside this factory").option("--criterion <id>", "the criterion thread to post on (default: the general thread)").option("--body <text>", "the comment's text").option("--artifact <id>", "artifact id \u2014 required with --attach, naming the criterion's new proof").option("--pass", "record a pass (a person's verdict; refused from an agent session or a key)").option("--fail", "record a fail (a person's verdict; refused from an agent session or a key)").option("--attach", "make --artifact the named criterion's proof").option("--ready", "hand the thread back to a person (refused from a person)").option("--session-id <id>", "session id to record as-is, skipping detection (used by the SessionStart hook)").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "factory id the key is bound to (or OUTERLAYER_APP_ID / config)").addHelpText(
4234
+ "after",
4235
+ `
4236
+ outerlayer work comment --item 7 --criterion AC-9-01 --body "fixed" --artifact 3f2a1c9d --attach
4237
+
4238
+ Posts one comment. Omitting --criterion posts on the general thread;
4239
+ naming one posts on that criterion's own thread. --pass/--fail are a
4240
+ person's verdict on the whole item, so only on the general thread: the
4241
+ gateway refuses them with --criterion. --attach makes --artifact the
4242
+ criterion's new proof; --ready hands the thread back to a person. At
4243
+ most one action flag may be given.
4244
+ `
4245
+ ).action(async (opts) => {
4246
+ warnIfApiKeyFlag(opts);
4247
+ const { runWorkComment, WorkCommentCommandError } = await import('./work-comment-cmd-WF3EOLAE.js');
4248
+ const { WorkCommandError } = await import('./work-cmd-UOQO2AD4.js');
4249
+ try {
4250
+ await runWorkComment(opts);
4251
+ } catch (err) {
4252
+ if (err instanceof WorkCommentCommandError || err instanceof WorkCommandError) {
4253
+ fail(err.message);
4254
+ return;
4255
+ }
4256
+ throw err;
4257
+ }
4258
+ });
4259
+ workCmd.command("threads").description("Read a work item's general thread and its criterion threads").requiredOption("--item <number>", "the item's number inside this factory").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "factory id the key is bound to (or OUTERLAYER_APP_ID / config)").addHelpText(
4260
+ "after",
4261
+ "\n outerlayer work threads --item 7\n\nPrints the item's general thread, then one thread per acceptance\ncriterion that carries proof or a comment, each with who is waiting\non whom next.\n"
4262
+ ).action(async (opts) => {
4263
+ warnIfApiKeyFlag(opts);
4264
+ const { runWorkThreads, WorkCommentCommandError } = await import('./work-comment-cmd-WF3EOLAE.js');
4265
+ const { WorkCommandError } = await import('./work-cmd-UOQO2AD4.js');
4266
+ try {
4267
+ await runWorkThreads(opts);
4268
+ } catch (err) {
4269
+ if (err instanceof WorkCommentCommandError || err instanceof WorkCommandError) {
4270
+ fail(err.message);
4271
+ return;
4272
+ }
4273
+ throw err;
4274
+ }
4275
+ });
4276
+ workCmd.command("remove").description("Withdraw a source's addition \u2014 never deletes the item").option("--item <number>", "the item's number inside this factory").option("--issue <number>", "issue number (default: named by --item instead)").option("--repo <owner/name>", "repository (default: the current checkout's remote)").requiredOption("--reason <text>", "why this source is no longer working on it").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "factory id the key is bound to (or OUTERLAYER_APP_ID / config)").addHelpText(
4712
4277
  "after",
4713
4278
  `
4714
4279
  outerlayer work remove --item 42 --reason "done"
4715
4280
 
4716
4281
  Withdraws the caller's own addition, recording the reason. Never
4717
- deletes anything \u2014 the item is removed from the Floor only once no
4282
+ deletes anything \u2014 the item is removed from Work only once no
4718
4283
  live addition remains on it.
4719
4284
  `
4720
4285
  ).action(async (opts) => {
4721
4286
  warnIfApiKeyFlag(opts);
4722
- const { runWorkRemove, WorkCommandError } = await import('./work-cmd-2P4BVX47.js');
4287
+ const { runWorkRemove, WorkCommandError } = await import('./work-cmd-UOQO2AD4.js');
4723
4288
  try {
4724
4289
  await runWorkRemove(opts);
4725
4290
  } catch (err) {
@@ -4730,9 +4295,9 @@ live addition remains on it.
4730
4295
  throw err;
4731
4296
  }
4732
4297
  });
4733
- workCmd.command("status").description("Show one item's stage, gate ledger, and additions").option("--item <number>", "the item's number inside this factory").option("--issue <number>", "issue number").option("--repo <owner/name>", "repository (default: the current checkout's remote)").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "app id the key is bound to (or OUTERLAYER_APP_ID / config)").action(async (opts) => {
4298
+ workCmd.command("status").description("Show one item's stage, gate ledger, and additions").option("--item <number>", "the item's number inside this factory").option("--issue <number>", "issue number").option("--repo <owner/name>", "repository (default: the current checkout's remote)").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "factory id the key is bound to (or OUTERLAYER_APP_ID / config)").action(async (opts) => {
4734
4299
  warnIfApiKeyFlag(opts);
4735
- const { runWorkStatus, WorkCommandError } = await import('./work-cmd-2P4BVX47.js');
4300
+ const { runWorkStatus, WorkCommandError } = await import('./work-cmd-UOQO2AD4.js');
4736
4301
  try {
4737
4302
  await runWorkStatus(opts);
4738
4303
  } catch (err) {
@@ -4743,9 +4308,9 @@ live addition remains on it.
4743
4308
  throw err;
4744
4309
  }
4745
4310
  });
4746
- workCmd.command("list").description("List live work items for this factory").option("--stage <spec|build|review>", "filter by stage").option("--section <attention|waiting|flowing>", "filter by Queue section").option("--repo <owner/name>", "repository (default: the current checkout's remote)").option("--unclaimed", "only items with no live claim").option("--startable", "only items with no live claim, linked session, or linked pull request").option("--needs <amend>", "only items an open artifact fail is waiting on").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "app id the key is bound to (or OUTERLAYER_APP_ID / config)").action(async (opts) => {
4311
+ workCmd.command("list").description("List live work items for this factory").option("--stage <spec|build|review>", "filter by stage").option("--section <attention|waiting|flowing>", "filter by Queue section").option("--repo <owner/name>", "repository (default: the current checkout's remote)").option("--unclaimed", "only items with no live lease").option("--claimed", "only items with a live lease, held by a host or a local session").option("--startable", "only items with an unused build request and no live lease").option("--needs <amend>", "only items with a thread waiting on an agent").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "factory id the key is bound to (or OUTERLAYER_APP_ID / config)").action(async (opts) => {
4747
4312
  warnIfApiKeyFlag(opts);
4748
- const { runWorkList, WorkCommandError } = await import('./work-cmd-2P4BVX47.js');
4313
+ const { runWorkList, WorkCommandError } = await import('./work-cmd-UOQO2AD4.js');
4749
4314
  try {
4750
4315
  await runWorkList(opts);
4751
4316
  } catch (err) {
@@ -4756,12 +4321,12 @@ live addition remains on it.
4756
4321
  throw err;
4757
4322
  }
4758
4323
  });
4759
- workCmd.command("claim").description("Claim a work item under this host's lease before working on it").option("--item <number>", "the item's number inside this factory").option("--issue <number>", "issue number (default: named by --item instead)").option("--repo <owner/name>", "repository (default: the current checkout's remote)").requiredOption("--kind <implement|amend>", "what this host is doing with the item").option("--host <name>", "the claiming host (default: this machine's hostname)").option("--seconds <n>", "lease length in seconds, clamped to 900 (default: 900)").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "app id the key is bound to (or OUTERLAYER_APP_ID / config)").addHelpText(
4324
+ workCmd.command("claim").description("Claim a work item under this host's lease before working on it").option("--item <number>", "the item's number inside this factory").option("--issue <number>", "issue number (default: named by --item instead)").option("--repo <owner/name>", "repository (default: the current checkout's remote)").requiredOption("--kind <implement|amend>", "what this host is doing with the item").option("--host <name>", "the claiming host (default: this machine's hostname)").option("--seconds <n>", "lease length in seconds, clamped to 900 (default: 900)").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "factory id the key is bound to (or OUTERLAYER_APP_ID / config)").addHelpText(
4760
4325
  "after",
4761
4326
  "\n outerlayer work claim --item 42 --kind implement\n\nRecords a lease for this host. A live lease already held by a\ndifferent host is refused; claiming again under the same host extends\nit.\n"
4762
4327
  ).action(async (opts) => {
4763
4328
  warnIfApiKeyFlag(opts);
4764
- const { runWorkClaim, WorkCommandError } = await import('./work-claim-cmd-5AUNCLS2.js');
4329
+ const { runWorkClaim, WorkCommandError } = await import('./work-claim-cmd-LSO2B2EX.js');
4765
4330
  try {
4766
4331
  await runWorkClaim(opts);
4767
4332
  } catch (err) {
@@ -4772,12 +4337,12 @@ live addition remains on it.
4772
4337
  throw err;
4773
4338
  }
4774
4339
  });
4775
- workCmd.command("renew").description("Extend this host's lease on a work item").option("--item <number>", "the item's number inside this factory").option("--issue <number>", "issue number (default: named by --item instead)").option("--repo <owner/name>", "repository (default: the current checkout's remote)").option("--host <name>", "the claiming host (default: this machine's hostname)").option("--seconds <n>", "lease length in seconds, clamped to 900 (default: 900)").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "app id the key is bound to (or OUTERLAYER_APP_ID / config)").addHelpText(
4340
+ workCmd.command("renew").description("Extend this host's lease on a work item").option("--item <number>", "the item's number inside this factory").option("--issue <number>", "issue number (default: named by --item instead)").option("--repo <owner/name>", "repository (default: the current checkout's remote)").option("--host <name>", "the claiming host (default: this machine's hostname)").option("--seconds <n>", "lease length in seconds, clamped to 900 (default: 900)").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "factory id the key is bound to (or OUTERLAYER_APP_ID / config)").addHelpText(
4776
4341
  "after",
4777
4342
  "\n outerlayer work renew --item 42\n\nExtends this host's own live lease. Refused with a 410 if the lease\nhas expired, or a 403 if a different host holds it.\n"
4778
4343
  ).action(async (opts) => {
4779
4344
  warnIfApiKeyFlag(opts);
4780
- const { runWorkRenew, WorkCommandError } = await import('./work-claim-cmd-5AUNCLS2.js');
4345
+ const { runWorkRenew, WorkCommandError } = await import('./work-claim-cmd-LSO2B2EX.js');
4781
4346
  try {
4782
4347
  await runWorkRenew(opts);
4783
4348
  } catch (err) {
@@ -4791,12 +4356,12 @@ live addition remains on it.
4791
4356
  workCmd.command("release").description("Release this host's lease on a work item").option("--item <number>", "the item's number inside this factory").option("--issue <number>", "issue number (default: named by --item instead)").option("--repo <owner/name>", "repository (default: the current checkout's remote)").option("--host <name>", "the claiming host (default: this machine's hostname)").option(
4792
4357
  "--outcome <ok|failed|timed_out|idle|lease_lost|interrupted|stopped>",
4793
4358
  "how the attempt ended (default: none)"
4794
- ).option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "app id the key is bound to (or OUTERLAYER_APP_ID / config)").addHelpText(
4359
+ ).option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "factory id the key is bound to (or OUTERLAYER_APP_ID / config)").addHelpText(
4795
4360
  "after",
4796
4361
  "\n outerlayer work release --item 42 --outcome ok\n\nMarks this host's lease released, optionally with an outcome.\nReleasing an already-released lease is a no-op that returns the\nrecorded release time and outcome.\n"
4797
4362
  ).action(async (opts) => {
4798
4363
  warnIfApiKeyFlag(opts);
4799
- const { runWorkRelease, WorkCommandError } = await import('./work-claim-cmd-5AUNCLS2.js');
4364
+ const { runWorkRelease, WorkCommandError } = await import('./work-claim-cmd-LSO2B2EX.js');
4800
4365
  try {
4801
4366
  await runWorkRelease(opts);
4802
4367
  } catch (err) {
@@ -4807,12 +4372,12 @@ live addition remains on it.
4807
4372
  throw err;
4808
4373
  }
4809
4374
  });
4810
- workCmd.command("pr <number>").description("Declare that a pull request belongs to the work item this session is on").option("--repo <owner/name>", "repository (default: the current checkout's remote)").option("--session-id <id>", "session id to record as-is, skipping detection").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "app id the key is bound to (or OUTERLAYER_APP_ID / config)").addHelpText(
4375
+ workCmd.command("pr <number>").description("Declare that a pull request belongs to the work item this session is on").option("--repo <owner/name>", "repository (default: the current checkout's remote)").option("--session-id <id>", "session id to record as-is, skipping detection").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "factory id the key is bound to (or OUTERLAYER_APP_ID / config)").addHelpText(
4811
4376
  "after",
4812
4377
  "\n outerlayer work pr 481\n\nRun inside a session already on a work item. Records that pull\nrequest 481 in the checkout's repository belongs to that item \u2014 the\nsame fact a `pr-link` in the session's own transcript records on\nsync. Idempotent: declaring the same pull request twice is a no-op.\n"
4813
4378
  ).action(async (number, opts) => {
4814
4379
  warnIfApiKeyFlag(opts);
4815
- const { runWorkPr, WorkPrCommandError } = await import('./work-pr-cmd-AZQPWH4H.js');
4380
+ const { runWorkPr, WorkPrCommandError } = await import('./work-pr-cmd-6AN6RRQJ.js');
4816
4381
  try {
4817
4382
  await runWorkPr({ ...opts, number });
4818
4383
  } catch (err) {
@@ -4823,17 +4388,35 @@ live addition remains on it.
4823
4388
  throw err;
4824
4389
  }
4825
4390
  });
4391
+ workCmd.command("open-pr").description("Open, or edit, the pull request for the work item this session is on").requiredOption("--title <title>", "the pull request's title").requiredOption("--body-file <path>", "a file holding the pull request's description").option("--repo <owner/name>", "repository, on the gh path (default: the current checkout's remote)").option("--session-id <id>", "session id to record as-is, on the gh path, skipping detection").option("--json", "machine-readable JSON output").option("--url <url>", "cloud base URL (or OUTERLAYER_URL / config)").addOption(apiKeyOption()).option("--app-id <id>", "factory id the key is bound to (or OUTERLAYER_APP_ID / config)").addHelpText(
4392
+ "after",
4393
+ '\n outerlayer work open-pr --title "Add the thing" --body-file pr.md\n\nIn a host build, which holds an item key, the gateway opens the pull\nrequest as the factory\'s GitHub App, from the branch the claim named,\nor edits the one already open on that branch. No gh command runs.\nIn your own session it runs `gh pr create` (or `gh pr edit`) under your\nlogin, then declares the pull request on the item, as `work pr` does.\nRun it again after a change to the title or description.\n'
4394
+ ).action(async (opts) => {
4395
+ warnIfApiKeyFlag(opts);
4396
+ const { runWorkOpenPr, WorkOpenPrCommandError } = await import('./work-open-pr-cmd-4SSQZMYG.js');
4397
+ const { WorkCommandError } = await import('./work-cmd-UOQO2AD4.js');
4398
+ try {
4399
+ await runWorkOpenPr(opts);
4400
+ } catch (err) {
4401
+ if (err instanceof WorkOpenPrCommandError || err instanceof WorkCommandError) {
4402
+ fail(err.message);
4403
+ return;
4404
+ }
4405
+ throw err;
4406
+ }
4407
+ });
4826
4408
  }
4827
4409
  function registerRunnerCommands(program2) {
4828
4410
  const runnerCmd = program2.command("runner").description("Take work from the implement and amend queues on this machine");
4829
4411
  const configOption = "the cloud config file to run against (default: ~/.outerlayer/config.json)";
4830
4412
  runnerCmd.command("start").description("Run the loop: claim work, run it, report, clean up, repeat").option("--config <path>", configOption).action(async (opts) => {
4831
- const { defaultRunnerConfigPath, RunnerConfigError } = await import('./config-POF7DEQW.js');
4832
- const { start, RunnerStartError } = await import('./loop-ASZRX3CZ.js');
4833
- const { RunnerAlreadyRunningError } = await import('./pidfile-PTW76F56.js');
4413
+ const { defaultRunnerConfigPath, RunnerConfigError } = await import('./config-XVJYZ4IQ.js');
4414
+ const { start, RunnerStartError } = await import('./loop-VZOLL4WQ.js');
4415
+ const { RunnerAlreadyRunningError } = await import('./pidfile-UZRH774M.js');
4834
4416
  const configPath = opts.config ?? defaultRunnerConfigPath();
4835
4417
  try {
4836
- await start(configPath, { fetchImpl: fetch });
4418
+ const { realUpdateDeps } = await import('./real-deps-SU24ZA2K.js');
4419
+ await start(configPath, { update: realUpdateDeps(), fetchImpl: fetch });
4837
4420
  } catch (err) {
4838
4421
  if (err instanceof RunnerConfigError || err instanceof RunnerAlreadyRunningError || err instanceof RunnerStartError) {
4839
4422
  fail(err.message);
@@ -4843,89 +4426,214 @@ function registerRunnerCommands(program2) {
4843
4426
  }
4844
4427
  });
4845
4428
  runnerCmd.command("check").description("Validate the config, the hooks and the key, and print the effective settings").option("--config <path>", configOption).action(async (opts) => {
4846
- const { defaultRunnerConfigPath, loadRunnerConfig, checkHooks: checkHooks2, RunnerConfigError } = await import('./config-POF7DEQW.js');
4847
- const { probeCredentials } = await import('./loop-ASZRX3CZ.js');
4429
+ const { defaultRunnerConfigPath } = await import('./config-XVJYZ4IQ.js');
4430
+ const { runRunnerCheck } = await import('./check-WQP2CZDW.js');
4848
4431
  const configPath = opts.config ?? defaultRunnerConfigPath();
4849
- let config;
4850
- try {
4851
- config = loadRunnerConfig(configPath);
4852
- } catch (err) {
4853
- fail(err instanceof RunnerConfigError ? err.message : String(err));
4854
- return;
4855
- }
4856
- const hookProblem = checkHooks2(config);
4857
- if (hookProblem) {
4858
- fail(`${configPath}: ${hookProblem}`);
4859
- return;
4860
- }
4861
- const probe = await probeCredentials(config);
4862
- if (!probe.ok && probe.fatal) {
4863
- fail(probe.reason);
4432
+ const result = await runRunnerCheck(configPath);
4433
+ if (!result.ok) {
4434
+ fail(result.message);
4864
4435
  return;
4865
4436
  }
4866
4437
  process.stdout.write(
4867
- probe.ok ? `${C.green}\u2713${C.reset} credentials ok \xB7 ${config.url} \xB7 app ${config.appId}
4868
- ` : `${C.yellow}!${C.reset} credentials unverified \xB7 ${config.url} \xB7 app ${config.appId} \xB7 ${probe.reason}
4438
+ result.unverifiedReason === void 0 ? `${C.green}\u2713${C.reset} credentials ok \xB7 ${result.summary}
4439
+ ` : `${C.yellow}!${C.reset} credentials unverified \xB7 ${result.summary} \xB7 ${result.unverifiedReason}
4869
4440
  `
4870
4441
  );
4871
- const lines = [
4872
- `config ${configPath}`,
4873
- `host ${config.host}`,
4874
- `queues ${config.queues.join(", ")}`,
4875
- `concurrency ${config.concurrency}`,
4876
- `poll ${config.pollSeconds}s`,
4877
- `time limit ${config.timeLimitMinutes}m`,
4878
- `idle limit ${config.idleLimitMinutes}m`,
4879
- ...config.queues.map((queue) => `command ${queue.padEnd(11)} ${config.commands[queue] ?? "(none)"}`),
4880
- `hook provision ${config.hooks.provision}`,
4881
- `hook cleanup ${config.hooks.cleanup}`,
4882
- `hook report ${config.hooks.report ?? "(none)"}`,
4883
- `repos include ${config.repos?.include?.join(", ") ?? "(all)"}`,
4884
- `repos exclude ${config.repos?.exclude?.join(", ") ?? "(none)"}`
4885
- ];
4886
- process.stdout.write(`${lines.join("\n")}
4442
+ process.stdout.write(`${result.lines.join("\n")}
4887
4443
  `);
4888
4444
  });
4889
- runnerCmd.command("init").description("Write a runner block and example hooks into the config file").option("--config <path>", configOption).action(async (opts) => {
4890
- const { defaultRunnerConfigPath } = await import('./config-POF7DEQW.js');
4891
- const { runRunnerInit, RunnerInitError } = await import('./init-PTBITAUO.js');
4445
+ runnerCmd.command("install").description("Put this CLI into the runner's install layout, so the runner can update itself").option("--config <path>", configOption).action(async (opts) => {
4446
+ const { defaultRunnerConfigPath } = await import('./config-XVJYZ4IQ.js');
4447
+ const { runRunnerInstall, RunnerInstallError } = await import('./install-cmd-7WXOIFDO.js');
4892
4448
  const configPath = opts.config ?? defaultRunnerConfigPath();
4893
4449
  try {
4894
- const result = runRunnerInit(configPath);
4450
+ const result = runRunnerInstall({ configPath, entryPath: process.argv[1] ?? "", nodePath: process.execPath });
4451
+ process.stdout.write(`${C.green}\u2713${C.reset} installed ${result.version} for the runner; \`current\` names it
4452
+ `);
4453
+ process.stdout.write(
4454
+ `
4455
+ Start the runner from the install layout by putting this line in the service unit, then restart the service:
4456
+ ${C.yellow}${result.execStart}${C.reset}
4457
+ `
4458
+ );
4459
+ } catch (err) {
4460
+ if (err instanceof RunnerInstallError) {
4461
+ fail(err.message);
4462
+ return;
4463
+ }
4464
+ throw err;
4465
+ }
4466
+ });
4467
+ runnerCmd.command("init").description("Write a runner block that fits this machine; on macOS, offers a Linux VM").option("--config <path>", configOption).option("--vm", "on macOS, run the runner in a Linux VM (Lima), with container builds").option("--no-vm", "on macOS, skip the VM and run builds as unisolated processes").option("--allow-process-builds", "where builds cannot be isolated, run them as processes anyway").addOption(new Option("--config-stdin", "the VM-side hand-off: read the whole config as JSON on stdin").hideHelp()).addOption(new Option("--vm-cli-package <path>", "install this packed CLI tarball in the VM instead of the published version").hideHelp()).action(async (opts) => {
4468
+ const { defaultRunnerConfigPath, RunnerConfigError } = await import('./config-XVJYZ4IQ.js');
4469
+ const { runRunnerInit, RunnerInitError } = await import('./init-DXBCOAJK.js');
4470
+ const { detectMachine } = await import('./machine-WSG52J75.js');
4471
+ const { chooseVm, runVmInit, VmInitError } = await import('./init-YEXK35CF.js');
4472
+ const configPath = opts.config ?? defaultRunnerConfigPath();
4473
+ try {
4474
+ const machine = detectMachine();
4475
+ if (opts.configStdin === true) {
4476
+ const chunks = [];
4477
+ for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
4478
+ const result2 = runRunnerInit(configPath, { machine, configFromStdin: Buffer.concat(chunks).toString("utf8"), requireContainer: true });
4479
+ const { ensureHostKey: ensureHostKey2 } = await import('./host-key-KDYZ5ECC.js');
4480
+ const hostKey2 = await ensureHostKey2(configPath);
4481
+ process.stdout.write(`host key ${hostKey2.fingerprint}
4482
+ hooks: ${result2.hooks}
4483
+ `);
4484
+ return;
4485
+ }
4486
+ let declinedVm = opts.vm === false;
4487
+ const useVm = await chooseVm({
4488
+ platform: machine.platform,
4489
+ flag: opts.vm,
4490
+ isTTY: process.stdin.isTTY === true,
4491
+ ask: async () => {
4492
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
4493
+ const answer = await rl.question(
4494
+ "Run the runner in a Linux VM, so builds are isolated in containers? It needs Lima (brew install lima). Answer no to run builds as processes under your user, without isolation. [Y/n] "
4495
+ );
4496
+ rl.close();
4497
+ const yes = answer.trim() === "" || /^y(es)?$/i.test(answer.trim());
4498
+ declinedVm = !yes;
4499
+ return yes;
4500
+ }
4501
+ });
4502
+ if (useVm) {
4503
+ const { realLima } = await import('./lima-XN65D7GN.js');
4504
+ const { cpus, totalmem } = await import('os');
4505
+ const { resolvePackageVersion } = await import('./version-BWM6VLDI.js');
4506
+ const result2 = await runVmInit(configPath, {
4507
+ lima: realLima(),
4508
+ home: homeFromEnv(),
4509
+ hostCpus: cpus().length,
4510
+ hostMemoryBytes: totalmem(),
4511
+ version: resolvePackageVersion(),
4512
+ cliPackage: opts.vmCliPackage,
4513
+ progress: (line) => process.stdout.write(`${line}
4514
+ `)
4515
+ });
4516
+ for (const line of result2.lines) process.stdout.write(`${C.green}\u2713${C.reset} ${line}
4517
+ `);
4518
+ return;
4519
+ }
4520
+ const allowProcess = opts.allowProcessBuilds === true || machine.platform === "darwin" && declinedVm;
4521
+ const result = runRunnerInit(configPath, { machine, allowProcess });
4522
+ const { ensureHostKey } = await import('./host-key-KDYZ5ECC.js');
4523
+ const hostKey = await ensureHostKey(configPath);
4895
4524
  process.stdout.write(`${C.green}\u2713${C.reset} wrote a runner block into ${result.configPath}
4896
4525
  `);
4897
- for (const hook of result.hooks) {
4898
- process.stdout.write(`${C.green}\u2713${C.reset} wrote ${hook}
4526
+ process.stdout.write(
4527
+ `${C.green}\u2713${C.reset} host key ${hostKey.fingerprint}: the runner key binds to it on its first claim
4528
+ `
4529
+ );
4530
+ process.stdout.write(`${C.green}\u2713${C.reset} hooks: ${result.hooks}${result.hooks === "builtin:vm" ? " (builds run in microVMs)" : result.hooks === "builtin:container" ? " (builds run in containers)" : " (builds run as processes)"}
4531
+ `);
4532
+ if (result.note !== void 0) process.stdout.write(`
4533
+ ${C.yellow}${result.note}${C.reset}
4534
+ `);
4535
+ if (machine.platform === "darwin" && opts.vm === void 0) {
4536
+ process.stdout.write(`
4537
+ To isolate builds in containers instead, run ${C.yellow}outerlayer runner init --vm${C.reset}.
4538
+ `);
4539
+ }
4540
+ const { runRunnerInstall, RunnerInstallError } = await import('./install-cmd-7WXOIFDO.js');
4541
+ let execStart;
4542
+ try {
4543
+ const installed = runRunnerInstall({ configPath, entryPath: process.argv[1] ?? "", nodePath: process.execPath });
4544
+ execStart = installed.execStart;
4545
+ process.stdout.write(`${C.green}\u2713${C.reset} installed ${installed.version} for the runner, so it can update itself
4546
+ `);
4547
+ } catch (installErr) {
4548
+ if (!(installErr instanceof RunnerInstallError)) throw installErr;
4549
+ process.stdout.write(`
4550
+ ${C.yellow}The runner will not update itself: ${installErr.message}${C.reset}
4899
4551
  `);
4900
4552
  }
4901
4553
  process.stdout.write(
4902
4554
  `
4903
4555
  Next:
4904
4556
  1. Edit the command lines in ${result.configPath} to start your agent.
4905
- 2. Edit the hooks in ${result.hooksDir} to make and remove a workspace.
4906
- 3. ${C.yellow}outerlayer runner check${C.reset}
4907
- 4. ${C.yellow}outerlayer runner start${C.reset}
4908
- `
4557
+ 2. ${C.yellow}outerlayer runner check${C.reset}
4558
+ 3. ${C.yellow}outerlayer runner start${C.reset}
4559
+ ` + (execStart === void 0 ? "" : ` To run it as a service that updates itself, start it with this line in the unit:
4560
+ ${C.yellow}${execStart}${C.reset}
4561
+ `)
4909
4562
  );
4910
4563
  } catch (err) {
4911
- if (err instanceof RunnerInitError) {
4564
+ if (err instanceof RunnerInitError || err instanceof VmInitError || err instanceof RunnerConfigError) {
4912
4565
  fail(err.message);
4913
4566
  return;
4914
4567
  }
4915
4568
  throw err;
4916
4569
  }
4917
4570
  });
4571
+ runnerCmd.command("image").description("Build a repository's image from its default branch's devcontainer file").requiredOption("--repository <owner/name>", "the repository to build the image for").option("--remote <url>", "where to fetch from (default: the repository's GitHub URL)").option("--config <path>", configOption).option("--json", "machine-readable JSON output").addHelpText(
4572
+ "after",
4573
+ "\nFetches the head of the default branch into the runner's cache, reads its\n.devcontainer/devcontainer.json, and builds the image with the devcontainer CLI. Settings\nthat reach the host or widen the container's privileges are ignored and listed. The\nimage holds this CLI and a pinned Claude Code. Nothing runs inside the image.\n"
4574
+ ).action(async (opts) => {
4575
+ const { defaultRunnerConfigPath } = await import('./config-XVJYZ4IQ.js');
4576
+ const { buildRepositoryImage, RecipeError } = await import('./build-TZUUTRGH.js');
4577
+ const { currentCliBuild } = await import('./cli-build-K56DK4DS.js');
4578
+ const { createDockerHost } = await import('./docker-NEGDLU6D.js');
4579
+ try {
4580
+ const result = await buildRepositoryImage(
4581
+ {
4582
+ repository: opts.repository,
4583
+ remote: opts.remote,
4584
+ configPath: opts.config ?? defaultRunnerConfigPath(),
4585
+ cli: currentCliBuild()
4586
+ },
4587
+ { host: createDockerHost() }
4588
+ );
4589
+ if (opts.json) {
4590
+ writeJson(result);
4591
+ return;
4592
+ }
4593
+ const { report } = result;
4594
+ const what = result.reused ? "reused" : result.toolsBuilt && !result.recipeBuilt ? "rebuilt the tools layer of" : "built";
4595
+ process.stdout.write(`${C.green}\u2713${C.reset} ${what} ${report.tag}
4596
+ `);
4597
+ process.stdout.write(` recipe: ${report.recipe} at ${report.sourceCommit.slice(0, 12)} on ${report.defaultBranch}
4598
+ `);
4599
+ if (result.checkedCommit !== report.sourceCommit) {
4600
+ process.stdout.write(` checked: ${result.checkedCommit.slice(0, 12)}, the recipe has not changed since
4601
+ `);
4602
+ }
4603
+ process.stdout.write(` record: ${result.recordDir}
4604
+ `);
4605
+ if (report.ignored.length > 0) {
4606
+ process.stdout.write(` ignored:
4607
+ `);
4608
+ for (const { property, source } of report.ignored) process.stdout.write(` ${property} (from ${source})
4609
+ `);
4610
+ }
4611
+ if (result.removed.length > 0) process.stdout.write(` pruned ${result.removed.length} older image(s)
4612
+ `);
4613
+ } catch (err) {
4614
+ if (err instanceof RecipeError) {
4615
+ if (opts.json) {
4616
+ writeJson({ error: { code: err.code, message: err.message, ...err.logPath ? { logPath: err.logPath } : {} } });
4617
+ process.exitCode = 1;
4618
+ return;
4619
+ }
4620
+ fail(`${err.code}: ${err.message}`);
4621
+ return;
4622
+ }
4623
+ throw err;
4624
+ }
4625
+ });
4918
4626
  runnerCmd.command("stop").description("Take no more work; wait for running jobs to finish, then exit").option("--config <path>", "the cloud config file naming the runner to stop").option("--now", "end running jobs immediately, with outcome stopped").action(async (opts) => {
4919
- const { defaultRunnerConfigPath } = await import('./config-POF7DEQW.js');
4920
- const { requestStop } = await import('./loop-ASZRX3CZ.js');
4627
+ const { defaultRunnerConfigPath } = await import('./config-XVJYZ4IQ.js');
4628
+ const { requestStop } = await import('./loop-VZOLL4WQ.js');
4921
4629
  const configPath = opts.config ?? defaultRunnerConfigPath();
4922
4630
  await requestStop(configPath, Boolean(opts.now));
4923
4631
  });
4924
4632
  runnerCmd.command("status").description("List running jobs; with --recent, finished ones too").option("--config <path>", "the cloud config file naming the runner to inspect").option("--recent", "also list finished jobs, newest first, capped at 20").option("--json", "machine-readable JSON output").action(async (opts) => {
4925
- const { defaultRunnerConfigPath, loadRunnerConfig } = await import('./config-POF7DEQW.js');
4926
- const { jobsDir } = await import('./paths-D2VGWWFI.js');
4927
- const { listJobFiles, runnerProcessState } = await import('./loop-ASZRX3CZ.js');
4928
- const { renderStatus } = await import('./status-I27IST4L.js');
4633
+ const { defaultRunnerConfigPath, loadRunnerConfig } = await import('./config-XVJYZ4IQ.js');
4634
+ const { jobsDir } = await import('./paths-OYKMVYJP.js');
4635
+ const { listJobFiles, runnerProcessState } = await import('./loop-VZOLL4WQ.js');
4636
+ const { renderStatus } = await import('./status-2UZKKNB7.js');
4929
4637
  const path2 = await import('path');
4930
4638
  const configPath = opts.config ?? defaultRunnerConfigPath();
4931
4639
  const all = listJobFiles(configPath);
@@ -4944,7 +4652,7 @@ Next:
4944
4652
  configPath,
4945
4653
  process: processState,
4946
4654
  concurrency,
4947
- jobs: all.map(({ dirName, job }) => ({ ...job, jobDir: path2.join(jobsDir(configPath), dirName) }))
4655
+ jobs: all.map(({ dirName, job }) => ({ ...withoutItemKey(job), jobDir: path2.join(jobsDir(configPath), dirName) }))
4948
4656
  });
4949
4657
  return;
4950
4658
  }
@@ -4962,9 +4670,9 @@ Next:
4962
4670
  );
4963
4671
  });
4964
4672
  runnerCmd.command("logs").argument("<item>", "the work item's number in the factory").description("Print the newest attempt's log for one item").option("--config <path>", "the cloud config file naming the runner to inspect").option("-f, --follow", "keep printing as the log grows").action(async (itemArg, opts) => {
4965
- const { defaultRunnerConfigPath } = await import('./config-POF7DEQW.js');
4966
- const { listJobFiles } = await import('./loop-ASZRX3CZ.js');
4967
- const { newestAttempt, logPathFor, tail, RunnerLogsError } = await import('./logs-TRNPQM42.js');
4673
+ const { defaultRunnerConfigPath } = await import('./config-XVJYZ4IQ.js');
4674
+ const { listJobFiles } = await import('./loop-VZOLL4WQ.js');
4675
+ const { newestAttempt, logPathFor, tail, RunnerLogsError } = await import('./logs-7RYUH5A6.js');
4968
4676
  const configPath = opts.config ?? defaultRunnerConfigPath();
4969
4677
  const item = parseTrackerNumber(itemArg);
4970
4678
  if (item === void 0) {
@@ -5183,11 +4891,11 @@ ${C.dim}${file.path}${C.reset}
5183
4891
  }
5184
4892
  function registerMcpCommands(program2) {
5185
4893
  const mcpCmd = program2.command("mcp").description("Configure MCP clients against the OuterLayer gateway");
5186
- mcpCmd.command("install").description("Write or update the OuterLayer entry in .mcp.json").option("--transport <stdio|http>", "how the client connects (default: stdio)").option("--command <exe>", "stdio only: executable the client spawns (default: outerlayer)").option("--url <url>", "gateway MCP endpoint (defaults to the gateway you logged in to)").option("--name <name>", 'mcpServers key to write under (default: "outerlayer")').option("--dir <path>", "repo root to write into (default: cwd)").option("--app-id <uuid>", "self-host only: app id sent as X-Outerlayer-App-Id").option("--json", "machine-readable JSON output").addHelpText(
4894
+ mcpCmd.command("install").description("Write or update the OuterLayer entry in .mcp.json").option("--transport <stdio|http>", "how the client connects (default: stdio)").option("--command <exe>", "stdio only: executable the client spawns (default: outerlayer)").option("--url <url>", "gateway MCP endpoint (defaults to the gateway you logged in to)").option("--name <name>", 'mcpServers key to write under (default: "outerlayer")').option("--dir <path>", "repo root to write into (default: cwd)").option("--app-id <uuid>", "self-host only: factory id sent as X-Outerlayer-App-Id").option("--json", "machine-readable JSON output").addHelpText(
5187
4895
  "after",
5188
4896
  "\nThe API key is never a flag and never written to .mcp.json as a literal value.\n stdio (default): the client spawns `outerlayer mcp serve`, which reads the key\n from ~/.outerlayer/config.json (or OUTERLAYER_API_KEY) on every connect, so a\n rotated key is picked up without rewriting the entry.\n http: the entry carries the ${OUTERLAYER_API_KEY} placeholder, which the client\n resolves from the environment it was launched with.\n\n--command: use an absolute or ${HOME}-relative path when the CLI is not on PATH.\n--url: with stdio the endpoint resolves from OUTERLAYER_URL / the config file at\nconnect time; for self-host pass your gateway-node origin + /v1/mcp.\nSelf-host deployments also need --app-id: without a key service to resolve the\ncaller from the bearer token, the gateway answers 401 without that header.\n"
5189
4897
  ).action(async (opts) => {
5190
- const { runMcpInstall, handleMcpInstallError, McpInstallError } = await import('./mcp-install-cmd-FDQH6SEN.js');
4898
+ const { runMcpInstall, handleMcpInstallError, McpInstallError } = await import('./mcp-install-cmd-RLK4SO3N.js');
5191
4899
  try {
5192
4900
  if (opts.transport !== void 0 && opts.transport !== "stdio" && opts.transport !== "http") {
5193
4901
  throw new McpInstallError(`--transport must be "stdio" or "http", got "${opts.transport}"`);
@@ -5205,11 +4913,11 @@ function registerMcpCommands(program2) {
5205
4913
  handleMcpInstallError(err);
5206
4914
  }
5207
4915
  });
5208
- mcpCmd.command("serve").description("Run a stdio MCP server that bridges to the OuterLayer gateway").option("--url <url>", "gateway MCP endpoint (default: OUTERLAYER_URL or config + /v1/mcp)").option("--app-id <uuid>", "app id sent as X-Outerlayer-App-Id (default: config)").addHelpText(
4916
+ mcpCmd.command("serve").description("Run a stdio MCP server that bridges to the OuterLayer gateway").option("--url <url>", "gateway MCP endpoint (default: OUTERLAYER_URL or config + /v1/mcp)").option("--app-id <uuid>", "factory id sent as X-Outerlayer-App-Id (default: config)").addHelpText(
5209
4917
  "after",
5210
4918
  "\nWhat the stdio .mcp.json entry spawns. Reads newline-delimited JSON-RPC 2.0 on\nstdin and writes responses on stdout. The API key comes from OUTERLAYER_API_KEY\nor ~/.outerlayer/config.json at start-up; with neither the command exits 1 and\nprints why, which the MCP client shows as the connect error.\n"
5211
4919
  ).action(async (opts) => {
5212
- const { runMcpServe, handleMcpServeError } = await import('./mcp-serve-cmd-57EZZOTL.js');
4920
+ const { runMcpServe, handleMcpServeError } = await import('./mcp-serve-cmd-JPTP5FMS.js');
5213
4921
  try {
5214
4922
  await runMcpServe({ url: opts.url, appId: opts.appId });
5215
4923
  } catch (err) {
@@ -5220,7 +4928,7 @@ function registerMcpCommands(program2) {
5220
4928
  function registerImportCommands(program2) {
5221
4929
  const importCmd = program2.command("import").description("Import an existing config format into .outerlayer/");
5222
4930
  importCmd.command("ruler").description("Port a .ruler/ tree into the equivalent .outerlayer/ tree").option("--dir <path>", "repo root to read from (default: cwd)").option("--json", "machine-readable JSON output").addHelpText("after", "\nRuler: https://github.com/intellectronica/ruler. Mostly a rename.\n").action(async (opts) => {
5223
- const { runImportRuler, ImportRulerError } = await import('./import-ruler-cmd-7LH2QOMG.js');
4931
+ const { runImportRuler, ImportRulerError } = await import('./import-ruler-cmd-GUHL6IY5.js');
5224
4932
  try {
5225
4933
  const result = runImportRuler({ cwd: opts.dir, json: opts.json });
5226
4934
  if (result.exitCode !== 0) process.exitCode = result.exitCode;
@@ -5232,11 +4940,11 @@ function registerImportCommands(program2) {
5232
4940
  throw err;
5233
4941
  }
5234
4942
  });
5235
- importCmd.command("capture").description("Install the evidence capture pack into .outerlayer/").option("--dir <path>", "repo root to write into (default: cwd)").option("--json", "machine-readable JSON output").addHelpText(
4943
+ importCmd.command("capture").description("Install the maintained skills into .outerlayer/").option("--dir <path>", "repo root to write into (default: cwd)").option("--json", "machine-readable JSON output").addHelpText(
5236
4944
  "after",
5237
- "\nThe emitting-evidence skill plus an AGENTS.md snippet teaching agents to run\n`emit artifact`.\n"
4945
+ "\nInstalls the three maintained skills (emitting-evidence, amend,\nreporting-findings) and an AGENTS.md snippet teaching agents to run\n`emit artifact`. A skill that differs from this CLI's bundle is rewritten to it, and the output says so.\n"
5238
4946
  ).action(async (opts) => {
5239
- const { runImportCapture, ImportCaptureError } = await import('./import-capture-cmd-EUMBGIV3.js');
4947
+ const { runImportCapture, ImportCaptureError } = await import('./import-capture-cmd-AVZ4VIVJ.js');
5240
4948
  try {
5241
4949
  runImportCapture({ cwd: opts.dir, json: opts.json });
5242
4950
  } catch (err) {
@@ -5274,15 +4982,21 @@ function registerContextCommands(program2) {
5274
4982
  const contextCmd = program2.command("context").description("Compile and materialize agent context");
5275
4983
  contextCmd.command("emit").description("Compile .outerlayer/ into each configured target tool's native files").option("--check", "compare against disk, write nothing, exit 1 on drift (CI mode)").option("--dir <path>", "repo root to read from (default: cwd)").option("--json", "machine-readable JSON output").addHelpText(
5276
4984
  "after",
5277
- '\nTargets come from .outerlayer/config.json only; there is no --target flag.\nMissing or empty config: {"targets": [...]}, e.g. {"targets": ["claude-code"]}.\nKnown targets: claude-code, cursor, codex, copilot, factory.\n\nOrphan detection (--check) only covers header-carrying outputs: every markdown\ntarget file gets a generated-by comment, but .mcp.json and .cursor/mcp.json are\nJSON with no comment syntax, so they can never be flagged as orphaned; missing\nand content drift still apply to them.\n'
4985
+ '\nTargets come from .outerlayer/config.json only; there is no --target flag.\nMissing or empty config: {"targets": [...]}, e.g. {"targets": ["claude-code"]}.\nKnown targets: claude-code, cursor, codex, copilot, factory.\n\nThe maintained skills (emitting-evidence, amend, reporting-findings)\nfollow this CLI: emit rewrites one that differs from its bundle, and --check reports it as\ndrift. Templates (spec, writing-specs, build) are never rewritten or reported.\n\nOrphan detection (--check) only covers header-carrying outputs: every markdown\ntarget file gets a generated-by comment, but .mcp.json and .cursor/mcp.json are\nJSON with no comment syntax, so they can never be flagged as orphaned; missing\nand content drift still apply to them.\n'
5278
4986
  ).action(async (opts) => {
5279
4987
  await compileContext({ dir: opts.dir, check: opts.check, json: opts.json });
5280
4988
  });
4989
+ contextCmd.command("adopt <stagedDir>", { hidden: true }).description("Copy context the runner staged into this checkout (container builds)").option("--json", "machine-readable JSON output").action(async (stagedDir) => {
4990
+ const { runContextAdopt } = await import('./context-adopt-UMA4O5NY.js');
4991
+ const { exitCode, stdout } = runContextAdopt(stagedDir, process.cwd());
4992
+ process.stdout.write(stdout);
4993
+ process.exitCode = exitCode;
4994
+ });
5281
4995
  contextCmd.command("materialize").description("Materialize this repository's governed context from its control plane").option("--json", "machine-readable JSON output").addHelpText(
5282
4996
  "after",
5283
4997
  "\nThe same flow SessionStart runs automatically, exposed for a manual retry or a\nfirst check outside a session. Writes AGENTS.md, .claude/**, and .outerlayer/**.\n"
5284
4998
  ).action(async (opts) => {
5285
- const { materializeContext } = await import('./context-materialize-6WKT3RBQ.js');
4999
+ const { materializeContext } = await import('./context-materialize-G2FUFC72.js');
5286
5000
  const result = await materializeContext({ cwd: process.cwd(), sessionId: `manual-${Date.now()}` });
5287
5001
  const output = formatContextMaterializeOutput(result);
5288
5002
  if (output.exitCode !== void 0) process.exitCode = output.exitCode;