@ucsandman/legcli 0.8.0 → 0.9.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 (110) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/NOTICE +8 -0
  3. package/README.md +601 -560
  4. package/bin/fake-agent.mjs +4 -4
  5. package/bin/leg.mjs +21 -12
  6. package/docs/DECISIONS.md +20 -2
  7. package/docs/ERRORS.md +71 -0
  8. package/docs/README.md +2 -0
  9. package/docs/REUSE.md +1 -1
  10. package/docs/VOCABULARY.md +21 -0
  11. package/docs/board-guide.md +13 -0
  12. package/docs/cli-contracts.md +22 -1
  13. package/docs/concepts.md +42 -3
  14. package/docs/configuration.md +22 -1
  15. package/docs/faq.md +19 -0
  16. package/docs/getting-started.md +272 -251
  17. package/docs/harness.md +319 -0
  18. package/fixtures/verified.json +1 -1
  19. package/package.json +7 -3
  20. package/scripts/build-docs-site.mjs +11 -4
  21. package/scripts/check-branding.mjs +118 -0
  22. package/scripts/check-claims.mjs +1 -1
  23. package/scripts/license-sign.mjs +1 -1
  24. package/scripts/limits-table.mjs +1 -1
  25. package/scripts/live-limits.mjs +1 -1
  26. package/scripts/npm-publish-gate.mjs +114 -0
  27. package/scripts/probe.mjs +4 -3
  28. package/scripts/seed-fake-cards.mjs +4 -3
  29. package/scripts/seed-floor-board.mjs +5 -4
  30. package/scripts/seed-wes-board.mjs +5 -4
  31. package/scripts/stripe-setup.mjs +1 -1
  32. package/scripts/sync-harness-engine.mjs +159 -0
  33. package/scripts/sync-leg-agents.mjs +127 -0
  34. package/src/accounts.mjs +1 -2
  35. package/src/adapters/codex.mjs +1 -1
  36. package/src/attach.mjs +75 -19
  37. package/src/auth.mjs +2 -2
  38. package/src/board/board.js +3 -3
  39. package/src/board/sessions.js +77 -3
  40. package/src/bundle.mjs +54 -8
  41. package/src/chain.mjs +1 -1
  42. package/src/contract.mjs +4 -3
  43. package/src/fsx.mjs +5 -2
  44. package/src/handoff.mjs +6 -6
  45. package/src/harness/cli.mjs +281 -0
  46. package/src/harness/fingerprint.mjs +68 -0
  47. package/src/harness/index.mjs +407 -0
  48. package/src/harness/registry.mjs +124 -0
  49. package/src/harness/vendor/agnostic-ai/LICENSE +21 -0
  50. package/src/harness/vendor/agnostic-ai/UPSTREAM.json +30 -0
  51. package/src/harness/vendor/agnostic-ai/core/safety/guards.json +96 -0
  52. package/src/harness/vendor/agnostic-ai/core/templates/targets.json +252 -0
  53. package/src/harness/vendor/agnostic-ai/engine/harness/README.md +199 -0
  54. package/src/harness/vendor/agnostic-ai/engine/harness/apply.cjs +247 -0
  55. package/src/harness/vendor/agnostic-ai/engine/harness/bundle.cjs +243 -0
  56. package/src/harness/vendor/agnostic-ai/engine/harness/capture.cjs +119 -0
  57. package/src/harness/vendor/agnostic-ai/engine/harness/common.cjs +375 -0
  58. package/src/harness/vendor/agnostic-ai/engine/harness/index.cjs +55 -0
  59. package/src/harness/vendor/agnostic-ai/engine/harness/sources/claude.cjs +330 -0
  60. package/src/harness/vendor/agnostic-ai/engine/harness/sources/codex.cjs +314 -0
  61. package/src/harness/vendor/agnostic-ai/engine/harness/status.cjs +171 -0
  62. package/src/harness/vendor/agnostic-ai/engine/harness/targets/agy.cjs +113 -0
  63. package/src/harness/vendor/agnostic-ai/engine/harness/targets/claude.cjs +158 -0
  64. package/src/harness/vendor/agnostic-ai/engine/harness/targets/codex.cjs +832 -0
  65. package/src/harness/vendor/agnostic-ai/engine/harness/targets/cursor.cjs +87 -0
  66. package/src/harness/vendor/agnostic-ai/engine/harness/targets/gemini.cjs +128 -0
  67. package/src/harness/vendor/agnostic-ai/engine/harness/targets/generic.cjs +424 -0
  68. package/src/harness/vendor/agnostic-ai/engine/harness/toml.cjs +149 -0
  69. package/src/harness/vendor/agnostic-ai/engine/hooks/shim.cjs +431 -0
  70. package/src/hook.mjs +49 -49
  71. package/src/land.mjs +7 -35
  72. package/src/launcher.mjs +38 -26
  73. package/src/ledger.mjs +6 -6
  74. package/src/license.mjs +10 -9
  75. package/src/live-capture.mjs +1 -1
  76. package/src/mergequeue.mjs +5 -5
  77. package/src/orchestrator.mjs +28 -4
  78. package/src/preferences.mjs +37 -3
  79. package/src/redact.mjs +1 -1
  80. package/src/resume.mjs +17 -15
  81. package/src/runner.mjs +2 -2
  82. package/src/scheduler.mjs +1 -1
  83. package/src/server.mjs +38 -10
  84. package/src/session-detail.mjs +15 -1
  85. package/src/sessions.mjs +6 -3
  86. package/src/share.mjs +2 -2
  87. package/src/stations/agent.mjs +1 -1
  88. package/src/sync/dashclaw.mjs +4 -4
  89. package/src/synthesis.mjs +165 -0
  90. package/src/taps/agy.mjs +2 -2
  91. package/src/taps/claude-usage.mjs +1 -1
  92. package/src/taps/claude.mjs +170 -170
  93. package/src/taps/codex.mjs +286 -286
  94. package/src/taps/grok.mjs +2 -2
  95. package/src/trust.mjs +205 -36
  96. package/src/usage.mjs +5 -1
  97. package/src/worktree.mjs +5 -4
  98. package/fixtures/live/agy/attempt-1-scratch-workspace.out.log +0 -1
  99. package/fixtures/live/agy/err.log +0 -0
  100. package/fixtures/live/agy/out.log +0 -1
  101. package/fixtures/live/agy/supervisor.log +0 -2
  102. package/fixtures/live/claude/err.log +0 -0
  103. package/fixtures/live/claude/out.log +0 -1
  104. package/fixtures/live/claude/supervisor.log +0 -2
  105. package/fixtures/live/codex/err.log +0 -1
  106. package/fixtures/live/codex/out.log +0 -8
  107. package/fixtures/live/codex/supervisor.log +0 -2
  108. package/fixtures/live/grok/err.log +0 -32
  109. package/fixtures/live/grok/out.log +0 -7
  110. package/fixtures/live/grok/supervisor.log +0 -2
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // fake-agent — stands in for a coding-agent CLI in tests and the demo. It acts
3
3
  // in the process cwd (the card's worktree). FAKE_MODE selects behaviour:
