@ucsandman/legcli 0.7.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 +57 -0
- package/NOTICE +8 -0
- package/README.md +601 -558
- package/bin/fake-agent.mjs +4 -4
- package/bin/leg.mjs +64 -34
- 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/adapters.md +17 -3
- package/docs/board-guide.md +13 -0
- package/docs/cli-contracts.md +57 -3
- package/docs/concepts.md +42 -3
- package/docs/configuration.md +42 -2
- package/docs/faq.md +19 -0
- package/docs/getting-started.md +272 -251
- package/docs/harness.md +319 -0
- package/fixtures/limits/grok/grok-rate-limit.json +11 -0
- package/fixtures/live/agy/limit-agy-resource-exhausted.json +11 -0
- package/fixtures/verified.json +1 -1
- package/package.json +8 -4
- package/scripts/build-docs-site.mjs +15 -7
- 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 +10 -2
- package/src/adapters/codex.mjs +1 -1
- package/src/adapters/grok.mjs +4 -7
- package/src/attach.mjs +162 -37
- package/src/auth.mjs +2 -2
- package/src/board/board.css +45 -17
- package/src/board/board.js +4 -4
- package/src/board/floor.js +2 -2
- package/src/board/sessions.js +181 -38
- 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 +660 -47
- package/src/launcher.mjs +40 -27
- package/src/ledger.mjs +6 -6
- package/src/license.mjs +10 -9
- package/src/live-capture.mjs +1 -1
- package/src/mergequeue.mjs +6 -6
- package/src/orchestrator.mjs +28 -4
- package/src/preferences.mjs +63 -9
- package/src/redact.mjs +1 -1
- package/src/resume.mjs +17 -15
- package/src/runner.mjs +3 -3
- package/src/scheduler.mjs +1 -1
- package/src/server.mjs +69 -20
- package/src/session-detail.mjs +15 -1
- package/src/sessions.mjs +9 -5
- 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 +251 -0
- 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
|
@@ -20,12 +20,13 @@ import { availableActions } from '../src/chain.mjs'
|
|
|
20
20
|
import { up, down, stopBoard, status, openBoard } from '../src/launcher.mjs'
|
|
21
21
|
import { attach, ensureBoard } from '../src/attach.mjs'
|
|
22
22
|
import { readShare, addPerson, removePerson, rotate as rotateToken, turnOn, turnOff, linkFor, personNamed } from '../src/share.mjs'
|
|
23
|
-
import {
|
|
23
|
+
import { SUPERVISED_AGENTS, listSessions, readSession, readEvents as readSessionEvents, requestControl, removeSession, isActive, readLand, sessionDir, appendEvent } from '../src/sessions.mjs'
|
|
24
24
|
import { addAccount, removeAccount, listAccountRows, LAYOUT } from '../src/accounts.mjs'
|
|
25
25
|
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,9 +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') {
|
|
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")
|
|
94
|
+
appendEvent(s.session_id, { type: 'status', summary: 'simulated rate limit appended to the grok log' })
|
|
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'}`)
|
|
96
|
+
}
|
|
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.`)
|
|
92
98
|
}
|
|
93
99
|
|
|
94
100
|
function fmtCard(c) {
|
|
@@ -97,36 +103,51 @@ function fmtCard(c) {
|
|
|
97
103
|
return `${c.card_id} [${c.status}] ${c.station}${leg} leases=${(c.leases?.length ? c.leases : ['**']).join(',')} ${String(c.title ?? c.task).slice(0, 60)}`
|
|
98
104
|
}
|
|
99
105
|
|
|
100
|
-
const TERMS = `Terms check (fetched 2026-09-11): Anthropic Consumer Terms forbid sharing account credentials and "bypassing any of our systems or protective measures"; the Anthropic Usage Policy forbids coordinating across multiple accounts to circumvent product guardrails; OpenAI's Terms of Use forbid sharing credentials and "circumvent any rate limits or restrictions". Two paid logins you own are not banned by name, but rotating to a second account of the same vendor because the first is rate-limited is close to that wording. Leg's default chain switches vendors (claude -> codex -> agy); a second account of one vendor is your call.`
|
|
106
|
+
const TERMS = `Terms check (fetched 2026-09-11): Anthropic Consumer Terms forbid sharing account credentials and "bypassing any of our systems or protective measures"; the Anthropic Usage Policy forbids coordinating across multiple accounts to circumvent product guardrails; OpenAI's Terms of Use forbid sharing credentials and "circumvent any rate limits or restrictions". Two paid logins you own are not banned by name, but rotating to a second account of the same vendor because the first is rate-limited is close to that wording. Leg's default chain switches vendors (claude -> codex -> agy -> grok); a second account of one vendor is your call.`
|
|
101
107
|
|
|
102
108
|
async function main() {
|
|
103
109
|
const [group, cmd, ...rest] = process.argv.slice(2)
|
|
104
110
|
const args = parseArgs(rest)
|
|
105
111
|
if (group === '--version' || group === '-v') return out(VERSION)
|
|
106
112
|
if (group === '🦿' || group === 'prosthetic' || group === 'easter-egg') {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
'
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
113
|
+
const ORANGE = '\x1b[38;5;208m'
|
|
114
|
+
const RESET = '\x1b[0m'
|
|
115
|
+
const LEG_ART = [
|
|
116
|
+
' ███████',
|
|
117
|
+
' ███████',
|
|
118
|
+
' ███████',
|
|
119
|
+
' ███████',
|
|
120
|
+
' ███████',
|
|
121
|
+
' ███████',
|
|
122
|
+
' ███████',
|
|
123
|
+
' ███████',
|
|
124
|
+
' ███████',
|
|
125
|
+
' ███████',
|
|
126
|
+
' ███████',
|
|
127
|
+
' ███████████',
|
|
128
|
+
' █████████████',
|
|
129
|
+
` ██████(${ORANGE}00${RESET})███ ← knee servo`,
|
|
130
|
+
' █████████████',
|
|
131
|
+
' ███████████',
|
|
132
|
+
' ███████',
|
|
133
|
+
' █████',
|
|
134
|
+
' █████',
|
|
135
|
+
' █████',
|
|
136
|
+
' █████',
|
|
137
|
+
' █████',
|
|
138
|
+
' █████',
|
|
139
|
+
' ██████████████████████████',
|
|
140
|
+
' ████ ████ ████ ████ ████ ████',
|
|
141
|
+
].join('\n')
|
|
142
|
+
out('🦿 Leg: the mechanical relay runner for coding agents.\n')
|
|
143
|
+
out(LEG_ART)
|
|
144
|
+
out('\nPassing the leg to the next runner when limits hit.')
|
|
124
145
|
return
|
|
125
146
|
}
|
|
126
|
-
if (
|
|
127
|
-
// leg claude|codex|agy [agent args...]: everything after the agent name
|
|
147
|
+
if (SUPERVISED_AGENTS.includes(group)) {
|
|
148
|
+
// leg claude|codex|agy|grok [agent args...]: everything after the agent name
|
|
128
149
|
// goes straight through.
|
|
129
|
-
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' })
|
|
130
151
|
process.exit(code)
|
|
131
152
|
}
|
|
132
153
|
if (group === 'sessions') {
|
|
@@ -172,15 +193,15 @@ async function main() {
|
|
|
172
193
|
const v = resumeVerdict(where)
|
|
173
194
|
if (a.json) { out(JSON.stringify(v, null, 2)); process.exit(v.exit_code) }
|
|
174
195
|
if (v.state === 'missing') {
|
|
175
|
-
out(`no resume pointer in this checkout (looked for .
|
|
176
|
-
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.')
|
|
177
198
|
process.exit(v.exit_code)
|
|
178
199
|
}
|
|
179
200
|
const head = v.head?.now ? `${v.head.now.slice(0, 7)}${v.head.branch ? ` on ${v.head.branch}` : ''}` : 'no commit'
|
|
180
201
|
const line = v.state === 'fresh'
|
|
181
202
|
? `${v.file} is current: written ${v.written_at ? ago(v.age_ms) : 'at an unrecorded time'}, and the repository is still at ${head}.`
|
|
182
203
|
: v.state === 'unstamped'
|
|
183
|
-
? `${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.`
|
|
184
205
|
: `${v.file} is STALE: ${v.reasons.join('; ')}.`
|
|
185
206
|
if (a.check) {
|
|
186
207
|
out(line)
|
|
@@ -280,7 +301,7 @@ async function main() {
|
|
|
280
301
|
out('')
|
|
281
302
|
out('Log in once (paste in PowerShell):')
|
|
282
303
|
out(` ${r.login}`)
|
|
283
|
-
out(`Then: $env:
|
|
304
|
+
out(`Then: $env:LEG_ACCOUNT='${name}'; leg ${agent} (or let a limit hand off to it)`)
|
|
284
305
|
} catch (err) { die(2, err.message) }
|
|
285
306
|
return
|
|
286
307
|
}
|
|
@@ -302,6 +323,12 @@ async function main() {
|
|
|
302
323
|
if (cmd === 'terms') return out(TERMS)
|
|
303
324
|
die(2, `unknown accounts command "${cmd}" (ls|add|rm|terms)`)
|
|
304
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
|
+
}
|
|
305
332
|
if (group === 'license') {
|
|
306
333
|
// The paid gate. Keys verify offline against the public key in
|
|
307
334
|
// src/license.mjs; nothing here talks to the network except refresh.
|
|
@@ -330,7 +357,7 @@ async function main() {
|
|
|
330
357
|
}
|
|
331
358
|
if (group === 'uninstall') {
|
|
332
359
|
// Leg never edits ~/.claude or ~/.codex; everything it added lives under
|
|
333
|
-
// $
|
|
360
|
+
// $LEG_HOME (sessions, usage, extra-account dirs, cards).
|
|
334
361
|
const dir = home()
|
|
335
362
|
if (!args.yes) {
|
|
336
363
|
out(`leg uninstall removes ${dir} (sessions, usage, extra-account dirs, cards, board pidfile) and nothing else.`)
|
|
@@ -340,7 +367,7 @@ async function main() {
|
|
|
340
367
|
for (const r of listAccountRows()) if (r.name !== 'default') removeAccount(r.agent, r.name)
|
|
341
368
|
await down()
|
|
342
369
|
rmSync(dir, { recursive: true, force: true })
|
|
343
|
-
return out(`removed ${dir}; now: npm rm -g legcli`)
|
|
370
|
+
return out(`removed ${dir}; now: npm rm -g @ucsandman/legcli`)
|
|
344
371
|
}
|
|
345
372
|
if (group === 'card') {
|
|
346
373
|
if (cmd === 'add') return cardAdd(args)
|
|
@@ -427,16 +454,19 @@ async function main() {
|
|
|
427
454
|
out(openBoard(url) ? `opened ${url}` : `could not open a browser; visit ${url}`)
|
|
428
455
|
return
|
|
429
456
|
}
|
|
430
|
-
if (group && group !== '--help' && group !== 'help') die(2, `unknown command "${group}" (claude|codex|agy|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)`)
|
|
431
458
|
out(`leg ${VERSION}, your coding agents, with a board alongside and a handoff when one hits its limit
|
|
432
|
-
claude|codex|agy [args...] the normal interactive agent in this terminal; args pass straight through
|
|
433
|
-
|
|
434
|
-
|
|
459
|
+
claude|codex|agy|grok [args...] the normal interactive agent in this terminal; args pass straight through
|
|
460
|
+
the board opens once, the session shows as a card, usage is tracked, a limit hands off
|
|
461
|
+
a second live session in one checkout gets its own worktree (--no-worktree to share)
|
|
462
|
+
auto-approve mode (--no-auto-approve to opt out)
|
|
435
463
|
sessions ls|show|events|handoff|end|rm|simulate-limit <id>
|
|
436
464
|
resume [--check] [--json] [--path <dir>] the hand-off waiting in this checkout, and whether it is still true
|
|
437
465
|
freshness is recomputed from git at read time; --check prints only the verdict
|
|
438
466
|
exit 0 current, 1 stale or unstamped, 3 no pointer here
|
|
439
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)
|
|
440
470
|
license [status|activate <key>|deactivate|refresh]
|
|
441
471
|
personal or team license status and management
|
|
442
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/adapters.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Adapters
|
|
2
2
|
|
|
3
3
|
Two things per agent: what Leg reads from an interactive session
|
|
4
|
-
(`leg claude|codex|agy`), and the headless argv the v0.1 pipeline spawns.
|
|
4
|
+
(`leg claude|codex|agy|grok`), and the headless argv the v0.1 pipeline spawns.
|
|
5
5
|
Every fact here was written against `src/taps/*.mjs`, `src/attach.mjs` and
|
|
6
6
|
`src/adapters/*.mjs`; the evidence trail, including which lines an artifact
|
|
7
7
|
backs, is [cli-contracts.md](cli-contracts.md).
|
|
@@ -120,7 +120,7 @@ documentation say docs-only.
|
|
|
120
120
|
- **The wall**: `RESOURCE_EXHAUSTED`, "it resets in %s" and "out of quota" in
|
|
121
121
|
the log. Those strings are present in `agy.exe`, and `scanLog()` also reads a
|
|
122
122
|
relative reset out of "resets in \<n>\<s|m|h|d>". Status:
|
|
123
|
-
**
|
|
123
|
+
**observed-live 2026-09-16** <!-- live:agy/agy-resource-exhausted --> —
|
|
124
124
|
`RESOURCE_EXHAUSTED (code 429): Individual quota reached … Resets in
|
|
125
125
|
71h19m42s.` appeared in a session's `agy.log` at 08:02:42Z and walled the
|
|
126
126
|
agent. No payload was kept: the capture call in `src/attach.mjs` was added
|
|
@@ -133,11 +133,25 @@ documentation say docs-only.
|
|
|
133
133
|
- **One account only**: agy 1.2.0 has no config-directory override, so
|
|
134
134
|
`leg accounts add agy …` is refused.
|
|
135
135
|
|
|
136
|
+
### grok
|
|
137
|
+
|
|
138
|
+
- **How Leg attaches**: `grok <your args>` with `--debug-file <~/.leg/sessions/<id>/grok.log>` passed by Leg (`src/attach.mjs` `spawnSpec`).
|
|
139
|
+
- **Usage percentages**: `GET https://cli-chat-proxy.grok.com/v1/billing?format=credits` and `GET https://cli-chat-proxy.grok.com/v1/user?include=subscription` (`LEG_GROK_BILLING_URL` and `LEG_GROK_USER_URL` override), reading the OAuth token stored in `~/.grok/auth.json`. The billing endpoint reports `creditUsagePercent` and `currentPeriod` (with weekly resets). Polled every 60 s (`LEG_USAGE_POLL_MS`). If the token expires or returns 401, usage is marked unknown without crashing, and re-reads `auth.json` on the next poll.
|
|
140
|
+
- **The wall**: rate limit signals cited directly from `xai-org/grok-build`:
|
|
141
|
+
- `SamplingError::Api { status: StatusCode::TOO_MANY_REQUESTS }` (`crates/codegen/xai-grok-sampling-types/src/error.rs:304`)
|
|
142
|
+
- `RATE_LIMITED_ERROR_CODE = -32003` and user messages `RATE_LIMITED_USER_MESSAGE_OAUTH` ("You've hit the rate limit for your plan. Try again later.") and `RATE_LIMITED_USER_MESSAGE_API_KEY` ("You've hit the rate limit for your API key. Try again later.") (`crates/codegen/xai-grok-shell/src/sampling/error.rs:15, 18-21`)
|
|
143
|
+
- Headline "Rate limited (429)" and "You've hit the rate limit for your plan" (`crates/codegen/xai-grok-pager/src/app/error_display.rs:263-267`)
|
|
144
|
+
- `StopFailureKind::RateLimit` ("rate_limit") (`crates/codegen/xai-grok-hooks/src/event.rs:306-315`)
|
|
145
|
+
- Free usage exhausted: `FREE_USAGE_USER_MESSAGE` and `FREE_USAGE_EXHAUSTED_ERROR_CODE` ("subscription:free-usage-exhausted") (`crates/codegen/xai-grok-shell/src/sampling/error.rs:30, 33`)
|
|
146
|
+
`src/taps/grok.mjs` scans `grok.log` for these exact signals and extracts reset durations when available.
|
|
147
|
+
- **Prompts and session id**: `~/.grok/sessions/<url-encoded-cwd>/prompt_history.jsonl`, recorded per prompt with timestamp, `session_id`, and `prompt`.
|
|
148
|
+
- **Accounts**: Supports `GROK_HOME` override. `leg accounts add grok <name>` creates junctioned directories copying `config.toml`.
|
|
149
|
+
|
|
136
150
|
### Resume prompt per agent
|
|
137
151
|
|
|
138
152
|
After a hand-off the next agent starts in the same terminal with the pointer
|
|
139
153
|
prompt as its first positional argument: `claude "<prompt>"`,
|
|
140
|
-
`codex "<prompt>"`, `agy -i "<prompt>"` (`src/attach.mjs` `spawnSpec`).
|
|
154
|
+
`codex "<prompt>"`, `agy -i "<prompt>"`, `grok "<prompt>"` (`src/attach.mjs` `spawnSpec`).
|
|
141
155
|
|
|
142
156
|
## Headless adapters (the v0.1 pipeline)
|
|
143
157
|
|
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,9 +189,30 @@ 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
|
-
What `leg claude|codex|agy` reads while the real interactive CLI runs. Same
|
|
215
|
+
What `leg claude|codex|agy|grok` reads while the real interactive CLI runs. Same
|
|
195
216
|
tagging rule: `observed-live 2026-09-11` means the build machine did it;
|
|
196
217
|
`docs-only` means the CLI's own source or documentation says so and Leg has
|
|
197
218
|
not seen it happen. Machine: Claude Code 2.1.268, codex-cli 0.153.4, agy 1.2.0.
|
|
@@ -295,7 +316,7 @@ variable and `LEG_SESSION` (source: src/attach.mjs, src/env.mjs).
|
|
|
295
316
|
"quota exhausted/exceeded" in the log, plus a relative reset parsed out of
|
|
296
317
|
"resets in \<n>\<s|m|h|d>" (source: strings present in `agy.exe`;
|
|
297
318
|
src/taps/agy.mjs `scanLog`).
|
|
298
|
-
**
|
|
319
|
+
**observed-live 2026-09-16** <!-- live:agy/agy-resource-exhausted -->:
|
|
299
320
|
`RESOURCE_EXHAUSTED (code 429): Individual quota reached … Resets in
|
|
300
321
|
71h19m42s.` appeared in a session's `agy.log` at 08:02:42Z, `scanLog` read
|
|
301
322
|
the relative reset, and the agent was walled. No payload was kept, the
|
|
@@ -310,6 +331,38 @@ variable and `LEG_SESSION` (source: src/attach.mjs, src/env.mjs).
|
|
|
310
331
|
`LAYOUT.agy.env` is `null` and `accounts add agy` is refused (source:
|
|
311
332
|
src/accounts.mjs).
|
|
312
333
|
|
|
334
|
+
### grok tap
|
|
335
|
+
|
|
336
|
+
- Attach: `grok <args> --debug-file <LEG_HOME>/sessions/<id>/grok.log` (source:
|
|
337
|
+
src/attach.mjs `spawnSpec`).
|
|
338
|
+
- Usage: `GET https://cli-chat-proxy.grok.com/v1/billing?format=credits` and
|
|
339
|
+
`GET https://cli-chat-proxy.grok.com/v1/user?include=subscription` with
|
|
340
|
+
`Authorization: Bearer <token>` read from `~/.grok/auth.json` (per-scope
|
|
341
|
+
key map or direct object). The billing endpoint reports `creditUsagePercent`
|
|
342
|
+
and `currentPeriod` (with weekly reset timestamp `end`). Polled every 60 s
|
|
343
|
+
(`LEG_USAGE_POLL_MS`). Token expiry and HTTP 401 are handled gracefully
|
|
344
|
+
without throwing, marking usage unknown until re-authenticated.
|
|
345
|
+
(source: src/taps/grok.mjs; probe against xAI proxy).
|
|
346
|
+
- The wall: exact rate-limit signals cited from `xai-org/grok-build` (Rust):
|
|
347
|
+
- `SamplingError::Api { status: StatusCode::TOO_MANY_REQUESTS }`
|
|
348
|
+
(`crates/codegen/xai-grok-sampling-types/src/error.rs:304`)
|
|
349
|
+
- `RATE_LIMITED_ERROR_CODE = -32003` and messages
|
|
350
|
+
`RATE_LIMITED_USER_MESSAGE_OAUTH` ("You've hit the rate limit for your plan. Try again later.")
|
|
351
|
+
and `RATE_LIMITED_USER_MESSAGE_API_KEY` ("You've hit the rate limit for your API key. Try again later.")
|
|
352
|
+
(`crates/codegen/xai-grok-shell/src/sampling/error.rs:15, 18-21`)
|
|
353
|
+
- Headline "Rate limited (429)" and "You've hit the rate limit for your plan"
|
|
354
|
+
(`crates/codegen/xai-grok-pager/src/app/error_display.rs:263-267`)
|
|
355
|
+
- `StopFailureKind::RateLimit` ("rate_limit")
|
|
356
|
+
(`crates/codegen/xai-grok-hooks/src/event.rs:306-315`)
|
|
357
|
+
- Free usage exhausted: `FREE_USAGE_USER_MESSAGE` and `FREE_USAGE_EXHAUSTED_ERROR_CODE`
|
|
358
|
+
("subscription:free-usage-exhausted")
|
|
359
|
+
(`crates/codegen/xai-grok-shell/src/sampling/error.rs:30, 33`)
|
|
360
|
+
`src/taps/grok.mjs` scans `grok.log` for these patterns and parses reset duration.
|
|
361
|
+
- Prompts and session id: `~/.grok/sessions/<url-encoded-cwd>/prompt_history.jsonl`,
|
|
362
|
+
one `{ timestamp, session_id, prompt }` per entry (source: src/taps/grok.mjs `promptsSince`).
|
|
363
|
+
- Accounts: `GROK_HOME` override supported; `leg accounts add grok <name>` creates
|
|
364
|
+
junctioned directories with copied `config.toml` (source: src/accounts.mjs).
|
|
365
|
+
|
|
313
366
|
### Usage store and the chooser
|
|
314
367
|
|
|
315
368
|
- `<LEG_HOME>/usage/<agent>--<account>.json`:
|
|
@@ -361,7 +414,7 @@ non-zero exit → `failed`. Every outcome except `completed`, `auth_failed` and
|
|
|
361
414
|
`killed` asks the chain to hand off.
|
|
362
415
|
|
|
363
416
|
<!-- limits-table:start -->
|
|
364
|
-
Generated by `node scripts/limits-table.mjs` from
|
|
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.
|
|
365
418
|
|
|
366
419
|
| id | adapter | class | where | source | text (excerpt) | produced by |
|
|
367
420
|
|----|---------|-------|-------|--------|----------------|-------------|
|
|
@@ -383,6 +436,7 @@ Generated by `node scripts/limits-table.mjs` from 21 fixtures (4 observed-live,
|
|
|
383
436
|
| generic-resource-exhausted | * | limit | any | **docs-only** | RESOURCE_EXHAUSTED | generic matcher (gRPC RESOURCE_EXHAUSTED); lowest priority |
|
|
384
437
|
| generic-usage-limit | * | limit | any | **docs-only** | usage limit | generic matcher; lowest priority |
|
|
385
438
|
| grok-not-logged-in | grok | auth | stderr | **observed-live** | To sign in, open this URL in your browser: https://accounts.x.ai/oauth2/device?user_cod | fixtures/live/grok/err.log from scripts/probe.mjs --adapter grok, 2026-09-10 (stdout JSON stopReason: Cancelled, exit 0) |
|
|
439
|
+
| grok-rate-limit | grok | limit | any | **docs-only** | You've hit the rate limit for your plan. Try again later. | crates/codegen/xai-grok-shell/src/sampling/error.rs:18 (RATE_LIMITED_USER_MESSAGE_OAUTH: "You've hit the rate limit for your plan. Try again later.") |
|
|
386
440
|
| auth-source-set | * | auth | stderr | **docs-only** | another auth source is set | project brief (Wes, 2026-09-10): stderr saying "another auth source is set" counts as a failed launch; wording not yet observed live |
|
|
387
441
|
| compile-error | * | info | stderr | **docs-only** | SyntaxError: Unexpected token ) at compileSourceTextModule (node:internal/modules/esm/ | synthetic negative fixture (a crashed agent is not a limit) |
|
|
388
442
|
| empty-stdout-exit-0 | * | info | stdout | **observed-live** | | fixtures/live/grok (exit 0, no work): silence is not a limit |
|