@outerlayer/cli 0.1.1 → 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 (143) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/README.md +238 -77
  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-3YPKXE5L.js +1683 -0
  10. package/dist/{chunk-HNCCD2IX.js → chunk-5DLBAXY5.js} +209 -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-RCQXYLMO.js → chunk-DVDEBNQJ.js} +18 -18
  29. package/dist/{work-pr-cmd-GOYSPEN2.js → chunk-DVYHCMB2.js} +10 -25
  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-BQB3U2VD.js → chunk-G7FSV7JX.js} +5113 -3729
  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-3VQ4XXQA.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-WZBHBH3J.js +20 -0
  58. package/dist/{chunk-IWIUYLDR.js → chunk-XCCLFVXM.js} +118 -189
  59. package/dist/{context-materialize-J6CRQ7O7.js → chunk-XDDW4FRS.js} +134 -26
  60. package/dist/chunk-YYYXRJUW.js +101 -0
  61. package/dist/chunk-ZM2IMMYN.js +76 -0
  62. package/dist/chunk-ZMYPLFG3.js +71 -0
  63. package/dist/chunk-ZNA27WEV.js +470 -0
  64. package/dist/{cli-EFJIG2VT.js → cli-NHUXABYH.js} +597 -965
  65. package/dist/{paths-D2VGWWFI.js → cli-build-K56DK4DS.js} +1 -1
  66. package/dist/config-XVJYZ4IQ.js +9 -0
  67. package/dist/connect-cmd-OOG4TPU7.js +16 -0
  68. package/dist/context-adopt-UMA4O5NY.js +105 -0
  69. package/dist/context-materialize-G2FUFC72.js +19 -0
  70. package/dist/dist-RW4UPPC2.js +6 -0
  71. package/dist/docker-NEGDLU6D.js +7 -0
  72. package/dist/doctor-53F2PYY7.js +43 -0
  73. package/dist/doctor-UFVL4PZY.js +426 -0
  74. package/dist/{emit-artifact-cmd-KPPD3YLO.js → emit-artifact-cmd-KTYW2TN2.js} +25 -19
  75. package/dist/emit-cmd-DCERAU27.js +9 -0
  76. package/dist/emit-criteria-cmd-VAUUSAYD.js +166 -0
  77. package/dist/{emit-finding-cmd-VR5VEGXJ.js → emit-finding-cmd-UR4ID45Z.js} +35 -30
  78. package/dist/{emit-result-cmd-YOA5BJKL.js → emit-result-cmd-FDTUKGKE.js} +35 -61
  79. package/dist/exec-client-4XGXV3XN.js +8 -0
  80. package/dist/guest-init-GENRHNP7.js +8 -0
  81. package/dist/hook-fast-EYENPK6F.js +9 -0
  82. package/dist/{hook-wrap-fast-KXYNX3AD.js → hook-wrap-fast-NPLHJXFK.js} +2 -2
  83. package/dist/host-key-KDYZ5ECC.js +7 -0
  84. package/dist/import-capture-cmd-AVZ4VIVJ.js +68 -0
  85. package/dist/{import-ruler-cmd-7LH2QOMG.js → import-ruler-cmd-GUHL6IY5.js} +1 -1
  86. package/dist/index.js +4 -4
  87. package/dist/init-DXBCOAJK.js +36 -0
  88. package/dist/init-YEXK35CF.js +221 -0
  89. package/dist/init-cmd-NHR7PRSQ.js +135 -0
  90. package/dist/install-cmd-7WXOIFDO.js +55 -0
  91. package/dist/lima-XN65D7GN.js +55 -0
  92. package/dist/login-browser-7XGSV7MA.js +10 -0
  93. package/dist/{config-POF7DEQW.js → logout-cmd-QAUFDJ6J.js} +3 -1
  94. package/dist/{logs-TRNPQM42.js → logs-7RYUH5A6.js} +1 -1
  95. package/dist/loop-VZOLL4WQ.js +43 -0
  96. package/dist/machine-WSG52J75.js +35 -0
  97. package/dist/mcp-install-cmd-RLK4SO3N.js +9 -0
  98. package/dist/{mcp-serve-cmd-57EZZOTL.js → mcp-serve-cmd-JPTP5FMS.js} +20 -9
  99. package/dist/paths-OYKMVYJP.js +6 -0
  100. package/dist/{pidfile-PTW76F56.js → pidfile-UZRH774M.js} +2 -3
  101. package/dist/real-deps-SU24ZA2K.js +21 -0
  102. package/dist/relay-L76HDX72.js +46 -0
  103. package/dist/settings-J2652U5N.js +7 -0
  104. package/dist/starter-pack-LIZYMKYQ.js +10 -0
  105. package/dist/{status-JHVSZYC5.js → status-2UZKKNB7.js} +31 -12
  106. package/dist/{statusline-fast-3C5OXHDD.js → statusline-fast-SVTMBMD7.js} +3 -2
  107. package/dist/sync-cmd-VPPYCNNM.js +28 -0
  108. package/dist/version-BWM6VLDI.js +6 -0
  109. package/dist/{watch-NRLXJUST.js → watch-5C4ZBBGO.js} +20 -7
  110. package/dist/{work-claim-cmd-HXB7K27H.js → work-claim-cmd-LSO2B2EX.js} +34 -19
  111. package/dist/work-cmd-UOQO2AD4.js +17 -0
  112. package/dist/work-comment-cmd-WF3EOLAE.js +106 -0
  113. package/dist/{work-launch-YNNCKIH3.js → work-launch-YI4CJDAP.js} +2 -1
  114. package/dist/work-open-pr-cmd-4SSQZMYG.js +156 -0
  115. package/dist/work-pr-cmd-6AN6RRQJ.js +16 -0
  116. package/package.json +13 -3
  117. package/skill-pack/maintained/amend/SKILL.md +104 -0
  118. package/skill-pack/maintained/emitting-evidence/SKILL.md +99 -0
  119. package/skill-pack/maintained/emitting-evidence/references/agents-snippet.md +20 -0
  120. package/skill-pack/maintained/outerlayer/SKILL.md +55 -0
  121. package/skill-pack/maintained/reporting-findings/SKILL.md +148 -0
  122. package/skill-pack/template/build/SKILL.md +193 -0
  123. package/skill-pack/template/build/references/agent-briefs.md +243 -0
  124. package/skill-pack/template/build/references/criteria-judge.md +91 -0
  125. package/skill-pack/template/build/references/evidence.md +42 -0
  126. package/skill-pack/template/build/references/release.md +74 -0
  127. package/skill-pack/template/build/references/review-briefs.md +275 -0
  128. package/skill-pack/template/build/references/review-loop.md +158 -0
  129. package/skill-pack/template/build/scripts/record-criteria.mjs +235 -0
  130. package/skill-pack/template/spec/SKILL.md +84 -0
  131. package/skill-pack/template/writing-specs/SKILL.md +134 -0
  132. package/dist/chunk-JJP7YLMN.js +0 -25
  133. package/dist/chunk-KKEU6FFR.js +0 -589
  134. package/dist/chunk-OZ7C3XUE.js +0 -34
  135. package/dist/chunk-WQ6VGRGZ.js +0 -150
  136. package/dist/emit-commit-credit-cmd-656BRAQ4.js +0 -121
  137. package/dist/hook-fast-I2XTHDE6.js +0 -8
  138. package/dist/import-capture-cmd-EUMBGIV3.js +0 -176
  139. package/dist/init-PTBITAUO.js +0 -103
  140. package/dist/login-cmd-IRX6LZT7.js +0 -62
  141. package/dist/loop-BWE2BS6J.js +0 -646
  142. package/dist/sync-cmd-S7Q6DWU3.js +0 -15
  143. package/dist/work-cmd-ABP6XDBP.js +0 -16
@@ -1,23 +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, readSessionRepos } from './chunk-BQB3U2VD.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
- import { parseTrackerNumber, trackerNumberError } from './chunk-RCQXYLMO.js';
9
- import './chunk-ZJSNLVCE.js';
10
- import { hookFiredMarkerPath, installedCliDistDir, sanitizeForContext, spoolDir, assessSyncHealth, readSyncStatus, SYNC_STALE_AFTER_MS, formatAgo, checkInstalledCliStaleness, workspaceRootOf, refreshCommand } from './chunk-3VQ4XXQA.js';
11
- import { managedHookScripts, installPostRewriteHook, installPreCommitGuardHook, removePostRewriteHook, removePreCommitGuardHook, deadHooksState, preparePackageManagerCommand, contextExcludesPresent, postRewriteHookPresence } from './chunk-IWIUYLDR.js';
12
- import './chunk-YOOSBOKS.js';
13
- 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';
14
- import { readCloudConfig } from './chunk-TFUIDMOB.js';
14
+ import { parseTrackerNumber, trackerNumberError } from './chunk-WZBHBH3J.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';
15
27
  import { homeFromEnv } from './chunk-VNVZDWO3.js';
16
28
  import './chunk-Z5GEFMRW.js';
17
29
  import { EventEmitter } from 'events';
18
30
  import childProcess, { execFileSync } from 'child_process';
19
- import path, { dirname, join, isAbsolute } from 'path';
20
- 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';
21
33
  import process2 from 'process';
22
34
  import { stripVTControlCharacters } from 'util';
23
35
  import { fileURLToPath } from 'url';
@@ -3396,6 +3408,19 @@ function samePath(a, b) {
3396
3408
  return false;
3397
3409
  }
3398
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
+ }
3399
3424
  function installCliCopy(opts) {
3400
3425
  const rename = opts.rename ?? renameSync;
3401
3426
  const now = opts.now ?? Date.now;
@@ -3410,12 +3435,28 @@ function installCliCopy(opts) {
3410
3435
  cpSync(sourceDist, join(staged, "dist"), { recursive: true });
3411
3436
  const sourcePkg = join(dirname(sourceDist), "package.json");
3412
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 });
3413
3440
  const incoming = readdirSync(join(staged, "dist"));
3414
3441
  for (const name of incoming) {
3415
- 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
+ }
3416
3445
  }
3417
- if (existsSync(join(staged, "package.json"))) rename(join(staged, "package.json"), join(targetRoot, "package.json"));
3418
- rename(join(staged, "dist", "index.js"), bin);
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
+ }
3458
+ }
3459
+ if (!sameContent(join(staged, "dist", "index.js"), bin)) rename(join(staged, "dist", "index.js"), bin);
3419
3460
  const shipped = new Set(incoming);
3420
3461
  for (const name of readdirSync(targetDist)) {
3421
3462
  const path2 = join(targetDist, name);
@@ -3426,96 +3467,6 @@ function installCliCopy(opts) {
3426
3467
  }
3427
3468
  return { bin, copied: true };
3428
3469
  }