4
- // success write the target file and .baton/DONE, print a result JSON, exit 0
4
+ // success write the target file and .leg/DONE, print a result JSON, exit 0
5
5
  // incomplete write the target file but no DONE marker, exit 0
6
6
  // limit print the recorded limit text (FAKE_LIMIT_FIXTURE, default
7
7
  // claude-session-limit) to the fixture's stream, exit with its code
@@ -58,8 +58,8 @@ function writeTarget() {
58
58
  }
59
59
 
60
60
  function writeDone(line) {
61
- mkdirSync(join(cwd, '.baton'), { recursive: true })
62
- writeFileSync(join(cwd, '.baton', 'DONE'), line + '\n')
61
+ mkdirSync(join(cwd, '.leg'), { recursive: true })
62
+ writeFileSync(join(cwd, '.leg', 'DONE'), line + '\n')
63
63
  }
64
64
 
65
65
  const prompt = process.stdin.isTTY ? '' : await readStdin()
@@ -68,7 +68,7 @@ if (delay > 0) await sleep(delay)
68
68
  if (mode === 'success') {
69
69
  writeTarget()
70
70
  writeDone(`wrote ${target}`)
71
- out({ session_id: 'sess-fake', result: `wrote ${target} and .baton/DONE`, prompt_chars: prompt.length, argv: process.argv.slice(2) })
71
+ out({ session_id: 'sess-fake', result: `wrote ${target} and .leg/DONE`, prompt_chars: prompt.length, argv: process.argv.slice(2) })
72
72
  process.exit(0)
73
73
  } else if (mode === 'break-test') {
74
74
  // land demo: ship the change together with a failing test
package/bin/leg.mjs CHANGED
@@ -26,6 +26,7 @@ import { listUsage, fmtReset } from '../src/usage.mjs'
26
26
  import { home } from '../src/store.mjs'
27
27
  import { entitlement, allows, describe as describeLicense, activate as activateLicense, deactivate as deactivateLicense, refresh as refreshLicense, licensePath, BUY_URL } from '../src/license.mjs'
28
28
  import { resumeVerdict, bodyOf, ago } from '../src/resume.mjs'
29
+ import { harnessCommand } from '../src/harness/cli.mjs'
29
30
 
30
31
  const SRC = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'src')
31
32
  // one source of truth for the version, so the help text cannot drift from the package
@@ -86,14 +87,14 @@ function simulateLimit(s) {
86
87
  if (s.agent === 'agy') {
87
88
  appendFileSync(join(sessionDir(s.session_id), 'agy.log'), '\nrpc error: code = ResourceExhausted desc = RESOURCE_EXHAUSTED quota (simulated by leg sessions simulate-limit)\n')
88
89
  appendEvent(s.session_id, { type: 'status', summary: 'simulated RESOURCE_EXHAUSTED appended to the session log' })
89
- return out(`simulated: RESOURCE_EXHAUSTED appended to ${join(sessionDir(s.session_id), 'agy.log')}; the runner reads it within ${process.env.BATON_ATTACH_POLL_MS || 2000} ms and hands off to ${s.chain?.[0]?.agent ?? 'nothing'}`)
90
+ return out(`simulated: RESOURCE_EXHAUSTED appended to ${join(sessionDir(s.session_id), 'agy.log')}; the runner reads it within ${(process.env.LEG_ATTACH_POLL_MS || process.env.BATON_ATTACH_POLL_MS) || 2000} ms and hands off to ${s.chain?.[0]?.agent ?? 'nothing'}`)
90
91
  }
91
92
  if (s.agent === 'grok') {
92
93
  appendFileSync(join(sessionDir(s.session_id), 'grok.log'), "\nRate limited (429): You've hit the rate limit for your plan. Try again later. (simulated by leg sessions simulate-limit)\n")
93
94
  appendEvent(s.session_id, { type: 'status', summary: 'simulated rate limit appended to the grok log' })
94
- return out(`simulated: rate limit appended to ${join(sessionDir(s.session_id), 'grok.log')}; the runner reads it within ${process.env.BATON_ATTACH_POLL_MS || 2000} ms and hands off to ${s.chain?.[0]?.agent ?? 'nothing'}`)
95
+ return out(`simulated: rate limit appended to ${join(sessionDir(s.session_id), 'grok.log')}; the runner reads it within ${(process.env.LEG_ATTACH_POLL_MS || process.env.BATON_ATTACH_POLL_MS) || 2000} ms and hands off to ${s.chain?.[0]?.agent ?? 'nothing'}`)
95
96
  }
96
- die(2, `simulate-limit drives the claude hook path (and the agy/grok log); codex's wall comes from its own rollout file, which Baton never writes. Use "leg sessions handoff ${s.session_id}" to force the switch.`)
97
+ die(2, `simulate-limit drives the claude hook path (and the agy/grok log); codex's wall comes from its own rollout file, which Leg never writes. Use "leg sessions handoff ${s.session_id}" to force the switch.`)
97
98
  }
98
99
 
