@ucsandman/legcli 0.8.0 → 0.10.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 (125) hide show
  1. package/CHANGELOG.md +121 -0
  2. package/NOTICE +8 -0
  3. package/README.md +639 -560
  4. package/bin/fake-agent.mjs +4 -4
  5. package/bin/leg.mjs +43 -12
  6. package/docs/DECISIONS.md +20 -2
  7. package/docs/ERRORS.md +205 -0
  8. package/docs/README.md +5 -1
  9. package/docs/REUSE.md +1 -1
  10. package/docs/VOCABULARY.md +22 -0
  11. package/docs/board-guide.md +33 -1
  12. package/docs/cli-contracts.md +36 -1
  13. package/docs/concepts.md +42 -3
  14. package/docs/configuration.md +23 -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/docs/history.md +172 -0
  19. package/docs/runtime-tap.md +156 -0
  20. package/fixtures/verified.json +1 -1
  21. package/package.json +7 -3
  22. package/scripts/build-docs-site.mjs +18 -4
  23. package/scripts/check-branding.mjs +118 -0
  24. package/scripts/check-claims.mjs +1 -1
  25. package/scripts/license-sign.mjs +1 -1
  26. package/scripts/limits-table.mjs +1 -1
  27. package/scripts/live-limits.mjs +1 -1
  28. package/scripts/npm-publish-gate.mjs +114 -0
  29. package/scripts/probe.mjs +4 -3
  30. package/scripts/seed-fake-cards.mjs +4 -3
  31. package/scripts/seed-floor-board.mjs +5 -4
  32. package/scripts/seed-wes-board.mjs +5 -4
  33. package/scripts/stripe-setup.mjs +1 -1
  34. package/scripts/sync-harness-engine.mjs +159 -0
  35. package/scripts/sync-leg-agents.mjs +127 -0
  36. package/src/accounts.mjs +6 -4
  37. package/src/adapters/codex.mjs +1 -1
  38. package/src/attach.mjs +125 -23
  39. package/src/auth.mjs +2 -2
  40. package/src/board/board.css +23 -1
  41. package/src/board/board.js +17 -5
  42. package/src/board/history.js +377 -0
  43. package/src/board/index.html +33 -0
  44. package/src/board/sessions.js +95 -7
  45. package/src/bundle.mjs +54 -8
  46. package/src/chain.mjs +1 -1
  47. package/src/contract.mjs +4 -3
  48. package/src/fsx.mjs +5 -2
  49. package/src/handoff.mjs +6 -6
  50. package/src/harness/cli.mjs +281 -0
  51. package/src/harness/fingerprint.mjs +68 -0
  52. package/src/harness/index.mjs +407 -0
  53. package/src/harness/registry.mjs +124 -0
  54. package/src/harness/vendor/agnostic-ai/LICENSE +21 -0
  55. package/src/harness/vendor/agnostic-ai/UPSTREAM.json +30 -0
  56. package/src/harness/vendor/agnostic-ai/core/safety/guards.json +96 -0
  57. package/src/harness/vendor/agnostic-ai/core/templates/targets.json +252 -0
  58. package/src/harness/vendor/agnostic-ai/engine/harness/README.md +199 -0
  59. package/src/harness/vendor/agnostic-ai/engine/harness/apply.cjs +247 -0
  60. package/src/harness/vendor/agnostic-ai/engine/harness/bundle.cjs +243 -0
  61. package/src/harness/vendor/agnostic-ai/engine/harness/capture.cjs +119 -0
  62. package/src/harness/vendor/agnostic-ai/engine/harness/common.cjs +375 -0
  63. package/src/harness/vendor/agnostic-ai/engine/harness/index.cjs +55 -0
  64. package/src/harness/vendor/agnostic-ai/engine/harness/sources/claude.cjs +330 -0
  65. package/src/harness/vendor/agnostic-ai/engine/harness/sources/codex.cjs +314 -0
  66. package/src/harness/vendor/agnostic-ai/engine/harness/status.cjs +171 -0
  67. package/src/harness/vendor/agnostic-ai/engine/harness/targets/agy.cjs +113 -0
  68. package/src/harness/vendor/agnostic-ai/engine/harness/targets/claude.cjs +158 -0
  69. package/src/harness/vendor/agnostic-ai/engine/harness/targets/codex.cjs +832 -0
  70. package/src/harness/vendor/agnostic-ai/engine/harness/targets/cursor.cjs +87 -0
  71. package/src/harness/vendor/agnostic-ai/engine/harness/targets/gemini.cjs +128 -0
  72. package/src/harness/vendor/agnostic-ai/engine/harness/targets/generic.cjs +424 -0
  73. package/src/harness/vendor/agnostic-ai/engine/harness/toml.cjs +149 -0
  74. package/src/harness/vendor/agnostic-ai/engine/hooks/shim.cjs +431 -0
  75. package/src/history/cli.mjs +159 -0
  76. package/src/history/common.mjs +119 -0
  77. package/src/history/index.mjs +429 -0
  78. package/src/history/providers/agy.mjs +91 -0
  79. package/src/history/providers/claude.mjs +161 -0
  80. package/src/history/providers/codex.mjs +133 -0
  81. package/src/history/providers/copilot.mjs +94 -0
  82. package/src/history/providers/grok.mjs +138 -0
  83. package/src/history/worktrees.mjs +116 -0
  84. package/src/hook.mjs +49 -49
  85. package/src/land.mjs +7 -35
  86. package/src/launcher.mjs +38 -26
  87. package/src/ledger.mjs +6 -6
  88. package/src/license.mjs +10 -9
  89. package/src/live-capture.mjs +1 -1
  90. package/src/mergequeue.mjs +5 -5
  91. package/src/orchestrator.mjs +28 -4
  92. package/src/preferences.mjs +37 -3
  93. package/src/redact.mjs +24 -6
  94. package/src/resume.mjs +17 -15
  95. package/src/runner.mjs +2 -2
  96. package/src/scheduler.mjs +1 -1
  97. package/src/server.mjs +224 -18
  98. package/src/session-detail.mjs +15 -1
  99. package/src/sessions.mjs +15 -3
  100. package/src/share.mjs +2 -2
  101. package/src/stations/agent.mjs +1 -1
  102. package/src/sync/dashclaw.mjs +4 -4
  103. package/src/synthesis.mjs +165 -0
  104. package/src/taps/agy.mjs +2 -2
  105. package/src/taps/claude-usage.mjs +1 -1
  106. package/src/taps/claude.mjs +177 -170
  107. package/src/taps/codex.mjs +286 -286
  108. package/src/taps/grok.mjs +2 -2
  109. package/src/taps/mod.mjs +340 -0
  110. package/src/trust.mjs +205 -36
  111. package/src/usage.mjs +5 -1
  112. package/src/worktree.mjs +6 -5
  113. package/fixtures/live/agy/attempt-1-scratch-workspace.out.log +0 -1
  114. package/fixtures/live/agy/err.log +0 -0
  115. package/fixtures/live/agy/out.log +0 -1
  116. package/fixtures/live/agy/supervisor.log +0 -2
  117. package/fixtures/live/claude/err.log +0 -0
  118. package/fixtures/live/claude/out.log +0 -1
  119. package/fixtures/live/claude/supervisor.log +0 -2
  120. package/fixtures/live/codex/err.log +0 -1
  121. package/fixtures/live/codex/out.log +0 -8
  122. package/fixtures/live/codex/supervisor.log +0 -2
  123. package/fixtures/live/grok/err.log +0 -32
  124. package/fixtures/live/grok/out.log +0 -7
  125. 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,8 @@ 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'
