@outerlayer/cli 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +36 -2
  3. package/dist/agent-setup-TPR4BBFV.js +27 -0
  4. package/dist/{artifact-link-cmd-2F47LT7F.js → artifact-link-cmd-W7XF5BFB.js} +4 -3
  5. package/dist/{build-BG2YENGK.js → build-TQ6X5PYN.js} +5 -4
  6. package/dist/build-info.json +1 -1
  7. package/dist/{builder-2D3WSOOY.js → builder-C4QNTYMS.js} +5 -6
  8. package/dist/{check-5XE7XMC4.js → check-UWQAL2MN.js} +56 -52
  9. package/dist/{chunk-R7PF5OOU.js → chunk-3CWUOFV6.js} +21 -14
  10. package/dist/{chunk-4NPZVMJU.js → chunk-3UZTIRLF.js} +60 -4
  11. package/dist/chunk-452TM7J4.js +502 -0
  12. package/dist/{chunk-NNRTPG7X.js → chunk-5GHGAF6P.js} +4 -4
  13. package/dist/{chunk-WVUIRTWA.js → chunk-5MYFOW5V.js} +196 -37
  14. package/dist/{chunk-HGECYFEI.js → chunk-5TN7GLYX.js} +138 -109
  15. package/dist/{chunk-VQYBXPI7.js → chunk-6D7GT22T.js} +5 -5
  16. package/dist/{chunk-G5NPRL2K.js → chunk-6U3MV7TZ.js} +687 -76
  17. package/dist/{chunk-42ZI52JW.js → chunk-6X4SQ5MY.js} +4 -3
  18. package/dist/{chunk-MAK5LYRF.js → chunk-7KSSBQTS.js} +278 -535
  19. package/dist/{chunk-NWQTTSBB.js → chunk-7SJPQWBM.js} +41 -16
  20. package/dist/{chunk-J5M7BSJQ.js → chunk-7VUK5RFS.js} +23 -4
  21. package/dist/{chunk-OZGHL5Y5.js → chunk-7YP7T6GV.js} +2 -2
  22. package/dist/{chunk-TI7B7UEO.js → chunk-AMFWYESY.js} +1 -1
  23. package/dist/{chunk-YRYMNBGQ.js → chunk-ANPNT6PM.js} +2 -1
  24. package/dist/{chunk-4KSQKQXE.js → chunk-BM63HB2M.js} +20 -16
  25. package/dist/{chunk-4Q7OMKAT.js → chunk-BQXFBI7T.js} +81 -5
  26. package/dist/{chunk-ADJRGVZC.js → chunk-BWSUPRDK.js} +1 -1
  27. package/dist/{chunk-3NSNIIBE.js → chunk-CG36IONK.js} +18 -17
  28. package/dist/chunk-CJJDJ7AT.js +56 -0
  29. package/dist/chunk-CRB3POP4.js +34 -0
  30. package/dist/{chunk-LWSJQJC6.js → chunk-DADWEUH3.js} +7 -7
  31. package/dist/{chunk-KG5ECZGJ.js → chunk-DJ3ZUSC3.js} +11 -11
  32. package/dist/{chunk-F47JBIAW.js → chunk-DSF2FWAZ.js} +2 -1
  33. package/dist/{chunk-N5FOE5PS.js → chunk-ECODQ7II.js} +4 -33
  34. package/dist/{chunk-HHJDN6AU.js → chunk-ESVW4MJY.js} +1 -1
  35. package/dist/{chunk-YO4PSLVB.js → chunk-FVMYKL6L.js} +1 -1
  36. package/dist/chunk-G2YNJIGF.js +44 -0
  37. package/dist/{chunk-WUYTWNMA.js → chunk-G3IJ3N6F.js} +5 -3
  38. package/dist/{chunk-QKFB4K7X.js → chunk-GGOMT6KF.js} +1 -1
  39. package/dist/{chunk-N6LURUEF.js → chunk-GOSFARMS.js} +1 -1
  40. package/dist/{chunk-S3NYLI2P.js → chunk-HPJNLETG.js} +93 -38
  41. package/dist/{chunk-IP7BIBXJ.js → chunk-IF2OSI5T.js} +35 -1
  42. package/dist/{chunk-C5Y5ZJJZ.js → chunk-IXYZUUYQ.js} +5 -4
  43. package/dist/chunk-JBJTC3UU.js +35 -0
  44. package/dist/chunk-KYS3TQAG.js +296 -0
  45. package/dist/chunk-LLM3I5YU.js +89 -0
  46. package/dist/{chunk-YU3VKZ5F.js → chunk-LRG3ZMZD.js} +6 -7
  47. package/dist/{chunk-V7NIVOFD.js → chunk-M2ZWWTWZ.js} +1 -1
  48. package/dist/{chunk-ERI5GBNI.js → chunk-MLAAT4S6.js} +3 -3
  49. package/dist/{chunk-O765JR4W.js → chunk-MTP2QEVO.js} +1 -1
  50. package/dist/{chunk-ZM2IMMYN.js → chunk-N2PXGQRL.js} +9 -1
  51. package/dist/{chunk-BWQ66M6N.js → chunk-NFIR3GEE.js} +98 -24
  52. package/dist/{chunk-POJMEEJJ.js → chunk-NNLA7LLQ.js} +1 -1
  53. package/dist/{chunk-PLMTLGYH.js → chunk-NTRCK4NH.js} +5 -4
  54. package/dist/{chunk-22RMZBGW.js → chunk-P53VEAOF.js} +1 -1
  55. package/dist/{chunk-RLYZLUQ7.js → chunk-QEFKA2QD.js} +2 -2
  56. package/dist/{chunk-JXYVA45C.js → chunk-QNLX477Q.js} +1200 -322
  57. package/dist/{chunk-UGP76IZ5.js → chunk-S5PCA3SN.js} +1 -1
  58. package/dist/{chunk-RMWLBUCJ.js → chunk-SADUQSO4.js} +1 -1
  59. package/dist/{chunk-GQD22KKN.js → chunk-T2Q7ISPE.js} +2 -1
  60. package/dist/chunk-TUSYOL2O.js +113 -0
  61. package/dist/{chunk-NBGDBTO2.js → chunk-TWF3RICI.js} +9 -8
  62. package/dist/{chunk-ONKJUM6R.js → chunk-UVG35MWF.js} +1 -1
  63. package/dist/{chunk-KMDLFI4P.js → chunk-VMJPFQNE.js} +1 -1
  64. package/dist/chunk-VRRGENS4.js +144 -0
  65. package/dist/{chunk-PTW6W5NR.js → chunk-VYAL7PUE.js} +1 -1
  66. package/dist/{chunk-OHVJA253.js → chunk-WFSUUUAN.js} +47 -30
  67. package/dist/{chunk-RSNSZ2LJ.js → chunk-X5H2WYMB.js} +1 -1
  68. package/dist/{chunk-HYZRY4KZ.js → chunk-XNGMQ27R.js} +16 -6
  69. package/dist/chunk-XSZD4AZ6.js +490 -0
  70. package/dist/chunk-YQR6E6OV.js +109 -0
  71. package/dist/{chunk-XCK2YAY5.js → chunk-YVZJ6GKF.js} +3 -2
  72. package/dist/{chunk-2MNPHL23.js → chunk-ZHAUSEEN.js} +38 -15
  73. package/dist/claude-launcher-FPPHZPMR.js +115 -0
  74. package/dist/{cli-A76AXELQ.js → cli-HROW5T7X.js} +567 -285
  75. package/dist/{command-R2QCVX55.js → command-OEAV7BVX.js} +42 -40
  76. package/dist/{config-N3C6DFQA.js → config-CLSGABD5.js} +2 -3
  77. package/dist/{connect-cmd-JBKAQNSF.js → connect-cmd-UNYKGDAL.js} +10 -9
  78. package/dist/context-cleanup-HSVYDGMI.js +9 -0
  79. package/dist/control-plane-launch-NYVDJESK.js +216 -0
  80. package/dist/{dist-LKDWQWZ7.js → dist-WO7NOJNG.js} +2 -2
  81. package/dist/{docker-4J77Q2BF.js → docker-BFU3AQGY.js} +7 -8
  82. package/dist/{doctor-4TAIARIG.js → doctor-23BBJWBX.js} +48 -42
  83. package/dist/{emit-artifact-cmd-MPZVR56N.js → emit-artifact-cmd-O6SCM7TV.js} +27 -24
  84. package/dist/emit-cmd-7SP3I6UZ.js +14 -0
  85. package/dist/{emit-criteria-cmd-EFGYI23V.js → emit-criteria-cmd-LQNERZRV.js} +17 -16
  86. package/dist/{emit-finding-cmd-5GF3V4LR.js → emit-finding-cmd-AJPKHIOV.js} +18 -17
  87. package/dist/{emit-result-cmd-I4OADKMD.js → emit-result-cmd-RUICOKWQ.js} +22 -19
  88. package/dist/emit-step-cmd-347XNGPQ.js +26 -0
  89. package/dist/{exec-client-CQV746UF.js → exec-client-7UMESIE2.js} +3 -4
  90. package/dist/governing-source-V5EEGPTA.js +34 -0
  91. package/dist/guest-init-KPBQOFE5.js +9 -0
  92. package/dist/{hook-fast-R4FVSZ2H.js → hook-fast-YAZG5MDX.js} +5 -2
  93. package/dist/host-key-U4B5QETR.js +8 -0
  94. package/dist/{import-capture-cmd-EQ35EARA.js → import-capture-cmd-KKFFOGWP.js} +7 -5
  95. package/dist/{import-ruler-cmd-BGAZXCQM.js → import-ruler-cmd-5P6JOXQN.js} +5 -3
  96. package/dist/index.js +8 -4
  97. package/dist/{init-PWU5YVL7.js → init-LWHDMZQG.js} +40 -38
  98. package/dist/init-MOINY2DD.js +53 -0
  99. package/dist/{init-cmd-XAVO7HYZ.js → init-cmd-HSMRID6A.js} +75 -30
  100. package/dist/{init-update-CVCWD6VS.js → init-update-LWX5MPPR.js} +29 -29
  101. package/dist/{install-HDBIPHTX.js → install-4SEFIIW2.js} +4 -5
  102. package/dist/{install-cmd-5HYMXLEW.js → install-cmd-QDOTR4OM.js} +5 -4
  103. package/dist/{login-browser-QZCS3WW6.js → login-browser-DX35MARZ.js} +5 -4
  104. package/dist/{logout-cmd-QAUFDJ6J.js → logout-cmd-5V5MMBSY.js} +3 -2
  105. package/dist/{logs-H55NWW4H.js → logs-AMLKC3VP.js} +3 -1
  106. package/dist/loop-SYYPVFYW.js +77 -0
  107. package/dist/machine-6X63G2KH.js +48 -0
  108. package/dist/{mcp-install-cmd-FDTPQQS6.js → mcp-install-cmd-GGWB3JJ2.js} +3 -2
  109. package/dist/{mcp-serve-cmd-3KVYEUXG.js → mcp-serve-cmd-N3UTY6EE.js} +11 -9
  110. package/dist/paths-7XP63OAX.js +7 -0
  111. package/dist/pending-532ABZYC.js +8 -0
  112. package/dist/{pidfile-UZRH774M.js → pidfile-3DM4OBKO.js} +4 -2
  113. package/dist/{record-C43U4PKS.js → record-3WP6V2K5.js} +3 -2
  114. package/dist/{runner-env-EN7YE7QX.js → runner-env-FRZFFHDF.js} +5 -6
  115. package/dist/{runner-key-6SLBI7PP.js → runner-key-276SJU5R.js} +40 -38
  116. package/dist/session-sender-GGIY5OAX.js +36 -0
  117. package/dist/{pending-MXLSRYMO.js → shell-function-NRD4KVUK.js} +1 -2
  118. package/dist/spec-cmd-SIZBRN7D.js +115 -0
  119. package/dist/{starter-pack-KF66HGZR.js → starter-pack-6LQVLBIA.js} +7 -5
  120. package/dist/{status-7VNQN5PW.js → status-NAK6DYSF.js} +46 -42
  121. package/dist/{statusline-fast-SVTMBMD7.js → statusline-fast-KN3BX67H.js} +2 -1
  122. package/dist/sync-cmd-G7GF4UTW.js +35 -0
  123. package/dist/{watch-23HSAEI6.js → watch-EVM5GSMP.js} +22 -15
  124. package/dist/{work-claim-cmd-LKL3GHFQ.js → work-claim-cmd-3J3QW56U.js} +26 -29
  125. package/dist/work-cmd-5EPPD33Y.js +24 -0
  126. package/dist/{work-comment-cmd-AWHP56M2.js → work-comment-cmd-2ACOZQAE.js} +16 -15
  127. package/dist/{work-launch-R5HTAP6X.js → work-launch-LEQ2MPM3.js} +3 -1
  128. package/dist/work-logs-cmd-QDD2NMJL.js +60 -0
  129. package/dist/{work-open-pr-cmd-IOM4AVQQ.js → work-open-pr-cmd-XNCEI4ZM.js} +18 -17
  130. package/dist/work-pr-cmd-H4OQR26N.js +25 -0
  131. package/package.json +3 -2
  132. package/skill-pack/maintained/outerlayer-amend/SKILL.md +4 -2
  133. package/skill-pack/maintained/outerlayer-cli/SKILL.md +9 -2
  134. package/skill-pack/maintained/outerlayer-cli/references/find-out-why-a-build-ended.md +76 -0
  135. package/skill-pack/shipped.json +18 -2
  136. package/skill-pack/template/build/SKILL.md +23 -10
  137. package/skill-pack/template/build/references/agent-briefs.md +3 -1
  138. package/skill-pack/template/build/references/criteria-judge.md +27 -0
  139. package/skill-pack/template/build/references/release.md +22 -29
  140. package/skill-pack/template/spec/SKILL.md +35 -10
  141. package/skill-pack/validators/acceptance-criteria.yaml +25 -26
  142. package/dist/agent-setup-T624CWPD.js +0 -27
  143. package/dist/chunk-6Y5RUTHO.js +0 -39
  144. package/dist/chunk-BYT5O5RV.js +0 -61
  145. package/dist/chunk-WTE3MQML.js +0 -133
  146. package/dist/chunk-XCCLFVXM.js +0 -401
  147. package/dist/chunk-YZ5F4PIS.js +0 -797
  148. package/dist/context-adopt-NFHCLVUB.js +0 -108
  149. package/dist/context-materialize-IV2TUUFO.js +0 -22
  150. package/dist/emit-cmd-IJ3GVRFD.js +0 -12
  151. package/dist/guest-init-LJP5GPZF.js +0 -9
  152. package/dist/host-key-YMLXY6QB.js +0 -7
  153. package/dist/init-6XPDRDTC.js +0 -51
  154. package/dist/loop-FK3LPMMZ.js +0 -73
  155. package/dist/machine-SQXEBAM2.js +0 -46
  156. package/dist/paths-OYKMVYJP.js +0 -6
  157. package/dist/session-sender-FO3LCREV.js +0 -29
  158. package/dist/sync-cmd-GVPPFZ3G.js +0 -28
  159. package/dist/work-cmd-Z2WXLZPH.js +0 -23
  160. package/dist/work-pr-cmd-HNEUIEN7.js +0 -24