3429
- function manifestPath(home = homeFromEnv()) {
3430
- return join(home, ".outerlayer", "hooks-manifest.json");
3431
- }
3432
- function readManifestFile(home) {
3433
- try {
3434
- const parsed = JSON.parse(readFileSync(manifestPath(home), "utf8"));
3435
- if (parsed?.version !== 1) {
3436
- return { manifest: null, newerVersion: typeof parsed?.version === "number" && parsed.version > 1 };
3437
- }
3438
- return {
3439
- manifest: {
3440
- version: 1,
3441
- installs: parsed.installs ?? {},
3442
- wrapped: parsed.wrapped ?? {},
3443
- statusline: parsed.statusline ?? {}
3444
- },
3445
- newerVersion: false
3446
- };
3447
- } catch {
3448
- return { manifest: null, newerVersion: false };
3449
- }
3450
- }
3451
- function readManifest(home = homeFromEnv()) {
3452
- return readManifestFile(home).manifest;
3453
- }
3454
- function writeManifest(home, manifest) {
3455
- const path2 = manifestPath(home);
3456
- try {
3457
- mkdirSync(dirname(path2), { recursive: true });
3458
- const tmp = `${path2}.tmp`;
3459
- writeFileSync(tmp, JSON.stringify(manifest, null, 2) + "\n", { mode: 384 });
3460
- renameSync(tmp, path2);
3461
- } catch {
3462
- }
3463
- }
3464
- function emptyManifest() {
3465
- return { version: 1, installs: {}, wrapped: {}, statusline: {} };
3466
- }
3467
- function updateManifest(home, opts, mutate) {
3468
- const { manifest, newerVersion } = readManifestFile(home);
3469
- if (newerVersion) return;
3470
- if (!manifest && !opts.createIfMissing) return;
3471
- const next = manifest ?? emptyManifest();
3472
- mutate(next);
3473
- writeManifest(home, next);
3474
- }
3475
- function recordInstall(home, settingsPath2, record, now = Date.now) {
3476
- updateManifest(home, { createIfMissing: true }, (manifest) => {
3477
- manifest.installs[settingsPath2] = {
3478
- cliBin: record.cliBin,
3479
- events: [...record.events],
3480
- updatedAt: new Date(now()).toISOString()
3481
- };
3482
- });
3483
- }
3484
- function recordRemoval(home, settingsPath2) {
3485
- updateManifest(home, { createIfMissing: false }, (manifest) => {
3486
- delete manifest.installs[settingsPath2];
3487
- delete manifest.wrapped[settingsPath2];
3488
- delete manifest.statusline[settingsPath2];
3489
- });
3490
- }
3491
- function recordWrapped(home, settingsPath2, wrapped, now = Date.now) {
3492
- if (wrapped.length === 0) return;
3493
- updateManifest(home, { createIfMissing: true }, (manifest) => {
3494
- const existing = manifest.wrapped[settingsPath2] ?? [];
3495
- const at = new Date(now()).toISOString();
3496
- for (const w of wrapped) {
3497
- if (existing.some((e) => e.event === w.event && e.original === w.original)) continue;
3498
- existing.push({ event: w.event, original: w.original, updatedAt: at });
3499
- }
3500
- manifest.wrapped[settingsPath2] = existing;
3501
- });
3502
- }
3503
- function recordUnwrapped(home, settingsPath2) {
3504
- updateManifest(home, { createIfMissing: false }, (manifest) => {
3505
- delete manifest.wrapped[settingsPath2];
3506
- });
3507
- }
3508
- function recordStatusline(home, settingsPath2, outcome, original, now = Date.now) {
3509
- updateManifest(home, { createIfMissing: true }, (manifest) => {
3510
- manifest.statusline[settingsPath2] = {
3511
- outcome,
3512
- ...original !== void 0 ? { original } : {},
3513
- updatedAt: new Date(now()).toISOString()
3514
- };
3515
- });
3516
- }
3517
-
3518
- // src/init.ts
3519
3470
  function runInit(opts) {
3520
3471
  const scope = opts.scope ?? "user";
3521
3472
  const path2 = settingsPath(scope, { home: opts.home, cwd: opts.cwd });
@@ -3527,7 +3478,7 @@ function runInit(opts) {
3527
3478
  const statuslineRemoved = removeStatusline(hooksUnwrapped.next, opts.cliBin);
3528
3479
  const changed2 = hooksRemoved.changed || hooksUnwrapped.changed || statuslineRemoved.changed;
3529
3480
  const backupPath2 = changed2 ? writeSettings(path2, statuslineRemoved.next) : void 0;
3530
- const gitHookRemoval = removePostRewriteHook(opts.cwd ?? process.cwd());
3481
+ const gitHookRemoval2 = removeJournalHooks(opts.cwd ?? process.cwd());
3531
3482
  const guardHookRemoval = removePreCommitGuardHook(opts.cwd ?? process.cwd());
3532
3483
  recordRemoval(manifestHome, path2);
3533
3484
  const remaining = readManifest(manifestHome);
@@ -3545,7 +3496,7 @@ function runInit(opts) {
3545
3496
  backupPath: backupPath2,
3546
3497
  events: REGISTERED_EVENTS,
3547
3498
  removed: true,
3548
- gitHookRemoved: gitHookRemoval.removed,
3499
+ gitHookRemoved: gitHookRemoval2.removed,
3549
3500
  guardHookRemoved: guardHookRemoval.removed,
3550
3501
  wrapped: [],
3551
3502
  unwrappedCount: hooksUnwrapped.unwrapped,
@@ -3585,9 +3536,7 @@ function runInit(opts) {
3585
3536
  if (statusline && statusline !== "skipped") {
3586
3537
  recordStatusline(manifestHome, path2, statuslineWrappedCommand !== void 0 ? "wrapped" : "installed", statuslineWrappedCommand);
3587
3538
  }
3588
- const gitHook = installPostRewriteHook(opts.cwd ?? process.cwd(), {
3589
- stampTrailers: readCloudConfig().gitHooks?.stampTrailers !== false
3590
- });
3539
+ const gitHookRemoval = removeJournalHooks(opts.cwd ?? process.cwd());
3591
3540
  const guardHook = installPreCommitGuardHook(opts.cwd ?? process.cwd());
3592
3541
  let gitignoreUpdated = false;
3593
3542
  if (scope === "project" && opts.addGitignore) {
@@ -3599,7 +3548,7 @@ function runInit(opts) {
3599
3548
  backupPath,
3600
3549
  events: REGISTERED_EVENTS,
3601
3550
  gitignoreUpdated,
3602
- gitHook,
3551
+ ...gitHookRemoval.removed ? { gitHookRemoved: true } : {},
3603
3552
  guardHook,
3604
3553
  wrapped,
3605
3554
  ...statusline ? { statusline } : {},
@@ -3637,7 +3586,7 @@ function ensureGitignore(cwd) {
3637
3586
  function orgRolloutSnippet(cliBin) {
3638
3587
  const hooks = {};
3639
3588
  for (const event of REGISTERED_EVENTS) {
3640
- hooks[event] = [{ hooks: [{ type: "command", command: `${cliBin} hook ${event}` }] }];
3589
+ hooks[event] = [{ hooks: [{ type: "command", command: `${cliCommand(cliBin)} hook ${event}` }] }];
3641
3590
  }
3642
3591
  const managed = JSON.stringify({ hooks }, null, 2);
3643
3592
  return [
@@ -3657,617 +3606,6 @@ function orgRolloutSnippet(cliBin) {
3657
3606
  "Each developer still runs `outerlayer login` once to attribute sessions."
3658
3607
  ].join("\n");
3659
3608
  }
3660
- function runDoctor(env = {}) {
3661
- const home = env.home ?? homeFromEnv();
3662
- const now = env.now ?? Date.now;
3663
- const transcripts = scanTranscripts(home);
3664
- return [
3665
- checkClaudeDir(home),
3666
- checkTranscripts(transcripts, now),
3667
- ...checkHooks(home, env.cwd, transcripts, now),
3668
- ...checkDeadHooksState(env.cwd),
3669
- checkSpoolWritable(home),
3670
- checkLastSync(home, now),
3671
- checkDaemon(home),
3672
- checkCleanupPeriod(home),
3673
- checkDiskHeadroom(home),
3674
- checkClaudeVersion(env),
3675
- checkSettingsValid(home, env.cwd),
3676
- ...checkStatusline(home, now, env.cwd),
3677
- ...checkInstalls(env),
3678
- ...checkContextSource(home, env.cwd),
3679
- ...checkInstalledCli(home, env.cwd ?? process.cwd())
3680
- ];
3681
- }
3682
- function checkInstalledCli(home, cwd) {
3683
- const name = "Installed CLI";
3684
- const result = checkInstalledCliStaleness({ home, cwd });
3685
- if (result === null) return [];
3686
- const installedLabel = result.installedId !== null ? sanitizeForContext(result.installedId) : "unknown";
3687
- const workspaceLabel = result.workspaceId !== null ? sanitizeForContext(result.workspaceId) : null;
3688
- const at = result.workspaceDistDir !== null ? ` at ${sanitizeForContext(workspaceRootOf(result.workspaceDistDir))}` : "";
3689
- if (result.stale) {
3690
- return [
3691
- {
3692
- name,
3693
- status: "warn",
3694
- detail: `installed copy (${installedLabel}) is behind the workspace build${at} (${workspaceLabel})`,
3695
- fix: installedCliFix(result.workspaceDistDir, result.workspaceRootTrusted)
3696
- }
3697
- ];
3698
- }
3699
- if (workspaceLabel === null) {
3700
- 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)`;
3701
- return [{ name, status: "pass", detail }];
3702
- }
3703
- if (result.unorderable) {
3704
- return [
3705
- {
3706
- name,
3707
- status: "pass",
3708
- detail: `installed (${installedLabel}) differs from the workspace build${at} (${workspaceLabel}) \u2014 build order could not be established`
3709
- }
3710
- ];
3711
- }
3712
- if (result.installedAhead) {
3713
- return [{ name, status: "pass", detail: `installed (${installedLabel}) is ahead of the workspace build${at} (${workspaceLabel})` }];
3714
- }
3715
- if (result.installedId !== result.workspaceId) {
3716
- return [{ name, status: "pass", detail: `installed (${installedLabel}) differs from the workspace build${at} (${workspaceLabel})` }];
3717
- }
3718
- return [{ name, status: "pass", detail: `matches the workspace build${at} (${installedLabel})` }];
3719
- }
3720
- function installedCliFix(workspaceDistDir, trusted) {
3721
- const command = refreshCommand(workspaceDistDir, trusted);
3722
- if (command) return `Refresh it: ${command}`;
3723
- const root = sanitizeForContext(workspaceRootOf(workspaceDistDir));
3724
- 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.`;
3725
- }
3726
- function checkStatusline(home, now, cwd) {
3727
- const name = "Status line";
3728
- let installed = false;
3729
- let wrapping;
3730
- let foreign;
3731
- for (const scope of ["user", "project"]) {
3732
- let settings;
3733
- try {
3734
- settings = readSettings(settingsPath(scope, { home, cwd }));
3735
- } catch {
3736
- continue;
3737
- }
3738
- const slot = settings?.statusLine;
3739
- if (!slot || typeof slot !== "object") continue;
3740
- if (isOurStatuslineSlot(slot)) {
3741
- installed = true;
3742
- const original = statuslineWrappedOriginal(slot);
3743
- if (original !== null) wrapping = original;
3744
- break;
3745
- }
3746
- if (typeof slot.command === "string") foreign = slot.command;
3747
- }
3748
- if (!installed) {
3749
- const manifest = readManifest(home);
3750
- const record = ["user", "project"].map((scope) => manifest?.statusline[settingsPath(scope, { home, cwd })]).find((r) => r !== void 0);
3751
- if (record) {
3752
- return [{
3753
- name,
3754
- status: "warn",
3755
- 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}` : ""}`,
3756
- fix: "Run `outerlayer init` to reinstall the segment."
3757
- }];
3758
- }
3759
- return [
3760
- 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`." }
3761
- ];
3762
- }
3763
- const checks = [
3764
- { name, status: "pass", detail: wrapping ? `installed, wrapping: ${wrapping}` : "installed" }
3765
- ];
3766
- const stateName = "Status-line state";
3767
- const state = readStatuslineState(home);
3768
- if (!state) {
3769
- 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)." });
3770
- return checks;
3771
- }
3772
- const age = now() - Date.parse(state.generatedAt);
3773
- if (!Number.isFinite(age) || age > STATE_FRESH_MS) {
3774
- checks.push({ name: stateName, status: "warn", detail: `state ${formatAgo(age)} \u2014 line runs degraded`, fix: "Restart `outerlayer daemon`." });
3775
- return checks;
3776
- }
3777
- checks.push({ name: stateName, status: "pass", detail: `fresh (${formatAgo(age)})` });
3778
- return checks;
3779
- }
3780
- function checkClaudeDir(home) {
3781
- const dir = join(home, ".claude");
3782
- 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." };
3783
- }
3784
- function scanTranscripts(home) {
3785
- const root = join(home, ".claude", "projects");
3786
- let count = 0;
3787
- let newest = 0;
3788
- if (!existsSync(root)) return { exists: false, count, newest };
3789
- const walk = (dir) => {
3790
- let entries;
3791
- try {
3792
- entries = readdirSync(dir, { withFileTypes: true });
3793
- } catch {
3794
- return;
3795
- }
3796
- for (const entry of entries) {
3797
- const p = join(dir, entry.name);
3798
- if (!entry.isDirectory() && !entry.isSymbolicLink() && !entry.name.endsWith(".jsonl")) continue;
3799
- let st;
3800
- try {
3801
- st = statSync(p);
3802
- } catch {
3803
- continue;
3804
- }
3805
- if (st.isDirectory()) walk(p);
3806
- else if (entry.name.endsWith(".jsonl") && st.size > 0) {
3807
- count += 1;
3808
- if (st.mtimeMs > newest) newest = st.mtimeMs;
3809
- }
3810
- }
3811
- };
3812
- walk(root);
3813
- return { exists: true, count, newest };
3814
- }
3815
- function checkTranscripts(transcripts, now) {
3816
- if (!transcripts.exists) {
3817
- return { name: "Transcripts", status: "warn", detail: "no projects directory yet", fix: "Run a Claude Code session, then re-check." };
3818
- }
3819
- if (transcripts.count === 0) {
3820
- return { name: "Transcripts", status: "warn", detail: "0 transcripts found", fix: "Run a Claude Code session, then run `outerlayer doctor` again." };
3821
- }
3822
- const age = formatAgeH(now() - transcripts.newest);
3823
- return { name: "Transcripts", status: "pass", detail: `${transcripts.count} transcripts, newest ${age}` };
3824
- }
3825
- function formatAgeH(ms) {
3826
- const ageH = ms / 36e5;
3827
- return ageH < 1 ? "<1h ago" : ageH < 48 ? `${Math.round(ageH)}h ago` : `${Math.round(ageH / 24)}d ago`;
3828
- }
3829
- var HOOK_FIRING_GAP_MS = 24 * 60 * 60 * 1e3;
3830
- function newestHookFiring(home) {
3831
- let newest = null;
3832
- for (const event of REGISTERED_EVENTS) {
3833
- try {
3834
- const ms = statSync(hookFiredMarkerPath(event, home)).mtimeMs;
3835
- if (newest === null || ms > newest) newest = ms;
3836
- } catch {
3837
- }
3838
- }
3839
- return newest;
3840
- }
3841
- function checkHooks(home, cwd, transcripts, now) {
3842
- const lastFired = newestHookFiring(home);
3843
- const firingFresh = lastFired !== null && transcripts.count > 0 && transcripts.newest - lastFired <= HOOK_FIRING_GAP_MS;
3844
- const checks = [checkHooksInstalled(home, cwd, firingFresh)];
3845
- const firing = checkHooksFiring(transcripts, lastFired, now);
3846
- if (firing) checks.push(firing);
3847
- const wrapped = checkWrappedHooks(home, cwd);
3848
- if (wrapped) checks.push(wrapped);
3849
- const journal = checkJournalHookCoverage(home);
3850
- if (journal) checks.push(journal);
3851
- return checks;
3852
- }
3853
- function checkJournalHookCoverage(home) {
3854
- const name = "Rewrite journal hooks";
3855
- const repos = readSessionRepos(home).filter((r) => existsSync(r.repoRoot));
3856
- if (repos.length === 0) return null;
3857
- let ours = 0;
3858
- let managed = 0;
3859
- let foreign = 0;
3860
- const absentRepos = [];
3861
- for (const r of repos) {
3862
- switch (postRewriteHookPresence(r.repoRoot)) {
3863
- case "ours":
3864
- ours += 1;
3865
- break;
3866
- case "custom-hooks-path":
3867
- managed += 1;
3868
- break;
3869
- case "foreign":
3870
- foreign += 1;
3871
- break;
3872
- case "absent":
3873
- absentRepos.push(r.gitRepo || r.repoRoot);
3874
- break;
3875
- }
3876
- }
3877
- const parts = [`${ours}/${repos.length} synced repos covered`];
3878
- if (managed > 0) parts.push(`${managed} hook-managed (README has the snippet)`);
3879
- if (foreign > 0) parts.push(`${foreign} foreign hook`);
3880
- if (absentRepos.length > 0) {
3881
- const sample = absentRepos.slice(0, 3).join(", ") + (absentRepos.length > 3 ? ", \u2026" : "");
3882
- return {
3883
- name,
3884
- status: "warn",
3885
- detail: `${parts.join(", ")} \u2014 ${absentRepos.length} without rewrite journaling: ${sample}`,
3886
- fix: 'Run `outerlayer hooks install-git` in each repo, or set {"gitHooks":{"autoInstall":true}} in ~/.outerlayer/config.json.'
3887
- };
3888
- }
3889
- return { name, status: "pass", detail: parts.join(", ") };
3890
- }
3891
- function checkHooksInstalled(home, cwd, firingFresh) {
3892
- const name = "Hooks installed";
3893
- const readablePaths = [];
3894
- for (const scope of ["user", "project"]) {
3895
- const path2 = settingsPath(scope, { home, cwd });
3896
- let settings;
3897
- try {
3898
- settings = readSettings(path2);
3899
- } catch {
3900
- continue;
3901
- }
3902
- readablePaths.push(path2);
3903
- if (!settings?.hooks) continue;
3904
- const installed = REGISTERED_EVENTS.filter(
3905
- (event) => (settings.hooks?.[event] ?? []).some((b) => (b.hooks ?? []).some(isOurHookCommand))
3906
- );
3907
- if (installed.length === REGISTERED_EVENTS.length) {
3908
- const missing = ourCommandBins(settings).filter((bin) => !cliBinResolvable(bin));
3909
- if (missing.length > 0) {
3910
- return {
3911
- name,
3912
- status: "fail",
3913
- detail: `hooks in ${path2} run ${missing.join(", ")}, which no longer exists \u2014 no session is being captured`,
3914
- fix: "Run `npx @outerlayer/cli init` to reinstall."
3915
- };
3916
- }
3917
- return { name, status: "pass", detail: `${installed.join(", ")} (${scope})` };
3918
- }
3919
- if (installed.length > 0) {
3920
- return { name, status: "warn", detail: `only ${installed.join(", ")} installed`, fix: "Run `outerlayer init` to (re)install all lifecycle hooks." };
3921
- }
3922
- }
3923
- const manifest = readManifest(home);
3924
- const recorded = readablePaths.find((p) => manifest?.installs[p]);
3925
- if (recorded) {
3926
- const record = manifest.installs[recorded];
3927
- return {
3928
- name,
3929
- status: "fail",
3930
- detail: `hooks were installed into ${recorded} (recorded ${record.updatedAt}) but the settings file no longer contains them \u2014 it was rewritten or reset`,
3931
- fix: "Run `outerlayer init` to reinstall."
3932
- };
3933
- }
3934
- if (firingFresh) {
3935
- return { name, status: "pass", detail: "not in user/project settings, but hooks are firing \u2014 installed in another scope (managed settings?)" };
3936
- }
3937
- return { name, status: "fail", detail: "no OuterLayer hooks found", fix: "Run `outerlayer init`." };
3938
- }
3939
- function checkWrappedHooks(home, cwd) {
3940
- const manifest = readManifest(home);
3941
- if (!manifest) return null;
3942
- const missing = [];
3943
- for (const scope of ["user", "project"]) {
3944
- const path2 = settingsPath(scope, { home, cwd });
3945
- const records = manifest.wrapped[path2];
3946
- if (!records || records.length === 0) continue;
3947
- let settings;
3948
- try {
3949
- settings = readSettings(path2);
3950
- } catch {
3951
- continue;
3952
- }
3953
- const present = /* @__PURE__ */ new Set();
3954
- const hooks = settings?.hooks ?? {};
3955
- for (const event of Object.keys(hooks)) {
3956
- for (const block of hooks[event] ?? []) {
3957
- for (const h of block.hooks ?? []) {
3958
- const original = wrappedOriginalCommand(h);
3959
- if (original !== null) present.add(`${event}\0${original}`);
3960
- }
3961
- }
3962
- }
3963
- for (const r of records) {
3964
- if (!present.has(`${r.event}\0${r.original}`)) missing.push(`${r.event}: ${r.original}`);
3965
- }
3966
- }
3967
- if (missing.length === 0) return null;
3968
- return {
3969
- name: "Wrapped hooks",
3970
- status: "warn",
3971
- detail: `recorded as wrapped but the settings no longer carry the wrapped form \u2014 ${missing.join("; ")}`,
3972
- fix: "Run `outerlayer hooks wrap` to re-wrap, or `outerlayer hooks unwrap` if the wrap is no longer wanted."
3973
- };
3974
- }
3975
- function checkHooksFiring(transcripts, lastFired, now) {
3976
- const name = "Hooks firing";
3977
- if (transcripts.count === 0) return null;
3978
- if (lastFired === null) {
3979
- return {
3980
- name,
3981
- status: "warn",
3982
- detail: "no firing evidence recorded yet \u2014 every hook run writes a marker, and none exists",
3983
- fix: "Start a Claude Code session, then re-run doctor."
3984
- };
3985
- }
3986
- if (transcripts.newest - lastFired > HOOK_FIRING_GAP_MS) {
3987
- return {
3988
- name,
3989
- status: "fail",
3990
- detail: `sessions as recent as ${formatAgeH(now() - transcripts.newest)}, but no lifecycle hook has fired since ${formatAgeH(now() - lastFired)}`,
3991
- fix: "Hooks are configured but not running \u2014 run `outerlayer init` and check `claude --debug` hook output."
3992
- };
3993
- }
3994
- return { name, status: "pass", detail: `last fired ${formatAgeH(now() - lastFired)}` };
3995
- }
3996
- function checkDeadHooksState(cwd) {
3997
- const dir = cwd ?? process.cwd();
3998
- const state = deadHooksState(dir);
3999
- if (state.hooksDir === void 0) return [];
4000
- const name = "Git hooks directory";
4001
- if (state.unreadableError) {
4002
- return [
4003
- {
4004
- name,
4005
- status: "fail",
4006
- detail: `${sanitizeForContext(state.hooksDir)} could not be read (${state.unreadableError.code ?? state.unreadableError.message}) \u2014 git's hooks status is unknown, not confirmed healthy`,
4007
- fix: "Fix the directory's permissions so it can be listed, then re-run `outerlayer doctor`."
4008
- }
4009
- ];
4010
- }
4011
- if (!state.dead) {
4012
- return [{ name, status: "pass", detail: `hooks present at ${sanitizeForContext(state.hooksDir)}` }];
4013
- }
4014
- const fix = `Run \`${preparePackageManagerCommand(state.repoRoot ?? dir)}\` to (re)generate the hooks directory.`;
4015
- return [
4016
- {
4017
- name,
4018
- status: "fail",
4019
- detail: `${sanitizeForContext(state.hooksDir)} is missing or empty \u2014 core.hooksPath points at a dead directory, so git runs no hooks at all`,
4020
- fix
4021
- }
4022
- ];
4023
- }
4024
- function checkSpoolWritable(home) {
4025
- const dir = spoolDir(home);
4026
- try {
4027
- if (!existsSync(dir)) return { name: "Spool writable", status: "warn", detail: "spool not created yet", fix: "Run `outerlayer init` (creates the spool on first hook)." };
4028
- accessSync(dir, constants.W_OK);
4029
- return { name: "Spool writable", status: "pass", detail: dir };
4030
- } catch {
4031
- return { name: "Spool writable", status: "fail", detail: `${dir} not writable`, fix: "Fix permissions on ~/.outerlayer/spool." };
4032
- }
4033
- }
4034
- function checkLastSync(home, now) {
4035
- const name = "Cloud sync";
4036
- if (!existsSync(join(home, ".outerlayer", "config.json"))) {
4037
- 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." };
4038
- }
4039
- const health = assessSyncHealth(readSyncStatus(home), now(), SYNC_STALE_AFTER_MS);
4040
- const detail = [health.headline, ...health.details].join("; ");
4041
- if (health.level === "error") {
4042
- 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." };
4043
- }
4044
- if (health.level === "warn" || health.level === "unknown") {
4045
- return { name, status: "warn", detail, fix: "Run `outerlayer sync` to confirm the connection works." };
4046
- }
4047
- return { name, status: "pass", detail };
4048
- }
4049
- function checkDaemon(home) {
4050
- const pidfile = join(home, ".outerlayer", "daemon.pid");
4051
- const heartbeat = join(home, ".outerlayer", "daemon.heartbeat");
4052
- if (!existsSync(pidfile)) {
4053
- return { name: "Daemon running", status: "warn", detail: "not running", fix: "Start it with `outerlayer daemon` (or a login agent)." };
4054
- }
4055
- try {
4056
- const beatMs = statSync(heartbeat).mtimeMs;
4057
- const staleMin = (Date.now() - beatMs) / 6e4;
4058
- if (staleMin > 30) {
4059
- return { name: "Daemon running", status: "warn", detail: `heartbeat ${Math.round(staleMin)}m stale`, fix: "Restart `outerlayer daemon`." };
4060
- }
4061
- return { name: "Daemon running", status: "pass", detail: "heartbeat fresh" };
4062
- } catch {
4063
- return { name: "Daemon running", status: "warn", detail: "pidfile present, no heartbeat", fix: "Restart `outerlayer daemon`." };
4064
- }
4065
- }
4066
- function checkCleanupPeriod(home) {
4067
- const path2 = join(home, ".claude", "settings.json");
4068
- let settings;
4069
- try {
4070
- settings = readSettings(path2);
4071
- } catch {
4072
- return { name: "Retention (cleanupPeriodDays)", status: "warn", detail: "settings unreadable" };
4073
- }
4074
- const raw = settings?.cleanupPeriodDays;
4075
- const days = typeof raw === "number" ? raw : 30;
4076
- if (days === 0) {
4077
- return {
4078
- name: "Retention (cleanupPeriodDays)",
4079
- status: "fail",
4080
- detail: "set to 0 \u2014 this DISABLES transcript persistence (upstream anthropics/claude-code#23710 and #59248)",
4081
- fix: "Remove cleanupPeriodDays or set it \u226514; run `outerlayer daemon` to mirror before deletion."
4082
- };
4083
- }
4084
- if (days < 14) {
4085
- return { name: "Retention (cleanupPeriodDays)", status: "warn", detail: `${days}d \u2014 transcripts deleted quickly`, fix: "Run `outerlayer daemon` so raws are mirrored before cleanup." };
4086
- }
4087
- return { name: "Retention (cleanupPeriodDays)", status: "pass", detail: `${days}d` };
4088
- }
4089
- function checkDiskHeadroom(home) {
4090
- try {
4091
- accessSync(home, constants.W_OK);
4092
- return { name: "Disk headroom", status: "pass", detail: "home writable (raw mirror capped at 5 GiB, LRU)" };
4093
- } catch {
4094
- return { name: "Disk headroom", status: "fail", detail: "home not writable", fix: "Free space / fix permissions on your home directory." };
4095
- }
4096
- }
4097
- function checkClaudeVersion(env) {
4098
- let version;
4099
- try {
4100
- version = (env.claudeVersion ?? defaultClaudeVersion)();
4101
- } catch {
4102
- return { name: "Claude Code version", status: "warn", detail: "could not run `claude --version`", fix: "Ensure `claude` is on PATH." };
4103
- }
4104
- const parsed = version.match(/\d+\.\d+\.\d+/)?.[0];
4105
- if (!parsed) return { name: "Claude Code version", status: "warn", detail: `unrecognized: ${version}` };
4106
- if (compareVersions(parsed, SUPPORTED_VERSIONS.max) > 0) {
4107
- 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." };
4108
- }
4109
- if (compareVersions(parsed, SUPPORTED_VERSIONS.min) < 0) {
4110
- return { name: "Claude Code version", status: "warn", detail: `${parsed} is older than validated (${SUPPORTED_VERSIONS.min})` };
4111
- }
4112
- return { name: "Claude Code version", status: "pass", detail: parsed };
4113
- }
4114
- function checkSettingsValid(home, cwd) {
4115
- for (const scope of ["user", "project"]) {
4116
- const path2 = settingsPath(scope, { home, cwd });
4117
- if (!existsSync(path2)) continue;
4118
- try {
4119
- readSettings(path2);
4120
- } catch (err) {
4121
- return { name: "Settings JSON valid", status: "fail", detail: `${path2} is corrupt`, fix: err instanceof Error ? err.message : "Fix the JSON syntax." };
4122
- }
4123
- }
4124
- return { name: "Settings JSON valid", status: "pass", detail: "all settings files parse" };
4125
- }
4126
- function checkInstalls(env) {
4127
- let installs;
4128
- try {
4129
- installs = (env.claudeInstalls ?? defaultClaudeInstalls)();
4130
- } catch {
4131
- return [];
4132
- }
4133
- if (installs.length > 1) {
4134
- return [{ name: "Claude Code installs", status: "warn", detail: `multiple found: ${installs.join(", ")}`, fix: "Multiple installs can diverge in version; keep one." }];
4135
- }
4136
- return [];
4137
- }
4138
- function defaultClaudeVersion() {
4139
- return execFileSync("claude", ["--version"], { encoding: "utf8", timeout: 5e3 }).trim();
4140
- }
4141
- function defaultClaudeInstalls() {
4142
- try {
4143
- const out = execFileSync(process.platform === "win32" ? "where" : "which", ["-a", "claude"], { encoding: "utf8", timeout: 5e3 });
4144
- return out.split(/\r?\n/).filter((l) => l.trim().length > 0);
4145
- } catch {
4146
- return [];
4147
- }
4148
- }
4149
- function contextDegradedFix(kind) {
4150
- switch (kind) {
4151
- case "unreachable":
4152
- return "Check network/VPN access to the control plane, then run `outerlayer context materialize`.";
4153
- case "no-access":
4154
- return "Check your git credentials (SSH key or token) and org/SSO access to the control plane, then retry.";
4155
- case "designation-unverified":
4156
- return "Check OuterLayer cloud connectivity/config (~/.outerlayer/config.json), then run `outerlayer context materialize`.";
4157
- case "not-governed":
4158
- return "Expected if the control plane's `governs` list changed intentionally; otherwise re-designate the source.";
4159
- case "adoption-conflict":
4160
- return "Remove the committed AGENTS.md/.claude/.outerlayer files in a commit, then run `outerlayer context materialize`.";
4161
- case "conflict":
4162
- return "Revoke the designation from all but one of the claiming factories, then run `outerlayer context materialize`.";
4163
- }
4164
- }
4165
- function checkContextSource(home, cwd) {
4166
- const dir = cwd ?? process.cwd();
4167
- let repoRoot;
4168
- let gitCommonDir;
4169
- try {
4170
- repoRoot = execFileSync("git", ["-C", dir, "rev-parse", "--show-toplevel"], {
4171
- encoding: "utf8",
4172
- stdio: ["ignore", "pipe", "ignore"],
4173
- timeout: 2e3
4174
- }).trim();
4175
- gitCommonDir = execFileSync("git", ["-C", dir, "rev-parse", "--path-format=absolute", "--git-common-dir"], {
4176
- encoding: "utf8",
4177
- stdio: ["ignore", "pipe", "ignore"],
4178
- timeout: 2e3
4179
- }).trim();
4180
- } catch {
4181
- return [];
4182
- }
4183
- if (!repoRoot || !gitCommonDir) return [];
4184
- const pin = readPin(repoRoot);
4185
- const manifest = readContextManifest(gitCommonDir);
4186
- const degraded = readDegraded(gitCommonDir);
4187
- const sourceCache = readSourceCache(gitCommonDir);
4188
- const looksGoverned = pin !== void 0 || sourceCache?.status === "governed";
4189
- if (!pin && !manifest && !degraded && !looksGoverned) return [];
4190
- const checks = [];
4191
- const name = "Context source";
4192
- if (pin && manifest && pin.sha !== manifest.sha) {
4193
- checks.push({
4194
- name,
4195
- status: "warn",
4196
- detail: `pin (${pin.sha.slice(0, 7)}) and manifest (${manifest.sha.slice(0, 7)}) disagree`,
4197
- fix: "Run `outerlayer context materialize` to reconcile."
4198
- });
4199
- } else if (pin ?? manifest) {
4200
- const sha = (pin ?? manifest).sha;
4201
- checks.push({ name, status: degraded ? "warn" : "pass", detail: `materialized at ${sha.slice(0, 7)}` });
4202
- }
4203
- if (degraded) {
4204
- checks.push({
4205
- name: "Context source degradation",
4206
- status: degraded.kind === "adoption-conflict" || degraded.kind === "conflict" ? "fail" : "warn",
4207
- detail: degraded.message,
4208
- fix: contextDegradedFix(degraded.kind)
4209
- });
4210
- }
4211
- if ((pin || manifest) && !contextExcludesPresent(gitCommonDir)) {
4212
- checks.push({
4213
- name: "Context exclude block",
4214
- status: "warn",
4215
- detail: "materialized paths are not excluded in .git/info/exclude",
4216
- fix: "Run `outerlayer context materialize` to repair it."
4217
- });
4218
- }
4219
- if (pin && !readCloudConfig(home).url) {
4220
- checks.push({
4221
- name,
4222
- status: "warn",
4223
- detail: "a pin exists but OuterLayer cloud sync isn't configured \u2014 the designation cannot be confirmed",
4224
- fix: "Run `outerlayer sync --url <url> --api-key <key> --app-id <id>` once to connect."
4225
- });
4226
- }
4227
- if (looksGoverned) {
4228
- let tracked = "";
4229
- try {
4230
- tracked = execFileSync("git", ["-C", repoRoot, "ls-files", "--", "AGENTS.md", ".claude", ".outerlayer"], {
4231
- encoding: "utf8",
4232
- stdio: ["ignore", "pipe", "ignore"],
4233
- timeout: 2e3
4234
- }).trim();
4235
- } catch {
4236
- tracked = "";
4237
- }
4238
- if (tracked) {
4239
- const paths = tracked.split("\n");
4240
- const sample = paths.slice(0, 3).join(", ") + (paths.length > 3 ? ", \u2026" : "");
4241
- checks.push({
4242
- name,
4243
- status: "fail",
4244
- detail: `committed context files shadow the control plane: ${sample}`,
4245
- fix: "Remove the committed AGENTS.md/.claude/.outerlayer files in a commit, then run `outerlayer context materialize`."
4246
- });
4247
- }
4248
- }
4249
- return checks;
4250
- }
4251
- function doctorExitCode(checks) {
4252
- return checks.some((c) => c.status === "fail") ? 1 : 0;
4253
- }
4254
- var FALLBACK_VERSION = "0.1.0";
4255
- function readJson(path2) {
4256
- if (!existsSync(path2)) return null;
4257
- try {
4258
- return JSON.parse(readFileSync(path2, "utf8"));
4259
- } catch {
4260
- return null;
4261
- }
4262
- }
4263
- function resolveVersion(entryPath = fileURLToPath(import.meta.url)) {
4264
- const distDir = dirname(entryPath);
4265
- const pkg = readJson(join(dirname(distDir), "package.json"));
4266
- const version = typeof pkg?.version === "string" ? pkg.version : FALLBACK_VERSION;
4267
- const buildInfo = readJson(join(distDir, "build-info.json"));
4268
- const buildId = typeof buildInfo?.id === "string" ? buildInfo.id : null;
4269
- return buildId ? `${version} (build ${buildId})` : version;
4270
- }
4271
3609
 
4272
3610
  // src/cli.ts
4273
3611
  function resolveCliBin() {
@@ -4278,11 +3616,6 @@ function resolveCliBin() {
4278
3616
  }
4279
3617
  }
4280
3618
  var SUPPORT_TEXT = "\nDocs: https://github.com/outerlayerai/outerlayer#readme\nIssues: https://github.com/outerlayerai/outerlayer/issues\n";
4281
- function icon(status) {
4282
- if (status === "pass") return `${C.green}\u2713${C.reset}`;
4283
- if (status === "warn") return `${C.yellow}!${C.reset}`;
4284
- return `${C.red}\u2717${C.reset}`;
4285
- }
4286
3619
  function fail(message) {
4287
3620
  process.stderr.write(`${C.red}\u2717${C.reset} ${message}
4288
3621
  `);
@@ -4298,7 +3631,18 @@ function apiKeyOption() {
4298
3631
  function warnIfApiKeyFlag(opts) {
4299
3632
  if (opts.apiKey !== void 0) warnDeprecated(API_KEY_FLAG_DEPRECATION);
4300
3633
  }
4301
- 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 = {}) {
4302
3646
  const lines = [];
4303
3647
  if (result.removed) {
4304
3648
  lines.push(
@@ -4368,15 +3712,12 @@ cannot be disabled.
4368
3712
  `
4369
3713
  );
4370
3714
  lines.push(
4371
- `
3715
+ (opts.next === false ? "\n" : `
4372
3716
  Next: launch a session naming its work item, then sync it:
4373
3717
  ${C.yellow}OUTERLAYER_WORK=42 claude${C.reset}
4374
3718
  ${C.yellow}outerlayer sync${C.reset}
4375
3719
 
4376
- Once you have run outerlayer login, launched sessions sync
4377
- automatically in the background after each agent turn and at the end
4378
- of the session, at most once every five minutes.
4379
- `
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")
4380
3721
  );
4381
3722
  return lines.join("");
4382
3723
  }
@@ -4402,115 +3743,182 @@ function formatGuardInstallOutput(guard) {
4402
3743
  if (guard.adopted) {
4403
3744
  return { lines: [`${C.green}\u2713${C.reset} materialized-context commit guard hand-adopted at ${guard.path} (core.hooksPath) \u2014 left untouched.`] };
4404
3745
  }
4405
- return { lines: [] };
3746
+ return {
3747
+ lines: [
3748
+ `${C.yellow}!${C.reset} this repo manages hooks in its working tree (core.hooksPath) \u2014 nothing written.`,
3749
+ ` Add the script to your hook manager instead: ${C.yellow}outerlayer hooks install-git --print${C.reset}`
3750
+ ],
3751
+ exitCode: 1
3752
+ };
4406
3753
  case "not-a-repo":
4407
- return { lines: [] };
3754
+ return { lines: [], error: "not inside a git repository." };
4408
3755
  }
4409
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
+ }
4410
3806
  function invokedCommandToken(argv) {
4411
3807
  return argv.slice(2).find((token) => !token.startsWith("-"));
4412
3808
  }
4413
3809
  function buildProgram(argv = process.argv) {
4414
3810
  const program2 = new Command();
4415
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);
4416
- 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(
4417
3813
  "after",
4418
- "\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"
4419
- ).action((opts) => {
4420
- 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();
4421
3835
  if (opts.org) {
4422
3836
  process.stdout.write(orgRolloutSnippet(cliBin) + "\n");
4423
3837
  return;
4424
3838
  }
4425
- const scope = opts.project ? "project" : "user";
4426
- try {
4427
- cliBin = initHookBin({ home: homeFromEnv(), runningBin: cliBin, remove: opts.remove === true });
4428
- } catch (err) {
4429
- fail(
4430
- `could not copy the CLI to ~/.outerlayer/cli: ${err instanceof Error ? err.message : String(err)}
4431
- Hooks were not changed. Fix the permissions on ~/.outerlayer, then re-run ${C.yellow}outerlayer init${C.reset}.`
4432
- );
4433
- return;
4434
- }
4435
- try {
4436
- const result = runInit({
4437
- scope,
4438
- remove: opts.remove,
4439
- cliBin,
4440
- addGitignore: opts.gitignore,
4441
- wrapHooks: opts.wrapHooks,
4442
- statusline: opts.statusline
4443
- });
4444
- if (result.cliBinUnresolved) {
4445
- fail(
4446
- `the resolved CLI path (${cliBin}) does not exist \u2014 refusing to write hook entries pointing at it.
4447
- Reinstall OuterLayer, then re-run ${C.yellow}outerlayer init${C.reset}.`
4448
- );
4449
- return;
4450
- }
4451
- if (opts.json) {
4452
- writeJson(result);
3839
+ if (opts.remove || opts.local) {
3840
+ const outcome = await installHooks(opts, cliBin, void 0, starterTemplate);
3841
+ if (!outcome.ok) {
3842
+ fail(outcome.text);
4453
3843
  return;
4454
3844
  }
4455
- process.stdout.write(formatInitOutput(result));
4456
- } catch (err) {
4457
- if (err instanceof SettingsParseError) {
4458
- fail(err.message);
4459
- return;
4460
- }
4461
- throw err;
3845
+ if (opts.json) writeJson(outcome.result);
3846
+ else process.stdout.write(outcome.text);
3847
+ return;
4462
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;
3868
+ });
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;
4463
3879
  });
4464
- program2.command("doctor").description("Diagnose the capture setup; every failing check names its fix").option("--json", "machine-readable JSON output").action((opts) => {
4465
- const checks = runDoctor();
4466
- const fails = checks.filter((c) => c.status === "fail").length;
4467
- const warns = checks.filter((c) => c.status === "warn").length;
4468
- const passed = checks.length - fails - warns;
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();
4469
3882
  process.exitCode = doctorExitCode(checks);
4470
3883
  if (opts.json) {
4471
- writeJson({ checks, summary: { passed, warnings: warns, failures: fails } });
3884
+ writeJson({ checks, summary: summarizeChecks(checks) });
4472
3885
  return;
4473
3886
  }
4474
- process.stdout.write("\nOuterLayer doctor\n\n");
4475
- for (const c of checks) {
4476
- process.stdout.write(` ${icon(c.status)} ${c.name}${C.dim} \u2014 ${c.detail}${C.reset}
4477
- `);
4478
- if (c.fix && c.status !== "pass") process.stdout.write(` ${C.dim}\u21B3 ${c.fix}${C.reset}
4479
- `);
4480
- }
4481
- process.stdout.write(`
4482
- ${fails === 0 ? `${C.green}\u2713${C.reset}` : `${C.red}\u2717${C.reset}`} ${passed} passed, ${warns} warnings, ${fails} failures
4483
- `);
3887
+ process.stdout.write(renderDoctorText(checks));
4484
3888
  });
4485
- 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(
4486
3890
  "after",
4487
- '\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'
4488
3892
  ).action(async (opts) => {
4489
- const { runLogin, readApiKey, LoginError } = await import('./login-cmd-IRX6LZT7.js');
4490
- try {
4491
- const apiKey = await readApiKey({ noInput: opts.input === false });
4492
- const result = runLogin({ apiKey, url: opts.url, appId: opts.appId });
4493
- if (opts.json) {
4494
- writeJson(result);
4495
- return;
4496
- }
4497
- process.stdout.write(
4498
- `${C.green}\u2713${C.reset} Saved credentials to ${result.path}` + (result.url ? ` for ${result.url}` : "") + (result.appId ? ` (app ${result.appId})` : "") + "\n"
4499
- );
4500
- } catch (err) {
4501
- if (err instanceof LoginError) {
4502
- fail(err.message);
4503
- return;
4504
- }
4505
- throw err;
4506
- }
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;
4507
3915
  });
4508
- 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(
4509
3917
  "after",
4510
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"
4511
3919
  ).action(async (opts) => {
4512
3920
  warnIfApiKeyFlag(opts);
4513
- const { runSync, SyncConfigError, SyncTransportError } = await import('./sync-cmd-S7Q6DWU3.js');
3921
+ const { runSync, SyncConfigError, SyncTransportError } = await import('./sync-cmd-VPPYCNNM.js');
4514
3922
  try {
4515
3923
  const result = await runSync({
4516
3924
  url: opts.url,
@@ -4539,7 +3947,7 @@ ${fails === 0 ? `${C.green}\u2713${C.reset}` : `${C.red}\u2717${C.reset}`} ${pas
4539
3947
  if (invokedCommandToken(argv) === "watch") {
4540
3948
  warnDeprecated("`outerlayer watch` is now `outerlayer daemon`; the old name will be removed in a future release.");
4541
3949
  }
4542
- const { runWatch } = await import('./watch-NRLXJUST.js');
3950
+ const { runWatch } = await import('./watch-5C4ZBBGO.js');
4543
3951
  await runWatch({ once: opts.once });
4544
3952
  });
4545
3953
  program2.command("hook <event>", { hidden: true }).description("capture-hook fast path, installed by `init`").action(() => {
@@ -4548,6 +3956,59 @@ ${fails === 0 ? `${C.green}\u2713${C.reset}` : `${C.red}\u2717${C.reset}`} ${pas
4548
3956
  });
4549
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(() => {
4550
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
+ });
4551
4012
  registerEmitCommands(program2);
4552
4013
  registerImportCommands(program2);
4553
4014
  registerHooksCommands(program2);
@@ -4557,13 +4018,17 @@ ${fails === 0 ? `${C.green}\u2713${C.reset}` : `${C.red}\u2717${C.reset}`} ${pas
4557
4018
  registerRunnerCommands(program2);
4558
4019
  return program2;
4559
4020
  }
4021
+ function withoutItemKey(job) {
4022
+ const { itemKey: _itemKey, ...rest } = job;
4023
+ return rest;
4024
+ }
4560
4025
  async function runCli(processArgv) {
4561
4026
  const { argv, noColor } = stripNoColor(processArgv);
4562
4027
  if (noColor) process.env.NO_COLOR = "1";
4563
4028
  await buildProgram(argv).parseAsync(argv);
4564
4029
  }
4565
4030
  async function compileContext(opts) {
4566
- const { runEmit, EmitError } = await import('./emit-cmd-TWSYYEZI.js');
4031
+ const { runEmit, EmitError } = await import('./emit-cmd-DCERAU27.js');
4567
4032
  try {
4568
4033
  const result = runEmit({ cwd: opts.dir, check: opts.check, json: opts.json });
4569
4034
  if (result.exitCode !== 0) process.exitCode = result.exitCode;
@@ -4576,9 +4041,9 @@ async function compileContext(opts) {
4576
4041
  }
4577
4042
  }
4578
4043
  function registerEmitCommands(program2) {
4579
- 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(
4580
4045
  "after",
4581
- '\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 commit-credit` to attest a commit belongs to a pull request;\n`emit finding` / `emit findings <file>` to record what was found wrong about\nthe 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"
4582
4047
  ).action(async (name, opts) => {
4583
4048
  if (name === void 0) {
4584
4049
  warnDeprecated("`outerlayer emit` with no name now lives at `outerlayer context emit`; the bare form will be removed in a future release.");
@@ -4586,7 +4051,7 @@ function registerEmitCommands(program2) {
4586
4051
  return;
4587
4052
  }
4588
4053
  warnIfApiKeyFlag(opts);
4589
- const { runEmitResult, EmitResultError } = await import('./emit-result-cmd-YOA5BJKL.js');
4054
+ const { runEmitResult, EmitResultError } = await import('./emit-result-cmd-FDTUKGKE.js');
4590
4055
  try {
4591
4056
  const result = await runEmitResult({
4592
4057
  name,
@@ -4595,7 +4060,6 @@ function registerEmitCommands(program2) {
4595
4060
  bodyFile: opts.bodyFile,
4596
4061
  result: opts.result,
4597
4062
  item: opts.item,
4598
- artifact: opts.artifact,
4599
4063
  json: opts.json,
4600
4064
  url: opts.url,
4601
4065
  apiKey: opts.apiKey,
@@ -4610,12 +4074,12 @@ function registerEmitCommands(program2) {
4610
4074
  throw err;
4611
4075
  }
4612
4076
  });
4613
- 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(
4614
4078
  "after",
4615
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'
4616
4080
  ).action(async (file, opts) => {
4617
4081
  warnIfApiKeyFlag(opts);
4618
- const { runEmitArtifact, EmitArtifactError } = await import('./emit-artifact-cmd-KPPD3YLO.js');
4082
+ const { runEmitArtifact, EmitArtifactError } = await import('./emit-artifact-cmd-KTYW2TN2.js');
4619
4083
  let pr;
4620
4084
  if (opts.pr !== void 0) {
4621
4085
  pr = parseTrackerNumber(opts.pr);
@@ -4644,28 +4108,24 @@ function registerEmitCommands(program2) {
4644
4108
  throw err;
4645
4109
  }
4646
4110
  });
4647
- 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(
4648
4112
  "after",
4649
- '\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'
4650
4114
  ).action(async (opts) => {
4651
4115
  warnIfApiKeyFlag(opts);
4652
- const { runEmitFinding, EmitFindingError } = await import('./emit-finding-cmd-VR5VEGXJ.js');
4116
+ const { runEmitFinding, refuseRemovedFindingFlags, EmitFindingError } = await import('./emit-finding-cmd-UR4ID45Z.js');
4653
4117
  try {
4118
+ refuseRemovedFindingFlags(opts);
4654
4119
  const result = await runEmitFinding({
4655
4120
  id: opts.id,
4656
- subject: opts.subject,
4121
+ category: opts.category,
4657
4122
  title: opts.title,
4658
4123
  file: opts.file,
4659
4124
  line: opts.line,
4660
- severity: opts.severity,
4661
- kind: opts.kind,
4662
- verdict: opts.verdict,
4663
- fixed: opts.fixed,
4664
4125
  rulePath: opts.rulePath,
4665
4126
  ruleLine: opts.ruleLine,
4666
4127
  ruleQuote: opts.ruleQuote,
4667
4128
  ruleRelation: opts.ruleRelation,
4668
- source: opts.source,
4669
4129
  where: opts.where,
4670
4130
  traceId: opts.traceId,
4671
4131
  turn: opts.turn,
@@ -4687,12 +4147,12 @@ function registerEmitCommands(program2) {
4687
4147
  throw err;
4688
4148
  }
4689
4149
  });
4690
- 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(
4691
4151
  "after",
4692
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"
4693
4153
  ).action(async (file, opts) => {
4694
4154
  warnIfApiKeyFlag(opts);
4695
- const { runEmitFindings, EmitFindingError } = await import('./emit-finding-cmd-VR5VEGXJ.js');
4155
+ const { runEmitFindings, EmitFindingError } = await import('./emit-finding-cmd-UR4ID45Z.js');
4696
4156
  try {
4697
4157
  const result = await runEmitFindings({
4698
4158
  file,
@@ -4711,23 +4171,24 @@ function registerEmitCommands(program2) {
4711
4171
  throw err;
4712
4172
  }
4713
4173
  });
4714
- emitCmd.command("commit-credit").description("Attest that a commit belongs to a pull request").requiredOption("--sha <sha>", "full 40-character hex commit SHA").requiredOption("--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(
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(
4715
4175
  "after",
4716
- '\nThe escape hatch for a checkout, and reflog, that no longer exists. Records\none explicit human attestation, uploaded immediately \u2014 no session detection,\nno spool. The attesting identity is the dashboard member the control plane\nresolves from the API key; it renders as "attested by <name>" in the PR\nevidence comment, distinct from recorded or recovered credit. Refused, with\nnothing stored, for a malformed SHA, an unknown PR, or a machine key with no\nmember to attest as.\n'
4717
- ).action(async (opts) => {
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) => {
4718
4178
  warnIfApiKeyFlag(opts);
4719
- const { runEmitCommitCredit, EmitCommitCreditError } = await import('./emit-commit-credit-cmd-656BRAQ4.js');
4179
+ const { runEmitCriteria, EmitCriteriaError } = await import('./emit-criteria-cmd-VAUUSAYD.js');
4720
4180
  try {
4721
- await runEmitCommitCredit({
4722
- sha: opts.sha,
4723
- pr: opts.pr,
4181
+ const result = await runEmitCriteria({
4182
+ file,
4183
+ item: opts.item,
4724
4184
  json: opts.json,
4725
4185
  url: opts.url,
4726
4186
  apiKey: opts.apiKey,
4727
4187
  appId: opts.appId
4728
4188
  });
4189
+ if (result.exitCode !== 0) process.exitCode = result.exitCode;
4729
4190
  } catch (err) {
4730
- if (err instanceof EmitCommitCreditError) {
4191
+ if (err instanceof EmitCriteriaError) {
4731
4192
  fail(err.message);
4732
4193
  return;
4733
4194
  }
@@ -4736,15 +4197,15 @@ function registerEmitCommands(program2) {
4736
4197
  });
4737
4198
  }
4738
4199
  function registerWorkCommands(program2) {
4739
- const workCmd = program2.command("work").description("Add, remove, or read what the factory is working on (the Floor)");
4740
- 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(
4741
4202
  "after",
4742
- "\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"
4743
4204
  ).action(async (opts) => {
4744
4205
  warnIfApiKeyFlag(opts);
4745
- const { runWorkAdd, WorkCommandError } = await import('./work-cmd-ABP6XDBP.js');
4206
+ const { runWorkBuild, WorkCommandError } = await import('./work-cmd-UOQO2AD4.js');
4746
4207
  try {
4747
- await runWorkAdd(opts);
4208
+ await runWorkBuild(opts);
4748
4209
  } catch (err) {
4749
4210
  if (err instanceof WorkCommandError) {
4750
4211
  fail(err.message);
@@ -4753,14 +4214,14 @@ function registerWorkCommands(program2) {
4753
4214
  throw err;
4754
4215
  }
4755
4216
  });
4756
- 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(
4757
4218
  "after",
4758
- "\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"
4759
4220
  ).action(async (number, opts) => {
4760
4221
  warnIfApiKeyFlag(opts);
4761
- const { runWorkLinkSession, WorkCommandError } = await import('./work-cmd-ABP6XDBP.js');
4222
+ const { runWorkLinkSession, WorkCommandError } = await import('./work-cmd-UOQO2AD4.js');
4762
4223
  try {
4763
- await runWorkLinkSession({ ...opts, number });
4224
+ await runWorkLinkSession({ ...opts, number, noLease: opts.lease === false });
4764
4225
  } catch (err) {
4765
4226
  if (err instanceof WorkCommandError) {
4766
4227
  fail(err.message);
@@ -4769,18 +4230,61 @@ function registerWorkCommands(program2) {
4769
4230
  throw err;
4770
4231
  }
4771
4232
  });
4772
- 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(
4773
4277
  "after",
4774
4278
  `
4775
4279
  outerlayer work remove --item 42 --reason "done"
4776
4280
 
4777
4281
  Withdraws the caller's own addition, recording the reason. Never
4778
- 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
4779
4283
  live addition remains on it.
4780
4284
  `
4781
4285
  ).action(async (opts) => {
4782
4286
  warnIfApiKeyFlag(opts);
4783
- const { runWorkRemove, WorkCommandError } = await import('./work-cmd-ABP6XDBP.js');
4287
+ const { runWorkRemove, WorkCommandError } = await import('./work-cmd-UOQO2AD4.js');
4784
4288
  try {
4785
4289
  await runWorkRemove(opts);
4786
4290
  } catch (err) {
@@ -4791,9 +4295,9 @@ live addition remains on it.
4791
4295
  throw err;
4792
4296
  }
4793
4297
  });
4794
- 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) => {
4795
4299
  warnIfApiKeyFlag(opts);
4796
- const { runWorkStatus, WorkCommandError } = await import('./work-cmd-ABP6XDBP.js');
4300
+ const { runWorkStatus, WorkCommandError } = await import('./work-cmd-UOQO2AD4.js');
4797
4301
  try {
4798
4302
  await runWorkStatus(opts);
4799
4303
  } catch (err) {
@@ -4804,9 +4308,9 @@ live addition remains on it.
4804
4308
  throw err;
4805
4309
  }
4806
4310
  });
4807
- 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) => {
4808
4312
  warnIfApiKeyFlag(opts);
4809
- const { runWorkList, WorkCommandError } = await import('./work-cmd-ABP6XDBP.js');
4313
+ const { runWorkList, WorkCommandError } = await import('./work-cmd-UOQO2AD4.js');
4810
4314
  try {
4811
4315
  await runWorkList(opts);
4812
4316
  } catch (err) {
@@ -4817,12 +4321,12 @@ live addition remains on it.
4817
4321
  throw err;
4818
4322
  }
4819
4323
  });
4820
- 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(
4821
4325
  "after",
4822
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"
4823
4327
  ).action(async (opts) => {
4824
4328
  warnIfApiKeyFlag(opts);
4825
- const { runWorkClaim, WorkCommandError } = await import('./work-claim-cmd-HXB7K27H.js');
4329
+ const { runWorkClaim, WorkCommandError } = await import('./work-claim-cmd-LSO2B2EX.js');
4826
4330
  try {
4827
4331
  await runWorkClaim(opts);
4828
4332
  } catch (err) {
@@ -4833,12 +4337,12 @@ live addition remains on it.
4833
4337
  throw err;
4834
4338
  }
4835
4339
  });
4836
- 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(
4837
4341
  "after",
4838
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"
4839
4343
  ).action(async (opts) => {
4840
4344
  warnIfApiKeyFlag(opts);
4841
- const { runWorkRenew, WorkCommandError } = await import('./work-claim-cmd-HXB7K27H.js');
4345
+ const { runWorkRenew, WorkCommandError } = await import('./work-claim-cmd-LSO2B2EX.js');
4842
4346
  try {
4843
4347
  await runWorkRenew(opts);
4844
4348
  } catch (err) {
@@ -4852,12 +4356,12 @@ live addition remains on it.
4852
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(
4853
4357
  "--outcome <ok|failed|timed_out|idle|lease_lost|interrupted|stopped>",
4854
4358
  "how the attempt ended (default: none)"
4855
- ).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(
4856
4360
  "after",
4857
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"
4858
4362
  ).action(async (opts) => {
4859
4363
  warnIfApiKeyFlag(opts);
4860
- const { runWorkRelease, WorkCommandError } = await import('./work-claim-cmd-HXB7K27H.js');
4364
+ const { runWorkRelease, WorkCommandError } = await import('./work-claim-cmd-LSO2B2EX.js');
4861
4365
  try {
4862
4366
  await runWorkRelease(opts);
4863
4367
  } catch (err) {
@@ -4868,12 +4372,12 @@ live addition remains on it.
4868
4372
  throw err;
4869
4373
  }
4870
4374
  });
4871
- 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(
4872
4376
  "after",
4873
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"
4874
4378
  ).action(async (number, opts) => {
4875
4379
  warnIfApiKeyFlag(opts);
4876
- const { runWorkPr, WorkPrCommandError } = await import('./work-pr-cmd-GOYSPEN2.js');
4380
+ const { runWorkPr, WorkPrCommandError } = await import('./work-pr-cmd-6AN6RRQJ.js');
4877
4381
  try {
4878
4382
  await runWorkPr({ ...opts, number });
4879
4383
  } catch (err) {
@@ -4884,17 +4388,35 @@ live addition remains on it.
4884
4388
  throw err;
4885
4389
  }
4886
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
+ });
4887
4408
  }
4888
4409
  function registerRunnerCommands(program2) {
4889
4410
  const runnerCmd = program2.command("runner").description("Take work from the implement and amend queues on this machine");
4890
4411
  const configOption = "the cloud config file to run against (default: ~/.outerlayer/config.json)";
4891
4412
  runnerCmd.command("start").description("Run the loop: claim work, run it, report, clean up, repeat").option("--config <path>", configOption).action(async (opts) => {
4892
- const { defaultRunnerConfigPath, RunnerConfigError } = await import('./config-POF7DEQW.js');
4893
- const { start, RunnerStartError } = await import('./loop-BWE2BS6J.js');
4894
- 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');
4895
4416
  const configPath = opts.config ?? defaultRunnerConfigPath();
4896
4417
  try {
4897
- await start(configPath, { fetchImpl: fetch });
4418
+ const { realUpdateDeps } = await import('./real-deps-SU24ZA2K.js');
4419
+ await start(configPath, { update: realUpdateDeps(), fetchImpl: fetch });
4898
4420
  } catch (err) {
4899
4421
  if (err instanceof RunnerConfigError || err instanceof RunnerAlreadyRunningError || err instanceof RunnerStartError) {
4900
4422
  fail(err.message);
@@ -4904,89 +4426,214 @@ function registerRunnerCommands(program2) {
4904
4426
  }
4905
4427
  });
4906
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) => {
4907
- const { defaultRunnerConfigPath, loadRunnerConfig, checkHooks: checkHooks2, RunnerConfigError } = await import('./config-POF7DEQW.js');
4908
- const { probeCredentials } = await import('./loop-BWE2BS6J.js');
4429
+ const { defaultRunnerConfigPath } = await import('./config-XVJYZ4IQ.js');
4430
+ const { runRunnerCheck } = await import('./check-WQP2CZDW.js');
4909
4431
  const configPath = opts.config ?? defaultRunnerConfigPath();
4910
- let config;
4911
- try {
4912
- config = loadRunnerConfig(configPath);
4913
- } catch (err) {
4914
- fail(err instanceof RunnerConfigError ? err.message : String(err));
4915
- return;
4916
- }
4917
- const hookProblem = checkHooks2(config);
4918
- if (hookProblem) {
4919
- fail(`${configPath}: ${hookProblem}`);
4920
- return;
4921
- }
4922
- const probe = await probeCredentials(config);
4923
- if (!probe.ok && probe.fatal) {
4924
- fail(probe.reason);
4432
+ const result = await runRunnerCheck(configPath);
4433
+ if (!result.ok) {
4434
+ fail(result.message);
4925
4435
  return;
4926
4436
  }
4927
4437
  process.stdout.write(
4928
- probe.ok ? `${C.green}\u2713${C.reset} credentials ok \xB7 ${config.url} \xB7 app ${config.appId}
4929
- ` : `${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}
4930
4440
  `
4931
4441
  );
4932
- const lines = [
4933
- `config ${configPath}`,
4934
- `host ${config.host}`,
4935
- `queues ${config.queues.join(", ")}`,
4936
- `concurrency ${config.concurrency}`,
4937
- `poll ${config.pollSeconds}s`,
4938
- `time limit ${config.timeLimitMinutes}m`,
4939
- `idle limit ${config.idleLimitMinutes}m`,
4940
- ...config.queues.map((queue) => `command ${queue.padEnd(11)} ${config.commands[queue] ?? "(none)"}`),
4941
- `hook provision ${config.hooks.provision}`,
4942
- `hook cleanup ${config.hooks.cleanup}`,
4943
- `hook report ${config.hooks.report ?? "(none)"}`,
4944
- `repos include ${config.repos?.include?.join(", ") ?? "(all)"}`,
4945
- `repos exclude ${config.repos?.exclude?.join(", ") ?? "(none)"}`
4946
- ];
4947
- process.stdout.write(`${lines.join("\n")}
4442
+ process.stdout.write(`${result.lines.join("\n")}
4948
4443
  `);
4949
4444
  });
4950
- runnerCmd.command("init").description("Write a runner block and example hooks into the config file").option("--config <path>", configOption).action(async (opts) => {
4951
- const { defaultRunnerConfigPath } = await import('./config-POF7DEQW.js');
4952
- 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');
4448
+ const configPath = opts.config ?? defaultRunnerConfigPath();
4449
+ try {
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');
4953
4472
  const configPath = opts.config ?? defaultRunnerConfigPath();
4954
4473
  try {
4955
- const result = runRunnerInit(configPath);
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);
4956
4524
  process.stdout.write(`${C.green}\u2713${C.reset} wrote a runner block into ${result.configPath}
4957
4525
  `);
4958
- for (const hook of result.hooks) {
4959
- 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}
4960
4551
  `);
4961
4552
  }
4962
4553
  process.stdout.write(
4963
4554
  `
4964
4555
  Next:
4965
4556
  1. Edit the command lines in ${result.configPath} to start your agent.
4966
- 2. Edit the hooks in ${result.hooksDir} to make and remove a workspace.
4967
- 3. ${C.yellow}outerlayer runner check${C.reset}
4968
- 4. ${C.yellow}outerlayer runner start${C.reset}
4969
- `
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
+ `)
4970
4562
  );
4971
4563
  } catch (err) {
4972
- if (err instanceof RunnerInitError) {
4564
+ if (err instanceof RunnerInitError || err instanceof VmInitError || err instanceof RunnerConfigError) {
4973
4565
  fail(err.message);
4974
4566
  return;
4975
4567
  }
4976
4568
  throw err;
4977
4569
  }
4978
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
+ });
4979
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) => {
4980
- const { defaultRunnerConfigPath } = await import('./config-POF7DEQW.js');
4981
- const { requestStop } = await import('./loop-BWE2BS6J.js');
4627
+ const { defaultRunnerConfigPath } = await import('./config-XVJYZ4IQ.js');
4628
+ const { requestStop } = await import('./loop-VZOLL4WQ.js');
4982
4629
  const configPath = opts.config ?? defaultRunnerConfigPath();
4983
4630
  await requestStop(configPath, Boolean(opts.now));
4984
4631
  });
4985
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) => {
4986
- const { defaultRunnerConfigPath, loadRunnerConfig } = await import('./config-POF7DEQW.js');
4987
- const { jobsDir } = await import('./paths-D2VGWWFI.js');
4988
- const { listJobFiles, runnerProcessState } = await import('./loop-BWE2BS6J.js');
4989
- const { renderStatus } = await import('./status-JHVSZYC5.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');
4990
4637
  const path2 = await import('path');
4991
4638
  const configPath = opts.config ?? defaultRunnerConfigPath();
4992
4639
  const all = listJobFiles(configPath);
@@ -5005,7 +4652,7 @@ Next:
5005
4652
  configPath,
5006
4653
  process: processState,
5007
4654
  concurrency,
5008
- 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) }))
5009
4656
  });
5010
4657
  return;
5011
4658
  }
@@ -5023,9 +4670,9 @@ Next:
5023
4670
  );
5024
4671
  });
5025
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) => {
5026
- const { defaultRunnerConfigPath } = await import('./config-POF7DEQW.js');
5027
- const { listJobFiles } = await import('./loop-BWE2BS6J.js');
5028
- 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');
5029
4676
  const configPath = opts.config ?? defaultRunnerConfigPath();
5030
4677
  const item = parseTrackerNumber(itemArg);
5031
4678
  if (item === void 0) {
@@ -5059,9 +4706,9 @@ function registerHooksCommands(program2) {
5059
4706
  "after",
5060
4707
  "\n`init` auto-wraps PreToolUse/PostToolUse hooks; these subcommands cover the\nrest: manual re-scans, other events, and undo.\n"
5061
4708
  );
5062
- hooksCmd.command("install-git").description("Install the rewrite-journal and commit-guard git hooks in this repo").option("--print", "print the hook scripts instead of installing").addHelpText(
4709
+ hooksCmd.command("install-git").description("Install the commit-guard git hook in this repo").option("--print", "print the hook scripts instead of installing").addHelpText(
5063
4710
  "after",
5064
- "\nThe same install `init` performs. `--print` writes the scripts to stdout for\nrepositories that manage hooks in-tree (husky and friends).\n"
4711
+ "\nThe same install `init` performs. `--print` writes the script to stdout for\nrepositories that manage hooks in-tree (husky and friends).\n"
5065
4712
  ).action((opts) => {
5066
4713
  if (opts.print) {
5067
4714
  for (const [name, script] of managedHookScripts()) {
@@ -5071,38 +4718,17 @@ ${script}
5071
4718
  }
5072
4719
  return;
5073
4720
  }
5074
- const { status, path: path2 } = installPostRewriteHook(process.cwd(), {
5075
- stampTrailers: readCloudConfig().gitHooks?.stampTrailers !== false
5076
- });
5077
- switch (status) {
5078
- case "installed":
5079
- case "updated":
5080
- case "unchanged":
5081
- process.stdout.write(`${C.green}\u2713${C.reset} post-rewrite journal hook ${status} \u2192 ${path2}
4721
+ const removal = removeJournalHooks(process.cwd());
4722
+ if (removal.removed) {
4723
+ process.stdout.write(`${C.green}\u2713${C.reset} removed the commit journal hooks an earlier version installed \u2192 ${removal.path}
5082
4724
  `);
5083
- break;
5084
- case "skipped-foreign":
5085
- process.stdout.write(
5086
- `${C.yellow}!${C.reset} a post-rewrite hook we did not write already exists (${path2}) \u2014 left untouched.
5087
- Append the journal script yourself: ${C.yellow}outerlayer hooks install-git --print${C.reset}
5088
- `
5089
- );
5090
- process.exitCode = 1;
5091
- break;
5092
- case "skipped-custom-hooks-path":
5093
- process.stdout.write(
5094
- `${C.yellow}!${C.reset} this repo manages hooks in its working tree (core.hooksPath) \u2014 nothing written.
5095
- Add the script to your hook manager instead: ${C.yellow}outerlayer hooks install-git --print${C.reset}
5096
- `
5097
- );
5098
- process.exitCode = 1;
5099
- break;
5100
- case "not-a-repo":
5101
- fail("not inside a git repository.");
5102
- break;
5103
4725
  }
5104
4726
  const guard = installPreCommitGuardHook(process.cwd());
5105
4727
  const report = formatGuardInstallOutput(guard);
4728
+ if (report.error !== void 0) {
4729
+ fail(report.error);
4730
+ return;
4731
+ }
5106
4732
  for (const line of report.lines) process.stdout.write(`${line}
5107
4733
  `);
5108
4734
  if (report.exitCode !== void 0) process.exitCode = report.exitCode;
@@ -5265,11 +4891,11 @@ ${C.dim}${file.path}${C.reset}
5265
4891
  }
5266
4892
  function registerMcpCommands(program2) {
5267
4893
  const mcpCmd = program2.command("mcp").description("Configure MCP clients against the OuterLayer gateway");
5268
- 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(
5269
4895
  "after",
5270
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"
5271
4897
  ).action(async (opts) => {
5272
- const { runMcpInstall, handleMcpInstallError, McpInstallError } = await import('./mcp-install-cmd-FDQH6SEN.js');
4898
+ const { runMcpInstall, handleMcpInstallError, McpInstallError } = await import('./mcp-install-cmd-RLK4SO3N.js');
5273
4899
  try {
5274
4900
  if (opts.transport !== void 0 && opts.transport !== "stdio" && opts.transport !== "http") {
5275
4901
  throw new McpInstallError(`--transport must be "stdio" or "http", got "${opts.transport}"`);
@@ -5287,11 +4913,11 @@ function registerMcpCommands(program2) {
5287
4913
  handleMcpInstallError(err);
5288
4914
  }
5289
4915
  });
5290
- 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(
5291
4917
  "after",
5292
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"
5293
4919
  ).action(async (opts) => {
5294
- const { runMcpServe, handleMcpServeError } = await import('./mcp-serve-cmd-57EZZOTL.js');
4920
+ const { runMcpServe, handleMcpServeError } = await import('./mcp-serve-cmd-JPTP5FMS.js');
5295
4921
  try {
5296
4922
  await runMcpServe({ url: opts.url, appId: opts.appId });
5297
4923
  } catch (err) {
@@ -5302,7 +4928,7 @@ function registerMcpCommands(program2) {
5302
4928
  function registerImportCommands(program2) {
5303
4929
  const importCmd = program2.command("import").description("Import an existing config format into .outerlayer/");
5304
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) => {
5305
- const { runImportRuler, ImportRulerError } = await import('./import-ruler-cmd-7LH2QOMG.js');
4931
+ const { runImportRuler, ImportRulerError } = await import('./import-ruler-cmd-GUHL6IY5.js');
5306
4932
  try {
5307
4933
  const result = runImportRuler({ cwd: opts.dir, json: opts.json });
5308
4934
  if (result.exitCode !== 0) process.exitCode = result.exitCode;
@@ -5314,11 +4940,11 @@ function registerImportCommands(program2) {
5314
4940
  throw err;
5315
4941
  }
5316
4942
  });
5317
- 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(
5318
4944
  "after",
5319
- "\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"
5320
4946
  ).action(async (opts) => {
5321
- const { runImportCapture, ImportCaptureError } = await import('./import-capture-cmd-EUMBGIV3.js');
4947
+ const { runImportCapture, ImportCaptureError } = await import('./import-capture-cmd-AVZ4VIVJ.js');
5322
4948
  try {
5323
4949
  runImportCapture({ cwd: opts.dir, json: opts.json });
5324
4950
  } catch (err) {
@@ -5356,15 +4982,21 @@ function registerContextCommands(program2) {
5356
4982
  const contextCmd = program2.command("context").description("Compile and materialize agent context");
5357
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(
5358
4984
  "after",
5359
- '\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'
5360
4986
  ).action(async (opts) => {
5361
4987
  await compileContext({ dir: opts.dir, check: opts.check, json: opts.json });
5362
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
+ });
5363
4995
  contextCmd.command("materialize").description("Materialize this repository's governed context from its control plane").option("--json", "machine-readable JSON output").addHelpText(
5364
4996
  "after",
5365
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"
5366
4998
  ).action(async (opts) => {
5367
- const { materializeContext } = await import('./context-materialize-J6CRQ7O7.js');
4999
+ const { materializeContext } = await import('./context-materialize-G2FUFC72.js');
5368
5000
  const result = await materializeContext({ cwd: process.cwd(), sessionId: `manual-${Date.now()}` });
5369
5001
  const output = formatContextMaterializeOutput(result);
5370
5002
  if (output.exitCode !== void 0) process.exitCode = output.exitCode;