@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.
- package/CHANGELOG.md +121 -0
- package/NOTICE +8 -0
- package/README.md +639 -560
- package/bin/fake-agent.mjs +4 -4
- package/bin/leg.mjs +43 -12
- package/docs/DECISIONS.md +20 -2
- package/docs/ERRORS.md +205 -0
- package/docs/README.md +5 -1
- package/docs/REUSE.md +1 -1
- package/docs/VOCABULARY.md +22 -0
- package/docs/board-guide.md +33 -1
- package/docs/cli-contracts.md +36 -1
- package/docs/concepts.md +42 -3
- package/docs/configuration.md +23 -1
- package/docs/faq.md +19 -0
- package/docs/getting-started.md +272 -251
- package/docs/harness.md +319 -0
- package/docs/history.md +172 -0
- package/docs/runtime-tap.md +156 -0
- package/fixtures/verified.json +1 -1
- package/package.json +7 -3
- package/scripts/build-docs-site.mjs +18 -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 +6 -4
- package/src/adapters/codex.mjs +1 -1
- package/src/attach.mjs +125 -23
- package/src/auth.mjs +2 -2
- package/src/board/board.css +23 -1
- package/src/board/board.js +17 -5
- package/src/board/history.js +377 -0
- package/src/board/index.html +33 -0
- package/src/board/sessions.js +95 -7
- 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/history/cli.mjs +159 -0
- package/src/history/common.mjs +119 -0
- package/src/history/index.mjs +429 -0
- package/src/history/providers/agy.mjs +91 -0
- package/src/history/providers/claude.mjs +161 -0
- package/src/history/providers/codex.mjs +133 -0
- package/src/history/providers/copilot.mjs +94 -0
- package/src/history/providers/grok.mjs +138 -0
- package/src/history/worktrees.mjs +116 -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 +24 -6
- package/src/resume.mjs +17 -15
- package/src/runner.mjs +2 -2
- package/src/scheduler.mjs +1 -1
- package/src/server.mjs +224 -18
- package/src/session-detail.mjs +15 -1
- package/src/sessions.mjs +15 -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 +177 -170
- package/src/taps/codex.mjs +286 -286
- package/src/taps/grok.mjs +2 -2
- package/src/taps/mod.mjs +340 -0
- package/src/trust.mjs +205 -36
- package/src/usage.mjs +5 -1
- package/src/worktree.mjs +6 -5
- 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,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
|
|
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('🦿
|
|
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 .
|
|
196
|
-
out('
|
|
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]}.
|
|
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:
|
|
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
|
-
// $
|
|
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 `
|
|
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,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
|
-
|
|
|
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 `
|
|
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
|
@@ -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
|
|
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
|
|
@@ -304,7 +317,7 @@ before saving.
|
|
|
304
317
|
|
|
305
318
|
## The ledger
|
|
306
319
|
|
|
307
|
-
Below the terminals,
|
|
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.`
|