@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.
- package/CHANGELOG.md +45 -0
- package/NOTICE +8 -0
- package/README.md +601 -560
- package/bin/fake-agent.mjs +4 -4
- package/bin/leg.mjs +21 -12
- package/docs/DECISIONS.md +20 -2
- package/docs/ERRORS.md +71 -0
- package/docs/README.md +2 -0
- package/docs/REUSE.md +1 -1
- package/docs/VOCABULARY.md +21 -0
- package/docs/board-guide.md +13 -0
- package/docs/cli-contracts.md +22 -1
- package/docs/concepts.md +42 -3
- package/docs/configuration.md +22 -1
- package/docs/faq.md +19 -0
- package/docs/getting-started.md +272 -251
- package/docs/harness.md +319 -0
- package/fixtures/verified.json +1 -1
- package/package.json +7 -3
- package/scripts/build-docs-site.mjs +11 -4
- package/scripts/check-branding.mjs +118 -0
- package/scripts/check-claims.mjs +1 -1
- package/scripts/license-sign.mjs +1 -1
- package/scripts/limits-table.mjs +1 -1
- package/scripts/live-limits.mjs +1 -1
- package/scripts/npm-publish-gate.mjs +114 -0
- package/scripts/probe.mjs +4 -3
- package/scripts/seed-fake-cards.mjs +4 -3
- package/scripts/seed-floor-board.mjs +5 -4
- package/scripts/seed-wes-board.mjs +5 -4
- package/scripts/stripe-setup.mjs +1 -1
- package/scripts/sync-harness-engine.mjs +159 -0
- package/scripts/sync-leg-agents.mjs +127 -0
- package/src/accounts.mjs +1 -2
- package/src/adapters/codex.mjs +1 -1
- package/src/attach.mjs +75 -19
- package/src/auth.mjs +2 -2
- package/src/board/board.js +3 -3
- package/src/board/sessions.js +77 -3
- package/src/bundle.mjs +54 -8
- package/src/chain.mjs +1 -1
- package/src/contract.mjs +4 -3
- package/src/fsx.mjs +5 -2
- package/src/handoff.mjs +6 -6
- package/src/harness/cli.mjs +281 -0
- package/src/harness/fingerprint.mjs +68 -0
- package/src/harness/index.mjs +407 -0
- package/src/harness/registry.mjs +124 -0
- package/src/harness/vendor/agnostic-ai/LICENSE +21 -0
- package/src/harness/vendor/agnostic-ai/UPSTREAM.json +30 -0
- package/src/harness/vendor/agnostic-ai/core/safety/guards.json +96 -0
- package/src/harness/vendor/agnostic-ai/core/templates/targets.json +252 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/README.md +199 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/apply.cjs +247 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/bundle.cjs +243 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/capture.cjs +119 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/common.cjs +375 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/index.cjs +55 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/sources/claude.cjs +330 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/sources/codex.cjs +314 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/status.cjs +171 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/targets/agy.cjs +113 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/targets/claude.cjs +158 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/targets/codex.cjs +832 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/targets/cursor.cjs +87 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/targets/gemini.cjs +128 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/targets/generic.cjs +424 -0
- package/src/harness/vendor/agnostic-ai/engine/harness/toml.cjs +149 -0
- package/src/harness/vendor/agnostic-ai/engine/hooks/shim.cjs +431 -0
- package/src/hook.mjs +49 -49
- package/src/land.mjs +7 -35
- package/src/launcher.mjs +38 -26
- package/src/ledger.mjs +6 -6
- package/src/license.mjs +10 -9
- package/src/live-capture.mjs +1 -1
- package/src/mergequeue.mjs +5 -5
- package/src/orchestrator.mjs +28 -4
- package/src/preferences.mjs +37 -3
- package/src/redact.mjs +1 -1
- package/src/resume.mjs +17 -15
- package/src/runner.mjs +2 -2
- package/src/scheduler.mjs +1 -1
- package/src/server.mjs +38 -10
- package/src/session-detail.mjs +15 -1
- package/src/sessions.mjs +6 -3
- package/src/share.mjs +2 -2
- package/src/stations/agent.mjs +1 -1
- package/src/sync/dashclaw.mjs +4 -4
- package/src/synthesis.mjs +165 -0
- package/src/taps/agy.mjs +2 -2
- package/src/taps/claude-usage.mjs +1 -1
- package/src/taps/claude.mjs +170 -170
- package/src/taps/codex.mjs +286 -286
- package/src/taps/grok.mjs +2 -2
- package/src/trust.mjs +205 -36
- package/src/usage.mjs +5 -1
- package/src/worktree.mjs +5 -4
- package/fixtures/live/agy/attempt-1-scratch-workspace.out.log +0 -1
- package/fixtures/live/agy/err.log +0 -0
- package/fixtures/live/agy/out.log +0 -1
- package/fixtures/live/agy/supervisor.log +0 -2
- package/fixtures/live/claude/err.log +0 -0
- package/fixtures/live/claude/out.log +0 -1
- package/fixtures/live/claude/supervisor.log +0 -2
- package/fixtures/live/codex/err.log +0 -1
- package/fixtures/live/codex/out.log +0 -8
- package/fixtures/live/codex/supervisor.log +0 -2
- package/fixtures/live/grok/err.log +0 -32
- package/fixtures/live/grok/out.log +0 -7
- package/fixtures/live/grok/supervisor.log +0 -2
package/bin/fake-agent.mjs
CHANGED
|
@@ -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 .
|
|
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, '.
|
|
62
|
-
writeFileSync(join(cwd, '.
|
|
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 .
|
|
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
|
|
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('🦿
|
|
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 .
|
|
196
|
-
out('
|
|
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]}.
|
|
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:
|
|
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
|
-
// $
|
|
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 `
|
|
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 `[
|
|
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 `
|
|
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
|
package/docs/VOCABULARY.md
CHANGED
|
@@ -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
|
|
package/docs/board-guide.md
CHANGED
|
@@ -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
|
package/docs/cli-contracts.md
CHANGED
|
@@ -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
|
|
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
|
|
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,
|
|
116
|
-
|
|
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>`
|
package/docs/configuration.md
CHANGED
|
@@ -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
|
|
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
|