@ucsandman/legcli 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/NOTICE +8 -0
  3. package/README.md +601 -560
  4. package/bin/fake-agent.mjs +4 -4
  5. package/bin/leg.mjs +21 -12
  6. package/docs/DECISIONS.md +20 -2
  7. package/docs/ERRORS.md +71 -0
  8. package/docs/README.md +2 -0
  9. package/docs/REUSE.md +1 -1
  10. package/docs/VOCABULARY.md +21 -0
  11. package/docs/board-guide.md +13 -0
  12. package/docs/cli-contracts.md +22 -1
  13. package/docs/concepts.md +42 -3
  14. package/docs/configuration.md +22 -1
  15. package/docs/faq.md +19 -0
  16. package/docs/getting-started.md +272 -251
  17. package/docs/harness.md +319 -0
  18. package/fixtures/verified.json +1 -1
  19. package/package.json +7 -3
  20. package/scripts/build-docs-site.mjs +11 -4
  21. package/scripts/check-branding.mjs +118 -0
  22. package/scripts/check-claims.mjs +1 -1
  23. package/scripts/license-sign.mjs +1 -1
  24. package/scripts/limits-table.mjs +1 -1
  25. package/scripts/live-limits.mjs +1 -1
  26. package/scripts/npm-publish-gate.mjs +114 -0
  27. package/scripts/probe.mjs +4 -3
  28. package/scripts/seed-fake-cards.mjs +4 -3
  29. package/scripts/seed-floor-board.mjs +5 -4
  30. package/scripts/seed-wes-board.mjs +5 -4
  31. package/scripts/stripe-setup.mjs +1 -1
  32. package/scripts/sync-harness-engine.mjs +159 -0
  33. package/scripts/sync-leg-agents.mjs +127 -0
  34. package/src/accounts.mjs +1 -2
  35. package/src/adapters/codex.mjs +1 -1
  36. package/src/attach.mjs +75 -19
  37. package/src/auth.mjs +2 -2
  38. package/src/board/board.js +3 -3
  39. package/src/board/sessions.js +77 -3
  40. package/src/bundle.mjs +54 -8
  41. package/src/chain.mjs +1 -1
  42. package/src/contract.mjs +4 -3
  43. package/src/fsx.mjs +5 -2
  44. package/src/handoff.mjs +6 -6
  45. package/src/harness/cli.mjs +281 -0
  46. package/src/harness/fingerprint.mjs +68 -0
  47. package/src/harness/index.mjs +407 -0
  48. package/src/harness/registry.mjs +124 -0
  49. package/src/harness/vendor/agnostic-ai/LICENSE +21 -0
  50. package/src/harness/vendor/agnostic-ai/UPSTREAM.json +30 -0
  51. package/src/harness/vendor/agnostic-ai/core/safety/guards.json +96 -0
  52. package/src/harness/vendor/agnostic-ai/core/templates/targets.json +252 -0
  53. package/src/harness/vendor/agnostic-ai/engine/harness/README.md +199 -0
  54. package/src/harness/vendor/agnostic-ai/engine/harness/apply.cjs +247 -0
  55. package/src/harness/vendor/agnostic-ai/engine/harness/bundle.cjs +243 -0
  56. package/src/harness/vendor/agnostic-ai/engine/harness/capture.cjs +119 -0
  57. package/src/harness/vendor/agnostic-ai/engine/harness/common.cjs +375 -0
  58. package/src/harness/vendor/agnostic-ai/engine/harness/index.cjs +55 -0
  59. package/src/harness/vendor/agnostic-ai/engine/harness/sources/claude.cjs +330 -0
  60. package/src/harness/vendor/agnostic-ai/engine/harness/sources/codex.cjs +314 -0
  61. package/src/harness/vendor/agnostic-ai/engine/harness/status.cjs +171 -0
  62. package/src/harness/vendor/agnostic-ai/engine/harness/targets/agy.cjs +113 -0
  63. package/src/harness/vendor/agnostic-ai/engine/harness/targets/claude.cjs +158 -0
  64. package/src/harness/vendor/agnostic-ai/engine/harness/targets/codex.cjs +832 -0
  65. package/src/harness/vendor/agnostic-ai/engine/harness/targets/cursor.cjs +87 -0
  66. package/src/harness/vendor/agnostic-ai/engine/harness/targets/gemini.cjs +128 -0
  67. package/src/harness/vendor/agnostic-ai/engine/harness/targets/generic.cjs +424 -0
  68. package/src/harness/vendor/agnostic-ai/engine/harness/toml.cjs +149 -0
  69. package/src/harness/vendor/agnostic-ai/engine/hooks/shim.cjs +431 -0
  70. package/src/hook.mjs +49 -49
  71. package/src/land.mjs +7 -35
  72. package/src/launcher.mjs +38 -26
  73. package/src/ledger.mjs +6 -6
  74. package/src/license.mjs +10 -9
  75. package/src/live-capture.mjs +1 -1
  76. package/src/mergequeue.mjs +5 -5
  77. package/src/orchestrator.mjs +28 -4
  78. package/src/preferences.mjs +37 -3
  79. package/src/redact.mjs +1 -1
  80. package/src/resume.mjs +17 -15
  81. package/src/runner.mjs +2 -2
  82. package/src/scheduler.mjs +1 -1
  83. package/src/server.mjs +38 -10
  84. package/src/session-detail.mjs +15 -1
  85. package/src/sessions.mjs +6 -3
  86. package/src/share.mjs +2 -2
  87. package/src/stations/agent.mjs +1 -1
  88. package/src/sync/dashclaw.mjs +4 -4
  89. package/src/synthesis.mjs +165 -0
  90. package/src/taps/agy.mjs +2 -2
  91. package/src/taps/claude-usage.mjs +1 -1
  92. package/src/taps/claude.mjs +170 -170
  93. package/src/taps/codex.mjs +286 -286
  94. package/src/taps/grok.mjs +2 -2
  95. package/src/trust.mjs +205 -36
  96. package/src/usage.mjs +5 -1
  97. package/src/worktree.mjs +5 -4
  98. package/fixtures/live/agy/attempt-1-scratch-workspace.out.log +0 -1
  99. package/fixtures/live/agy/err.log +0 -0
  100. package/fixtures/live/agy/out.log +0 -1
  101. package/fixtures/live/agy/supervisor.log +0 -2
  102. package/fixtures/live/claude/err.log +0 -0
  103. package/fixtures/live/claude/out.log +0 -1
  104. package/fixtures/live/claude/supervisor.log +0 -2
  105. package/fixtures/live/codex/err.log +0 -1
  106. package/fixtures/live/codex/out.log +0 -8
  107. package/fixtures/live/codex/supervisor.log +0 -2
  108. package/fixtures/live/grok/err.log +0 -32
  109. package/fixtures/live/grok/out.log +0 -7
  110. package/fixtures/live/grok/supervisor.log +0 -2