30
+ import { historyCommand, worktreesCommand } from '../src/history/cli.mjs'
29
31
 
30
32
  const SRC = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'src')
31
33
  // one source of truth for the version, so the help text cannot drift from the package
@@ -86,14 +88,14 @@ function simulateLimit(s) {
86
88
  if (s.agent === 'agy') {
87
89
  appendFileSync(join(sessionDir(s.session_id), 'agy.log'), '\nrpc error: code = ResourceExhausted desc = RESOURCE_EXHAUSTED quota (simulated by leg sessions simulate-limit)\n')
88
90
  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'}`)
91
+ 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
92
  }
91
93
  if (s.agent === 'grok') {
92
94
  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
95
  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'}`)
96
+ 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
97
  }
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.`)
98
+ 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
99
  }
98
100
 
99
101
  function fmtCard(c) {
@@ -138,7 +140,7 @@ async function main() {
138
140
  ' ██████████████████████████',
139
141
  ' ████ ████ ████ ████ ████ ████',
140
142
  ].join('\n')
141
- out('🦿 LegCli: the mechanical relay runner for coding agents.\n')
143
+ out('🦿 Leg: the mechanical relay runner for coding agents.\n')
142
144
  out(LEG_ART)
143
145
  out('\nPassing the leg to the next runner when limits hit.')
144
146
  return
@@ -146,7 +148,7 @@ async function main() {
146
148
  if (SUPERVISED_AGENTS.includes(group)) {
147
149
  // leg claude|codex|agy|grok [agent args...]: everything after the agent name
148
150
  // goes straight through.
149
- const code = await attach(group, [cmd, ...rest].filter((x) => x !== undefined), { open: process.env.BATON_NO_OPEN !== '1' })
151
+ const code = await attach(group, [cmd, ...rest].filter((x) => x !== undefined), { open: (process.env.LEG_NO_OPEN || process.env.BATON_NO_OPEN) !== '1' })
150
152
  process.exit(code)
151
153
  }
152
154
  if (group === 'sessions') {
@@ -192,15 +194,15 @@ async function main() {
192
194
  const v = resumeVerdict(where)
193
195
  if (a.json) { out(JSON.stringify(v, null, 2)); process.exit(v.exit_code) }
194
196
  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.')
197
+ out(`no resume pointer in this checkout (looked for .leg/RESUME.md from ${where} upward).`)
198
+ out('Leg writes one when a terminal hands off; `leg claude` in this directory starts one.')
197
199
  process.exit(v.exit_code)
198
200
  }
199
201
  const head = v.head?.now ? `${v.head.now.slice(0, 7)}${v.head.branch ? ` on ${v.head.branch}` : ''}` : 'no commit'
200
202
  const line = v.state === 'fresh'
201
203
  ? `${v.file} is current: written ${v.written_at ? ago(v.age_ms) : 'at an unrecorded time'}, and the repository is still at ${head}.`
202
204
  : v.state === 'unstamped'
203
- ? `${v.file} is UNSTAMPED: ${v.reasons[0]}. Baton did not write it, or an older Baton did.`
205
+ ? `${v.file} is UNSTAMPED: ${v.reasons[0]}. Leg did not write it, or an older version did.`
204
206
  : `${v.file} is STALE: ${v.reasons.join('; ')}.`
205
207
  if (a.check) {
206
208
  out(line)
@@ -300,7 +302,7 @@ async function main() {
300
302
  out('')
301
303
  out('Log in once (paste in PowerShell):')
302
304
  out(` ${r.login}`)
303
- out(`Then: $env:BATON_ACCOUNT='${name}'; baton ${agent} (or let a limit hand off to it)`)
305
+ out(`Then: $env:LEG_ACCOUNT='${name}'; leg ${agent} (or let a limit hand off to it)`)
304
306
  } catch (err) { die(2, err.message) }
305
307
  return
306
308
  }
@@ -322,6 +324,27 @@ async function main() {
322
324
  if (cmd === 'terms') return out(TERMS)
323
325
  die(2, `unknown accounts command "${cmd}" (ls|add|rm|terms)`)
324
326
  }
327
+ if (group === 'harness') {
328
+ // The portable harness: the working environment a hand-off carries with
329
+ // the task. Off until `leg harness enable` (src/harness/index.mjs).
330
+ const code = await harnessCommand(cmd, args, { out, die })
331
+ process.exit(code)
332
+ }
333
+ if (group === 'history' || group === 'worktrees') {
334
+ // Every conversation on this machine, Leg's own and the ones the agents'
335
+ // stores hold: a read-only index (src/history/index.mjs). `continue`
336
+ // starts a normal supervised leg on one of them. `leg history --json` is
337
+ // `leg history ls --json`: a leading flag names no verb.
338
+ const isHelp = cmd === '--help' || cmd === '-h' || cmd === 'help' || args.help || args.h
339
+ const bare = typeof cmd === 'string' && cmd.startsWith('--')
340
+ const verb = isHelp ? 'help' : (bare ? 'ls' : cmd)
341
+ const a = bare ? parseArgs([cmd, ...rest]) : args
342
+ // `continue <id> [agent args...]`: what follows the id goes to the agent
343
+ // untouched, the way `leg claude [args...]` passes its argv straight through
344
+ const raw = bare ? [cmd, ...rest] : rest
345
+ const code = group === 'history' ? await historyCommand(verb, a, { out, die, raw }) : worktreesCommand(verb, a, { out, die })
346
+ process.exit(code)
347
+ }
325
348
  if (group === 'license') {
326
349
  // The paid gate. Keys verify offline against the public key in
327
350
  // src/license.mjs; nothing here talks to the network except refresh.
@@ -350,7 +373,7 @@ async function main() {
350
373
  }
351
374
  if (group === 'uninstall') {
352
375
  // Leg never edits ~/.claude or ~/.codex; everything it added lives under
353
- // $BATON_HOME (sessions, usage, extra-account dirs, cards).
376
+ // $LEG_HOME (sessions, usage, extra-account dirs, cards).
354
377
  const dir = home()
355
378
  if (!args.yes) {
356
379
  out(`leg uninstall removes ${dir} (sessions, usage, extra-account dirs, cards, board pidfile) and nothing else.`)
@@ -360,7 +383,7 @@ async function main() {
360
383
  for (const r of listAccountRows()) if (r.name !== 'default') removeAccount(r.agent, r.name)
361
384
  await down()
362
385
  rmSync(dir, { recursive: true, force: true })
363
- return out(`removed ${dir}; now: npm rm -g legcli`)
386
+ return out(`removed ${dir}; now: npm rm -g @ucsandman/legcli`)
364
387
  }
365
388
  if (group === 'card') {
366
389
  if (cmd === 'add') return cardAdd(args)
@@ -447,17 +470,25 @@ async function main() {
447
470
  out(openBoard(url) ? `opened ${url}` : `could not open a browser; visit ${url}`)
448
471
  return
449
472
  }
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)`)
473
+ if (group && group !== '--help' && group !== 'help') die(2, `unknown command "${group}" (claude|codex|agy|grok|sessions|history|worktrees|resume|accounts|harness|license|share|up|down|status|open|card|scheduler|uninstall)`)
451
474
  out(`leg ${VERSION}, your coding agents, with a board alongside and a handoff when one hits its limit
452
475
  claude|codex|agy|grok [args...] the normal interactive agent in this terminal; args pass straight through
453
476
  the board opens once, the session shows as a card, usage is tracked, a limit hands off
454
477
  a second live session in one checkout gets its own worktree (--no-worktree to share)
455
478
  auto-approve mode (--no-auto-approve to opt out)
456
479
  sessions ls|show|events|handoff|end|rm|simulate-limit <id>
480
+ history [ls] [--provider p] [--repo r] [--search q] [--json]
481
+ every conversation on this machine: Leg's own, and the ones Claude Code, Codex,
482
+ Grok, Antigravity and Copilot keep in their own stores (read only, nothing moved)
483
+ history show|continue <id> | refresh | providers
484
+ one conversation, or start leg <agent> on it where the agent can resume by id
485
+ worktrees [--repo r] [--json] every checkout Leg can see: git's, its own, the ones conversations worked in
457
486
  resume [--check] [--json] [--path <dir>] the hand-off waiting in this checkout, and whether it is still true
458
487
  freshness is recomputed from git at read time; --check prints only the verdict
459
488
  exit 0 current, 1 stale or unstamped, 3 no pointer here
460
489
  accounts ls|add <agent> <name>|rm|terms optional second login for claude or codex
490
+ harness status|enable|sync|check|explain|... carry the source agent's rules, hooks, skills, agents, commands and MCP
491
+ servers to the agent a hand-off lands on; off until enabled (leg harness help)
461
492
  license [status|activate <key>|deactivate|refresh]
462
493
  personal or team license status and management
463
494
  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,157 @@
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-17: one running terminal saturated the board's event loop, and four separate symptoms came out of it
7
+
8
+ **Fixed in `src/server.mjs` (watcher filter, stat fingerprint, push floor, cached
9
+ `canLand`), `src/attach.mjs` (a taken port is a board; always open it) and
10
+ `src/board/{board,sessions}.js` (one render per push, hold an open confirm row).
11
+ Regression tests: `test/board-responsiveness.test.mjs`.**
12
+
13
+ Reported as four bugs: `leg claude` took a long time to start, the board never
14
+ opened by itself, the board was slow to load and to react, and Remove under
15
+ Terminals did nothing. All four were one cause. The board server had burned
16
+ 17,080 seconds of CPU — it answered `/api/health` in 7 to 14 seconds and a
17
+ 40 KB stylesheet in 14. A live agent rewrites its session record every six
18
+ seconds or so and takes a control lock about once a second; the recursive
19
+ watcher over the sessions directory answered every one of those — including
20
+ `.control.lock`, `.session.lock` and the `session.json.*.tmp` files atomic
21
+ writes leave behind — with a full rebuild of the terminals view, which costs
22
+ about two seconds (1.1 s of it `canLand` shelling out to git across twenty
23
+ worktrees). 105 filesystem events in 30 seconds against a 300 ms debounce and a
24
+ 2,000 ms rebuild: the queue could never drain.
25
+
26
+ What that one stall produced:
27
+
28
+ - **Slow start.** `ensureBoard`'s health probe times out at 2 s, so a merely
29
+ busy board read as no board. `leg` spawned a second server, which died of
30
+ `EADDRINUSE` (the evidence was already in `~/.baton/board.log`), then polled
31
+ the dead child every 200 ms for the full 15 s.
32
+ - **The board never opened.** `openBoard(url)` sat only on the branch that
33
+ successfully started a server. With one already running, or on the 15 s
34
+ give-up path, nothing was ever opened.
35
+ - **Slow to react.** `board.js` dispatched `leg:sessions` and `baton:sessions`
36
+ per push, parsing the quarter-megabyte payload twice, with the second
37
+ dispatch outside the `state.es === es` staleness guard (missing braces on a
38
+ one-line `if`). `sessions.js` was registered on `leg:sessions` twice and on
39
+ the alias once. One push rebuilt the whole grid three times.
40
+ - **Slow to react, part two.** `renderSessions` starts with
41
+ `grid.textContent = ''`, so with three rebuilds per push every button was
42
+ destroyed and recreated several times a second.
43
+
44
+ **Remove was a second, independent bug, and fixing the first did not fix it.**
45
+ Reported again as "it's still not removing them" after all of the above
46
+ shipped. `confirmRow` does `pendingConfirm = null; onYes(yes)`, and the call
47
+ site passed `(btn) => act(s.session_id, pendingConfirm.action, btn)` — a
48
+ closure over the module variable, not over its value. Every Yes on the
49
+ Terminals panel threw `TypeError: Cannot read properties of null (reading
50
+ 'action')` before reaching `act()`, so Remove, Remove record, End and Land had
51
+ never worked from the confirm row; the exception went to the console and
52
+ nowhere the reader would see it. Fixed by snapshotting
53
+ `const pending = pendingConfirm` before building the row. Caught by driving the
54
+ real page: the click produced zero network requests, which pointed at the
55
+ handler rather than at anything server-side. The instrument that found it was
56
+ five lines — wrap `window.fetch`, listen for `window.onerror`, click, read
57
+ both.
58
+
59
+ Three lessons. First, a name filter is not enough to classify a filesystem event:
60
+ taking a lock inside a directory changes that directory's own mtime, and the
61
+ event arrives naming only the directory. The watcher now treats an event as a
62
+ hint and compares a stat fingerprint of the files the view is really built from
63
+ — sub-millisecond, against a 2 s rebuild. Second, the CPU counter on a
64
+ long-lived process names a stall in one command, before any code is read.
65
+
66
+ Third, and the one that cost a round trip: several symptoms reported together
67
+ are not thereby one bug. Three of these four were, and finding that cause made
68
+ the fourth *look* explained — the render churn is a real way to drop a click,
69
+ so "Remove doesn't work" fit the story. It was a different bug in a different
70
+ file, and it was still there after the fix shipped. A symptom is only closed
71
+ once it has been driven and watched, not once a plausible cause for it has been
72
+ found and fixed. Clicking Remove in the real page would have taken two minutes
73
+ and would have shown zero network requests and a `TypeError` in the console.
74
+
75
+ Verified by running the new tests against `da453b7`: 7 of 8 fail there, 8 of 8
76
+ pass after, and the busy-board test takes 21 s before versus 4 s after. On the
77
+ live board, same 66 sessions: `/api/health` 14.19 s → 2.4 ms, `/board.css`
78
+ 14.24 s → 1.5 ms, `/api/sessions` 15.64 s → 109 ms, view rebuild 2,049 ms →
79
+ 115 ms, renders per push 3 → 1.
80
+
81
+ Also found while verifying: `npm run lint` reported "ESLint: No issues found"
82
+ with `node_modules/.bin` empty and eslint not installed — a wrapper was printing
83
+ it. Run `npm install` first and check that ESLint really ran; a lint result from
84
+ a tool that is not on disk is not a lint result.
85
+
86
+ ## 2026-09-16: a hand-edited `package.json` version left `package-lock.json` behind, and the publish gate refused 0.9.0
87
+
88
+ **Fixed by bumping the lockfile root version; the gate is `scripts/npm-publish-gate.mjs`.**
89
+
90
+ The version went from 0.8.1 to 0.9.0 by editing `package.json` directly, so
91
+ the lockfile's two root `version` fields still said 0.8.1 and CI's
92
+ `publish-npm` job stopped at "package.json and package-lock.json root metadata
93
+ do not match". The run before it had failed the same way, which is why npm
94
+ still served 0.8.0. Tests, lint and the docs job were green; only the publish
95
+ was refused, which is the gate doing its job. Lesson: bump with `npm version`
96
+ (it writes the lockfile and runs the alias sync) and run the gate locally
97
+ before pushing a release; a green test matrix says nothing about the publish.
98
+
99
+ ## 2026-09-16: the vendored engine's secret scan covered two fields; the review found the other five
100
+
101
+ **Fixed upstream (Agnostic AI 7e35b51) and re-vendored; regression in `test/harness-engine.test.mjs`.**
102
+
103
+ `docs/harness.md` promised "a bundle that still carries a credential is refused
104
+ at save time". The engine's `validate()` scanned `mcp.<n>.env` and
105
+ `mcp.<n>.headers` only; the rules text, identity, hook command lines, MCP
106
+ arguments, URLs and agent bodies were copied verbatim, and the four canary
107
+ tests planted tokens exactly where the scan already looked. The read-only
108
+ security review planted one everywhere else and got one problem back. Root
109
+ cause: a scan written for two map keys, and a test fixture shaped to it. Fix:
110
+ whole-bundle scanning with redaction of free text and drops of unsafe
111
+ handlers or servers, plus `PLANTED` tokens in every place the scan must reach.
112
+ Lesson: a canary proves the place it sits in, nothing else; a fixture written
113
+ by the same hands as the scan finds nothing the scan missed.
114
+
115
+ ## 2026-09-16: `CLAUDE_CONFIG_DIR` outside the OS home was captured from `~/.claude` instead
116
+
117
+ **Fixed upstream in `sources/claude.cjs` (`pick()`), regression in `test/harness-engine.test.mjs`.**
118
+
119
+ The engine kept a registry path only when it sat inside the OS home and fell
120
+ back to `~/.claude` otherwise, while Leg's registry, detection and fingerprint
121
+ honoured the override. A per-account config dir under a `LEG_HOME` on another
122
+ drive would have ported the dormant profile and never noticed edits to the
123
+ active one. Fix: the registry path is trusted as given. Lesson: three code
124
+ paths agreeing on a directory is a property to test, not to assume.
125
+
126
+ ## 2026-09-16: `leg harness sync` wrote before `leg harness enable`; the board could widen the policy
127
+
128
+ **Fixed in `src/harness/cli.mjs` and `src/server.mjs`, regressions in `test/harness-cli.test.mjs` and `test/harness-policy.test.mjs`.**
129
+
130
+ The consent gate lived in `enable` only, so `sync` on an install that never
131
+ enabled the feature wrote managed files while `status` said off; the settings
132
+ route accepted any policy value, so a board POST could take `warn` to
133
+ `strict`. Both were one-line fixes the review caught. Lesson: a consent rule
134
+ has to be checked at every writer, not at the one verb that grants it.
135
+
136
+ ## 2026-09-16: the backup count was always zero
137
+
138
+ **Fixed in `src/harness/index.mjs`.**
139
+
140
+ The engine's writer returns the backup path, but every adapter keeps only the
141
+ action, so counting `f.backup` counted nothing. The count now comes from the
142
+ backups directory before and after an apply. Lesson: a number that never moves
143
+ in a demo is a number nobody is computing.
144
+
145
+ ## 2026-09-16: alias `import(join(windowsPath))` is a `c:` URL scheme
146
+
147
+ **Fixed in `packages/leg-agents/bin/leg.mjs`.**
148
+
149
+ The first `leg-agents` wrapper did `await import(join(pkgRoot, 'bin', 'leg.mjs'))`.
150
+ On Windows that string is `C:\…\bin\leg.mjs`, which Node's ESM loader treats as a
151
+ URL with protocol `c:` and throws `ERR_UNSUPPORTED_ESM_URL_SCHEME`. `npm install`
152
+ of the tarball succeeded; `leg --version` died before printing `0.8.0`.
153
+
154
+ The fix is `await import(pathToFileURL(join(pkgRoot, 'bin', 'leg.mjs')).href)`.
155
+ `require.resolve` returning a path is not a valid ESM specifier on Windows.
156
+
6
157
  ## 2026-09-15: macOS `/var` symlink broke two e2e tests
7
158
 
8
159
  **Fixed in `test/helpers.mjs`.**
@@ -324,3 +475,57 @@ it), and check whether a board was listening on 4747 at the time.
324
475
  module that reads the environment at import time cannot be imported from a
325
476
  helper that runs before the environment is set, no matter how small the thing
326
477
  you wanted from it.
478
+
479
+ ## A 3,000-line feature landed with red CI on Windows and no docs page (2026-09-16)
480
+
481
+ - **Symptom.** Commit `c7f24a0` (the history index, committed by the agy
482
+ session that took over after a Claude usage limit) went green on Ubuntu and
483
+ macOS and red on Windows in two `history.test.mjs` cases, and the `docs` job
484
+ failed with "site/ is out of date". `/docs/history` did not exist and every
485
+ link to `history.md` pointed at the npm package page.
486
+ - **Root cause, tests.** GitHub's Windows runner hands out an 8.3 short TEMP
487
+ path (`RUNNER~1`) while git reports the long form (`runneradmin`). The two
488
+ assertions compared a `mkdtemp` path with a path that had been through git.
489
+ The repo already had the seam for this (`canonPath` in `src/fsx.mjs`,
490
+ used the same way in `handoff-order.test.mjs`); the new tests did not use it.
491
+ It cannot be seen on a developer machine with 8.3 names disabled.
492
+ - **Root cause, docs.** `scripts/build-docs-site.mjs` builds from a hard-coded
493
+ `PAGES` list. A new `docs/*.md` that is not in it gets no page, and
494
+ `rewriteHref` sends its links to the npm URL by design. Nobody ran
495
+ `npm run docs` before pushing, so the docs job was the first to notice.
496
+ - **Fix.** `ebda794`: both sides of the four path assertions through
497
+ `canonPath`; reproduced locally by pointing `TEMP` at a case-altered path
498
+ (seen failing, then 16/16); `history` registered in `PAGES`; site rebuilt.
499
+ - **The lesson that generalises.** A test that compares two filesystem paths
500
+ on Windows compares two spellings of the same folder unless both go through
501
+ `canonPath`. And a new `docs/*.md` is two edits, the file and the `PAGES`
502
+ entry, then `npm run docs`; the docs job exists because the second and third
503
+ get forgotten.
504
+
505
+ ## The review that was in flight when the limit hit never ran; the feature shipped with 38 defects (2026-09-17)
506
+
507
+ - **Symptom.** `c7f24a0` (history discovery) was committed and pushed by the
508
+ agent that took over after a Claude usage limit. The nine-lens adversarial
509
+ review the Claude session had launched died with that session. Re-run the
510
+ next day it confirmed 38 findings, 3 high: the board's Conversations cell
511
+ could never page past 150 rows (the total was counted after the cursor
512
+ slice), an unreachable UNC cwd blocked a refresh for seconds per record,
513
+ and an Antigravity retitle was invisible to every incremental refresh
514
+ (the directory's mtime was the cache key; an in-place rewrite never moves
515
+ it).
516
+ - **Root cause.** The hand-off bundle carried "what was established" and
517
+ "next moves" but not "what was in flight": a background workflow is not a
518
+ file, so the next agent saw a clean-looking task and shipped it. Nothing
519
+ in the ship path asks whether the change was reviewed.
520
+ - **Fix.** `582dfe5`: 20 findings closed, each with a test that fails on the
521
+ previous source (proved on a detached worktree of HEAD before the fix).
522
+ - **Left open, by choice.** Coverage-only findings (live markers, the
523
+ write-scope guard, the flag surfaces, extra accounts), a low-severity
524
+ dedup edge (a conversation split over two Claude transcript files when Leg
525
+ recorded only the older path), the `sessions ls` row format, and the
526
+ pretty-printed index file.
527
+ - **The lesson that generalises.** A review is part of the change, not a
528
+ step after it: launch it before the feature is committable and record it
529
+ in the hand-off as in flight, or the next agent ships without it. And a
530
+ cache keyed on a directory's mtime sees files added and removed, never a
531
+ file rewritten in place.
package/docs/README.md CHANGED
@@ -6,6 +6,8 @@
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.
10
+ - [history.md](history.md): `leg history` and `leg worktrees`, one read-only index over every agent's own conversation store and every checkout, the support matrix per provider, dedup against Leg's own sessions, privacy, and how to add a provider.
9
11
  - [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
12
  - [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
13
 
@@ -22,8 +24,10 @@
22
24
  | area | modules |
23
25
  |------|---------|
24
26
  | 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) |
27
+ | 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
28
  | taps | `src/taps/claude.mjs`, `src/taps/claude-usage.mjs`, `src/taps/codex.mjs`, `src/taps/agy.mjs` |
26
- | board | `src/server.mjs`, `src/board/sessions.js` (Terminals lane), `src/board/board.js` and `src/board/floor.js` (pipelines) |
29
+ | history | `src/history/index.mjs` (the index, dedup, resume), `src/history/providers/*.mjs` (one per agent store, read only), `src/history/worktrees.mjs` (the unified checkout view), `src/history/cli.mjs` (`leg history`, `leg worktrees`) |
30
+ | board | `src/server.mjs`, `src/board/sessions.js` (Terminals lane), `src/board/history.js` (Conversations), `src/board/board.js` and `src/board/floor.js` (pipelines) |
27
31
  | 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` |
28
32
 
29
33
  ## History
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
@@ -47,10 +47,13 @@ Source: the `appendEvent`/`updateSession` call sites in `src/attach.mjs`,
47
47
  | `handoff` | the switch happened: from, to, reason, bundle id |
48
48
  | `all_out` | every option is walled; the resets are printed and the terminal waits for the first one (`ended` with "quit while waiting" if Ctrl-C or End cuts the wait short, exit 3) |
49
49
  | `agent_exit` | the agent process exited, with its code |
50
+ | `continued` | the session was started by `leg history continue` on a conversation the agent's own store holds; `session.json` carries `continued_from: { id, provider, native_id }` |
50
51
  | `ended` | the session ended |
51
52
  | `lost` | the runner pid is gone; the session was marked `lost` |
52
53
  | `error` | a spawn error, a tap error, a failed bundle checkpoint, or an error the agent reported |
53
54
  | `status` | a note that does not fit another type |
55
+ | `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 |
56
+ | `harness_blocked` | the strict harness policy refused the chosen destination; the next option is tried |
54
57
  | `worktree` | another live session was in the checkout, so this one got its own worktree: path, branch, base |
55
58
  | `land_requested` | Land was pressed: the branch and its base |
56
59
  | `land_warning` | the landing ran without a test command |
@@ -58,6 +61,23 @@ Source: the `appendEvent`/`updateSession` call sites in `src/attach.mjs`,
58
61
  | `bounced` | the landing stopped with a [bounce reason](#bounce-reasons-land-station); the full detail is in `body` |
59
62
  | `land_noop` | Land found nothing on the branch beyond its base |
60
63
 
64
+ ## Harness states (terminal cards, drawer, `leg harness`)
65
+
66
+ Source: `STATES` in `src/harness/index.mjs`; recorded on `session.harness.state`.
67
+
68
+ | state | meaning |
69
+ |-------|---------|
70
+ | `off` | the portable harness is not enabled; nothing recorded |
71
+ | `same-client` | a hand-off to another login of the same client; the harness is shared already |
72
+ | `source` | the destination is the source client; never written |
73
+ | `synced` | every component the destination supports is current, nothing dropped |
74
+ | `partial` | current, some items could not be carried (each with a reason) |
75
+ | `stale` | the destination is behind the source (`warn` policy, or a check) |
76
+ | `attention` | a managed file was hand-edited (backed up, skipped) or a component errored |
77
+ | `unsupported` | no adapter for the destination (Grok), or it is not installed |
78
+ | `blocked` | the strict policy refused the destination |
79
+ | `error` | the preparation failed; the reason is recorded |
80
+
61
81
  ## Land states (terminal cards)
62
82
 
63
83
  `$LEG_HOME/sessions/<id>/land.json`, written by the board server only
@@ -166,6 +186,8 @@ across `src/chain.mjs`, `src/orchestrator.mjs`, `src/scheduler.mjs`,
166
186
  | `failed` | the card failed (chain exhausted, land attempts exhausted, or an environment fault) |
167
187
  | `error` | an unexpected error (orchestrator crash, handoff bundle write failure, land station crash) |
168
188
  | `status` | a status note that doesn't fit another type (e.g. "rerun from build leg 0") |
189
+ | `harness` | the portable harness was prepared for the adapter about to run; the summary names the state, the body what was dropped |
190
+ | `harness_blocked` | the strict harness policy refused the adapter; the leg fails as `launch_failed` and does not advance |
169
191
 
170
192
  ## Actor types
171
193
 
@@ -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
@@ -304,7 +317,7 @@ before saving.
304
317
 
305
318
  ## The ledger
306
319
 
307
- Below the terminals, three counts sitting on the ground with no panel, because a
320
+ Below the terminals, four counts sitting on the ground with no panel, because a
308
321
  raised surface here would compete with the terminals that are live. Each is a
309
322
  heading, a line of detail and a button that opens the detail below the row.
310
323
 
@@ -321,6 +334,25 @@ heading, a line of detail and a button that opens the detail below the row.
321
334
  visual weight as the live terminals, so the loudest thing on the page was a
322
335
  commit from eleven days ago. With nothing landed it reads `Nothing landed yet`
323
336
  and explains what Land does.
337
+ - **N conversations**, every coding-agent conversation on this machine, `799
338
+ claude, 198 codex, 92 agy, 14 grok, 5 copilot; looked 2m ago`: the sessions
339
+ Leg started and the ones the agents keep in their own stores, read from
340
+ there and never moved ([history.md](history.md)). **Browse N** opens a
341
+ drawer with filters (agent, search, repository, only what Leg started), a
342
+ page of fifty rows newest first and **Show 50 more**. A row is the title as a
343
+ button, then the agent chip, `leg` or `external` (`, live` when a process is
344
+ still on it), `repo on branch`, the prompt count and the short id. The title
345
+ opens the conversation in place: agent, who started it, folder, repository,
346
+ worktree, times, prompts, transcript path, id, the `leg history continue
347
+ <id>` command with **Copy the continue command** where the agent can resume
348
+ by id (or the reason it cannot), **Copy folder path**, and the last eight
349
+ messages newest first, each redacted the way the terminal drawer's are. Below
350
+ the rows, **Checkouts**: every worktree Leg can see with its repo and branch,
351
+ owner (the checkout itself, a Leg session, a Leg card, or not Leg's), how
352
+ many conversations point at it, and `missing`, `orphaned`, `stale`, `clean`
353
+ or `N uncommitted`. Nothing here is pushed over the live stream; the count
354
+ re-reads itself once a minute while the drawer is closed. On a shared board
355
+ a guest sees `The owner of this machine sees them.` and nothing else.
324
356
  - **N background tasks**, the card runtime below, as a count with **View N
325
357
  cards**, plus **New card**. With none it reads `Nothing is queued. Leg starts
326
358
  the next login only when a terminal hands off.`