@@ -1,37 +1,40 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
- import { gatherDoctorChecks, doctorExitCode, summarizeChecks, renderDoctorText, recordWrapped, recordUnwrapped, recordRemoval, readManifest, recordInstall, recordStatusline } from './chunk-HGECYFEI.js';
4
- import './chunk-NBGDBTO2.js';
5
- import './chunk-S3NYLI2P.js';
6
- import './chunk-6Y5RUTHO.js';
7
- import './chunk-IP7BIBXJ.js';
8
- import './chunk-F47JBIAW.js';
3
+ import { gatherDoctorChecks, doctorExitCode, summarizeChecks, renderDoctorText, recordWrapped, recordUnwrapped, recordRemoval, readManifest, recordInstall, recordStatusline } from './chunk-5TN7GLYX.js';
4
+ import './chunk-TWF3RICI.js';
5
+ import { verifiedTrackerNames, TRACKERS } from './chunk-HPJNLETG.js';
6
+ import './chunk-IF2OSI5T.js';
7
+ import './chunk-CG36IONK.js';
8
+ import './chunk-G3IJ3N6F.js';
9
+ import './chunk-BM63HB2M.js';
10
+ import { hookFiredMarkerPath, installedCliDistDir } from './chunk-NFIR3GEE.js';
11
+ import './chunk-DSF2FWAZ.js';
9
12
  import './chunk-BKO6JEZI.js';
10
- import './chunk-XCK2YAY5.js';
11
- import './chunk-42ZI52JW.js';
12
- import './chunk-3NSNIIBE.js';
13
- import './chunk-WUYTWNMA.js';
14
- import './chunk-JXYVA45C.js';
15
- import './chunk-4KSQKQXE.js';
16
- import './chunk-N6LURUEF.js';
17
- import { C, NO_COLOR_FLAG, warnDeprecated, stripNoColor, API_KEY_FLAG_DEPRECATION } from './chunk-YRYMNBGQ.js';
13
+ import './chunk-QNLX477Q.js';
14
+ import './chunk-YVZJ6GKF.js';
15
+ import './chunk-6X4SQ5MY.js';
16
+ import './chunk-GOSFARMS.js';
17
+ import { C, warnDeprecated, API_KEY_FLAG_DEPRECATION, NO_COLOR_FLAG, stripNoColor } from './chunk-ANPNT6PM.js';
18
18
  import { parseTrackerNumber, trackerNumberError } from './chunk-WZBHBH3J.js';
19
19
  import { settingsPath, readSettings, SettingsParseError, listHookCandidates, wrapHooks, writeSettings, unwrapHooks, REGISTERED_EVENTS, cliCommand, removeHooks, removeStatusline, cliBinResolvable, mergeHooks, repointWrappedHooks, AUTO_WRAP_EVENTS, mergeStatusline } from './chunk-5QRP3MVS.js';
20
- import { pinExists } from './chunk-WTE3MQML.js';
21
- import './chunk-C5Y5ZJJZ.js';
22
- import './chunk-YO4PSLVB.js';
23
- import './chunk-POJMEEJJ.js';
24
- import './chunk-OHVJA253.js';
25
- import './chunk-PLMTLGYH.js';
26
- import './chunk-J5M7BSJQ.js';
20
+ import './chunk-IXYZUUYQ.js';
21
+ import './chunk-FVMYKL6L.js';
22
+ import './chunk-NTRCK4NH.js';
23
+ import './chunk-ZJSNLVCE.js';
24
+ import { findGitDirs } from './chunk-BJ3KTMDH.js';
25
+ import './chunk-G2YNJIGF.js';
26
+ import './chunk-NNLA7LLQ.js';
27
+ import './chunk-WFSUUUAN.js';
28
+ import './chunk-7VUK5RFS.js';
27
29
  import './chunk-EABW6AJQ.js';
28
- import { resolveVersion } from './chunk-RA3O54FC.js';
29
- import './chunk-V7NIVOFD.js';
30
- import './chunk-ZM2IMMYN.js';
31
- import { hookFiredMarkerPath, installedCliDistDir } from './chunk-BWQ66M6N.js';
32
- import { managedHookScripts, removeJournalHooks, installPreCommitGuardHook, removePreCommitGuardHook } from './chunk-XCCLFVXM.js';
33
- import './chunk-BJ3KTMDH.js';
30
+ import { cliInvocation, packageRunnerOf } from './chunk-CRB3POP4.js';
31
+ import { cloneIsGoverned } from './chunk-YQR6E6OV.js';
32
+ import { removeJournalHooks, removePreCommitGuardHook } from './chunk-TUSYOL2O.js';
34
33
  import { homeFromEnv } from './chunk-VNVZDWO3.js';
34
+ import { resolveVersion } from './chunk-RA3O54FC.js';
35
+ import './chunk-M2ZWWTWZ.js';
36
+ import './chunk-N2PXGQRL.js';
37
+ import './chunk-BQXFBI7T.js';
35
38
  import './chunk-Z5GEFMRW.js';
36
39
  import { EventEmitter } from 'events';
37
40
  import childProcess, { execFileSync } from 'child_process';