@@ -0,0 +1,319 @@
1
+ # The portable harness
2
+
3
+ Off by default. When you turn it on, a hand-off carries two things instead of
4
+ one: the task (the context handoff bundle Leg has always written) and the
5
+ agent's **working environment**: the global working agreement, identity,
6
+ hooks, skills, subagents, slash commands, MCP servers and permissions of the
7
+ client you actually use, rendered into the native configuration of the client
8
+ the hand-off lands on. The promise is not byte-for-byte equivalence. It is:
9
+
10
+ > Preserve as much of the source agent's operating environment as the
11
+ > destination client can faithfully support, and say exactly what could not be
12
+ > carried over.
13
+
14
+ ```
15
+ leg harness enable # detect clients, capture, show the plan, apply after you say yes
16
+ leg harness status # what is on, the source, when it was captured, each client's state
17
+ leg harness sync # re-capture and write whatever is out of date
18
+ leg harness check # the same, writing nothing; exit 1 when something is stale
19
+ leg harness explain # every item a client could not receive, and why
20
+ leg harness disable # stop; nothing already written is removed
21
+ ```
22
+
23
+ ## What moves, and what does not
24
+
25
+ | component | carried as | what a destination may drop |
26
+ |---|---|---|
27
+ | rules | the source's global rules file with its `@imports` inlined, written as the destination's global rules file (`AGENTS.md`, `GEMINI.md`, or `leg-rules.md` imported from `CLAUDE.md`) | nothing; the whole agreement travels |
28
+ | identity | `SOUL.md` or the identity section, inlined or written to the client's own file | nothing |
29
+ | hooks | the same hook scripts, pointed at (never copied), in the destination's dialect; non-Codex clients run them through a shim that translates the payload | an event the client does not have (`MessageDisplay`, agy's missing `SessionStart`), a matcher whose tools have no counterpart |
30
+ | skills | a directory link per skill into the client's skills directory, so an edit at the source is live everywhere | a name that already exists as a real directory there, a skill the client reads natively from a shared directory |
31
+ | agents | one subagent file per agent in the client's format (`.md` or `.toml`), with the model tier mapped through the client's ladder | a client with no subagent surface (Gemini CLI) |
32
+ | commands | one prompt file per slash command (`prompts/`, `commands/`, `.toml` for Gemini) | a name that already exists and Leg did not write |
33
+ | MCP servers | each server in the client's own MCP file or config table, with every credential replaced by an environment reference | a transport the client lacks (Codex has no SSE), a server the user already configured there |
34
+ | permissions | `settings.json` allow/deny/ask for Claude Code; Codex prefix rules for `Bash(...)` patterns | anything but a `Bash(...)` pattern on Codex; clients with no machine-readable permission surface |
35
+
36
+ What never moves:
37
+
38
+ - **Credentials.** OAuth tokens, `.credentials.json`, `auth.json`, API keys in an
39
+ MCP `env` or `headers` block, and a token anywhere else the capture reads.
40
+ An `env` or `headers` value that looks like a credential becomes `${NAME}`
41
+ in the bundle and Leg tells you which variable to export for each client.
42
+ A token inside free text (the rules, the identity, an agent or command
43
+ body) is replaced by `[REDACTED]`; a hook whose command line carries one,
44
+ and an MCP server whose argument or URL carries one (a password in a
45
+ connection string, a secret query value, a token in the path), are not
46
+ carried at all, each with a warning naming it. The scan is fail-closed:
47
+ every field of the bundle is validated at save time and a bundle that
48
+ still carries a credential is refused. Tests plant tokens in every one of
49
+ those places, and in the places a capture must not read, and assert they
50
+ appear in no bundle file and no destination file.
51
+ - **Which login runs.** Accounts are Leg's account layer
52
+ (`leg accounts`, `src/accounts.mjs`); the harness describes behaviour and
53
+ capabilities, never who is signed in. A same-agent hand-off to a second
54
+ login carries no harness at all: the second login shares the first one's
55
+ home through the junctions the account layer made.
56
+ - **Project-local instructions.** A repository's `CLAUDE.md`, `AGENTS.md` or
57
+ `GEMINI.md` is read natively by each client from the checkout. The harness
58
+ carries the *global* working agreement only.
59
+ - **The source client itself.** The client the harness is captured from is
60
+ never written to.
61
+
62
+ ## Precedence
63
+
64
+ When a destination starts after a hand-off, four layers of instruction are in
65
+ play. From most to least specific:
66
+
67
+ 1. **The resume prompt and bundle** (`.leg/RESUME-<session>.md`): the task,
68
+ the last messages, the diff, why the previous agent stopped. Always
69
+ present, harness or not.
70
+ 2. **The repository's own instruction files**: `CLAUDE.md`, `AGENTS.md`,
71
+ `GEMINI.md` in the checkout. Never touched by Leg; each client reads them
72
+ itself.
73
+ 3. **The ported global working agreement**: the destination's global rules
74
+ file, written by the harness. This is where the source's `~/.claude/CLAUDE.md`
75
+ (with its imports) lands.
76
+ 4. The client's own defaults.
77
+
78
+ Leg does not merge layers 2 and 3. A repository file that repeats a global rule
79
+ repeats it; a repository file that contradicts one wins, because that is how
80
+ every client already resolves project-level instructions over global ones.
81
+ `test/harness-policy.test.mjs` runs a sync from inside a checkout that has its
82
+ own `CLAUDE.md` and `AGENTS.md` and asserts both are byte-identical afterwards:
83
+ a sync writes under the client homes only.
84
+
85
+ ## Turning it on: the first run
86
+
87
+ `leg harness enable` is the one explicit consent. It:
88
+
89
+ 1. Detects which sources exist on this machine (`~/.claude/CLAUDE.md`,
90
+ `~/.codex/AGENTS.md`) and picks the first, or the one you name with
91
+ `--source`. It never guesses destructively: with two sources it says which
92
+ it chose and how to choose the other.
93
+ 2. Captures the source into the neutral bundle under `~/.leg/harness/bundle/`,
94
+ scans it for credentials, and prints every warning.
95
+ 3. Runs a check against every installed client and prints the table: per
96
+ client, per component, `✓ synced`, `✗ stale`, `- unsupported`, and the
97
+ count carried (`skills 2/3`), then every item that will not be carried with
98
+ its reason.
99
+ 4. Asks before writing. In a terminal it prompts; without one (a script, CI)
100
+ it prints the plan, writes nothing, and exits 3 unless you passed `--yes`.
101
+ 5. Applies, records ownership of every file and region it wrote, and saves the
102
+ consent: `preferences.json` gains `"harness": { "enabled": true, "policy":
103
+ "sync", "source": "claude" }`.
104
+
105
+ After that, unattended hand-offs use the saved policy and never prompt.
106
+ `leg harness sync` writes only on an install that gave this consent; before
107
+ `enable` it exits 3 and points at it, while `check`, `diff` and `explain`
108
+ report without writing. The board can turn the feature off or narrow the
109
+ policy, never turn it on or widen it: that stays a terminal command.
110
+
111
+ ## Policies
112
+
113
+ `leg harness policy <warn|sync|strict>`, or `--policy` on `enable`. The
114
+ default when you enable is `sync`; an install that never enabled the feature
115
+ is `off` and behaves exactly as before.
116
+
117
+ | policy | at a hand-off, before the next agent starts |
118
+ |---|---|
119
+ | `off` | nothing is captured, checked, written or recorded |
120
+ | `warn` | the source is re-captured if it changed, the destination is checked, the result is recorded on the session and printed; nothing is written to any client |
121
+ | `sync` | as `warn`, and a stale destination is synced: managed files and regions are written, backups taken, hand-edited files skipped and reported. The hand-off proceeds whatever the outcome. |
122
+ | `strict` | as `sync`, and a destination that ends `attention`, `error` or `unsupported` is refused: Leg records `harness_blocked`, prints why, tries the next option in your hand-off order, and if none is left stops the terminal with exit 5 rather than launching an agent without its environment. Strict applies to hand-offs only; the agent you start yourself is never refused. |
123
+
124
+ There is no interactive prompt on this path under any policy. A limit usually
125
+ lands when nobody is at the keyboard, and a prompt would wedge the terminal
126
+ until morning.
127
+
128
+ ## States
129
+
130
+ One word per destination, the same word on the CLI, the session timeline, the
131
+ terminal card and the drawer:
132
+
133
+ | state | meaning |
134
+ |---|---|
135
+ | `synced` | everything the destination supports is current and nothing was dropped |
136
+ | `partial` | current, but some items could not be carried (each has a reason under **Dropped**) |
137
+ | `stale` | the destination is behind the source; `warn` policy, or a `check` |
138
+ | `attention` | a managed file was hand-edited since Leg wrote it (backed up, left alone), a component errored (a malformed config file, for instance), or a `deny` or `ask` permission the destination cannot express was dropped, leaving it less restricted than the source; needs you |
139
+ | `unsupported` | the destination has no harness adapter (Grok) or is not installed |
140
+ | `blocked` | the strict policy refused this destination |
141
+ | `source` | the destination is the source client; nothing to carry |
142
+ | `same-client` | a hand-off to another login of the same client; the harness is shared already |
143
+ | `error` | the preparation itself failed; the reason is recorded |
144
+
145
+ ## Ownership, backups, hand edits
146
+
147
+ Every file the harness writes carries `GENERATED by Leg harness` in its first
148
+ lines. Inside a file you also own (`config.toml`, `settings.json`,
149
+ `.claude.json`, agy's `hooks.json`) it writes a marked region:
150
+
151
+ ```
152
+ # >>> leg harness hooks start (generated, do not edit)
153
+ ...
154
+ # <<< leg harness hooks end
155
+ ```
156
+
157
+ or, for JSON, exactly the groups or keys it inserted last time, recorded in
158
+ `~/.leg/harness/harness-state.json`. Everything outside is preserved byte for
159
+ byte; a re-run only ever removes what the harness itself added; a skill link is
160
+ never made over a real directory; a file Leg did not create is never
161
+ overwritten. Before any overwrite the previous content goes to
162
+ `~/.leg/harness/backups/<client>-<file>-<timestamp>.bak`. A generated file
163
+ that is pruned because its source went away (a subagent or command deleted at
164
+ the source, a permissions file that became empty) is backed up the same way
165
+ before it is removed.
166
+
167
+ A file you edit by hand after Leg wrote it (the ownership line removed, or
168
+ the content changed) is detected by hash, backed up once, and **skipped**. The
169
+ sync reports `attention` with the path and the remedy: edit the source client
170
+ instead, or `leg harness sync --force` to replace it (the backup stays).
171
+
172
+ ## Performance
173
+
174
+ A hand-off must stay fast. Before capturing, Leg fingerprints the source's
175
+ surfaces from file metadata alone (the rules file and its imports,
176
+ `settings.json`, the MCP file, the agents, commands and skills directories):
177
+ a few dozen `stat` calls. An unchanged fingerprint reuses the bundle on disk.
178
+ The destination is then checked by reading its managed files, and written
179
+ only when something differs. `test/harness-perf.test.mjs` holds a warm
180
+ hand-off decision (unchanged source, synced destination) under 400 ms median
181
+ and a cached capture under 40 ms; on the development machine they measure
182
+ about 19 ms and 7 ms.
183
+
184
+ ## Failure model
185
+
186
+ | failure | what happens |
187
+ |---|---|
188
+ | no source configured or detected | `error`; hand-off proceeds under `warn`/`sync`, refused under `strict`; `leg harness source` names one |
189
+ | destination not installed | `unsupported`; the chooser never picks a missing CLI anyway |
190
+ | destination config is not valid JSON/TOML | that component is `error`, nothing is written to that file, the hand-off proceeds under `warn`/`sync` |
191
+ | a managed file was hand-edited | `attention`; backed up, skipped, named |
192
+ | a skill name is a real directory at the destination | dropped with a reason; the directory is untouched |
193
+ | an MCP value references a variable the destination does not expand | the component note names the variable to export |
194
+ | a hook event or matcher the destination lacks | dropped with a reason |
195
+ | a `deny` or `ask` permission the destination cannot express | `attention`: the destination would be less restricted than the source; strict refuses it, sync proceeds and names it |
196
+ | a credential in the rules, an agent, a command, a hook command line, an MCP argument or URL | free text is redacted to `[REDACTED]`; the hook or server is not carried; each with a warning |
197
+ | a subagent, command or skill with an unsafe name | not carried, with a warning; the rest of the bundle still travels |
198
+ | a skill path with a shell metacharacter on Windows when the junction fallback is needed | the link is refused and reported; nothing is passed to a shell |
199
+ | two Leg processes hand off at once | one takes `~/.leg/harness/.lock` (up to 30 s); the other fails its own step rather than tearing the ownership record |
200
+ | a model tier the destination cannot map | passed through as a raw id and noted |
201
+ | the captured bundle is corrupt on disk | re-captured from the source, never trusted |
202
+ | an interrupted previous apply | the next sync re-derives everything from ownership records; a region is rebuilt, never appended twice |
203
+ | the engine itself throws | `error` on the session; the session is never corrupted; the hand-off proceeds under `warn`/`sync` |
204
+
205
+ Nothing here rolls back a user file: every overwrite is preceded by a backup,
206
+ and a refusal leaves the file as it was.
207
+
208
+ ## Observability
209
+
210
+ Every operation leaves evidence:
211
+
212
+ - `~/.leg/harness/history.jsonl`: one line per capture, apply and hand-off
213
+ decision, with the source, target, bundle fingerprint, timestamps,
214
+ per-component states, dropped items, attention items, the files touched and
215
+ the backups made. `leg harness history` prints it.
216
+ - The session record (`session.json` → `harness`) carries the outcome for the
217
+ leg now running: source, fingerprint, capture and sync times, components,
218
+ dropped, attention, whether the hand-off proceeded.
219
+ - Session events `harness` and `harness_blocked`; card ledger events of the
220
+ same names on the background-task path.
221
+ - The board: a chip on the terminal row (`harness synced`, `harness partial`,
222
+ `harness attention`, `harness refused`) and a **Harness** section in the
223
+ drawer with the source, capture and sync times, the component table,
224
+ **Needs you**, **Dropped**, and the last eight trail entries.
225
+
226
+ No secret is ever logged: the bundle holds references, the trail holds paths
227
+ and states.
228
+
229
+ ## Files
230
+
231
+ ```
232
+ ~/.leg/harness/
233
+ bundle/ the neutral bundle: manifest.json, rules.md, identity.md,
234
+ hooks.json, mcp.json, skills.json, permissions.json, agents/, commands/
235
+ capture.json source, fingerprint, the surfaces hash the capture was taken at
236
+ harness-state.json ownership: every file and region written, per client
237
+ harness-report.json the last apply or check, per client, per component
238
+ backups/ every file before Leg overwrote it
239
+ history.jsonl the evidence trail
240
+ policy.json optional: what is deliberately not carried (below)
241
+ ```
242
+
243
+ `leg uninstall --yes` removes `~/.leg` and with it all of the above. It does
244
+ not remove what the harness wrote into other clients: run `leg harness
245
+ disable`, then delete the files carrying `GENERATED by Leg harness` and the
246
+ `leg harness` regions if you want them gone.
247
+
248
+ ## The port policy
249
+
250
+ `~/.leg/harness/policy.json`, optional, merged over the defaults:
251
+
252
+ ```jsonc
253
+ {
254
+ "rules": { "dropSectionsForTargets": ["Delegation and Model Routing"] },
255
+ "hooks": { "exclude": [{ "match": "capability-graph-guard", "reason": "polices Claude Code's model ladder" }] },
256
+ "skills": { "exclude": { "review-browser": "the destination has no browser tool" } },
257
+ "mcp": { "exclude": { "xapi": "one OAuth grant per client; copy nothing" } },
258
+ "agents": { "modelLadder": { "codex": { "opus": ["gpt-5.6-sol", "high"] } } }
259
+ }
260
+ ```
261
+
262
+ An excluded item shows in `explain` and on the board as **excluded by policy**
263
+ with your reason, and does not make a destination `partial`. The defaults
264
+ exclude nothing; the shipped Codex model ladder maps the four Claude tiers
265
+ (`fable`, `opus`, `sonnet`, `haiku`) onto Codex models and can be overridden.
266
+
267
+ ## Which clients
268
+
269
+ | client | source | destination | notes |
270
+ |---|---|---|---|
271
+ | Claude Code | yes | yes | the bundle's own dialect; hooks and matchers need no translation |
272
+ | Codex CLI | yes | yes | hooks pre-trusted with Codex's own trust hash (self-tested against a value Codex wrote); SSE servers dropped |
273
+ | Antigravity CLI (`agy`) | no | yes | hooks under one owned key in `hooks.json`; shares `GEMINI.md` with Gemini CLI; no permission surface |
274
+ | Gemini CLI | no | yes | kept coherent with agy; no subagent surface |
275
+ | Grok CLI | no | no | no adapter yet; reported `unsupported`, never guessed |
276
+
277
+ Paths honour `CLAUDE_CONFIG_DIR`, `CODEX_HOME` and `GEMINI_CONFIG_DIR`, the
278
+ same variables the trust and account layers honour.
279
+
280
+ ## Leg and Agnostic AI
281
+
282
+ The capture → neutral bundle → apply engine is the
283
+ [Agnostic AI](https://github.com/ucsandman/Agnostic-AI) port engine (MIT).
284
+ Leg embeds it as a library, byte for byte, under
285
+ `src/harness/vendor/agnostic-ai/`, and owns everything around it: consent,
286
+ policy, the client registry, where state lives, the fingerprint, the evidence
287
+ trail and the hand-off decision (`src/harness/*.mjs`).
288
+
289
+ Drift between the two is refused, not managed:
290
+
291
+ - `src/harness/vendor/agnostic-ai/UPSTREAM.json` records the upstream commit
292
+ and the sha256 of every vendored file. `npm test` runs
293
+ `scripts/sync-harness-engine.mjs --check`, which fails on any local edit
294
+ under `vendor/`.
295
+ - A fix to the engine lands upstream first, then
296
+ `node scripts/sync-harness-engine.mjs <path-to-agnostic-ai>` copies it in
297
+ and re-records the hashes. `--diff <path>` lists what a sync would change.
298
+ - Upstream exposes the library through `engine/harness/index.cjs` with
299
+ everything a host may own as an option (`configure({ brand, secretPatterns,
300
+ shimPath, importRoots })`, an injected registry and policy), and its own
301
+ regression suite proves that boundary. Node's ESM loader imports that
302
+ CommonJS entry directly, so there is no build step and no dependency.
303
+
304
+ Attribution is in `NOTICE`; the vendored copy keeps its MIT licence text and
305
+ is not relicensed.
306
+
307
+ ## Limits
308
+
309
+ - Grok has no adapter.
310
+ - Cursor, Windsurf and the other clients upstream supports are not in Leg's
311
+ registry, because Leg does not launch them.
312
+ - The Claude Code target imports the ported rules from `~/.claude/CLAUDE.md`
313
+ with one `@` line; a source that is itself Codex therefore lands as
314
+ `~/.claude/leg-rules.md`, not inline.
315
+ - A repository-level harness (project skills, project MCP) is not carried;
316
+ only the global one is.
317
+ - Persistent configuration is the only mechanism: none of the four clients
318
+ offers a per-session overlay for rules, hooks, skills and MCP together, so
319
+ the managed-region and ownership discipline above is what keeps writes safe.
@@ -1,5 +1,5 @@
1
1
  {
2
- "_comment": "The single source of truth for every dated, numbered claim Baton makes in public. scripts/check-claims.mjs asserts that each surface listed here states exactly these values, and --strict additionally compares the pinned CLI versions against what those CLIs currently publish. Re-verify on a real machine, then edit this file and every surface in the same commit.",
2
+ "_comment": "The single source of truth for every dated, numbered claim Leg makes in public. scripts/check-claims.mjs asserts that each surface listed here states exactly these values, and --strict additionally compares the pinned CLI versions against what those CLIs currently publish. Re-verify on a real machine, then edit this file and every surface in the same commit.",
3
3
  "verifiedOn": "2026-09-11",
4
4
  "verifiedOnLong": "11 September 2026",
5
5
  "tests": 473,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ucsandman/legcli",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Usage-limit monitor and automatic handoff for Claude Code, Codex, agy and Grok. Type leg claude|codex|agy|grok and get the same interactive agent with a board alongside, auto-approve on by default, usage tracking per agent and account, a live context handoff bundle, and at the limit the next agent continuing in the same terminal. $79 once, 30-day money-back guarantee.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -49,10 +49,14 @@
49
49
  "node": ">=22"
50
50
  },
51
51
  "scripts": {
52
- "start": "node --env-file-if-exists=.env bin/leg.mjs up",
53
- "test": "node --test --test-concurrency=4 && node scripts/privacy-check.mjs && node scripts/check-claims.mjs",
52
+ "test": "node --test --test-concurrency=4 && node scripts/privacy-check.mjs && node scripts/check-branding.mjs && node scripts/check-claims.mjs && node scripts/sync-leg-agents.mjs --check && node scripts/sync-harness-engine.mjs --check",
53
+ "sync-alias": "node scripts/sync-leg-agents.mjs",
54
+ "harness:engine-check": "node scripts/sync-harness-engine.mjs --check",
55
+ "harness:engine-sync": "node scripts/sync-harness-engine.mjs",
56
+ "version": "node scripts/sync-leg-agents.mjs",
54
57
  "lint": "eslint .",
55
58
  "privacy": "node scripts/privacy-check.mjs",
59
+ "branding": "node scripts/check-branding.mjs",
56
60
  "claims": "node scripts/check-claims.mjs",
57
61
  "claims:drift": "node scripts/check-claims.mjs --strict",
58
62
  "docs": "node scripts/build-docs-site.mjs",
@@ -58,12 +58,19 @@ const PAGES = [
58
58
  title: 'Leg configuration and environment variables',
59
59
  description: 'Every Leg environment variable, the config file, network exposure, the token seam and the folder-trust switch.',
60
60
  },
61
+ {
62
+ slug: 'harness',
63
+ nav: 'Harness',
64
+ source: 'docs/harness.md',
65
+ title: 'The portable harness: carry rules, hooks, skills and MCP servers between agents',
66
+ description: 'How Leg carries the source agent\'s working environment to the agent a handoff lands on, what moves and what does not, the policies, ownership and backups, and how secrets are handled.',
67
+ },
61
68
  {
62
69
  slug: 'cli-contracts',
63
70
  nav: 'What it reads',
64
71
  source: 'docs/cli-contracts.md',
65
72
  title: 'What Leg reads from Claude Code, Codex and agy',
66
- description: 'The exact usage endpoints, hooks, log lines and limit strings Baton reads from Claude Code, Codex and agy, each cited to its source.',
73
+ description: 'The exact usage endpoints, hooks, log lines and limit strings Leg reads from Claude Code, Codex and agy, each cited to its source.',
67
74
  },
68
75
  {
69
76
  slug: 'adapters',
@@ -176,14 +183,14 @@ function page ({ slug, title, description, body, headings }) {
176
183
  <head>
177
184
  <meta charset="utf-8">
178
185
  <meta name="viewport" content="width=device-width, initial-scale=1">
179
- <title>${escapeHtml(/legcli/i.test(title) ? title : `${title} | LegCli`)}</title>
186
+ <title>${escapeHtml(/legcli/i.test(title) ? title : `${title} | Leg`)}</title>
180
187
  <meta name="description" content="${escapeHtml(description)}">
181
188
  <link rel="canonical" href="${url}">
182
189
  <meta name="theme-color" content="#0E1012">
183
190
  <link rel="icon" href="/favicon.svg" type="image/svg+xml">
184
191
  <meta property="og:type" content="article">
185
192
  <meta property="og:url" content="${url}">
186
- <meta property="og:site_name" content="LegCli">
193
+ <meta property="og:site_name" content="Leg">
187
194
  <meta property="og:title" content="${escapeHtml(title)}">
188
195
  <meta property="og:image" content="${ORIGIN}/og.png?v=3">
189
196
  <meta property="og:image:secure_url" content="${ORIGIN}/og.png?v=3">
@@ -202,7 +209,7 @@ function page ({ slug, title, description, body, headings }) {
202
209
  <a class="skip" href="#doc">Skip to the document</a>
203
210
  <header class="docs-top">
204
211
  <div class="docs-top-wrap">
205
- <a class="docs-home" href="/">LegCli</a>
212
+ <a class="docs-home" href="/">Leg</a>
206
213
  <nav aria-label="Site"><a href="/docs">Docs</a> <a href="/#pricing">Pricing</a> <a href="/license">License</a></nav>
207
214
  </div>
208
215
  </header>
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env node
2
+ // check-branding — refuses user-facing copy that still calls the product
3
+ // Baton, LegCLI, or "Leg CLI". Compatibility identifiers (BATON_*, .baton
4
+ // paths, legacy license prefixes) and historical changelog entries are
5
+ // allowed via the lists below. Runs in `npm test` and the commit hook
6
+ // (`--staged`). Exit 1 on any hit.
7
+ import { readdirSync, readFileSync, statSync } from 'node:fs'
8
+ import { execFileSync } from 'node:child_process'
9
+ import { join, relative, resolve, dirname } from 'node:path'
10
+ import { fileURLToPath } from 'node:url'
11
+
12
+ const SELF = fileURLToPath(import.meta.url)
13
+ const ROOT = resolve(dirname(SELF), '..')
14
+
15
+ // Forbidden in product surfaces. Domain/package `legcli` is lowercase and
16
+ // is not matched. Add a file to ALLOW_FILES or a line pattern to ALLOW_LINE
17
+ // rather than deleting a pattern.
18
+ const PATTERNS = [
19
+ ['Baton', /\bBaton\b/],
20
+ ['LegCLI', /LegCLI/],
21
+ ['LegCli', /LegCli/],
22
+ ['Leg CLI', /Leg CLI/],
23
+ ]
24
+
25
+ const SKIP_DIRS = new Set([
26
+ 'node_modules', '.git', '.supergoal', 'fixtures', 'marketing',
27
+ '.leg', '.leg-worktrees', '.baton', '.baton-worktrees', '.context-handoffs',
28
+ '.playwright-cli', '.design',
29
+ ])
30
+ let GITIGNORED = new Set()
31
+ try {
32
+ GITIGNORED = new Set(readFileSync(resolve(ROOT, '.gitignore'), 'utf8').split(/\r?\n/)
33
+ .map((l) => l.trim().replace(/\/$/, ''))
34
+ .filter((l) => l && !l.startsWith('#') && !/[*?[]/.test(l)))
35
+ } catch {}
36
+ const BINARY_EXT = /\.(png|jpg|jpeg|gif|webp|ico|woff2?|ttf|pdf|zip|gz|tgz|mp4|mp3)$/i
37
+
38
+ // Whole files that may still name the old product: history, migration tests,
39
+ // this checker. Keep this list short.
40
+ const ALLOW_FILES = new Set([
41
+ 'CHANGELOG.md',
42
+ 'site/docs/changelog.html',
43
+ 'docs/DEVIATIONS.md',
44
+ 'test/resume.test.mjs',
45
+ 'scripts/check-branding.mjs',
46
+ ].map((p) => p.replace(/\\/g, '/')))
47
+
48
+ // A line is allowed when it is only documenting a compatibility identifier,
49
+ // not presenting the old name as the product.
50
+ const ALLOW_LINE = [
51
+ /\bBATON_[A-Z0-9_]+\b/,
52
+ /\bBATON-/,
53
+ /\.baton\b/,
54
+ /events-baton/,
55
+ /baton\.pid/,
56
+ /baton_simulated/,
57
+ /baton-lock/,
58
+ /baton_(price|site|personal|team)/,
59
+ /type['":\s]+baton/,
60
+ /\bbaton\//,
61
+ /CustomEvent\('baton:/,
62
+ /addEventListener\('baton:/,
63
+ /classList\.contains\('baton'\)/,
64
+ /\blegacy\b/i,
65
+ /\bLEGACY_/,
66
+ /or a baton/,
67
+ /older Baton/,
68
+ ]
69
+
70
+ function posix(p) { return p.replace(/\\/g, '/') }
71
+
72
+ function walk(dir, out) {
73
+ for (const name of readdirSync(dir)) {
74
+ if (SKIP_DIRS.has(name) || GITIGNORED.has(name) || name.includes('\uf03a')) continue
75
+ const full = join(dir, name)
76
+ const st = statSync(full)
77
+ if (st.isDirectory()) walk(full, out)
78
+ else if (!BINARY_EXT.test(name) && full !== SELF) out.push(full)
79
+ }
80
+ return out
81
+ }
82
+
83
+ function stagedFiles() {
84
+ const out = execFileSync('git', ['diff', '--cached', '--name-only', '--diff-filter=ACMR'],
85
+ { cwd: ROOT, encoding: 'utf8' })
86
+ return out.split(/\r?\n/).filter(Boolean).map((p) => join(ROOT, p))
87
+ .filter((p) => p !== SELF)
88
+ .filter((p) => { try { return statSync(p).isFile() && !BINARY_EXT.test(p) } catch { return false } })
89
+ }
90
+
91
+ function allowedFile(file) {
92
+ return ALLOW_FILES.has(posix(relative(ROOT, file)))
93
+ }
94
+
95
+ function allowedLine(line) {
96
+ return ALLOW_LINE.some((re) => re.test(line))
97
+ }
98
+
99
+ const staged = process.argv.includes('--staged')
100
+ const files = staged ? stagedFiles() : walk(ROOT, [])
101
+ let hits = 0
102
+ for (const file of files) {
103
+ if (allowedFile(file)) continue
104
+ let text
105
+ try { text = readFileSync(file, 'utf8') } catch { continue }
106
+ const lines = text.split('\n')
107
+ for (let i = 0; i < lines.length; i++) {
108
+ if (allowedLine(lines[i])) continue
109
+ for (const [label, re] of PATTERNS) {
110
+ if (re.test(lines[i])) {
111
+ hits += 1
112
+ process.stdout.write(`${posix(relative(ROOT, file))}:${i + 1}: ${label}\n`)
113
+ }
114
+ }
115
+ }
116
+ }
117
+ process.stdout.write(`check-branding: hits=${hits} files=${files.length}${staged ? ' (staged)' : ''}\n`)
118
+ process.exit(hits ? 1 : 0)
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // Every dated or numbered claim Baton makes in public, checked against one
2
+ // Every dated or numbered claim Leg makes in public, checked against one
3
3
  // file. Two separate jobs:
4
4
  //
5
5
  // node scripts/check-claims.mjs consistency, offline, runs in CI
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // Sign a Baton license key by hand (the site's /api/key does the same for
2
+ // Sign a Leg license key by hand (the site's /api/key does the same for
3
3
  // paid orders). Needs BATON_LICENSE_PRIVATE_KEY in the environment:
4
4
  // node --env-file=.env scripts/license-sign.mjs --plan personal --email you@example.com
5
5
  // node --env-file=.env scripts/license-sign.mjs --plan team --email ops@acme.com --seats 5 --months 1
@@ -15,7 +15,7 @@ const rows = SIGNALS.map((s) =>
15
15
  `| ${s.id} | ${s.adapter} | ${s.classification} | ${s.where} | **${s.source}** | ${cell(s.text).slice(0, 90)} | ${cell(s.produced_by)} |`)
16
16
  const counts = SIGNALS.reduce((m, s) => { m[s.source] = (m[s.source] ?? 0) + 1; return m }, {})
17
17
  const table = [
18
- `Generated by \`node scripts/limits-table.mjs\` from ${SIGNALS.length} fixtures (${counts['observed-live'] ?? 0} observed-live, ${counts['docs-only'] ?? 0} docs-only). Classification \`limit\` hands the card to the next agent as a usage limit; \`auth\` is a failed launch (never a limit); \`launch\` is a failed launch that the next agent may still try; \`budget\` is a turn or spend cap set by Baton itself; \`info\` must never classify as a limit.`,
18
+ `Generated by \`node scripts/limits-table.mjs\` from ${SIGNALS.length} fixtures (${counts['observed-live'] ?? 0} observed-live, ${counts['docs-only'] ?? 0} 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.`,
19
19
  '',
20
20
  '| id | adapter | class | where | source | text (excerpt) | produced by |',
21
21
  '|----|---------|-------|-------|--------|----------------|-------------|',
@@ -11,7 +11,7 @@ import { join, dirname, resolve } from 'node:path'
11
11
  import { fileURLToPath } from 'node:url'
12
12
 
13
13
  const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
14
- const LIVE = process.env.BATON_LIVE_DIR || join(ROOT, 'fixtures', 'live')
14
+ const LIVE = process.env.BATON_LIVE_DIR || process.env.LEG_LIVE_DIR || join(ROOT, 'fixtures', 'live')
15
15
  const DOCS = ['docs/adapters.md', 'docs/cli-contracts.md'].map((p) => join(ROOT, p))
16
16
  const MARK = /\*\*(docs-only|observed-live(?: [0-9-]+)?)\*\* <!-- live:([a-z]+)\/([a-z_-]+) -->/g
17
17