99
100
  function fmtCard(c) {
@@ -138,7 +139,7 @@ async function main() {
138
139
  ' ██████████████████████████',
139
140
  ' ████ ████ ████ ████ ████ ████',
140
141
  ].join('\n')
141
- out('🦿 LegCli: the mechanical relay runner for coding agents.\n')
142
+ out('🦿 Leg: the mechanical relay runner for coding agents.\n')
142
143
  out(LEG_ART)
143
144
  out('\nPassing the leg to the next runner when limits hit.')
144
145
  return
@@ -146,7 +147,7 @@ async function main() {
146
147
  if (SUPERVISED_AGENTS.includes(group)) {
147
148
  // leg claude|codex|agy|grok [agent args...]: everything after the agent name
148
149
  // goes straight through.
149
- const code = await attach(group, [cmd, ...rest].filter((x) => x !== undefined), { open: process.env.BATON_NO_OPEN !== '1' })
150
+ const code = await attach(group, [cmd, ...rest].filter((x) => x !== undefined), { open: (process.env.LEG_NO_OPEN || process.env.BATON_NO_OPEN) !== '1' })
150
151
  process.exit(code)
151
152
  }
152
153
  if (group === 'sessions') {
@@ -192,15 +193,15 @@ async function main() {
192
193
  const v = resumeVerdict(where)
193
194
  if (a.json) { out(JSON.stringify(v, null, 2)); process.exit(v.exit_code) }
194
195
  if (v.state === 'missing') {
195
- out(`no resume pointer in this checkout (looked for .baton/RESUME.md from ${where} upward).`)
196
- out('Baton writes one when a terminal hands off; `baton claude` in this directory starts one.')
196
+ out(`no resume pointer in this checkout (looked for .leg/RESUME.md from ${where} upward).`)
197
+ out('Leg writes one when a terminal hands off; `leg claude` in this directory starts one.')
197
198
  process.exit(v.exit_code)
198
199
  }
199
200
  const head = v.head?.now ? `${v.head.now.slice(0, 7)}${v.head.branch ? ` on ${v.head.branch}` : ''}` : 'no commit'
200
201
  const line = v.state === 'fresh'
201
202
  ? `${v.file} is current: written ${v.written_at ? ago(v.age_ms) : 'at an unrecorded time'}, and the repository is still at ${head}.`
202
203
  : v.state === 'unstamped'
203
- ? `${v.file} is UNSTAMPED: ${v.reasons[0]}. Baton did not write it, or an older Baton did.`
204
+ ? `${v.file} is UNSTAMPED: ${v.reasons[0]}. Leg did not write it, or an older version did.`
204
205
  : `${v.file} is STALE: ${v.reasons.join('; ')}.`
205
206
  if (a.check) {
206
207
  out(line)
@@ -300,7 +301,7 @@ async function main() {
300
301
  out('')
301
302
  out('Log in once (paste in PowerShell):')
302
303
  out(` ${r.login}`)
303
- out(`Then: $env:BATON_ACCOUNT='${name}'; baton ${agent} (or let a limit hand off to it)`)
304
+ out(`Then: $env:LEG_ACCOUNT='${name}'; leg ${agent} (or let a limit hand off to it)`)
304
305
  } catch (err) { die(2, err.message) }
305
306
  return
306
307
  }
@@ -322,6 +323,12 @@ async function main() {
322
323
  if (cmd === 'terms') return out(TERMS)
323
324
  die(2, `unknown accounts command "${cmd}" (ls|add|rm|terms)`)
324
325
  }
326
+ if (group === 'harness') {
327
+ // The portable harness: the working environment a hand-off carries with
328
+ // the task. Off until `leg harness enable` (src/harness/index.mjs).
329
+ const code = await harnessCommand(cmd, args, { out, die })
330
+ process.exit(code)
331
+ }
325
332
  if (group === 'license') {
326
333
  // The paid gate. Keys verify offline against the public key in
327
334
  // src/license.mjs; nothing here talks to the network except refresh.
@@ -350,7 +357,7 @@ async function main() {
350
357
  }
351
358
  if (group === 'uninstall') {
352
359
  // Leg never edits ~/.claude or ~/.codex; everything it added lives under
353
- // $BATON_HOME (sessions, usage, extra-account dirs, cards).
360
+ // $LEG_HOME (sessions, usage, extra-account dirs, cards).
354
361
  const dir = home()
355
362
  if (!args.yes) {
356
363
  out(`leg uninstall removes ${dir} (sessions, usage, extra-account dirs, cards, board pidfile) and nothing else.`)
@@ -360,7 +367,7 @@ async function main() {
360
367
  for (const r of listAccountRows()) if (r.name !== 'default') removeAccount(r.agent, r.name)
361
368
  await down()
362
369
  rmSync(dir, { recursive: true, force: true })
363
- return out(`removed ${dir}; now: npm rm -g legcli`)
370
+ return out(`removed ${dir}; now: npm rm -g @ucsandman/legcli`)
364
371
  }
365
372
  if (group === 'card') {
366
373
  if (cmd === 'add') return cardAdd(args)
@@ -447,7 +454,7 @@ async function main() {
447
454
  out(openBoard(url) ? `opened ${url}` : `could not open a browser; visit ${url}`)
448
455
  return
449
456
  }
450
- if (group && group !== '--help' && group !== 'help') die(2, `unknown command "${group}" (claude|codex|agy|grok|sessions|resume|accounts|license|share|up|down|status|open|card|scheduler|uninstall)`)
457
+ if (group && group !== '--help' && group !== 'help') die(2, `unknown command "${group}" (claude|codex|agy|grok|sessions|resume|accounts|harness|license|share|up|down|status|open|card|scheduler|uninstall)`)
451
458
  out(`leg ${VERSION}, your coding agents, with a board alongside and a handoff when one hits its limit
452
459
  claude|codex|agy|grok [args...] the normal interactive agent in this terminal; args pass straight through
453
460
  the board opens once, the session shows as a card, usage is tracked, a limit hands off
@@ -458,6 +465,8 @@ async function main() {
458
465
  freshness is recomputed from git at read time; --check prints only the verdict
459
466
  exit 0 current, 1 stale or unstamped, 3 no pointer here
460
467
  accounts ls|add <agent> <name>|rm|terms optional second login for claude or codex
468
+ harness status|enable|sync|check|explain|... carry the source agent's rules, hooks, skills, agents, commands and MCP
469
+ servers to the agent a hand-off lands on; off until enabled (leg harness help)
461
470
  license [status|activate <key>|deactivate|refresh]
462
471
  personal or team license status and management
463
472
  share status|on|add <name>|rotate <name>|rm <name>|off
package/docs/DECISIONS.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  Durable product and design decisions that the code does not explain on its own. One entry per decision, newest first.
4
4
 
5
+ ## 2026-09-16: the portable harness is an opt-in subsystem over a vendored, hash-pinned engine
6
+
7
+ - **What.** `leg harness` carries the source agent's working environment (rules, identity, hooks, skills, subagents, commands, MCP servers, permissions) to the agent a hand-off lands on. The capture, neutral bundle and apply engine is the Agnostic AI port engine (MIT), embedded byte for byte under `src/harness/vendor/agnostic-ai/` and driven through its library entry; Leg owns consent, policy, the client registry, state, the fingerprint, the trail and the hand-off decision (`src/harness/*.mjs`).
8
+ - **Why vendor, and why verbatim.** Leg ships with zero runtime dependencies and no build step, and Agnostic AI is a private-by-default template repo, so a package dependency was not on the table. A copy that is edited locally drifts forever; a copy that is verified against recorded hashes cannot. `scripts/sync-harness-engine.mjs --check` runs in `npm test` and fails on any local edit; a fix lands upstream, then the sync copies it in. Upstream was made embeddable first (`configure({ brand, secretPatterns, shimPath, importRoots })`, injected registry and policy, a library entry), with its own regression proving the boundary, so the vendored files need no patching.
9
+ - **Why CommonJS stays.** Node's ESM loader imports the CommonJS entry directly. Converting upstream to ESM would have rewritten working machinery for style and broken its dynamic adapter loading; keeping the boundary keeps the copy verbatim.
10
+ - **Why off by default, and why a policy.** Leg's promise that it leaves your settings files alone stands for every install that never runs `leg harness enable`. The first run shows what will be written and asks. After that a hand-off lands when nobody is at the keyboard, so the saved policy decides, never a prompt: `warn` reports, `sync` writes what is safe, `strict` refuses what is not and tries the next option, ending the terminal with exit 5 only when none is left. The strict refusal applies to hand-offs, not to the agent the human started.
11
+ - **What a write may do.** Only owned files (`GENERATED by Leg harness` in the head) and marked regions or owned keys inside files the user also owns; a backup before every overwrite; a hand-edited file skipped and named; the source client never written; credentials replaced by `${NAME}` and a bundle that still carries one refused. The same discipline as the folder-trust record, extended.
12
+ - **What stays separate.** Accounts. The harness describes behaviour; the account layer decides which login runs, and a same-agent hand-off to a second login carries no harness.
13
+ - **Registry.** Only the clients Leg launches, plus Gemini CLI because it shares `GEMINI.md` with agy. Grok is reported `unsupported`, never guessed. Upstream's other fifteen targets are not exposed.
14
+
15
+ ## 2026-09-16: @ucsandman/legcli and leg-agents stay on the same version
16
+
17
+ - **What.** The unscoped `leg-agents` package is an alias installer for `@ucsandman/legcli`. It lives in `packages/leg-agents`, always carries the root version, and pins `@ucsandman/legcli` to that exact version. CI publishes both from `.github/workflows/ci.yml` after the same registry gate.
18
+ - **Why.** `npm i -g leg-agents` is the name agents and muscle memory will type. A hand-kept alias is how the two versions drift, and a drifted alias installs yesterday's CLI.
19
+ - **How drift is refused.** `scripts/sync-leg-agents.mjs --check` is part of `npm test`. The publish gate refuses to ship if the two package.json versions differ, or if the alias pin is not the root version. `npm version` / `npm run sync-alias` writes the alias in the same step as the root bump. The alias tarball is not included in the scoped package (`files` does not list `packages/`).
20
+ - **Windows.** The alias wrapper loads `bin/leg.mjs` through `pathToFileURL`. A bare `import(join(absPath))` is a `c:` URL scheme on Windows and throws `ERR_UNSUPPORTED_ESM_URL_SCHEME`.
21
+ - **First publish.** `leg-agents` is a new npm name. Bind a trusted publisher on npmjs.com for `leg-agents` to `ucsandman/legcli` + `ci.yml` (same as the scoped package) before the first CI publish, or publish the first version once with OTP.
22
+
5
23
  ## 2026-09-15: the board is dark cobalt, and there is no light mode
6
24
 
7
25
  - **What.** The board ground is a saturated deep cobalt at hue 258, the same hue the marketing site is drenched in, taken to its dark end. Not a neutral near-black: measured in OKLab, `--e0` sits 0.0507 from `#0f1115` at 5.7 times its chroma, so the anti-reference colour `PRODUCT.md` bans is not reachable from this palette.
@@ -22,7 +40,7 @@ Durable product and design decisions that the code does not explain on its own.
22
40
 
23
41
  ## 2026-09-15: Leg records the folder-trust answer, and never overrides one already given
24
42
 
25
- - **What.** Before starting an agent, Leg writes the folder-trust answer for the repository the user chose by typing `baton <agent>` in it: `hasTrustDialogAccepted` in `~/.claude.json`, `trust_level` in `~/.codex/config.toml`, an entry in `~/.gemini/trustedFolders.json`. `LEG_TRUST=never` turns it off.
43
+ - **What.** Before starting an agent, Leg writes the folder-trust answer for the repository the user chose by typing `leg <agent>` in it: `hasTrustDialogAccepted` in `~/.claude.json`, `trust_level` in `~/.codex/config.toml`, `trustedWorkspaces` in `~/.gemini/antigravity-cli/settings.json` (and `default-cli-project.json` for Gemini project resources). For worktrees, both the repo root and the worktree directory are recorded because Antigravity CLI does an exact string match against `Store.workspacePath`. `LEG_TRUST=never` turns it off.
26
44
  - **Why.** The handoff is the product, and it fires when the limit hits, which is usually when nobody is watching. An agent that had never run in that folder stopped on its first-run trust prompt and waited for a keypress that was not coming, so the bundle was written and the terminal sat idle until morning.
27
45
  - **Why writing those files is allowed at all.** `stdio: 'inherit'` in `src/attach.mjs` hands the real terminal to the agent, so Leg cannot watch for the prompt and answer it. Pre-seeding is the only mechanism that does not change Leg's architecture. For Claude Code it is also the documented remedy: its permissions guide prescribes exactly this edit.
28
46
  - **The three rules that bound it.** Never create a config file that is not already there. Never rewrite a file to say what it already says. Never override an answer already on file: only an absent key is an unanswered question, so a recorded refusal stays a refusal.
@@ -41,7 +59,7 @@ Durable product and design decisions that the code does not explain on its own.
41
59
  - **What.** One static HTML page (`site/index.html`, `style.css`, `site.js`), self-hosted fonts, deployed to Vercel from the `site/` directory with `vercel.json` headers. No framework, no build step. PRODUCT.md and DESIGN.md at the repo root carry the brief and the tokens so later edits inherit them.
42
60
  - **How it was chosen.** A four-concept tournament (light restrained, drenched racing green, committed cobalt, product-led dark terminal) judged against a written rubric. Committed cobalt won and borrowed the DOM-recreated board from the light concept and the typed full-bleed terminal from the product-led one. The scores and disqualifications are recorded in the session notes; the design tokens are in DESIGN.md.
43
61
  - **Why cobalt and not the board's own dark palette.** The operator board and the sibling site declick.dev are both near-black; a third near-black surface from the same author would read as one family and as the generic dark AI-tool page. The site's warmth comes only from the agent colors inside product visuals.
44
- - **What the page promises.** Every number, path, version and date on it is copied from the README as verified on 2026-09-11. The terminal transcript is a labeled sample session whose `[baton]` lines are the strings `src/attach.mjs` prints and whose pointer prompt is the one `src/bundle.mjs` sends. The two-session cards are from the live run the README documents.
62
+ - **What the page promises.** Every number, path, version and date on it is copied from the README as verified on 2026-09-11. The terminal transcript is a labeled sample session whose `[leg]` lines are the strings `src/attach.mjs` prints and whose pointer prompt is the one `src/bundle.mjs` sends. The two-session cards are from the live run the README documents.
45
63
  - **Analytics and search.** Vercel Web Analytics is the only script besides `site.js`; nothing on the page depends on it. Search Console and Bing registration state is recorded below this entry when done.
46
64
 
47
65
  ### Registration state, 2026-09-11
package/docs/ERRORS.md CHANGED
@@ -3,6 +3,77 @@
3
3
  What broke, why, and what fixed it. One entry per failure, newest first. A first
4
4
  occurrence has to be written down or a repeat is never countable.
5
5
 
6
+ ## 2026-09-16: a hand-edited `package.json` version left `package-lock.json` behind, and the publish gate refused 0.9.0
7
+
8
+ **Fixed by bumping the lockfile root version; the gate is `scripts/npm-publish-gate.mjs`.**
9
+
10
+ The version went from 0.8.1 to 0.9.0 by editing `package.json` directly, so
11
+ the lockfile's two root `version` fields still said 0.8.1 and CI's
12
+ `publish-npm` job stopped at "package.json and package-lock.json root metadata
13
+ do not match". The run before it had failed the same way, which is why npm
14
+ still served 0.8.0. Tests, lint and the docs job were green; only the publish
15
+ was refused, which is the gate doing its job. Lesson: bump with `npm version`
16
+ (it writes the lockfile and runs the alias sync) and run the gate locally
17
+ before pushing a release; a green test matrix says nothing about the publish.
18
+
19
+ ## 2026-09-16: the vendored engine's secret scan covered two fields; the review found the other five
20
+
21
+ **Fixed upstream (Agnostic AI 7e35b51) and re-vendored; regression in `test/harness-engine.test.mjs`.**
22
+
23
+ `docs/harness.md` promised "a bundle that still carries a credential is refused
24
+ at save time". The engine's `validate()` scanned `mcp.<n>.env` and
25
+ `mcp.<n>.headers` only; the rules text, identity, hook command lines, MCP
26
+ arguments, URLs and agent bodies were copied verbatim, and the four canary
27
+ tests planted tokens exactly where the scan already looked. The read-only
28
+ security review planted one everywhere else and got one problem back. Root
29
+ cause: a scan written for two map keys, and a test fixture shaped to it. Fix:
30
+ whole-bundle scanning with redaction of free text and drops of unsafe
31
+ handlers or servers, plus `PLANTED` tokens in every place the scan must reach.
32
+ Lesson: a canary proves the place it sits in, nothing else; a fixture written
33
+ by the same hands as the scan finds nothing the scan missed.
34
+
35
+ ## 2026-09-16: `CLAUDE_CONFIG_DIR` outside the OS home was captured from `~/.claude` instead
36
+
37
+ **Fixed upstream in `sources/claude.cjs` (`pick()`), regression in `test/harness-engine.test.mjs`.**
38
+
39
+ The engine kept a registry path only when it sat inside the OS home and fell
40
+ back to `~/.claude` otherwise, while Leg's registry, detection and fingerprint
41
+ honoured the override. A per-account config dir under a `LEG_HOME` on another
42
+ drive would have ported the dormant profile and never noticed edits to the
43
+ active one. Fix: the registry path is trusted as given. Lesson: three code
44
+ paths agreeing on a directory is a property to test, not to assume.
45
+
46
+ ## 2026-09-16: `leg harness sync` wrote before `leg harness enable`; the board could widen the policy
47
+
48
+ **Fixed in `src/harness/cli.mjs` and `src/server.mjs`, regressions in `test/harness-cli.test.mjs` and `test/harness-policy.test.mjs`.**
49
+
50
+ The consent gate lived in `enable` only, so `sync` on an install that never
51
+ enabled the feature wrote managed files while `status` said off; the settings
52
+ route accepted any policy value, so a board POST could take `warn` to
53
+ `strict`. Both were one-line fixes the review caught. Lesson: a consent rule
54
+ has to be checked at every writer, not at the one verb that grants it.
55
+
56
+ ## 2026-09-16: the backup count was always zero
57
+
58
+ **Fixed in `src/harness/index.mjs`.**
59
+
60
+ The engine's writer returns the backup path, but every adapter keeps only the
61
+ action, so counting `f.backup` counted nothing. The count now comes from the
62
+ backups directory before and after an apply. Lesson: a number that never moves
63
+ in a demo is a number nobody is computing.
64
+
65
+ ## 2026-09-16: alias `import(join(windowsPath))` is a `c:` URL scheme
66
+
67
+ **Fixed in `packages/leg-agents/bin/leg.mjs`.**
68
+
69
+ The first `leg-agents` wrapper did `await import(join(pkgRoot, 'bin', 'leg.mjs'))`.
70
+ On Windows that string is `C:\…\bin\leg.mjs`, which Node's ESM loader treats as a
71
+ URL with protocol `c:` and throws `ERR_UNSUPPORTED_ESM_URL_SCHEME`. `npm install`
72
+ of the tarball succeeded; `leg --version` died before printing `0.8.0`.
73
+
74
+ The fix is `await import(pathToFileURL(join(pkgRoot, 'bin', 'leg.mjs')).href)`.
75
+ `require.resolve` returning a path is not a valid ESM specifier on Windows.
76
+
6
77
  ## 2026-09-15: macOS `/var` symlink broke two e2e tests
7
78
 
8
79
  **Fixed in `test/helpers.mjs`.**
package/docs/README.md CHANGED
@@ -6,6 +6,7 @@
6
6
  - [concepts.md](concepts.md): sessions, accounts, usage windows and the interactive handoff, then cards, stations, chains, outcomes, leases, the land station and the card status state diagram.
7
7
  - [board-guide.md](board-guide.md): the instrument head (a row per login, two window rails each, the 85 percent post), the Terminals panels, overlap flags, Landed on main, Background tasks, Settings, and the floor view.
8
8
  - [configuration.md](configuration.md): every environment variable, the accounts layout, `.env`, network exposure, card-level options.
9
+ - [harness.md](harness.md): the portable harness, off by default: what moves between agents and what does not, the first run, policies, ownership and backups, secrets, the evidence trail, and how Leg relates to the Agnostic AI engine it embeds.
9
10
  - [adapters.md](adapters.md): what Leg reads from each CLI in an interactive session, each adapter's headless argv, modes, forbidden flags, gotchas, and how to add a new one.
10
11
  - [faq.md](faq.md): short answers to real questions (the status line, codex's missing hook, agy's missing percentage, second accounts, uninstall, limits, secrets, Windows support).
11
12
 
@@ -22,6 +23,7 @@
22
23
  | area | modules |
23
24
  |------|---------|
24
25
  | interactive sessions | `src/attach.mjs` (the `leg <agent>` runner), `src/sessions.mjs` (the session store), `src/usage.mjs` (usage windows and the chooser), `src/accounts.mjs` (extra logins), `src/bundle.mjs` (the per-session bundle), `src/hook.mjs` (what Claude Code's hooks run) |
26
+ | portable harness | `src/harness/index.mjs` (capture, compare, apply, status, the hand-off decision), `src/harness/registry.mjs` (which clients, where their files are), `src/harness/fingerprint.mjs`, `src/harness/cli.mjs` (`leg harness`), `src/harness/vendor/agnostic-ai/` (the engine, verbatim; `scripts/sync-harness-engine.mjs` is the only writer) |
25
27
  | taps | `src/taps/claude.mjs`, `src/taps/claude-usage.mjs`, `src/taps/codex.mjs`, `src/taps/agy.mjs` |
26
28
  | board | `src/server.mjs`, `src/board/sessions.js` (Terminals lane), `src/board/board.js` and `src/board/floor.js` (pipelines) |
27
29
  | pipelines | `src/orchestrator.mjs`, `src/scheduler.mjs`, `src/chain.mjs`, `src/pipeline.mjs`, `src/runner.mjs`, `src/ledger.mjs`, `src/leases.mjs`, `src/mergequeue.mjs`, `src/adapters/*.mjs` |
package/docs/REUSE.md CHANGED
@@ -229,7 +229,7 @@ station):
229
229
  Network off by default: `-c sandbox_workspace_write.network_access=false`.
230
230
  - Git workflow: snapshot before a leg, snapshot `--diff-since` after; the
231
231
  `worktree` recommendation is Leg's only mode (one worktree per card,
232
- branch `baton/<card-id>`); the diff, never prose, is what review and landing
232
+ branch `leg/<card-id>`); the diff, never prose, is what review and landing
233
233
  trust.
234
234
  - Failure rules that carry over: exit 11 means a supervisor is already running,
235
235
  never relaunch over it; a failed launch is retried once then the chain moves
@@ -51,6 +51,8 @@ Source: the `appendEvent`/`updateSession` call sites in `src/attach.mjs`,
51
51
  | `lost` | the runner pid is gone; the session was marked `lost` |
52
52
  | `error` | a spawn error, a tap error, a failed bundle checkpoint, or an error the agent reported |
53
53
  | `status` | a note that does not fit another type |
54
+ | `harness` | the portable harness was prepared for the leg starting now: the destination's state (`synced`, `partial`, `stale`, `attention`, `unsupported`, `source`, `error`) with what was dropped in the body |
55
+ | `harness_blocked` | the strict harness policy refused the chosen destination; the next option is tried |
54
56
  | `worktree` | another live session was in the checkout, so this one got its own worktree: path, branch, base |
55
57
  | `land_requested` | Land was pressed: the branch and its base |
56
58
  | `land_warning` | the landing ran without a test command |
@@ -58,6 +60,23 @@ Source: the `appendEvent`/`updateSession` call sites in `src/attach.mjs`,
58
60
  | `bounced` | the landing stopped with a [bounce reason](#bounce-reasons-land-station); the full detail is in `body` |
59
61
  | `land_noop` | Land found nothing on the branch beyond its base |
60
62
 
63
+ ## Harness states (terminal cards, drawer, `leg harness`)
64
+
65
+ Source: `STATES` in `src/harness/index.mjs`; recorded on `session.harness.state`.
66
+
67
+ | state | meaning |
68
+ |-------|---------|
69
+ | `off` | the portable harness is not enabled; nothing recorded |
70
+ | `same-client` | a hand-off to another login of the same client; the harness is shared already |
71
+ | `source` | the destination is the source client; never written |
72
+ | `synced` | every component the destination supports is current, nothing dropped |
73
+ | `partial` | current, some items could not be carried (each with a reason) |
74
+ | `stale` | the destination is behind the source (`warn` policy, or a check) |
75
+ | `attention` | a managed file was hand-edited (backed up, skipped) or a component errored |
76
+ | `unsupported` | no adapter for the destination (Grok), or it is not installed |
77
+ | `blocked` | the strict policy refused the destination |
78
+ | `error` | the preparation failed; the reason is recorded |
79
+
61
80
  ## Land states (terminal cards)
62
81
 
63
82
  `$LEG_HOME/sessions/<id>/land.json`, written by the board server only
@@ -166,6 +185,8 @@ across `src/chain.mjs`, `src/orchestrator.mjs`, `src/scheduler.mjs`,
166
185
  | `failed` | the card failed (chain exhausted, land attempts exhausted, or an environment fault) |
167
186
  | `error` | an unexpected error (orchestrator crash, handoff bundle write failure, land station crash) |
168
187
  | `status` | a status note that doesn't fit another type (e.g. "rerun from build leg 0") |
188
+ | `harness` | the portable harness was prepared for the adapter about to run; the summary names the state, the body what was dropped |
189
+ | `harness_blocked` | the strict harness policy refused the adapter; the leg fails as `launch_failed` and does not advance |
169
190
 
170
191
  ## Actor types
171
192
 
@@ -184,6 +184,11 @@ is history, and after a day's work it is most of the list. It moves to the
184
184
  ledger (below) as part of a count that opens. A finished terminal that still
185
185
  needs you, or whose expansion you have open, stays in place.
186
186
 
187
+ With the portable harness on, the register also carries one chip for the leg
188
+ now running: `harness synced`, `harness partial`, `harness stale`, `harness
189
+ attention` or `harness refused` (`harness.md`, "States"). Nothing shows when
190
+ the feature is off.
191
+
187
192
  ### The one sentence
188
193
 
189
194
  `rankedNotes` in `src/board/sessions.js` is the single source of every sentence
@@ -289,6 +294,14 @@ from the right, and the page keeps one scroll container
289
294
  Hand off now. A normal exit ends this terminal.`, the **Change order**
290
295
  editor, the current bundle id, and whether `.leg/RESUME.md` still describes
291
296
  the repository (recomputed from git on every poll).
297
+ 8. **Harness** (only when the [portable harness](harness.md) is on): the
298
+ source client, when it was captured and synced, the policy, one line for
299
+ the leg now running (`codex harness partial · 8/8 components, 3 dropped ·
300
+ 1 file(s) written`), a row per component with its state and `carried /
301
+ total`, **Needs you** for a hand-edited file or an unreadable config,
302
+ **Dropped** with a reason per item (`excluded by policy` when the drop was
303
+ yours), and the last eight entries of the harness trail. Every word comes
304
+ from what the session recorded when the leg started, never from a guess.
292
305
 
293
306
  It refetches every 3 seconds while it is open, and stops on **Pause updates**,
294
307
  when the tab is in the background, or when it is closed. The rest of the page
@@ -189,6 +189,27 @@ stderr are 0 bytes, and codex's is the one stdin notice. observed-live.
189
189
  out of `src/adapters/index.mjs` until `grok login` has been completed on the
190
190
  machine and `node scripts/probe.mjs --adapter grok --repo <toy>` passes.
191
191
 
192
+ ## `leg harness` (the portable harness)
193
+
194
+ What the command reads and writes per client, each fact from the engine's
195
+ adapter source (`src/harness/vendor/agnostic-ai/engine/harness/{sources,targets}/*.cjs`,
196
+ byte for byte the Agnostic AI engine) and verified by
197
+ `test/harness-*.test.mjs` against fixture homes on 2026-09-16.
198
+
199
+ | client | read as a source | written as a destination | shim |
200
+ |---|---|---|---|
201
+ | Claude Code | `~/.claude/CLAUDE.md` (+ `@imports` inside the home), `SOUL.md`, the hooks and permissions of its settings file, the `mcpServers` of `~/.claude.json` and `~/.claude/.mcp.json` (the oauth block is never read), `agents/*.md`, `commands/*.md`, `skills/*/SKILL.md` | `~/.claude/leg-rules.md` + one `@` line appended to `CLAUDE.md`; owned hook groups and permission entries in its settings file; owned servers in `~/.claude.json`; `agents/`, `commands/`, skill links | none: the bundle is Claude's dialect |
202
+ | Codex CLI | `~/.codex/AGENTS.md`, `config.toml` (`hooks.*`, `mcp_servers.*`), `agents/*.toml`, `prompts/*.md`, `skills/`, `rules/*.rules` (per-invocation absolute-path approvals skipped; `auth.json` never read) | `AGENTS.md` (whole file, owned), `config.toml` regions `hooks` (with `[hooks.state]` trust hashes, self-tested), `skills` (duplicate disables), `mcp`; `agents/*.toml`; `prompts/*.md`; skill links; `rules/leg-harness.rules` prefix rules | none |
203
+ | Antigravity CLI | not a source | `~/.gemini/GEMINI.md` (owned), the `leg-harness` key in `~/.gemini/config/hooks.json`, `mcp_config.json` servers, `config/agents`, `config/commands`, `config/skills` links | the engine's hook shim, chained with `++` |
204
+ | Gemini CLI | not a source | `GEMINI.md`, owned hook groups in its settings file, `commands/*.toml`, `mcpServers`, skill links | shim |
205
+ | Grok CLI | no | no (reported `unsupported`) | |
206
+
207
+ Exit codes: `0` fine; `1` stale or attention (`check`, `sync`) or a doctor
208
+ failure; `2` usage; `3` not captured, no source, or consent declined
209
+ (`enable` without `--yes` and without a terminal). `--json` on `status`,
210
+ `inspect`, `sync`, `check`, `explain`, `history`, `doctor` prints the same
211
+ record the board reads.
212
+
192
213
  ## Interactive taps
193
214
 
194
215
  What `leg claude|codex|agy|grok` reads while the real interactive CLI runs. Same
@@ -393,7 +414,7 @@ non-zero exit → `failed`. Every outcome except `completed`, `auth_failed` and
393
414
  `killed` asks the chain to hand off.
394
415
 
395
416
  <!-- limits-table:start -->
396
- Generated by `node scripts/limits-table.mjs` from 22 fixtures (4 observed-live, 18 docs-only). Classification `limit` hands the card to the next agent as a usage limit; `auth` is a failed launch (never a limit); `launch` is a failed launch that the next agent may still try; `budget` is a turn or spend cap set by Baton itself; `info` must never classify as a limit.
417
+ Generated by `node scripts/limits-table.mjs` from 22 fixtures (4 observed-live, 18 docs-only). Classification `limit` hands the card to the next agent as a usage limit; `auth` is a failed launch (never a limit); `launch` is a failed launch that the next agent may still try; `budget` is a turn or spend cap set by Leg itself; `info` must never classify as a limit.
397
418
 
398
419
  | id | adapter | class | where | source | text (excerpt) | produced by |
399
420
  |----|---------|-------|-------|--------|----------------|-------------|
package/docs/concepts.md CHANGED
@@ -108,12 +108,14 @@ things in order (`src/attach.mjs`, `src/bundle.mjs`):
108
108
  it again at the transition and during all-out waiting. Machine Settings is
109
109
  copied only when a new terminal starts.
110
110
  3. **Switch.** The agent process is stopped and the terminal restored. The
111
- bundle's `context-handoff-bundle load <id>` output is written to
111
+ bundle's `context-handoff-bundle load <id>` output (with the `## Synthesis`
112
+ section prepended if `.leg/SYNTHESIS-<session-id>.md` is present) is written to
112
113
  `.leg/RESUME-<session-id>.md` and copied to `.leg/RESUME.md`, and the next
113
114
  agent starts in the same terminal with a short pointer prompt as its first
114
115
  positional argument: `claude "<prompt>"`, `codex "<prompt>"`,
115
- `agy -i "<prompt>"`. The prompt names the per-session file, and says to check
116
- `git status` and `git diff`, continue, and not ask the human to restate the
116
+ `agy -i "<prompt>"`. The prompt names the per-session file, directs the agent
117
+ to read Synthesis first and treat ruled-out approaches as settled, and says to
118
+ check `git status` and `git diff`, continue, and not ask the human to restate the
117
119
  task.
118
120
  4. **All out.** If every option is walled, Leg prints each one with its reset
119
121
  time, soonest first, then waits in the terminal with a one-line countdown
@@ -123,6 +125,33 @@ things in order (`src/attach.mjs`, `src/bundle.mjs`):
123
125
  (`{ agent, account, resets_at, since }`) and shows status `waiting`. Ctrl-C
124
126
  in the terminal, or End on the card, quits with exit 3.
125
127
 
128
+ ## The portable harness
129
+
130
+ Off by default. `leg harness enable` adds a step to the hand-off, between
131
+ **Choose** and **Switch**: the destination's working environment is prepared
132
+ before the destination starts. The source client (the one you actually
133
+ configure: Claude Code or Codex) is captured into a client-neutral bundle
134
+ under `~/.leg/harness/bundle/` (rules with their imports inlined, identity,
135
+ hooks, skills, subagents, slash commands, MCP servers with every credential
136
+ replaced by `${NAME}`, permissions), and that bundle is rendered into the
137
+ destination client's own files: `AGENTS.md`, `config.toml` regions and hook
138
+ trust entries for Codex; `GEMINI.md`, `hooks.json` and `mcp_config.json` for
139
+ agy; `leg-rules.md` plus one import line, hook groups and MCP servers for
140
+ Claude Code. Skills are linked, not copied. What a destination cannot
141
+ represent is dropped with a reason, and the session records exactly what
142
+ transferred (`session.harness`, the `harness` event, the **Harness** section
143
+ of the drawer).
144
+
145
+ The saved policy decides what an unattended hand-off may do: `warn` reports
146
+ and writes nothing, `sync` writes managed state when that is safe, `strict`
147
+ refuses a destination it cannot make safe and tries the next option. A
148
+ fingerprint of the source's files makes an unchanged environment free to
149
+ check. Everything Leg writes this way carries `GENERATED by Leg harness`, sits
150
+ in a marked region or an owned key inside files you also own, is backed up
151
+ before it is overwritten, and is skipped when you hand-edited it. The source
152
+ client is never written; credentials never move; which login runs is still the
153
+ account layer's decision. The whole contract is in [harness.md](harness.md).
154
+
126
155
  ## The resume pointer
127
156
 
128
157
  `.leg/RESUME.md` is the file humans and other agents open by habit, so Leg
@@ -303,6 +332,16 @@ notes carry six sections in the bundle's own vocabulary:
303
332
  The next leg's prompt starts with the bundle's `load` output (the resume
304
333
  text) followed by the same contract.
305
334
 
335
+ ### The synthesis layer
336
+
337
+ Alongside raw state, Leg supports an agent-maintained judgment record in `.leg/SYNTHESIS-<session-id>.md`. At handoff time, Leg reads this file and inlines it verbatim into `.leg/RESUME-<session-id>.md` and `.leg/RESUME.md` as a `## Synthesis` section before the raw bundle dump.
338
+
339
+ - **Schema v1**: A 2-line header (`synthesis_version: 1`, `session: <id> updated: <ISO-8601 UTC>`) followed by up to four optional sections in fixed order: `## Ruled out`, `## Decisions`, `## Next steps`, `## Open questions` (max 5 bullets each, one line per bullet).
340
+ - **Size cap**: 4 KB. Beyond that, Leg includes the first 4 KB plus a trailing `[synthesis truncated]` marker.
341
+ - **Resilience**: If the header is malformed, Leg still inlines the body prefixed with `[synthesis header invalid, rendering body as-is]`. If the file is absent or empty, no `## Synthesis` section is emitted and handoff degrades to today's raw dump.
342
+ - **Pointer prompt**: Directs the taking-over agent to read the Synthesis section first if present, treat ruled-out approaches as settled, and start from the top-ranked next step.
343
+ - **Board indicator**: Shows a `synthesis` chip on the terminal card when the synthesis file exists and was modified within the last 3 checkpoints.
344
+
306
345
  ## Worktrees
307
346
 
308
347
  Every card runs in its own git worktree: `<repo>/.leg-worktrees/<card-id>`
@@ -29,6 +29,7 @@ These apply to `leg claude|codex|agy|grok`.
29
29
  | `LEG_NO_AUTO_APPROVE` | (unset) | set to `1` to opt out of auto-approve mode | `src/preferences.mjs` |
30
30
  | `LEG_WARN_PCT` | `85` | the percentage of either usage window that turns the card amber, records a `warning` event and rings the terminal bell once | `src/usage.mjs` |
31
31
  | `LEG_NO_HANDOFF` | (unset, hand-off on) | set to `1` to warn and record but never switch agents | `src/attach.mjs` |
32
+ | `LEG_HARNESS_HOME` | the OS home | where the portable harness reads and writes client configuration (`~/.claude`, `~/.codex`, `~/.gemini` under it); the test suite points it at a throwaway directory. `CLAUDE_CONFIG_DIR`, `CODEX_HOME` and `GEMINI_CONFIG_DIR` move one client each, as everywhere else in Leg | `src/harness/registry.mjs` |
32
33
  | `LEG_NO_OPEN` | (unset, opens once) | set to `1` to start the board without opening a browser | `bin/leg.mjs` |
33
34
  | `LEG_NO_BOARD` | (unset) | set to `1` to run a session with no board at all (the record under `$LEG_HOME/sessions/` is still kept; the test suite uses this) | `src/attach.mjs` |
34
35
  | `LEG_WAIT_TICK_MS` | `1000` | how often the all-out countdown redraws and re-checks Ctrl-C / End while waiting for the first reset | `src/attach.mjs` |
@@ -99,7 +100,7 @@ Off unless explicitly enabled; a sync failure never blocks or fails a card
99
100
  ## Seller and checkout site
100
101
 
101
102
  These are for the checkout/key-delivery site and its deployment helpers, not
102
- normal Leg CLI use.
103
+ normal Leg use.
103
104
 
104
105
  | variable | meaning | read in |
105
106
  |----------|---------|---------|
@@ -171,6 +172,26 @@ junctions as links, never following them, then deletes the directory. Nothing
171
172
  under your real home is written at any point. Start a session on a named
172
173
  account with `LEG_ACCOUNT=<name>`, or let a limit hand off to it.
173
174
 
175
+ ## The portable harness
176
+
177
+ Off until `leg harness enable`. Its settings live in `~/.leg/preferences.json`
178
+ beside the hand-off order:
179
+
180
+ ```json
181
+ { "harness": { "enabled": true, "policy": "sync", "source": "claude" } }
182
+ ```
183
+
184
+ | key | values | meaning |
185
+ |-----|--------|---------|
186
+ | `enabled` | `true`, `false` (default) | the consent `leg harness enable` records; `leg harness disable` clears it and removes nothing |
187
+ | `policy` | `warn`, `sync` (default on enable), `strict` | what an unattended hand-off may do: report only; write managed state when safe; refuse a destination that cannot be made safe |
188
+ | `source` | `claude`, `codex` | the client whose harness is carried; auto-detected from `~/.claude/CLAUDE.md` then `~/.codex/AGENTS.md` when unset |
189
+
190
+ `~/.leg/harness/policy.json` (optional) names what is deliberately not
191
+ carried: sections to drop, hooks, skills and MCP servers to exclude, each with
192
+ a reason, and the Codex model ladder. State, backups and the evidence trail
193
+ live under `~/.leg/harness/`. Every detail: [harness.md](harness.md).
194
+
174
195
  ## Test and development seams
175
196
 
176
197
  Not meant for normal use; documented for completeness.
package/docs/faq.md CHANGED
@@ -162,6 +162,25 @@ pre-commit hook both run `scripts/privacy-check.mjs`, which additionally
162
162
  scans the whole tree for a short list of strings specific to the private
163
163
  codebase Leg's runner/ledger were ported from.
164
164
 
165
+ **Does the next agent get my rules, hooks, skills and MCP servers, or just the task?**
166
+ Just the task, unless you turn on the [portable harness](harness.md)
167
+ (`leg harness enable`). Then a hand-off also renders the source client's
168
+ global working agreement, identity, hooks, skills, subagents, slash commands,
169
+ MCP servers and permissions into the destination's own files, as far as that
170
+ client can represent them, and the terminal's details say what was dropped and
171
+ why (Codex has no SSE transport; agy has no `SessionStart` hook; a skill that
172
+ already exists as a real directory is left alone). Credentials never move: an
173
+ MCP key becomes `${NAME}` and you export it for each client. Off by default,
174
+ and `leg harness disable` turns it off without removing anything.
175
+
176
+ **Will the portable harness overwrite a file I edited?**
177
+ No. Every file it writes carries `GENERATED by Leg harness` in its first lines,
178
+ and inside a file you also own it writes a marked `leg harness` region. A
179
+ file whose content changed since Leg wrote it is backed up once under
180
+ `~/.leg/harness/backups` and skipped; the sync reports `attention` with the
181
+ path, and `leg harness sync --force` is the only way it gets replaced. The
182
+ source client is never written at all.
183
+
165
184
  **What happens if I close the terminal instead of Ctrl-C?**
166
185
  The board server and any running agents keep running as detached processes.
167
186
  Run `node bin/leg.mjs down` (or `npm run stop`) from another terminal to