@@ -3400,7 +3403,7 @@ function useColor() {
3400
3403
  // ../../node_modules/commander/index.js
3401
3404
  new Command();
3402
3405
  function isEphemeralBin(bin) {
3403
- return bin.replace(/\\/g, "/").split("/").some((p) => p === "_npx" || p === "dlx" || p.startsWith("dlx-") || p.startsWith("bunx-"));
3406
+ return packageRunnerOf(bin) !== void 0;
3404
3407
  }
3405
3408
  function initHookBin(opts) {
3406
3409
  const { runningBin } = opts;
@@ -3544,7 +3547,6 @@ function runInit(opts) {
3544
3547
  recordStatusline(manifestHome, path2, statuslineWrappedCommand !== void 0 ? "wrapped" : "installed", statuslineWrappedCommand);
3545
3548
  }
3546
3549
  const gitHookRemoval = removeJournalHooks(opts.cwd ?? process.cwd());
3547
- const guardHook = installPreCommitGuardHook(opts.cwd ?? process.cwd());
3548
3550
  let gitignoreUpdated = false;
3549
3551
  if (scope === "project" && opts.addGitignore) {
3550
3552
  gitignoreUpdated = ensureGitignore(opts.cwd ?? process.cwd());
@@ -3556,7 +3558,6 @@ function runInit(opts) {
3556
3558
  events: REGISTERED_EVENTS,
3557
3559
  gitignoreUpdated,
3558
3560
  ...gitHookRemoval.removed ? { gitHookRemoved: true } : {},
3559
- guardHook,
3560
3561
  wrapped,
3561
3562
  ...statusline ? { statusline } : {},
3562
3563
  ...statuslineWrappedCommand ? { statuslineWrappedCommand } : {}
@@ -3574,8 +3575,12 @@ function tracksOwnOuterlayerDir(cwd) {
3574
3575
  return false;
3575
3576
  }
3576
3577
  }
3578
+ function isGovernedCheckout(cwd) {
3579
+ const dirs = findGitDirs(cwd);
3580
+ return dirs !== void 0 && cloneIsGoverned(dirs.gitCommonDir);
3581
+ }
3577
3582
  function ensureGitignore(cwd) {
3578
- if (tracksOwnOuterlayerDir(cwd) || pinExists(cwd)) return false;
3583
+ if (tracksOwnOuterlayerDir(cwd) || isGovernedCheckout(cwd)) return false;
3579
3584
  const gitignore = join(cwd, ".gitignore");
3580
3585
  const entry = ".outerlayer/";
3581
3586
  let contents = "";
@@ -3610,11 +3615,12 @@ function orgRolloutSnippet(cliBin) {
3610
3615
  "managed-settings.json:",
3611
3616
  managed,
3612
3617
  "",
3613
- "Each developer still runs `outerlayer login` once to attribute sessions."
3618
+ `Each developer still runs \`${cliInvocation()} login\` once to attribute sessions.`
3614
3619
  ].join("\n");
3615
3620
  }
3616
3621
 
3617
3622
  // src/cli.ts
3623
+ var TRACKER_OPTION_FLAG = `--tracker <${verifiedTrackerNames(TRACKERS).join("|")}>`;
3618
3624
  function resolveCliBin() {
3619
3625
  try {
3620
3626
  return fileURLToPath(new URL("./index.js", import.meta.url));
@@ -3640,13 +3646,10 @@ function writeJson(value) {
3640
3646
  `);
3641
3647
  }
3642
3648
  function apiKeyOption() {
3643
- return new Option("--api-key <key>", "deprecated: use `outerlayer login` or OUTERLAYER_API_KEY").hideHelp();
3644
- }
3645
- function warnIfApiKeyFlag(opts) {
3646
- if (opts.apiKey !== void 0) warnDeprecated(API_KEY_FLAG_DEPRECATION);
3649
+ return new Option("--api-key <key>", `deprecated: use \`${cliInvocation()} login\` or OUTERLAYER_API_KEY`).hideHelp();
3647
3650
  }
3648
3651
  async function installStarterPackInRepo() {
3649
- const { installStarterPack, formatStarterPackOutput } = await import('./starter-pack-KF66HGZR.js');
3652
+ const { installStarterPack, formatStarterPackOutput } = await import('./starter-pack-6LQVLBIA.js');
3650
3653
  const cwd = process.cwd();
3651
3654
  let repoRoot = cwd;
3652
3655
  try {
@@ -3687,7 +3690,7 @@ ${C.dim} backup: ${result.backupPath}${C.reset}` : ""}
3687
3690
  }
3688
3691
  if (result.wrapped.length > 0) {
3689
3692
  lines.push(
3690
- `${C.green}\u2713${C.reset} Auto-wrapped ${result.wrapped.length} hook(s) for execution evidence (adds one spawn per firing \u2014 see ${C.yellow}outerlayer hooks unwrap${C.reset} to undo):
3693
+ `${C.green}\u2713${C.reset} Auto-wrapped ${result.wrapped.length} hook(s) for execution evidence (adds one spawn per firing \u2014 see ${C.yellow}${cliInvocation()} hooks unwrap${C.reset} to undo):
3691
3694
  `
3692
3695
  );
3693
3696
  for (const w of result.wrapped) {
@@ -3729,45 +3732,15 @@ cannot be disabled.
3729
3732
  (opts.next === false ? "\n" : `
3730
3733
  Next: launch a session naming its work item, then sync it:
3731
3734
  ${C.yellow}OUTERLAYER_WORK=42 claude${C.reset}
3732
- ${C.yellow}outerlayer sync${C.reset}
3735
+ ${C.yellow}${cliInvocation()} sync${C.reset}
3733
3736
 
3734
- `) + (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")
3737
+ `) + (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 ${cliInvocation()} login and ${cliInvocation()} init in this repository,
3738
+ launched sessions sync automatically in the background after each agent
3739
+ turn and at the end of the session, at most once every five minutes.
3740
+ `)
3735
3741
  );
3736
3742
  return lines.join("");
3737
3743
  }
3738
- function formatGuardInstallOutput(guard) {
3739
- switch (guard.status) {
3740
- case "installed":
3741
- case "updated":
3742
- case "unchanged":
3743
- return { lines: [`${C.green}\u2713${C.reset} materialized-context commit guard ${guard.status} \u2192 ${guard.path}`] };
3744
- case "skipped-foreign":
3745
- if (guard.adopted) {
3746
- return { lines: [`${C.green}\u2713${C.reset} materialized-context commit guard hand-adopted into ${guard.path} \u2014 left untouched.`] };
3747
- }
3748
- return {
3749
- lines: [
3750
- `${C.yellow}!${C.reset} a pre-commit hook we did not write already exists (${guard.path}) \u2014 left untouched.`,
3751
- ` Append the guard script yourself: ${C.yellow}outerlayer hooks install-git --print${C.reset}`,
3752
- ` Place it before any exit in the existing hook \u2014 code after an early exit never runs.`
3753
- ],
3754
- exitCode: 1
3755
- };
3756
- case "skipped-custom-hooks-path":
3757
- if (guard.adopted) {
3758
- return { lines: [`${C.green}\u2713${C.reset} materialized-context commit guard hand-adopted at ${guard.path} (core.hooksPath) \u2014 left untouched.`] };
3759
- }
3760
- return {
3761
- lines: [
3762
- `${C.yellow}!${C.reset} this repo manages hooks in its working tree (core.hooksPath) \u2014 nothing written.`,
3763
- ` Add the script to your hook manager instead: ${C.yellow}outerlayer hooks install-git --print${C.reset}`
3764
- ],
3765
- exitCode: 1
3766
- };
3767
- case "not-a-repo":
3768
- return { lines: [], error: "not inside a git repository." };
3769
- }
3770
- }
3771
3744
  async function installHooks(opts, resolvedBin, connected, starterTemplate, withNext = true) {
3772
3745
  const scope = opts.project ? "project" : "user";
3773
3746
  let cliBin;
@@ -3775,7 +3748,7 @@ async function installHooks(opts, resolvedBin, connected, starterTemplate, withN
3775
3748
  cliBin = initHookBin({ home: homeFromEnv(), runningBin: resolvedBin, remove: opts.remove === true });
3776
3749
  } catch (err) {
3777
3750
  const text = `could not copy the CLI to ~/.outerlayer/cli: ${err instanceof Error ? err.message : String(err)}
3778
- Hooks were not changed. Fix the permissions on ~/.outerlayer, then re-run ${C.yellow}outerlayer init${C.reset}.`;
3751
+ Hooks were not changed. Fix the permissions on ~/.outerlayer, then re-run ${C.yellow}${cliInvocation()} init${C.reset}.`;
3779
3752
  return { ok: false, text, detail: "could not copy the CLI to ~/.outerlayer/cli" };
3780
3753
  }
3781
3754
  try {
@@ -3789,7 +3762,7 @@ async function installHooks(opts, resolvedBin, connected, starterTemplate, withN
3789
3762
  });
3790
3763
  if (result.cliBinUnresolved) {
3791
3764
  const text = `the resolved CLI path (${cliBin}) does not exist \u2014 refusing to write hook entries pointing at it.
3792
- Reinstall OuterLayer, then re-run ${C.yellow}outerlayer init${C.reset}.`;
3765
+ Reinstall OuterLayer, then re-run ${C.yellow}${cliInvocation()} init${C.reset}.`;
3793
3766
  return { ok: false, text, detail: "the CLI path does not exist" };
3794
3767
  }
3795
3768
  const starter = starterTemplate === void 0 ? null : await installStarterPackInRepo();
@@ -3806,8 +3779,8 @@ ${starter.output}`),
3806
3779
  }
3807
3780
  }
3808
3781
  async function realConnectDepsForCwd() {
3809
- const { realConnectDeps } = await import('./connect-cmd-JBKAQNSF.js');
3810
- const { resolveRepoIdentity } = await import('./dist-LKDWQWZ7.js');
3782
+ const { realConnectDeps } = await import('./connect-cmd-UNYKGDAL.js');
3783
+ const { resolveRepoIdentity } = await import('./dist-WO7NOJNG.js');
3811
3784
  return {
3812
3785
  ...realConnectDeps(),
3813
3786
  home: homeFromEnv(),
@@ -3822,13 +3795,55 @@ function invokedCommandToken(argv) {
3822
3795
  }
3823
3796
  function buildProgram(argv = process.argv) {
3824
3797
  const program2 = new Command();
3825
- 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);
3826
- 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("--update", "update the files init wrote that you have not edited, and write missing ones").option("--dry-run", "with --update: print the plan and write nothing").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(
3798
+ program2.name(cliInvocation()).description("OuterLayer \u2014 see what your coding agents actually do").enablePositionalOptions().version(resolveVersion()).hook("preAction", (_root, command) => {
3799
+ if (command.opts().apiKey !== void 0) warnDeprecated(API_KEY_FLAG_DEPRECATION);
3800
+ }).option(NO_COLOR_FLAG, "disable color (NO_COLOR=1 does the same)").addHelpText("after", SUPPORT_TEXT);
3801
+ 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 \`${cliInvocation()} connect\`)`).option("--gitignore", "also add .outerlayer/ to .gitignore (project scope)").option("--template <name>", "install the starter skills and a policy file (only: default)").option("--update", "update the files init wrote that you have not edited, and write missing ones").option("--dry-run", "with --update: print the plan and write nothing").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("--shell-function", "make plain claude run `outerlayer claude` (edits ~/.zshrc or ~/.bashrc)").option("--no-shell-function", "do not set up the shell function, and remove it if it is there").option("--json", "machine-readable JSON output").addHelpText(
3827
3802
  "after",
3828
- "\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 and outerlayer-docs MCP servers to\n.mcp.json. Nothing you wrote is changed: an existing config, policy, edited\nskill, MCP entry or root CLAUDE.md and AGENTS.md is left as it is, and a\nhand-written file the emit would replace is kept and listed. The files are\nleft 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\n--update brings the files init wrote once up to this CLI's version: the\ntemplate skills, the policy, the two starter checks and the two MCP\nentries. A file still equal to a version an earlier release wrote is\nreplaced, and a missing one is written. A file you edited is never\ntouched. It then runs `outerlayer context emit` and leaves the changes\nuncommitted. --dry-run prints the plan, with a command to compare each\nedited file, and writes nothing. `outerlayer doctor` reports the same\nstates under Starter files.\n"
3803
+ `
3804
+ In a repository, init runs four steps: connect the repository to a
3805
+ factory (\`${cliInvocation()} connect\`), install the hooks, set up the coding agent,
3806
+ and run \`${cliInvocation()} doctor\`. It never signs in. With no
3807
+ login it skips the connect step and says to run \`${cliInvocation()} login\`. With
3808
+ --json it prints one document naming the outcome of each step: done,
3809
+ skipped or failed. The exit code is 1 when a step failed.
3810
+
3811
+ The agent setup step installs the starter skills and the \`outerlayer\` skill
3812
+ into .outerlayer/skills/, writes .outerlayer/config.json for Claude Code when
3813
+ there is none, runs \`${cliInvocation()} context emit\` so the skills appear in
3814
+ .claude/skills/, and adds the outerlayer and outerlayer-docs MCP servers to
3815
+ .mcp.json. Nothing you wrote is changed: an existing config, policy, edited
3816
+ skill, MCP entry or root CLAUDE.md and AGENTS.md is left as it is, and a
3817
+ hand-written file the emit would replace is kept and listed. The files are
3818
+ left uncommitted.
3819
+ The step is skipped outside a git repository and in a repository whose
3820
+ context comes from a control plane, where it arrives at session start.
3821
+
3822
+ --local installs only the hooks: no connect step, no agent setup and no
3823
+ network call. Add \`--template default\` to also write the starter skills and
3824
+ a policy file into .outerlayer/ without compiling them; \`${cliInvocation()} context
3825
+ emit\` does that.
3826
+
3827
+ Idempotent: settings are backed up before any write, and a second run
3828
+ changes nothing. Wrapping adds one spawn per hook firing; undo it with
3829
+ \`${cliInvocation()} hooks unwrap\`. An occupied status-line slot is wrapped, never
3830
+ replaced.
3831
+
3832
+ Three of the starter skills are copied once and are yours to edit. The
3833
+ others are rewritten from this CLI on every \`context emit\`.
3834
+
3835
+ --update brings the files init wrote once up to this CLI's version: the
3836
+ template skills, the policy, the two starter checks and the two MCP
3837
+ entries. A file still equal to a version an earlier release wrote is
3838
+ replaced, and a missing one is written. A file you edited is never
3839
+ touched. It then runs \`${cliInvocation()} context emit\` and leaves the changes
3840
+ uncommitted. --dry-run prints the plan, with a command to compare each
3841
+ edited file, and writes nothing. \`${cliInvocation()} doctor\` reports the same
3842
+ states under Starter files.
3843
+ `
3829
3844
  ).action(async (opts) => {
3830
3845
  if (opts.update || opts.dryRun) {
3831
- const { runInitUpdate, formatInitUpdate, updateFlagProblem } = await import('./init-update-CVCWD6VS.js');
3846
+ const { runInitUpdate, formatInitUpdate, updateFlagProblem } = await import('./init-update-LWX5MPPR.js');
3832
3847
  const problem = updateFlagProblem(opts);
3833
3848
  if (problem !== void 0) {
3834
3849
  fail(problem);
@@ -3842,7 +3857,7 @@ function buildProgram(argv = process.argv) {
3842
3857
  }
3843
3858
  let starterTemplate;
3844
3859
  if (opts.template !== void 0) {
3845
- const { assertKnownTemplate, StarterPackError } = await import('./starter-pack-KF66HGZR.js');
3860
+ const { assertKnownTemplate, StarterPackError } = await import('./starter-pack-6LQVLBIA.js');
3846
3861
  try {
3847
3862
  assertKnownTemplate(opts.template);
3848
3863
  } catch (err) {
@@ -3869,13 +3884,19 @@ function buildProgram(argv = process.argv) {
3869
3884
  fail(outcome.text);
3870
3885
  return;
3871
3886
  }
3887
+ if (opts.remove) {
3888
+ const { removeShellFunction } = await import('./shell-function-NRD4KVUK.js');
3889
+ const changed = removeShellFunction({ home: homeFromEnv() });
3890
+ if (changed.length > 0 && !opts.json) process.stdout.write(`Removed the claude function from ${changed.join(", ")}.
3891
+ `);
3892
+ }
3872
3893
  if (opts.json) writeJson(outcome.result);
3873
3894
  else process.stdout.write(outcome.text);
3874
3895
  return;
3875
3896
  }
3876
- const { runInitCommand } = await import('./init-cmd-XAVO7HYZ.js');
3897
+ const { runInitCommand } = await import('./init-cmd-HSMRID6A.js');
3877
3898
  const code = await runInitCommand(
3878
- { json: opts.json, factory: opts.factory },
3899
+ { json: opts.json, factory: opts.factory, shellFunction: opts.shellFunction },
3879
3900
  {
3880
3901
  connect: await realConnectDepsForCwd(),
3881
3902
  stdout: process.stdout,
@@ -3885,19 +3906,55 @@ function buildProgram(argv = process.argv) {
3885
3906
  return { ok, text, detail };
3886
3907
  },
3887
3908
  runAgentSetup: async () => {
3888
- const { runAgentSetup } = await import('./agent-setup-T624CWPD.js');
3909
+ const { runAgentSetup } = await import('./agent-setup-TPR4BBFV.js');
3889
3910
  return runAgentSetup({ cwd: process.cwd() });
3890
3911
  },
3891
- runDoctorChecks: gatherDoctorChecks
3912
+ runDoctorChecks: gatherDoctorChecks,
3913
+ shell: {
3914
+ home: homeFromEnv(),
3915
+ shellPath: process.env.SHELL,
3916
+ interactive: Boolean(process.stdin.isTTY && process.stderr.isTTY) && opts.json !== true,
3917
+ ask: async () => {
3918
+ const { promptOnTerminal } = await import('./connect-cmd-UNYKGDAL.js');
3919
+ const answer = await promptOnTerminal({
3920
+ message: `Should plain claude start Claude Code through ${cliInvocation()} claude? That is how a session in a governed repository gets its control plane. It adds one block to your zsh or bash startup file.`,
3921
+ options: [
3922
+ { label: "Yes, add the block", value: "yes" },
3923
+ { label: "No, I will run outerlayer claude myself", value: "no" }
3924
+ ]
3925
+ });
3926
+ return answer === "yes";
3927
+ }
3928
+ }
3892
3929
  }
3893
3930
  );
3894
3931
  if (code !== 0) process.exitCode = code;
3895
3932
  });
3896
3933
  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(
3897
3934
  "after",
3898
- "\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"
3935
+ `
3936
+ Run it in a repository, after \`${cliInvocation()} login\`. With a login, connect lists
3937
+ the factories you can use and takes the first rule that leaves one answer:
3938
+ 1. --factory <org>/<factory>, or a factory id
3939
+ 2. the factory this repository is already connected to
3940
+ 3. the one factory this repository is linked to
3941
+ 4. the one factory you can use
3942
+ 5. on a terminal, ask; with no terminal, exit 1 and list each --factory value
3943
+
3944
+ It says what it chose and why. The choice is saved in this repository's git
3945
+ directory (outerlayer/connection.json), shared by every worktree of the
3946
+ clone and never committed. A factory key already belongs to one factory, so
3947
+ connect changes nothing for it. Connect never links a repository.
3948
+
3949
+ After it saves the choice, connect checks that this repository is linked to
3950
+ the factory. If it is not, connect says so, prints the factory's Work page
3951
+ link where you link it, and exits 2. The choice is saved either way. Run
3952
+ connect again once the repository is linked. With --json, the document has
3953
+ linked (true, false, or null when it could not be checked) and, when false,
3954
+ linkUrl.
3955
+ `
3899
3956
  ).action(async (opts) => {
3900
- const { runConnectCommand } = await import('./connect-cmd-JBKAQNSF.js');
3957
+ const { runConnectCommand } = await import('./connect-cmd-UNYKGDAL.js');
3901
3958
  const code = await runConnectCommand({ factory: opts.factory, json: opts.json }, await realConnectDepsForCwd(), {
3902
3959
  stdout: process.stdout,
3903
3960
  stderr: process.stderr
@@ -3915,9 +3972,28 @@ function buildProgram(argv = process.argv) {
3915
3972
  });
3916
3973
  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(
3917
3974
  "after",
3918
- '\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'
3975
+ `
3976
+ With no key on stdin, login prints an approve link and a code. A signed-in
3977
+ person approves it on the dashboard. The login is a token for that person's
3978
+ account, made for this machine, saved here and never printed. It picks no
3979
+ factory: run \`${cliInvocation()} connect\` for that. \`${cliInvocation()} logout\` revokes it.
3980
+
3981
+ When stdout is not a terminal, login does not wait. It prints one JSON
3982
+ document with the link, the code and a \`login --check <id>\` command, and
3983
+ exits 75. Run the check until it stops exiting 75: 0 is signed in, 1 is
3984
+ denied or expired. A dropped connection or a 5xx also exits 75.
3985
+
3986
+ A factory key piped on stdin is saved as it always was, never as a flag and
3987
+ never printed back. A factory key always wins over the login, so CI and
3988
+ runners keep acting as their key.
3989
+
3990
+ echo "$OUTERLAYER_KEY" | ${cliInvocation()} login --url <gateway url> --app-id <factory id>
3991
+
3992
+ Every later command reads the saved file, so none of them needs a key on
3993
+ its command line. A saved key is checked for real on the first \`sync\`.
3994
+ `
3919
3995
  ).action(async (opts) => {
3920
- const { runLoginCommand, realLoginDeps } = await import('./login-browser-QZCS3WW6.js');
3996
+ const { runLoginCommand, realLoginDeps } = await import('./login-browser-DX35MARZ.js');
3921
3997
  const code = await runLoginCommand(
3922
3998
  {
3923
3999
  browser: opts.browser !== false,
@@ -3934,9 +4010,14 @@ function buildProgram(argv = process.argv) {
3934
4010
  });
3935
4011
  program2.command("logout").description("Revoke this machine's login and remove it").addHelpText(
3936
4012
  "after",
3937
- "\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"
4013
+ `
4014
+ Revokes the token \`${cliInvocation()} login\` saved, then deletes it from the config
4015
+ file. A factory key and the chosen factory are left alone. When the
4016
+ dashboard cannot be reached the local copy is still deleted and the exit
4017
+ code is 1: revoke the token on /profile/security.
4018
+ `
3938
4019
  ).action(async () => {
3939
- const { runLogoutCommand, realLogoutDeps } = await import('./logout-cmd-QAUFDJ6J.js');
4020
+ const { runLogoutCommand, realLogoutDeps } = await import('./logout-cmd-5V5MMBSY.js');
3940
4021
  const code = await runLogoutCommand(realLogoutDeps());
3941
4022
  if (code !== 0) process.exitCode = code;
3942
4023
  });
@@ -3944,15 +4025,14 @@ function buildProgram(argv = process.argv) {
3944
4025
  "after",
3945
4026
  "\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"
3946
4027
  ).action(async (opts) => {
3947
- warnIfApiKeyFlag(opts);
3948
4028
  if (opts.link !== void 0) {
3949
- const { lookupArtifactLink } = await import('./artifact-link-cmd-2F47LT7F.js');
4029
+ const { lookupArtifactLink } = await import('./artifact-link-cmd-W7XF5BFB.js');
3950
4030
  const found = lookupArtifactLink(opts.link);
3951
4031
  if (found.ok) process.stdout.write(found.url + "\n");
3952
4032
  else fail(found.reason);
3953
4033
  return;
3954
4034
  }
3955
- const { runSync, SyncConfigError, SyncTransportError } = await import('./sync-cmd-GVPPFZ3G.js');
4035
+ const { runSync, SyncConfigError, SyncTransportError } = await import('./sync-cmd-G7GF4UTW.js');
3956
4036
  try {
3957
4037
  const result = await runSync({
3958
4038
  url: opts.url,
@@ -3977,11 +4057,13 @@ function buildProgram(argv = process.argv) {
3977
4057
  throw err;
3978
4058
  }
3979
4059
  });
3980
- program2.command("daemon").alias("watch").description("Run the copy-out daemon that mirrors transcripts before they are deleted").option("--once", "do a single mirror sweep and exit").addHelpText("after", "\n`outerlayer watch` is the former name and still works, with a warning.\n").action(async (opts) => {
4060
+ program2.command("daemon").alias("watch").description("Run the copy-out daemon that mirrors transcripts before they are deleted").option("--once", "do a single mirror sweep and exit").addHelpText("after", `
4061
+ \`${cliInvocation()} watch\` is the former name and still works, with a warning.
4062
+ `).action(async (opts) => {
3981
4063
  if (invokedCommandToken(argv) === "watch") {
3982
- warnDeprecated("`outerlayer watch` is now `outerlayer daemon`; the old name will be removed in a future release.");
4064
+ warnDeprecated(`\`${cliInvocation()} watch\` is now \`${cliInvocation()} daemon\`; the old name will be removed in a future release.`);
3983
4065
  }
3984
- const { runWatch } = await import('./watch-23HSAEI6.js');
4066
+ const { runWatch } = await import('./watch-EVM5GSMP.js');
3985
4067
  await runWatch({ once: opts.once });
3986
4068
  });
3987
4069
  program2.command("hook <event>", { hidden: true }).description("capture-hook fast path, installed by `init`").action(() => {
@@ -3990,6 +4072,8 @@ function buildProgram(argv = process.argv) {
3990
4072
  });
3991
4073
  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(() => {
3992
4074
  });
4075
+ program2.command("claude [args...]").description("start Claude Code with the repository's control plane attached").allowUnknownOption().helpOption(false).action(() => {
4076
+ });
3993
4077
  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) => {
3994
4078
  const { startRelay } = await import('./relay-L76HDX72.js');
3995
4079
  const port = Number(opts.port);
@@ -4010,7 +4094,7 @@ function buildProgram(argv = process.argv) {
4010
4094
  await startRelay({ port, socketPath: opts.socket, forwards });
4011
4095
  });
4012
4096
  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) => {
4013
- const { runVmExec } = await import('./exec-client-CQV746UF.js');
4097
+ const { runVmExec } = await import('./exec-client-7UMESIE2.js');
4014
4098
  const env = {};
4015
4099
  for (const entry of opts.env) {
4016
4100
  const eq = entry.indexOf("=");
@@ -4035,7 +4119,7 @@ function buildProgram(argv = process.argv) {
4035
4119
  }
4036
4120
  });
4037
4121
  program2.command("session-sender", { hidden: true }).description("stream a runner build's sessions to its work item while the build runs").action(async () => {
4038
- const { runSessionSender } = await import('./session-sender-FO3LCREV.js');
4122
+ const { runSessionSender } = await import('./session-sender-GGIY5OAX.js');
4039
4123
  try {
4040
4124
  await runSessionSender();
4041
4125
  } catch (err) {
@@ -4043,7 +4127,7 @@ function buildProgram(argv = process.argv) {
4043
4127
  }
4044
4128
  });
4045
4129
  program2.command("vm-init", { hidden: true }).description("lay out a microVM build's guest; the guest helper runs it").action(async () => {
4046
- const { realGuestHost, runGuestInit } = await import('./guest-init-LJP5GPZF.js');
4130
+ const { realGuestHost, runGuestInit } = await import('./guest-init-KPBQOFE5.js');
4047
4131
  const { readFileSync: readFileSync3 } = await import('fs');
4048
4132
  try {
4049
4133
  runGuestInit(realGuestHost(), readFileSync3("/proc/cmdline", "utf8"));
@@ -4057,6 +4141,7 @@ function buildProgram(argv = process.argv) {
4057
4141
  registerMcpCommands(program2);
4058
4142
  registerContextCommands(program2);
4059
4143
  registerWorkCommands(program2);
4144
+ registerSpecCommands(program2);
4060
4145
  registerRunnerCommands(program2);
4061
4146
  return program2;
4062
4147
  }
@@ -4070,7 +4155,7 @@ async function runCli(processArgv) {
4070
4155
  await buildProgram(argv).parseAsync(argv);
4071
4156
  }
4072
4157
  async function compileContext(opts) {
4073
- const { runEmit, EmitError } = await import('./emit-cmd-IJ3GVRFD.js');
4158
+ const { runEmit, EmitError } = await import('./emit-cmd-7SP3I6UZ.js');
4074
4159
  try {
4075
4160
  const result = runEmit({ cwd: opts.dir, check: opts.check, json: opts.json });
4076
4161
  if (result.exitCode !== 0) process.exitCode = result.exitCode;
@@ -4085,15 +4170,39 @@ async function compileContext(opts) {
4085
4170
  function registerEmitCommands(program2) {
4086
4171
  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(
4087
4172
  "after",
4088
- "\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"
4173
+ `
4174
+ ${cliInvocation()} emit smoke.pass --item 412 --link https://ci.example.com/run/42
4175
+ ${cliInvocation()} emit code-review --result fail --item 412 --body "The button should be red."
4176
+
4177
+ Records that a named check passed or failed, anchored to a work item.
4178
+ --item is always required, in or out of a recorded session \u2014 there is no
4179
+ session fallback. A check that ran carries the run URL as proof (--link);
4180
+ a judgment you are making yourself carries one sentence (--body, or
4181
+ --body-file to read it from a file or standard input). A fail needs at
4182
+ least one of the two. Run from inside a session, the check is stored as
4183
+ the session's; a session cannot record over a person's fail. With
4184
+ nothing to attach it to, the command refuses.
4185
+
4186
+ Who recorded a check is decided by your API key, never by what you send:
4187
+ a key bound to your membership records it as yours, and nothing automated
4188
+ can then clear it.
4189
+
4190
+ A person's pass or fail on one criterion's proof is recorded on that
4191
+ criterion's thread instead \u2014 see \`${cliInvocation()} work comment\`.
4192
+
4193
+ Subcommands: \`emit artifact <file>\` for screenshots, recordings, reports and
4194
+ logs; \`emit finding\` / \`emit findings <file>\` to record a problem in the factory;
4195
+ \`emit criteria <file>\` to record a work item's acceptance criteria;
4196
+ \`emit step <name>\` to report that you started a step.
4197
+ Compiling .outerlayer/ moved to \`${cliInvocation()} context emit\`.
4198
+ `
4089
4199
  ).action(async (name, opts) => {
4090
4200
  if (name === void 0) {
4091
- warnDeprecated("`outerlayer emit` with no name now lives at `outerlayer context emit`; the bare form will be removed in a future release.");
4201
+ warnDeprecated(`\`${cliInvocation()} emit\` with no name now lives at \`${cliInvocation()} context emit\`; the bare form will be removed in a future release.`);
4092
4202
  await compileContext({ dir: opts.dir, check: opts.check, json: opts.json });
4093
4203
  return;
4094
4204
  }
4095
- warnIfApiKeyFlag(opts);
4096
- const { runEmitResult, EmitResultError } = await import('./emit-result-cmd-I4OADKMD.js');
4205
+ const { runEmitResult, EmitResultError } = await import('./emit-result-cmd-RUICOKWQ.js');
4097
4206
  try {
4098
4207
  const result = await runEmitResult({
4099
4208
  name,
@@ -4123,10 +4232,10 @@ function registerEmitCommands(program2) {
4123
4232
  ).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(
4124
4233
  "after",
4125
4234
  `
4126
- outerlayer emit artifact shot.png --caption "the dot row, current dot marked" --for LOGIN-01
4235
+ ${cliInvocation()} emit artifact shot.png --caption "the dot row, current dot marked" --for LOGIN-01
4127
4236
 
4128
4237
  Inside a recorded Claude Code session the artifact spools locally and ships,
4129
- bound to that session, on the next \`outerlayer sync\` \u2014 but only if that
4238
+ bound to that session, on the next \`${cliInvocation()} sync\` \u2014 but only if that
4130
4239
  session was launched with OUTERLAYER_WORK; without it nothing the session
4131
4240
  produces may leave the machine, and the command refuses rather than queue an
4132
4241
  artifact that can never ship. Otherwise it uploads immediately, anchored to a
@@ -4143,8 +4252,7 @@ everywhere, bytes and deep link untouched; a still-queued one is cancelled
4143
4252
  locally so it never uploads. One correction can retire several duplicates.
4144
4253
  `
4145
4254
  ).action(async (file, opts) => {
4146
- warnIfApiKeyFlag(opts);
4147
- const { runEmitArtifact, EmitArtifactError } = await import('./emit-artifact-cmd-MPZVR56N.js');
4255
+ const { runEmitArtifact, EmitArtifactError } = await import('./emit-artifact-cmd-O6SCM7TV.js');
4148
4256
  let pr;
4149
4257
  if (opts.pr !== void 0) {
4150
4258
  pr = parseTrackerNumber(opts.pr);
@@ -4176,10 +4284,34 @@ locally so it never uploads. One correction can retire several duplicates.
4176
4284
  });
4177
4285
  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("--detail <text>", "what happened, a few paragraphs").option("--cause <text>", 'what is actually wrong, or "unknown"').option("--suggestion <text>", "what would help").option("--command <text>", "the command that misbehaved").option("--error <text>", "the first error line").option("--test <name>", "the test name, for a flaky test").option("--symbol <name>", "the function the problem is in, for a defect").option("--started-at <event>", "event id where you hit the problem (default: first output showing --error)").option("--ended-at <event>", "event id where you got past it (default: the newest event)").option("--untimed", "record no time, only when the problem cost none in this session").option("--find <text>", "list this session's events that contain the text, and record nothing").option("--problem <id>", "a problem this finding reports; repeat for each one (needs --link-reason)", (value, previous = []) => [...previous, value]).option("--link-reason <text>", "why the finding reports each --problem").option("--new", "record a new problem instead of linking an existing one").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(
4178
4286
  "after",
4179
- '\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. Add --cause, --detail and --suggestion, and\n--command, --error (first error line), --test or --symbol when there is one.\n\nEvery finding joins a factory-level problem. When one of its keys matches a\nknown problem it joins that problem. Otherwise nothing is stored: the command\nprints the closest problems and exits with code 3. Run it again with\n--problem <id> --link-reason <why> (repeat --problem for each problem the\nfinding reports), or with --new if none of them is the same problem.\nA finding records the time it cost, read from two events of your session or\nits subagents. The start is --started-at, else the first tool output that\nshows --error. The end is --ended-at, else the newest event. When the session\nhas events and no start is found, nothing is stored: the command lists the\nrecent events with their ids. --find <text> searches all of them.\n--item names the work item; without it, inside a recorded session, the\nitem that session was launched for is used automatically.\n'
4287
+ `
4288
+ ${cliInvocation()} emit finding --id gate-skip --category tooling \\
4289
+ --title "The pre-push gate prints green when a step was skipped" \\
4290
+ --file scripts/git/pre-push-checks.mjs --where "build session"
4291
+
4292
+ Records one problem you hit in the factory: an instruction that is wrong or
4293
+ missing (context), a flaky test, a misbehaving script (tooling), a local
4294
+ setup trap (environment), a bug outside your own change (defect), or
4295
+ anything else. A problem in your own change is fixed, not recorded.
4296
+ A context finding needs --rule-path and --rule-relation, and --rule-quote
4297
+ unless the relation is missing. Add --cause, --detail and --suggestion, and
4298
+ --command, --error (first error line), --test or --symbol when there is one.
4299
+
4300
+ Every finding joins a factory-level problem. When one of its keys matches a
4301
+ known problem it joins that problem. Otherwise nothing is stored: the command
4302
+ prints the closest problems and exits with code 3. Run it again with
4303
+ --problem <id> --link-reason <why> (repeat --problem for each problem the
4304
+ finding reports), or with --new if none of them is the same problem.
4305
+ A finding records the time it cost, read from two events of your session or
4306
+ its subagents. The start is --started-at, else the first tool output that
4307
+ shows --error. The end is --ended-at, else the newest event. When the session
4308
+ has events and no start is found, nothing is stored: the command lists the
4309
+ recent events with their ids. --find <text> searches all of them.
4310
+ --item names the work item; without it, inside a recorded session, the
4311
+ item that session was launched for is used automatically.
4312
+ `
4180
4313
  ).action(async (opts) => {
4181
- warnIfApiKeyFlag(opts);
4182
- const { runEmitFinding, refuseRemovedFindingFlags, EmitFindingError } = await import('./emit-finding-cmd-5GF3V4LR.js');
4314
+ const { runEmitFinding, refuseRemovedFindingFlags, EmitFindingError } = await import('./emit-finding-cmd-AJPKHIOV.js');
4183
4315
  try {
4184
4316
  refuseRemovedFindingFlags(opts);
4185
4317
  const result = await runEmitFinding({
@@ -4229,10 +4361,24 @@ locally so it never uploads. One correction can retire several duplicates.
4229
4361
  });
4230
4362
  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(
4231
4363
  "after",
4232
- '\n outerlayer emit findings review.json --item 412\n\nRecords a whole batch at once \u2014 a FindingBatch JSON file (schemaVersion 3,\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.\nFindings that need a choice between candidate problems are listed and not\nstored; the rest are stored, and the exit code is 3.\nEach finding records its time as `emit finding` does: "startedAt" is an event\nid, else the first tool output that shows its "error"; "endedAt" defaults to\nthe newest event. A finding with neither, in a session that has events, stops\nthe whole batch. "untimed": true records one with no time on purpose.\n--item names the work item; without it, inside a recorded session, the item\nthat session was launched for is used automatically.\n'
4364
+ `
4365
+ ${cliInvocation()} emit findings review.json --item 412
4366
+
4367
+ Records a whole batch at once \u2014 a FindingBatch JSON file (schemaVersion 3,
4368
+ optional headSha / contextSourceSha, and 1-500 findings), validated against
4369
+ the same contract \`emit finding\` uses before anything is sent. Re-emitting
4370
+ an id already on the item replaces that row rather than adding a second one.
4371
+ Findings that need a choice between candidate problems are listed and not
4372
+ stored; the rest are stored, and the exit code is 3.
4373
+ Each finding records its time as \`emit finding\` does: "startedAt" is an event
4374
+ id, else the first tool output that shows its "error"; "endedAt" defaults to
4375
+ the newest event. A finding with neither, in a session that has events, stops
4376
+ the whole batch. "untimed": true records one with no time on purpose.
4377
+ --item names the work item; without it, inside a recorded session, the item
4378
+ that session was launched for is used automatically.
4379
+ `
4233
4380
  ).action(async (file, opts) => {
4234
- warnIfApiKeyFlag(opts);
4235
- const { runEmitFindings, EmitFindingError } = await import('./emit-finding-cmd-5GF3V4LR.js');
4381
+ const { runEmitFindings, EmitFindingError } = await import('./emit-finding-cmd-AJPKHIOV.js');
4236
4382
  try {
4237
4383
  const result = await runEmitFindings({
4238
4384
  file,
@@ -4251,10 +4397,44 @@ locally so it never uploads. One correction can retire several duplicates.
4251
4397
  throw err;
4252
4398
  }
4253
4399
  });
4400
+ emitCmd.command("step <name>").description("Report that you started a step; the work item's Activity tab shows it").option("--item <number>", "work item to report on, 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(
4401
+ "after",
4402
+ `
4403
+ ${cliInvocation()} emit step "Review round 2"
4404
+ ${cliInvocation()} emit step "Plan" --item 412
4405
+
4406
+ Run it when you start a step. The name is free text: 1 to 80 characters on one
4407
+ line. A step has a start and nothing else, so run it again for the next step.
4408
+ --item names the work item; without it, inside a recorded session, the item that
4409
+ session was launched for is used.
4410
+
4411
+ If the gateway cannot be reached, the step is queued with the time you ran the
4412
+ command, the command exits 0, and \`sync\` sends it later. If the gateway refuses
4413
+ the step, the command prints why and exits 1.
4414
+ `
4415
+ ).action(async (name, opts) => {
4416
+ const { runEmitStep, EmitStepError } = await import('./emit-step-cmd-347XNGPQ.js');
4417
+ try {
4418
+ await runEmitStep({
4419
+ name,
4420
+ item: opts.item,
4421
+ json: opts.json,
4422
+ url: opts.url,
4423
+ apiKey: opts.apiKey,
4424
+ appId: opts.appId
4425
+ });
4426
+ } catch (err) {
4427
+ if (err instanceof EmitStepError) {
4428
+ fail(err.message);
4429
+ return;
4430
+ }
4431
+ throw err;
4432
+ }
4433
+ });
4254
4434
  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(
4255
4435
  "after",
4256
4436
  `
4257
- outerlayer emit criteria criteria.json --item 412
4437
+ ${cliInvocation()} emit criteria criteria.json --item 412
4258
4438
 
4259
4439
  Records the item's acceptance criteria as one list. The file is JSON:
4260
4440
  { "criteria": [ { "id": "AC-1", "text": "Given ...", "proof": null },
@@ -4271,8 +4451,7 @@ agent session, with your own login. A key bound to no member cannot replace it e
4271
4451
  session was launched for is used automatically.
4272
4452
  `
4273
4453
  ).action(async (file, opts) => {
4274
- warnIfApiKeyFlag(opts);
4275
- const { runEmitCriteria, EmitCriteriaError } = await import('./emit-criteria-cmd-EFGYI23V.js');
4454
+ const { runEmitCriteria, EmitCriteriaError } = await import('./emit-criteria-cmd-LQNERZRV.js');
4276
4455
  try {
4277
4456
  const result = await runEmitCriteria({
4278
4457
  file,
@@ -4292,14 +4471,54 @@ session was launched for is used automatically.
4292
4471
  }
4293
4472
  });
4294
4473
  }
4474
+ function registerSpecCommands(program2) {
4475
+ const specCmd = program2.command("spec").description("Find, file and edit the issue a spec lives in, in your factory's tracker");
4476
+ const credentialOptions = (command) => command.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)");
4477
+ const run = async (action) => {
4478
+ const { WorkCommandError } = await import('./work-cmd-5EPPD33Y.js');
4479
+ try {
4480
+ await action(await import('./spec-cmd-SIZBRN7D.js'));
4481
+ } catch (err) {
4482
+ if (err instanceof WorkCommandError) {
4483
+ fail(err.message);
4484
+ return;
4485
+ }
4486
+ throw err;
4487
+ }
4488
+ };
4489
+ credentialOptions(
4490
+ specCmd.command("target").description("Say where a spec would be filed, and whether this factory can file there").option("--tracker <name>", "one connected tracker, when the factory has more than one")
4491
+ ).action((opts) => run((spec) => spec.runSpecTarget(opts)));
4492
+ credentialOptions(
4493
+ specCmd.command("search").description("Find open and closed issues that match a query, before filing a duplicate").argument("<query>", "words from the title or body").option("--tracker <name>", "one connected tracker, when the factory has more than one")
4494
+ ).action((query, opts) => run((spec) => spec.runSpecSearch({ ...opts, query })));
4495
+ credentialOptions(
4496
+ specCmd.command("file").description("File a spec as an issue and print its key, URL and the command that builds it").requiredOption("--title <text>", "the issue title").option("--body-file <path>", "the spec as Markdown; - reads standard input").option("--repo <owner/name>", "the repository to file in (GitHub), or the one the work lands in").option("--tracker <name>", "the tracker, when the factory files in more than one").option("--scope <key>", "the Linear team or Jira project, when the connection covers several")
4497
+ ).action((opts) => run((spec) => spec.runSpecFile(opts)));
4498
+ credentialOptions(
4499
+ specCmd.command("edit").description("Replace the body of an issue that holds a spec").argument("<key>", "the issue key, such as acme/api#12 or ENG-142").option("--body-file <path>", "the new body as Markdown; - reads standard input").option("--updated-at <timestamp>", "the updatedAt you read; the edit is refused if the issue has changed since")
4500
+ ).action((key, opts) => run((spec) => spec.runSpecEdit({ ...opts, key })));
4501
+ }
4295
4502
  function registerWorkCommands(program2) {
4296
4503
  const workCmd = program2.command("work").description("Start, remove, or read what the factory is working on (the Work page)");
4297
- 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(
4504
+ 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_OPTION_FLAG, "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(
4298
4505
  "after",
4299
- "\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"
4506
+ `
4507
+ ${cliInvocation()} work build --issue 42 # a host builds it
4508
+ ${cliInvocation()} work build --item 7 # retry an item already on Work
4509
+ ${cliInvocation()} work build --issue 42 --local # you build it
4510
+ ${cliInvocation()} work build --issue ENG-123 --tracker linear # a Linear issue
4511
+
4512
+ Without --local, records a build request: a host takes the item when it
4513
+ has a free slot. It never links the calling session and never takes a
4514
+ lease, even run from inside one.
4515
+
4516
+ With --local, puts the item on Work with no build request, so no host
4517
+ takes it, and prints the line that starts your session on it. That
4518
+ session links itself and holds the item's lease while it works.
4519
+ `
4300
4520
  ).action(async (opts) => {
4301
- warnIfApiKeyFlag(opts);
4302
- const { runWorkBuild, WorkCommandError } = await import('./work-cmd-Z2WXLZPH.js');
4521
+ const { runWorkBuild, WorkCommandError } = await import('./work-cmd-5EPPD33Y.js');
4303
4522
  try {
4304
4523
  await runWorkBuild(opts);
4305
4524
  } catch (err) {
@@ -4312,10 +4531,17 @@ function registerWorkCommands(program2) {
4312
4531
  });
4313
4532
  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").addOption(new Option("--log-failures", "append a failure to the hook error log (used by hooks)").hideHelp()).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(
4314
4533
  "after",
4315
- "\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"
4534
+ `
4535
+ ${cliInvocation()} work link-session 42
4536
+
4537
+ Names an item that already exists on the calling session \u2014 it never
4538
+ creates one. Create one with \`${cliInvocation()} work build --local\`. Linking
4539
+ the same session to the same item twice is a no-op. Also takes or
4540
+ renews that session's own 20-minute lease, unless --no-lease or
4541
+ --ended is given, or another worker already holds a live one.
4542
+ `
4316
4543
  ).action(async (number, opts) => {
4317
- warnIfApiKeyFlag(opts);
4318
- const { runWorkLinkSession, WorkCommandError } = await import('./work-cmd-Z2WXLZPH.js');
4544
+ const { runWorkLinkSession, WorkCommandError } = await import('./work-cmd-5EPPD33Y.js');
4319
4545
  try {
4320
4546
  await runWorkLinkSession({ ...opts, number, noLease: opts.lease === false, logFailures: opts.logFailures === true });
4321
4547
  } catch (err) {
@@ -4329,7 +4555,7 @@ function registerWorkCommands(program2) {
4329
4555
  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(
4330
4556
  "after",
4331
4557
  `
4332
- outerlayer work comment --item 7 --criterion AC-9-01 --body "fixed" --artifact 3f2a1c9d --attach
4558
+ ${cliInvocation()} work comment --item 7 --criterion AC-9-01 --body "fixed" --artifact 3f2a1c9d --attach
4333
4559
 
4334
4560
  Posts one comment. Omitting --criterion posts on the general thread;
4335
4561
  naming one posts on that criterion's own thread. --pass/--fail are a
@@ -4339,9 +4565,8 @@ criterion's new proof; --ready hands the thread back to a person. At
4339
4565
  most one action flag may be given.
4340
4566
  `
4341
4567
  ).action(async (opts) => {
4342
- warnIfApiKeyFlag(opts);
4343
- const { runWorkComment, WorkCommentCommandError } = await import('./work-comment-cmd-AWHP56M2.js');
4344
- const { WorkCommandError } = await import('./work-cmd-Z2WXLZPH.js');
4568
+ const { runWorkComment, WorkCommentCommandError } = await import('./work-comment-cmd-2ACOZQAE.js');
4569
+ const { WorkCommandError } = await import('./work-cmd-5EPPD33Y.js');
4345
4570
  try {
4346
4571
  await runWorkComment(opts);
4347
4572
  } catch (err) {
@@ -4354,11 +4579,16 @@ most one action flag may be given.
4354
4579
  });
4355
4580
  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(
4356
4581
  "after",
4357
- "\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"
4582
+ `
4583
+ ${cliInvocation()} work threads --item 7
4584
+
4585
+ Prints the item's general thread, then one thread per acceptance
4586
+ criterion that carries proof or a comment, each with who is waiting
4587
+ on whom next.
4588
+ `
4358
4589
  ).action(async (opts) => {
4359
- warnIfApiKeyFlag(opts);
4360
- const { runWorkThreads, WorkCommentCommandError } = await import('./work-comment-cmd-AWHP56M2.js');
4361
- const { WorkCommandError } = await import('./work-cmd-Z2WXLZPH.js');
4590
+ const { runWorkThreads, WorkCommentCommandError } = await import('./work-comment-cmd-2ACOZQAE.js');
4591
+ const { WorkCommandError } = await import('./work-cmd-5EPPD33Y.js');
4362
4592
  try {
4363
4593
  await runWorkThreads(opts);
4364
4594
  } catch (err) {
@@ -4369,18 +4599,42 @@ most one action flag may be given.
4369
4599
  throw err;
4370
4600
  }
4371
4601
  });
4372
- 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(
4602
+ workCmd.command("logs").description("Read the runner's log for a host attempt on a work item").requiredOption("--item <number>", "the item's number inside this factory").option("--attempt <claim id>", "an attempt's id, from `work status --json` (default: the newest)").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(
4603
+ "after",
4604
+ `
4605
+ ${cliInvocation()} work logs --item 7
4606
+ ${cliInvocation()} work logs --item 7 --attempt 7b0a3c52-9f1e-4d6b-8a21-5c3e9d2f1a40
4607
+
4608
+ Prints what the runner did around the agent on that attempt, one line
4609
+ per event with its timestamp, with secrets removed on the host. The
4610
+ runner sends it when it releases the attempt, so an attempt that is
4611
+ still running, or that an older runner released, has no log and the
4612
+ command exits 1.
4613
+ `
4614
+ ).action(async (opts) => {
4615
+ const { runWorkLogs, WorkLogsCommandError } = await import('./work-logs-cmd-QDD2NMJL.js');
4616
+ const { WorkCommandError } = await import('./work-cmd-5EPPD33Y.js');
4617
+ try {
4618
+ await runWorkLogs(opts);
4619
+ } catch (err) {
4620
+ if (err instanceof WorkLogsCommandError || err instanceof WorkCommandError) {
4621
+ fail(err.message);
4622
+ return;
4623
+ }
4624
+ throw err;
4625
+ }
4626
+ });
4627
+ 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 or Linear/Jira key (default: named by --item instead)").option(TRACKER_OPTION_FLAG, "tracker for a keyed --issue, when both hold the key").option("--repo <owner/name>", "repository for an issue number (default: the 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(
4373
4628
  "after",
4374
4629
  `
4375
- outerlayer work remove --item 42 --reason "done"
4630
+ ${cliInvocation()} work remove --item 42 --reason "done"
4376
4631
 
4377
4632
  Withdraws the caller's own addition, recording the reason. Never
4378
4633
  deletes anything \u2014 the item is removed from Work only once no
4379
4634
  live addition remains on it.
4380
4635
  `
4381
4636
  ).action(async (opts) => {
4382
- warnIfApiKeyFlag(opts);
4383
- const { runWorkRemove, WorkCommandError } = await import('./work-cmd-Z2WXLZPH.js');
4637
+ const { runWorkRemove, WorkCommandError } = await import('./work-cmd-5EPPD33Y.js');
4384
4638
  try {
4385
4639
  await runWorkRemove(opts);
4386
4640
  } catch (err) {
@@ -4391,9 +4645,8 @@ live addition remains on it.
4391
4645
  throw err;
4392
4646
  }
4393
4647
  });
4394
- 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) => {
4395
- warnIfApiKeyFlag(opts);
4396
- const { runWorkStatus, WorkCommandError } = await import('./work-cmd-Z2WXLZPH.js');
4648
+ 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, or a Linear/Jira key like ENG-123").option(TRACKER_OPTION_FLAG, "tracker for a keyed --issue, when both hold the key").option("--repo <owner/name>", "repository for an issue number (default: the 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) => {
4649
+ const { runWorkStatus, WorkCommandError } = await import('./work-cmd-5EPPD33Y.js');
4397
4650
  try {
4398
4651
  await runWorkStatus(opts);
4399
4652
  } catch (err) {
@@ -4405,8 +4658,7 @@ live addition remains on it.
4405
4658
  }
4406
4659
  });
4407
4660
  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 a host can start: a build request, no open PR, 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) => {
4408
- warnIfApiKeyFlag(opts);
4409
- const { runWorkList, WorkCommandError } = await import('./work-cmd-Z2WXLZPH.js');
4661
+ const { runWorkList, WorkCommandError } = await import('./work-cmd-5EPPD33Y.js');
4410
4662
  try {
4411
4663
  await runWorkList(opts);
4412
4664
  } catch (err) {
@@ -4417,12 +4669,17 @@ live addition remains on it.
4417
4669
  throw err;
4418
4670
  }
4419
4671
  });
4420
- workCmd.command("claim").description("Claim a work item under a lease this API key holds 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(
4672
+ workCmd.command("claim").description("Claim a work item under a lease this API key holds before working on it").option("--item <number>", "the item's number inside this factory").option("--issue <number>", "issue number or Linear/Jira key (default: named by --item instead)").option(TRACKER_OPTION_FLAG, "tracker for a keyed --issue, when both hold the key").option("--repo <owner/name>", "repository for an issue number (default: the 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(
4421
4673
  "after",
4422
- "\n outerlayer work claim --item 42 --kind implement\n\nRecords a lease held by the API key that sends it. A live lease held\nby a different key is refused; claiming again with the same key extends\nit. --host is a label only.\n"
4674
+ `
4675
+ ${cliInvocation()} work claim --item 42 --kind implement
4676
+
4677
+ Records a lease held by the API key that sends it. A live lease held
4678
+ by a different key is refused; claiming again with the same key extends
4679
+ it. --host is a label only.
4680
+ `
4423
4681
  ).action(async (opts) => {
4424
- warnIfApiKeyFlag(opts);
4425
- const { runWorkClaim, WorkCommandError } = await import('./work-claim-cmd-LKL3GHFQ.js');
4682
+ const { runWorkClaim, WorkCommandError } = await import('./work-claim-cmd-3J3QW56U.js');
4426
4683
  try {
4427
4684
  await runWorkClaim(opts);
4428
4685
  } catch (err) {
@@ -4433,12 +4690,16 @@ live addition remains on it.
4433
4690
  throw err;
4434
4691
  }
4435
4692
  });
4436
- workCmd.command("renew").description("Extend the lease this API key holds 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(
4693
+ workCmd.command("renew").description("Extend the lease this API key holds on a work item").option("--item <number>", "the item's number inside this factory").option("--issue <number>", "issue number or Linear/Jira key (default: named by --item instead)").option(TRACKER_OPTION_FLAG, "tracker for a keyed --issue, when both hold the key").option("--repo <owner/name>", "repository for an issue number (default: the 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(
4437
4694
  "after",
4438
- "\n outerlayer work renew --item 42\n\nExtends the live lease this API key holds. Refused with a 410 if the\nlease has expired, or a 403 if a different key holds it.\n"
4695
+ `
4696
+ ${cliInvocation()} work renew --item 42
4697
+
4698
+ Extends the live lease this API key holds. Refused with a 410 if the
4699
+ lease has expired, or a 403 if a different key holds it.
4700
+ `
4439
4701
  ).action(async (opts) => {
4440
- warnIfApiKeyFlag(opts);
4441
- const { runWorkRenew, WorkCommandError } = await import('./work-claim-cmd-LKL3GHFQ.js');
4702
+ const { runWorkRenew, WorkCommandError } = await import('./work-claim-cmd-3J3QW56U.js');
4442
4703
  try {
4443
4704
  await runWorkRenew(opts);
4444
4705
  } catch (err) {
@@ -4449,15 +4710,28 @@ live addition remains on it.
4449
4710
  throw err;
4450
4711
  }
4451
4712
  });
4452
- workCmd.command("release").description("Release the lease this API key holds on a work item, or this session's own lease").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); releases a host's lease").option("--session-id <id>", "the session whose lease to release, skipping detection").option(
4713
+ workCmd.command("release").description("Release the lease this API key holds on a work item, or this session's own lease").option("--item <number>", "the item's number inside this factory").option("--issue <number>", "issue number or Linear/Jira key (default: named by --item instead)").option(TRACKER_OPTION_FLAG, "tracker for a keyed --issue, when both hold the key").option("--repo <owner/name>", "repository for an issue number (default: the checkout's remote)").option("--host <name>", "the claiming host (default: this machine's hostname); releases a host's lease").option("--session-id <id>", "the session whose lease to release, skipping detection").option(
4453
4714
  "--outcome <ok|incomplete|failed|timed_out|idle|lease_lost|interrupted|stopped>",
4454
4715
  "how a host's attempt ended (default: none)"
4455
4716
  ).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(
4456
4717
  "after",
4457
- "\n outerlayer work release --item 42 --outcome ok\n outerlayer work release --item 42\n\nMarks the lease this API key holds released, optionally with an\noutcome. A lease held by a different key is refused. Releasing an\nalready-released lease is a no-op that returns the recorded release\ntime and outcome.\n\nRun inside a session with no --host, it releases that session's own\nlease instead, so the item's review opens without ending the\nsession. It never releases another session's lease. --outcome\napplies to a host's lease only. A later tool call in the session\ntakes the lease again.\n"
4718
+ `
4719
+ ${cliInvocation()} work release --item 42 --outcome ok
4720
+ ${cliInvocation()} work release --item 42
4721
+
4722
+ Marks the lease this API key holds released, optionally with an
4723
+ outcome. A lease held by a different key is refused. Releasing an
4724
+ already-released lease is a no-op that returns the recorded release
4725
+ time and outcome.
4726
+
4727
+ Run inside a session with no --host, it releases that session's own
4728
+ lease instead, so the item's review opens without ending the
4729
+ session. It never releases another session's lease. --outcome
4730
+ applies to a host's lease only. A later tool call in the session
4731
+ takes the lease again.
4732
+ `
4458
4733
  ).action(async (opts) => {
4459
- warnIfApiKeyFlag(opts);
4460
- const { runWorkRelease, WorkCommandError } = await import('./work-claim-cmd-LKL3GHFQ.js');
4734
+ const { runWorkRelease, WorkCommandError } = await import('./work-claim-cmd-3J3QW56U.js');
4461
4735
  try {
4462
4736
  await runWorkRelease(opts);
4463
4737
  } catch (err) {
@@ -4470,10 +4744,21 @@ live addition remains on it.
4470
4744
  });
4471
4745
  workCmd.command("pr <number>").description("Declare a pull request onto the launched work item, or withdraw it").option("--repo <owner/name>", "repository (default: the current checkout's remote)").option("--session-id <id>", "session id to record as-is, skipping detection").option("--withdraw", "remove the declaration instead of making one (needs --item)").option("--item <number>", "with --withdraw, the item to remove the declaration from").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(
4472
4746
  "after",
4473
- "\n outerlayer work pr 481\n outerlayer work pr 481 --withdraw --item 250\n\nRun inside a session already on a work item. Records that pull\nrequest 481 in the checkout's repository belongs to the item named by\nOUTERLAYER_WORK, or refuses when the session is not on it. The same\nfact a `pr-link` in the session's own transcript records on sync.\nIdempotent: declaring the same pull request twice is a no-op.\n\n--withdraw removes one item's declaration of the pull request. It\nneeds your own login; an agent session or a factory key is refused.\n"
4747
+ `
4748
+ ${cliInvocation()} work pr 481
4749
+ ${cliInvocation()} work pr 481 --withdraw --item 250
4750
+
4751
+ Run inside a session already on a work item. Records that pull
4752
+ request 481 in the checkout's repository belongs to the item named by
4753
+ OUTERLAYER_WORK, or refuses when the session is not on it. The same
4754
+ fact a \`pr-link\` in the session's own transcript records on sync.
4755
+ Idempotent: declaring the same pull request twice is a no-op.
4756
+
4757
+ --withdraw removes one item's declaration of the pull request. It
4758
+ needs your own login; an agent session or a factory key is refused.
4759
+ `
4474
4760
  ).action(async (number, opts) => {
4475
- warnIfApiKeyFlag(opts);
4476
- const { runWorkPr, WorkPrCommandError } = await import('./work-pr-cmd-HNEUIEN7.js');
4761
+ const { runWorkPr, WorkPrCommandError } = await import('./work-pr-cmd-H4OQR26N.js');
4477
4762
  try {
4478
4763
  await runWorkPr({ ...opts, number });
4479
4764
  } catch (err) {
@@ -4486,11 +4771,20 @@ live addition remains on it.
4486
4771
  });
4487
4772
  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("--repository <owner/name>", "repository to open the pull request in (default: the primary)").option("--repo <owner/name>", "same as --repository, on the gh path").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(
4488
4773
  "after",
4489
- '\n outerlayer work open-pr --title "Add the thing" --body-file pr.md\n outerlayer work open-pr --repository acme/web --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'
4774
+ `
4775
+ ${cliInvocation()} work open-pr --title "Add the thing" --body-file pr.md
4776
+ ${cliInvocation()} work open-pr --repository acme/web --title "Add the thing" --body-file pr.md
4777
+
4778
+ In a host build, which holds an item key, the gateway opens the pull
4779
+ request as the factory's GitHub App, from the branch the claim named,
4780
+ or edits the one already open on that branch. No gh command runs.
4781
+ In your own session it runs \`gh pr create\` (or \`gh pr edit\`) under your
4782
+ login, then declares the pull request on the item, as \`work pr\` does.
4783
+ Run it again after a change to the title or description.
4784
+ `
4490
4785
  ).action(async (opts) => {
4491
- warnIfApiKeyFlag(opts);
4492
- const { runWorkOpenPr, WorkOpenPrCommandError } = await import('./work-open-pr-cmd-IOM4AVQQ.js');
4493
- const { WorkCommandError } = await import('./work-cmd-Z2WXLZPH.js');
4786
+ const { runWorkOpenPr, WorkOpenPrCommandError } = await import('./work-open-pr-cmd-XNCEI4ZM.js');
4787
+ const { WorkCommandError } = await import('./work-cmd-5EPPD33Y.js');
4494
4788
  try {
4495
4789
  await runWorkOpenPr(opts);
4496
4790
  } catch (err) {
@@ -4506,9 +4800,9 @@ function registerRunnerCommands(program2) {
4506
4800
  const runnerCmd = program2.command("runner").description("Take work from the implement and amend queues on this machine");
4507
4801
  const configOption = "the cloud config file to run against (default: ~/.outerlayer/config.json)";
4508
4802
  runnerCmd.command("start").description("Run the loop: claim work, run it, report, clean up, repeat").option("--config <path>", configOption).action(async (opts) => {
4509
- const { defaultRunnerConfigPath, RunnerConfigError } = await import('./config-N3C6DFQA.js');
4510
- const { start, RunnerStartError } = await import('./loop-FK3LPMMZ.js');
4511
- const { RunnerAlreadyRunningError } = await import('./pidfile-UZRH774M.js');
4803
+ const { defaultRunnerConfigPath, RunnerConfigError } = await import('./config-CLSGABD5.js');
4804
+ const { start, RunnerStartError } = await import('./loop-SYYPVFYW.js');
4805
+ const { RunnerAlreadyRunningError } = await import('./pidfile-3DM4OBKO.js');
4512
4806
  const configPath = opts.config ?? defaultRunnerConfigPath();
4513
4807
  try {
4514
4808
  const { realUpdateDeps } = await import('./real-deps-KNNBQC75.js');
@@ -4522,8 +4816,8 @@ function registerRunnerCommands(program2) {
4522
4816
  }
4523
4817
  });
4524
4818
  runnerCmd.command("check").description("Validate the config, the hooks and the key, and print the effective settings").option("--config <path>", configOption).action(async (opts) => {
4525
- const { defaultRunnerConfigPath } = await import('./config-N3C6DFQA.js');
4526
- const { runRunnerCheck } = await import('./check-5XE7XMC4.js');
4819
+ const { defaultRunnerConfigPath } = await import('./config-CLSGABD5.js');
4820
+ const { runRunnerCheck } = await import('./check-UWQAL2MN.js');
4527
4821
  const configPath = opts.config ?? defaultRunnerConfigPath();
4528
4822
  const result = await runRunnerCheck(configPath);
4529
4823
  if (!result.ok) {
@@ -4539,8 +4833,8 @@ function registerRunnerCommands(program2) {
4539
4833
  `);
4540
4834
  });
4541
4835
  runnerCmd.command("install").description("Put this CLI in the runner's install layout; --service adds its service").option("--config <path>", configOption).option("--service", "set up the service that keeps the runner running").option("--print", "with --service, print the unit and set up nothing").option("--user <account>", "with --service, the account the service runs as").option("--replace", "replace a unit or launcher file this command did not write").option("--allow-downgrade", "install this CLI even when it is older than the version the runner runs").option("--unit-name <name>", "with --service, the unit's name, to run a second runner beside the first").action(async (opts) => {
4542
- const { defaultRunnerConfigPath, RunnerConfigError } = await import('./config-N3C6DFQA.js');
4543
- const { runRunnerInstall, RunnerInstallError, launcherSummary, restartNote } = await import('./install-cmd-5HYMXLEW.js');
4836
+ const { defaultRunnerConfigPath, RunnerConfigError } = await import('./config-CLSGABD5.js');
4837
+ const { runRunnerInstall, RunnerInstallError, launcherSummary, restartNote } = await import('./install-cmd-QDOTR4OM.js');
4544
4838
  const configPath = opts.config ?? defaultRunnerConfigPath();
4545
4839
  if (opts.service !== true) {
4546
4840
  for (const flag of ["print", "user", "unitName"]) {
@@ -4552,7 +4846,7 @@ function registerRunnerCommands(program2) {
4552
4846
  }
4553
4847
  try {
4554
4848
  if (opts.service === true) {
4555
- const { installServiceCommand } = await import('./command-R2QCVX55.js');
4849
+ const { installServiceCommand } = await import('./command-OEAV7BVX.js');
4556
4850
  const result2 = await installServiceCommand({
4557
4851
  configPath,
4558
4852
  print: opts.print,
@@ -4590,7 +4884,7 @@ function registerRunnerCommands(program2) {
4590
4884
  printLauncher(result, launcherSummary);
4591
4885
  process.stdout.write(
4592
4886
  `
4593
- To keep it running, run ${C.yellow}outerlayer runner install --service${C.reset}. To start the runner from the install layout yourself, use this line, then restart the service:
4887
+ To keep it running, run ${C.yellow}${cliInvocation()} runner install --service${C.reset}. To start the runner from the install layout yourself, use this line, then restart the service:
4594
4888
  ${C.yellow}${result.execStart}${C.reset}
4595
4889
  `
4596
4890
  );
@@ -4604,11 +4898,11 @@ To keep it running, run ${C.yellow}outerlayer runner install --service${C.reset}
4604
4898
  });
4605
4899
  runnerCmd.command("uninstall").description("Stop the runner's service and remove it (needs --service)").option("--config <path>", configOption).option("--service", "remove the service, and the launch agent or scheduled task").option("--unit-name <name>", "the unit's name, when it is not the default").action(async (opts) => {
4606
4900
  if (opts.service !== true) {
4607
- fail("pass --service: `outerlayer runner uninstall --service` removes the runner's service.");
4901
+ fail(`pass --service: \`${cliInvocation()} runner uninstall --service\` removes the runner's service.`);
4608
4902
  return;
4609
4903
  }
4610
- const { defaultRunnerConfigPath } = await import('./config-N3C6DFQA.js');
4611
- const { uninstallServiceCommand } = await import('./command-R2QCVX55.js');
4904
+ const { defaultRunnerConfigPath } = await import('./config-CLSGABD5.js');
4905
+ const { uninstallServiceCommand } = await import('./command-OEAV7BVX.js');
4612
4906
  const result = await uninstallServiceCommand({ configPath: opts.config ?? defaultRunnerConfigPath(), unitName: opts.unitName, home: homeFromEnv() });
4613
4907
  if (result.ok) for (const line of result.lines) process.stdout.write(`${C.green}\u2713${C.reset} ${line}
4614
4908
  `);
@@ -4616,14 +4910,31 @@ To keep it running, run ${C.yellow}outerlayer runner install --service${C.reset}
4616
4910
  });
4617
4911
  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()).option("--vm-cli-package <path>", "with --vm, install this packed CLI tarball in the VM").option("--unit-name <name>", "the service unit's name, to run a second runner beside the first").option("--agent <name>", "write commands that start this agent (claude); its credential comes on stdin").option("--factory <org/factory>", "factory to create the runner key on, as <org>/<factory>").option("--app-id <id>", "factory id, instead of --factory (Settings \u2192 General)").addHelpText(
4618
4912
  "after",
4619
- "\nWith no apiKey in the config file, and a login saved by `outerlayer login`,\nrunner init names the factory the runner is for: --factory, then --app-id,\nOUTERLAYER_APP_ID, or this repository's connection. With none of those, a\nlogin that can use one factory uses it, and one that can use several lists\nthem. It then prints the factory's API keys page, where you create a key with\nthe Runner preset, and asks you to paste it. Nothing is shown as you paste.\nWith no terminal to ask on, it stops and says what to put in the config file.\n\nWith --agent claude, the queue commands start Claude Code instead of the\nplaceholder, and Claude's credential is read from stdin: a token from\n`claude setup-token` (sk-ant-oat...) or an API key (sk-ant-api...). It is\nnever printed, and is stored in the runner.env beside the config with mode\n600. Other lines in the file are kept. With nothing piped, a credential\nalready in runner.env is used.\n\n printf '%s' \"$CLAUDE_CODE_OAUTH_TOKEN\" | outerlayer runner init --agent claude --factory <org>/<factory>\n"
4913
+ `
4914
+ With no apiKey in the config file, and a login saved by \`${cliInvocation()} login\`,
4915
+ runner init names the factory the runner is for: --factory, then --app-id,
4916
+ OUTERLAYER_APP_ID, or this repository's connection. With none of those, a
4917
+ login that can use one factory uses it, and one that can use several lists
4918
+ them. It then prints the factory's API keys page, where you create a key with
4919
+ the Runner preset, and asks you to paste it. Nothing is shown as you paste.
4920
+ With no terminal to ask on, it stops and says what to put in the config file.
4921
+
4922
+ With --agent claude, the queue commands start Claude Code instead of the
4923
+ placeholder, and Claude's credential is read from stdin: a token from
4924
+ \`claude setup-token\` (sk-ant-oat...) or an API key (sk-ant-api...). It is
4925
+ never printed, and is stored in the runner.env beside the config with mode
4926
+ 600. Other lines in the file are kept. With nothing piped, a credential
4927
+ already in runner.env is used.
4928
+
4929
+ printf '%s' "$CLAUDE_CODE_OAUTH_TOKEN" | ${cliInvocation()} runner init --agent claude --factory <org>/<factory>
4930
+ `
4620
4931
  ).action(async (opts) => {
4621
- const { defaultRunnerConfigPath, RunnerConfigError } = await import('./config-N3C6DFQA.js');
4622
- const { prepareRunnerInit, runRunnerInit, RunnerInitError, writeRunnerInit } = await import('./init-6XPDRDTC.js');
4623
- const { obtainRunnerKey, runnerKeyLogin } = await import('./runner-key-6SLBI7PP.js');
4624
- const { detectMachine } = await import('./machine-SQXEBAM2.js');
4625
- const { chooseVm, runVmInit, VmInitError } = await import('./init-PWU5YVL7.js');
4626
- const { AGENT_COMMANDS, credentialFromStdin, isAgentName, restartForCredential, RunnerEnvError } = await import('./runner-env-EN7YE7QX.js');
4932
+ const { defaultRunnerConfigPath, RunnerConfigError } = await import('./config-CLSGABD5.js');
4933
+ const { prepareRunnerInit, runRunnerInit, RunnerInitError, writeRunnerInit } = await import('./init-MOINY2DD.js');
4934
+ const { obtainRunnerKey, runnerKeyLogin } = await import('./runner-key-276SJU5R.js');
4935
+ const { detectMachine } = await import('./machine-6X63G2KH.js');
4936
+ const { chooseVm, runVmInit, VmInitError } = await import('./init-LWHDMZQG.js');
4937
+ const { AGENT_COMMANDS, credentialFromStdin, isAgentName, restartForCredential, RunnerEnvError } = await import('./runner-env-FRZFFHDF.js');
4627
4938
  const configPath = opts.config ?? defaultRunnerConfigPath();
4628
4939
  if (opts.agent !== void 0 && !isAgentName(opts.agent)) {
4629
4940
  fail(`--agent ${opts.agent}: runner init writes commands for ${Object.keys(AGENT_COMMANDS).join(", ")}. For another agent, leave --agent out and edit runner.commands.`);
@@ -4637,7 +4948,7 @@ To keep it running, run ${C.yellow}outerlayer runner install --service${C.reset}
4637
4948
  try {
4638
4949
  let credential;
4639
4950
  if (agent !== void 0) {
4640
- const { readPipedKey } = await import('./login-browser-QZCS3WW6.js');
4951
+ const { readPipedKey } = await import('./login-browser-DX35MARZ.js');
4641
4952
  credential = credentialFromStdin(
4642
4953
  await readPipedKey(process.stdin, {
4643
4954
  onWaiting: () => process.stderr.write(`runner init is waiting for Claude's credential on stdin; close stdin to use the one already in runner.env
@@ -4654,7 +4965,7 @@ To keep it running, run ${C.yellow}outerlayer runner install --service${C.reset}
4654
4965
  const chunks = [];
4655
4966
  for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
4656
4967
  const result2 = runRunnerInit(configPath, { machine, configFromStdin: Buffer.concat(chunks).toString("utf8"), requireContainer: true });
4657
- const { ensureHostKey: ensureHostKey2 } = await import('./host-key-YMLXY6QB.js');
4968
+ const { ensureHostKey: ensureHostKey2 } = await import('./host-key-U4B5QETR.js');
4658
4969
  const hostKey2 = await ensureHostKey2(configPath);
4659
4970
  process.stdout.write(`host key ${hostKey2.fingerprint}
4660
4971
  hooks: ${result2.hooks}
@@ -4713,7 +5024,7 @@ hooks: ${result2.hooks}
4713
5024
  const prepared = prepareRunnerInit(configPath, { machine, allowProcess, agent, credential, keyFromLogin: makeKey !== void 0 });
4714
5025
  const created = makeKey === void 0 ? void 0 : await makeKey();
4715
5026
  const result = writeRunnerInit(prepared, created);
4716
- const { ensureHostKey } = await import('./host-key-YMLXY6QB.js');
5027
+ const { ensureHostKey } = await import('./host-key-U4B5QETR.js');
4717
5028
  const hostKey = await ensureHostKey(configPath);
4718
5029
  if (created !== void 0) {
4719
5030
  process.stdout.write(`${C.green}\u2713${C.reset} saved the runner key in ${result.configPath}
@@ -4738,10 +5049,10 @@ ${C.yellow}${result.note}${C.reset}
4738
5049
  `);
4739
5050
  if (machine.platform === "darwin" && opts.vm === void 0) {
4740
5051
  process.stdout.write(`
4741
- To isolate builds in containers instead, run ${C.yellow}outerlayer runner init --vm${C.reset}.
5052
+ To isolate builds in containers instead, run ${C.yellow}${cliInvocation()} runner init --vm${C.reset}.
4742
5053
  `);
4743
5054
  }
4744
- const { runRunnerInstall, RunnerInstallError, RunnerDowngradeError, launcherSummary } = await import('./install-cmd-5HYMXLEW.js');
5055
+ const { runRunnerInstall, RunnerInstallError, RunnerDowngradeError, launcherSummary } = await import('./install-cmd-QDOTR4OM.js');
4745
5056
  let installedLayout = false;
4746
5057
  let noService = "it starts the runner from the install layout, which needs a CLI installed with npm";
4747
5058
  try {
@@ -4773,7 +5084,7 @@ ${C.yellow}The runner will not update itself: ${installErr.message}${C.reset}
4773
5084
  let serviceUp = false;
4774
5085
  let restart;
4775
5086
  if (installedLayout) {
4776
- const { installServiceCommand } = await import('./command-R2QCVX55.js');
5087
+ const { installServiceCommand } = await import('./command-OEAV7BVX.js');
4777
5088
  const service = await installServiceCommand({
4778
5089
  configPath,
4779
5090
  entryPath: process.argv[1] ?? "",
@@ -4795,14 +5106,14 @@ ${C.yellow}The runner will not update itself: ${installErr.message}${C.reset}
4795
5106
  if (restart !== void 0) process.stdout.write(`
4796
5107
  ${C.yellow}${restart}${C.reset}
4797
5108
  `);
4798
- const steps = agent === void 0 ? [`Edit the command lines in ${result.configPath} to start your agent.`, `${C.yellow}outerlayer runner check${C.reset}`] : [`${C.yellow}outerlayer runner check${C.reset}`];
4799
- if (!serviceUp) steps.push(`${C.yellow}outerlayer runner start${C.reset}, or ${C.yellow}outerlayer runner install --service${C.reset} to keep it running.`);
5109
+ const steps = agent === void 0 ? [`Edit the command lines in ${result.configPath} to start your agent.`, `${C.yellow}${cliInvocation()} runner check${C.reset}`] : [`${C.yellow}${cliInvocation()} runner check${C.reset}`];
5110
+ if (!serviceUp) steps.push(`${C.yellow}${cliInvocation()} runner start${C.reset}, or ${C.yellow}${cliInvocation()} runner install --service${C.reset} to keep it running.`);
4800
5111
  process.stdout.write(
4801
5112
  `
4802
5113
  Next:
4803
5114
  ` + steps.map((step, i) => ` ${i + 1}. ${step}
4804
- `).join("") + (serviceUp && agent === void 0 ? ` The runner service claims nothing until step 1 is done and a Claude credential is in ${join(dirname(configPath), "runner.env")}; ${C.yellow}outerlayer runner status${C.reset} shows what it is doing.
4805
- ` : serviceUp ? ` The runner service is running; ${C.yellow}outerlayer runner status${C.reset} shows what it is doing.
5115
+ `).join("") + (serviceUp && agent === void 0 ? ` The runner service claims nothing until step 1 is done and a Claude credential is in ${join(dirname(configPath), "runner.env")}; ${C.yellow}${cliInvocation()} runner status${C.reset} shows what it is doing.
5116
+ ` : serviceUp ? ` The runner service is running; ${C.yellow}${cliInvocation()} runner status${C.reset} shows what it is doing.
4806
5117
  ` : "")
4807
5118
  );
4808
5119
  } catch (err) {
@@ -4813,29 +5124,42 @@ Next:
4813
5124
  throw err;
4814
5125
  }
4815
5126
  });
4816
- 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(
5127
+ runnerCmd.command("image").description("Build a repository's image from its governing repository's devcontainer").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(
4817
5128
  "after",
4818
- "\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"
5129
+ "\nA repository governed by a control plane builds the control plane's image, at the commit\nthe gateway has stored for it. A repository that governs itself fetches the head of its\ndefault branch into the runner's cache. Either way it reads .devcontainer/devcontainer.json\nand 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"
4819
5130
  ).action(async (opts) => {
4820
- const { defaultRunnerConfigPath } = await import('./config-N3C6DFQA.js');
4821
- const { buildRepositoryImage, RecipeError } = await import('./build-BG2YENGK.js');
5131
+ const { defaultRunnerConfigPath } = await import('./config-CLSGABD5.js');
5132
+ const { buildRepositoryImage, RecipeError } = await import('./build-TQ6X5PYN.js');
4822
5133
  const { currentCliBuild } = await import('./cli-build-K56DK4DS.js');
4823
- const { createDockerHost } = await import('./docker-4J77Q2BF.js');
4824
- const { builderSpec } = await import('./builder-2D3WSOOY.js');
4825
- const { loadRunnerConfig } = await import('./config-N3C6DFQA.js');
5134
+ const { createDockerHost } = await import('./docker-BFU3AQGY.js');
5135
+ const { builderSpec } = await import('./builder-C4QNTYMS.js');
5136
+ const { loadRunnerConfig } = await import('./config-CLSGABD5.js');
4826
5137
  const configPath = opts.config ?? defaultRunnerConfigPath();
4827
- const memory = (() => {
5138
+ const loaded = (() => {
4828
5139
  try {
4829
- return loadRunnerConfig(configPath).build?.memory ?? "8g";
5140
+ return loadRunnerConfig(configPath);
4830
5141
  } catch {
4831
- return "8g";
5142
+ return void 0;
4832
5143
  }
4833
5144
  })();
5145
+ const memory = loaded?.build?.memory ?? "8g";
4834
5146
  try {
5147
+ const { imageTargetFor } = await import('./governing-source-V5EEGPTA.js');
5148
+ const target = await imageTargetFor({ config: loaded, repository: opts.repository, fetchImpl: fetch });
5149
+ if (!target.ok) {
5150
+ if (opts.json) {
5151
+ writeJson({ error: { code: "governing_source_unavailable", message: target.reason } });
5152
+ process.exitCode = 1;
5153
+ return;
5154
+ }
5155
+ fail(target.reason);
5156
+ return;
5157
+ }
4835
5158
  const result = await buildRepositoryImage(
4836
5159
  {
4837
- repository: opts.repository,
4838
- remote: opts.remote,
5160
+ repository: target.repository,
5161
+ ...target.pinned === void 0 ? {} : { pinned: target.pinned },
5162
+ remote: target.pinned === void 0 ? opts.remote : void 0,
4839
5163
  configPath,
4840
5164
  cli: currentCliBuild(),
4841
5165
  log: (line) => {
@@ -4883,22 +5207,22 @@ Next:
4883
5207
  }
4884
5208
  });
4885
5209
  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) => {
4886
- const { defaultRunnerConfigPath } = await import('./config-N3C6DFQA.js');
4887
- const { requestStop } = await import('./loop-FK3LPMMZ.js');
5210
+ const { defaultRunnerConfigPath } = await import('./config-CLSGABD5.js');
5211
+ const { requestStop } = await import('./loop-SYYPVFYW.js');
4888
5212
  const configPath = opts.config ?? defaultRunnerConfigPath();
4889
5213
  await requestStop(configPath, Boolean(opts.now));
4890
5214
  });
4891
5215
  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) => {
4892
- const { defaultRunnerConfigPath, loadRunnerConfig } = await import('./config-N3C6DFQA.js');
4893
- const { jobsDir, runnerDir } = await import('./paths-OYKMVYJP.js');
4894
- const { listJobFiles, runnerProcessState } = await import('./loop-FK3LPMMZ.js');
4895
- const { renderStatus } = await import('./status-7VNQN5PW.js');
4896
- const { readHousekeepingRecord } = await import('./record-C43U4PKS.js');
5216
+ const { defaultRunnerConfigPath, loadRunnerConfig } = await import('./config-CLSGABD5.js');
5217
+ const { jobsDir, runnerDir } = await import('./paths-7XP63OAX.js');
5218
+ const { listJobFiles, runnerProcessState } = await import('./loop-SYYPVFYW.js');
5219
+ const { renderStatus } = await import('./status-NAK6DYSF.js');
5220
+ const { readHousekeepingRecord } = await import('./record-3WP6V2K5.js');
4897
5221
  const path2 = await import('path');
4898
5222
  const configPath = opts.config ?? defaultRunnerConfigPath();
4899
5223
  const all = listJobFiles(configPath);
4900
5224
  const housekeeping = readHousekeepingRecord(runnerDir(configPath));
4901
- const { readPendingUpdate } = await import('./pending-MXLSRYMO.js');
5225
+ const { readPendingUpdate } = await import('./pending-532ABZYC.js');
4902
5226
  const pendingUpdate = readPendingUpdate(runnerDir(configPath));
4903
5227
  let host = "unknown";
4904
5228
  let concurrency = 1;
@@ -4912,7 +5236,7 @@ Next:
4912
5236
  const service = await (async () => {
4913
5237
  if (process.platform !== "linux" || !existsSync("/run/systemd/system")) return void 0;
4914
5238
  const { inspectService, realInspectProbes } = await import('./inspect-6UYAAXH7.js');
4915
- const { DEFAULT_UNIT_NAME } = await import('./install-HDBIPHTX.js');
5239
+ const { DEFAULT_UNIT_NAME } = await import('./install-4SEFIIW2.js');
4916
5240
  const facts = inspectService(DEFAULT_UNIT_NAME, path2.dirname(configPath), realInspectProbes());
4917
5241
  return facts === void 0 ? void 0 : { unit: facts.unit, running: facts.mainPid !== void 0, delegated: facts.delegate };
4918
5242
  })();
@@ -4947,9 +5271,9 @@ Next:
4947
5271
  );
4948
5272
  });
4949
5273
  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) => {
4950
- const { defaultRunnerConfigPath } = await import('./config-N3C6DFQA.js');
4951
- const { listJobFiles } = await import('./loop-FK3LPMMZ.js');
4952
- const { newestAttempt, logPathFor, tail, RunnerLogsError } = await import('./logs-H55NWW4H.js');
5274
+ const { defaultRunnerConfigPath } = await import('./config-CLSGABD5.js');
5275
+ const { listJobFiles } = await import('./loop-SYYPVFYW.js');
5276
+ const { newestAttempt, logPathFor, tail, RunnerLogsError } = await import('./logs-AMLKC3VP.js');
4953
5277
  const configPath = opts.config ?? defaultRunnerConfigPath();
4954
5278
  const item = parseTrackerNumber(itemArg);
4955
5279
  if (item === void 0) {
@@ -4983,32 +5307,22 @@ function registerHooksCommands(program2) {
4983
5307
  "after",
4984
5308
  "\n`init` auto-wraps PreToolUse/PostToolUse hooks; these subcommands cover the\nrest: manual re-scans, other events, and undo.\n"
4985
5309
  );
4986
- hooksCmd.command("install-git").description("Install the commit-guard git hook in this repo").option("--print", "print the hook scripts instead of installing").addHelpText(
5310
+ hooksCmd.command("install-git").description("Remove the git hooks an earlier version installed in this repo").addHelpText(
4987
5311
  "after",
4988
- "\nThe same install `init` performs. `--print` writes the script to stdout for\nrepositories that manage hooks in-tree (husky and friends).\n"
4989
- ).action((opts) => {
4990
- if (opts.print) {
4991
- for (const [name, script] of managedHookScripts()) {
4992
- process.stdout.write(`# ==== ${name} ====
4993
- ${script}
4994
- `);
4995
- }
4996
- return;
4997
- }
5312
+ "\nEarlier versions installed a pre-commit guard and commit journal hooks. This removes\nthe ones OuterLayer wrote and leaves any hook it did not write untouched.\n"
5313
+ ).action(() => {
4998
5314
  const removal = removeJournalHooks(process.cwd());
4999
5315
  if (removal.removed) {
5000
5316
  process.stdout.write(`${C.green}\u2713${C.reset} removed the commit journal hooks an earlier version installed \u2192 ${removal.path}
5001
5317
  `);
5002
5318
  }
5003
- const guard = installPreCommitGuardHook(process.cwd());
5004
- const report = formatGuardInstallOutput(guard);
5005
- if (report.error !== void 0) {
5006
- fail(report.error);
5007
- return;
5319
+ const guard = removePreCommitGuardHook(process.cwd());
5320
+ if (guard.removed) {
5321
+ process.stdout.write(`${C.green}\u2713${C.reset} removed the pre-commit guard an earlier version installed \u2192 ${guard.path}
5322
+ `);
5008
5323
  }
5009
- for (const line of report.lines) process.stdout.write(`${line}
5324
+ if (!removal.removed && !guard.removed) process.stdout.write(`${C.dim}No git hooks from an earlier version were found.${C.reset}
5010
5325
  `);
5011
- if (report.exitCode !== void 0) process.exitCode = report.exitCode;
5012
5326
  });
5013
5327
  hooksCmd.command("status").description("List hook entries in user and project settings, marking the wrapped ones").option("--json", "machine-readable JSON output").action((opts) => {
5014
5328
  const report = [];
@@ -5170,9 +5484,22 @@ function registerMcpCommands(program2) {
5170
5484
  const mcpCmd = program2.command("mcp").description("Configure MCP clients against the OuterLayer gateway");
5171
5485
  mcpCmd.command("install").description("Write or update the OuterLayer entry in .mcp.json or .outerlayer/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(
5172
5486
  "after",
5173
- "\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"
5487
+ `
5488
+ The API key is never a flag and never written to .mcp.json as a literal value.
5489
+ stdio (default): the client spawns \`${cliInvocation()} mcp serve\`, which reads the key
5490
+ from ~/.outerlayer/config.json (or OUTERLAYER_API_KEY) on every connect, so a
5491
+ rotated key is picked up without rewriting the entry.
5492
+ http: the entry carries the \${OUTERLAYER_API_KEY} placeholder, which the client
5493
+ resolves from the environment it was launched with.
5494
+
5495
+ --command: use an absolute or \${HOME}-relative path when the CLI is not on PATH.
5496
+ --url: with stdio the endpoint resolves from OUTERLAYER_URL / the config file at
5497
+ connect time; for self-host pass your gateway-node origin + /v1/mcp.
5498
+ Self-host deployments also need --app-id: without a key service to resolve the
5499
+ caller from the bearer token, the gateway answers 401 without that header.
5500
+ `
5174
5501
  ).action(async (opts) => {
5175
- const { runMcpInstall, handleMcpInstallError, McpInstallError } = await import('./mcp-install-cmd-FDTPQQS6.js');
5502
+ const { runMcpInstall, handleMcpInstallError, McpInstallError } = await import('./mcp-install-cmd-GGWB3JJ2.js');
5176
5503
  try {
5177
5504
  if (opts.transport !== void 0 && opts.transport !== "stdio" && opts.transport !== "http") {
5178
5505
  throw new McpInstallError(`--transport must be "stdio" or "http", got "${opts.transport}"`);
@@ -5194,7 +5521,7 @@ function registerMcpCommands(program2) {
5194
5521
  "after",
5195
5522
  "\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"
5196
5523
  ).action(async (opts) => {
5197
- const { runMcpServe, handleMcpServeError } = await import('./mcp-serve-cmd-3KVYEUXG.js');
5524
+ const { runMcpServe, handleMcpServeError } = await import('./mcp-serve-cmd-N3UTY6EE.js');
5198
5525
  try {
5199
5526
  await runMcpServe({ url: opts.url, appId: opts.appId });
5200
5527
  } catch (err) {
@@ -5205,7 +5532,7 @@ function registerMcpCommands(program2) {
5205
5532
  function registerImportCommands(program2) {
5206
5533
  const importCmd = program2.command("import").description("Import an existing config format into .outerlayer/");
5207
5534
  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) => {
5208
- const { runImportRuler, ImportRulerError } = await import('./import-ruler-cmd-BGAZXCQM.js');
5535
+ const { runImportRuler, ImportRulerError } = await import('./import-ruler-cmd-5P6JOXQN.js');
5209
5536
  try {
5210
5537
  const result = runImportRuler({ cwd: opts.dir, json: opts.json });
5211
5538
  if (result.exitCode !== 0) process.exitCode = result.exitCode;
@@ -5221,7 +5548,7 @@ function registerImportCommands(program2) {
5221
5548
  "after",
5222
5549
  "\nInstalls the four maintained skills (outerlayer-evidence, outerlayer-amend,\nouterlayer-findings, outerlayer-cli) 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"
5223
5550
  ).action(async (opts) => {
5224
- const { runImportCapture, ImportCaptureError } = await import('./import-capture-cmd-EQ35EARA.js');
5551
+ const { runImportCapture, ImportCaptureError } = await import('./import-capture-cmd-KKFFOGWP.js');
5225
5552
  try {
5226
5553
  runImportCapture({ cwd: opts.dir, json: opts.json });
5227
5554
  } catch (err) {
@@ -5233,59 +5560,14 @@ function registerImportCommands(program2) {
5233
5560
  }
5234
5561
  });
5235
5562
  }
5236
- function formatContextMaterializeOutput(result) {
5237
- switch (result.status) {
5238
- case "not-a-repo":
5239
- return { stdout: [], stderr: [`${C.red}\u2717${C.reset} not inside a git repository.`], exitCode: 1 };
5240
- case "self":
5241
- return { stdout: [`${C.dim}This repository is self-sourced \u2014 nothing to materialize.${C.reset}`], stderr: [] };
5242
- case "materialized": {
5243
- const stdout = [`${C.green}\u2713${C.reset} Materialized context at ${C.dim}${result.sha?.slice(0, 7)}${C.reset}`];
5244
- if (result.message) stdout.push(`${C.yellow}!${C.reset} ${result.message}`);
5245
- return { stdout, stderr: [] };
5246
- }
5247
- case "debounced": {
5248
- const stdout = [`${C.green}\u2713${C.reset} Already current at ${C.dim}${result.sha?.slice(0, 7)}${C.reset} (verification: ${result.verification})`];
5249
- if (result.message) stdout.push(`${C.yellow}!${C.reset} ${result.message}`);
5250
- return { stdout, stderr: [] };
5251
- }
5252
- case "adoption-conflict":
5253
- return { stdout: [`${C.yellow}!${C.reset} ${result.message}`], stderr: [], exitCode: 1 };
5254
- default:
5255
- return { stdout: [`${C.yellow}!${C.reset} ${result.message ?? result.status}`], stderr: [], exitCode: 1 };
5256
- }
5257
- }
5258
5563
  function registerContextCommands(program2) {
5259
- const contextCmd = program2.command("context").description("Compile and materialize agent context");
5564
+ const contextCmd = program2.command("context").description("Compile agent context");
5260
5565
  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(
5261
5566
  "after",
5262
5567
  '\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 (outerlayer-evidence, outerlayer-amend, outerlayer-findings,\nouterlayer-cli) follow this CLI: emit rewrites one that differs from its bundle, and --check\nreports it as drift. A copy under an earlier name (amend, emitting-evidence,\nreporting-findings, outerlayer) is deleted, and --check reports it as retired.\nTemplates (spec, writing-specs, writing-tests, 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'
5263
5568
  ).action(async (opts) => {
5264
5569
  await compileContext({ dir: opts.dir, check: opts.check, json: opts.json });
5265
5570
  });
5266
- 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) => {
5267
- const { runContextAdopt } = await import('./context-adopt-NFHCLVUB.js');
5268
- const { exitCode, stdout } = runContextAdopt(stagedDir, process.cwd());
5269
- process.stdout.write(stdout);
5270
- process.exitCode = exitCode;
5271
- });
5272
- contextCmd.command("materialize").description("Materialize this repository's governed context from its control plane").option("--json", "machine-readable JSON output").addHelpText(
5273
- "after",
5274
- "\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"
5275
- ).action(async (opts) => {
5276
- const { materializeContext } = await import('./context-materialize-IV2TUUFO.js');
5277
- const result = await materializeContext({ cwd: process.cwd(), sessionId: `manual-${Date.now()}` });
5278
- const output = formatContextMaterializeOutput(result);
5279
- if (output.exitCode !== void 0) process.exitCode = output.exitCode;
5280
- if (opts.json) {
5281
- writeJson(result);
5282
- return;
5283
- }
5284
- for (const line of output.stdout) process.stdout.write(`${line}
5285
- `);
5286
- for (const line of output.stderr) process.stderr.write(`${line}
5287
- `);
5288
- });
5289
5571
  }
5290
5572
 
5291
- export { buildProgram, formatContextMaterializeOutput, formatGuardInstallOutput, formatInitOutput, runCli };
5573
+ export { buildProgram, formatInitOutput, runCli };