Skip to content

Releases: ucsandman/LegCli

Leg 0.16.1: Three things that nagged every session, and the sharecard

Choose a tag to compare

@github-actions github-actions released this 20 Sep 00:33

Three things that nagged every session, and the sharecard.

  • The terminal no longer claims "no board is reading claude usage" while
    one is.
    A 429 from the usage endpoint backs the board's poller off for up
    to ten minutes; the terminal read the stale record as "no board", said so,
    and started its own reads, one more request a minute at an endpoint already
    refusing. The poller now writes next_poll_at on every attempt, a refusal
    included, and the terminal stands down while that promise is not overdue
    (boardIsPolling, src/usage.mjs). A board that is genuinely gone still
    gets the old line and the old fallback.
  • leg claude opens the board only when nobody is looking at it.
    /api/health reports viewers, the count of browser tabs on the event
    stream; a second terminal on a board that is already on screen adds no tab.
    A guarded board (share on) says no count and opens as before.
  • A second claude login fits its row on the capacity strip. The name
    column was a fixed 7ch, and "claude/work" ran into its own track. The
    column is now as wide as the longest login on the strip, set by strip.js,
    so every row's track still starts flush.
  • The sharecard reaches X. robots.txt said Disallow: /og to keep the
    card's source page out of the index, and that prefix also blocked
    /og.png, so X drew a bare link for four days. The line is /og$ with an
    explicit Allow: /og.png; the card's foot no longer overflows the frame
    and the image URL is ?v=5.

Leg 0.16.0: The board reads as one product: a polish pass inside the 2026-09-15 direction, a new…

Choose a tag to compare

@github-actions github-actions released this 19 Sep 07:06

The board reads as one product: a polish pass inside the 2026-09-15
direction, a new mark, and a first-run screen.

  • One colour rule, kept everywhere. Login names in the capacity strip,
    the rung in a terminal row's register and the rungs in the ladder editor are
    text beside an identity dot; the colour no longer lands on the word. A
    warning sentence is amber only on a row that is waiting on you. The accent
    blue appears on Land when it can run and on Start once a task is typed, and
    nowhere else. The capacity strip is an aligned table (dot, name, track,
    figure) instead of a ragged sentence.
  • Terminal rows. The prompt is clamped to two lines (Details holds the
    rest), the clock sits over one quiet row of buttons, a Land that cannot run
    is not drawn (its reason is said once above the panel), and End is quiet at
    rest. The register moved up to 15px and the scaffolding floor from 13 to
    14px. A row whose state changed since the last render is lit for 1.4 s, the
    one motion the rows own; rows carry a faint hover. Narrow rows lost 264px of
    empty height each: a flex-basis: 24ch meant for the row direction had been
    applied as height once the row stacked. The 420px horizontal overflow is
    gone.
  • One control language. The background entry is the terminals panel's
    footer, its choices quiet chips inside the sentence. Ledger cells share one
    anatomy with their verbs (View, Browse, New card) on one baseline; Settings
    is a row in the same grammar; disclosures carry a CSS chevron instead of a
    typed >; the Floor and Board links are buttons.
  • The verdict is a link. When the sentence names a terminal, pressing it
    scrolls to that row and focuses its prompt.
  • First run. A board with no terminal shows one lit panel: leg claude,
    leg codex and leg agy each with a Copy, and one line on what appears
    with the first turn.
  • The floor. An empty station is one line, heading beside sentence, so
    seven empty stations are seven lines rather than seven screens.
  • The mark. A tile with the L cut as hip, knee and foot replaces the 🦿
    emoji on the board, the floor, the favicon, the tab badge and the site.
    The emoji rendered as whichever system emoji font the machine had.
  • Docs. Nine screenshots retaken against this build; DESIGN.md and the
    board guide describe the new row and strip; the OG image carries the mark.

Leg 0.15.1: Your own Claude Code status line comes back

Choose a tag to compare

@ucsandman ucsandman released this 19 Sep 06:01

Your own Claude Code status line comes back.

  • Your status line runs first. leg claude passes a statusLine through
    --settings, and that key replaces yours rather than merging. Leg has
    always read your statusLine command, but it only ever used its padding;
    the command itself never ran, so on a build that honours a --settings
    status line you saw Leg's row instead of your own. The command is now kept
    on the session record and Leg's status-line hook runs it with the same JSON
    Claude Code sends, printing its rows above Leg's one line. Reported by a
    baton user on Discord. Claude Code 2.1.278 on Windows still shows its
    built-in status line in place of any custom one passed this way, so the 5h
    and 7d numbers keep coming from the usage endpoint (docs/faq.md).

Leg 0.15.0: A second login keeps the conversation, and the runtime does less work per minute

Choose a tag to compare

@ucsandman ucsandman released this 19 Sep 06:01

A second login keeps the conversation, and the runtime does less work per
minute.

  • A hand-off to your other claude login keeps the conversation. leg accounts add claude <name> now junctions Claude Code's projects store
    into the account beside the harness directories, so the second login sees
    the same conversations and the same auto-memory as the first. At a weekly
    or Fable wall the terminal moves to that login with claude --resume <id>
    under its CLAUDE_CONFIG_DIR and no bundle prompt, and the timeline says
    claude/fable → claude/work (kept the conversation). One rule decides it
    for the terminal and for the board's picker (keepsConversation in
    src/usage.mjs): a same-login downshift, or another login whose home can
    see the transcript file, checked at the switch. An account made by an
    older Leg gets the junction the next time it starts. leg history lists a
    junctioned store once. codex still takes the bundle across logins.
  • leg digest: what happened while you were away. The default window is
    8 hours (--since 2d, --since 30m, an ISO time; --json; GET /api/digest?since= on the board, owner only). The first line is the volume
    it was read from, then what needs you (a live terminal waiting on a
    question, a card parked for a human, a failed card, a lost terminal, in
    that order), then one block per repository with every terminal, card and
    landing that moved and the events worth a line, and last the walls
    standing now. Read only, over the records already on disk; a window with
    nothing in it says so with its counts.
  • The runtime does less work per minute, measured before and after. A
    profiler (fs and child_process counters plus --cpu-prof) sat on every
    path first; the numbers are medians on one Windows 11 box, Node 24, with a
    fake agent, an isolated LEG_HOME and a board on 4800–4899, and every
    change carries a test that was seen failing without it.
    • leg --version and every other command loaded 68 modules before main()
      ran. bin/leg.mjs now imports each command group inside its own branch,
      src/limits.mjs loads its fixture tree on first use, and the launcher
      asks a new leaf (src/scheduler-status.mjs) whether the scheduler runs
      instead of pulling in the orchestrator. leg --version: 127 → 52 ms
      wall, 79 → 0 ms CPU after Node's own boot, 254 → 4 fs calls, 68 → 1
      project module; leg sessions ls 116 → 75 ms; leg card ls 117 → 70 ms
      (test/cli-lazy.test.mjs).
    • An idle terminal spawned 59.5 git processes a minute (six per poll round)
      and blocked its own event loop 4.4–10.3 s of every minute doing it. One
      git status --porcelain=v2 --branch (src/git.mjs) now carries the
      head, the branch, the dirty list and the upstream's own ahead count:
      10.9 git processes a minute, 2.1 s of git wall time, terminal CPU 1.7 →
      0.1 s a minute (test/git-status.test.mjs, test/attach-perf.test.mjs).
    • The first leg <agent> of the day waited for the board it had just
      started (polling /api/health for about a second) before the agent got
      its first instruction. The agent starts at once and the wait runs behind
      it; a session that exits within a second still claims the board's
      pidfile before it goes. Time to the agent on that launch: 1,220 → 261 ms.
    • The two-minute bundle checkpoint ran the python CLI synchronously inside
      the poll tick, freezing limit detection and every board button for as
      long as it took (up to its 120 s timeout). It runs beside the tick now,
      one at a time, and the hand-off save waits for a checkpoint still
      writing.
    • installedAgents() ran <agent> --version for every agent on PATH at
      every launch (an 8 s budget each); the answer is kept for a day in
      $LEG_HOME/installed.json, keyed by the resolved bin. A LEG_<AGENT>_BIN
      override is never cached.
    • An idle board with 43 terminals and one page open rebuilt the whole
      sessions view every 10 s whether or not a byte had changed: 7,294 fs
      calls, 8 git processes and 1.1 CPU seconds a minute for nothing. The
      health tick now asks the same stat fingerprint the watcher asks, runs the
      liveness pass itself (a runner that died moves no file), and pushes only
      when the answer changed; a card that starts or stops waiting on a human
      forces the one push the fingerprint cannot see. Idle: 7,294 → 4,824 fs
      calls, 8.2 → 0 git processes, 1.14 → 0.54 CPU seconds and 6 → 0 sessions
      pushes a minute (what remains is the liveness pass and the scheduler's
      own tick). One
      sessions view also read the usage files 143 times (once per rung per
      terminal, then again for capacity and the accounts panel) and probed three
      paths per terminal for a synthesis file; one reader per view and one
      readdir per checkout: /api/sessions 53 → 34 ms, 511 → 212 fs calls per
      answer, and each row drops seven runner-bookkeeping fields nothing on the
      board read (8% off every push; GET /api/sessions/<id> keeps the whole
      record).
    • A cold /api/worktrees ran up to forty git processes on the board's one
      event loop: 9–25 s in which no stylesheet, click or SSE frame was served.
      The list is gathered through execFile, four at a time, one refresh per
      query, and a caller that arrives while a refresh runs gets the last list.
      Worst /api/health during a cold call: 11.8 → 3.2 s; the rest is the
      history index's synchronous listHistory, still open. /api/trunk
      (polled every 2 s by the floor) is cached 15 s and cleared by any card
      change or landing: 27.5 → 0.5 ms. A terminal's detail drawer ran one git ls-files per file it touched every 3 s; one process for the list: 1,247 →
      305 ms.
    • updateSession takes the lock budget run.json already had (250 tries, a
      10 s steal) instead of running unlocked after 1.2 s; control.json is
      cleared under its lock at exit; src/handoff.mjs takes scrub from
      src/redact.mjs instead of the card runner; the 12-leg stop names the
      bundle and leg resume instead of promising a hand-off a fresh launch
      never performed.
  • The README's hand-off step 3 said one bundle per leg. The code has
    written one bundle per session, updated in place, since 0.2.0; the sentence
    now says so. docs/concepts.md and the src/sessions.mjs header also said
    the runner was the only writer of session.json; the usage poller, the
    claude hooks and a board action patch it too, under one lock, and both now
    say that.

Leg 0.14.0: The 14-day trial is back

Choose a tag to compare

@ucsandman ucsandman released this 19 Sep 06:01

The 14-day trial is back.

  • Free for 14 days, no key, no card. The first leg <agent> on a machine
    starts a 14-day trial with every gate open, Team's leg share included, and
    records it in $LEG_HOME/trial.json. leg license status shows the days
    left without starting the clock. After the trial, or with a refused key once
    it is over, a session exits 4 with the price and the 30-day guarantee, which
    stays as the second net after buying. Deleting the file resets the clock,
    which is the known limit of an offline trial and is accepted. The license
    agreement gains the trial clause; the site, README, support page and
    llms.txt say "free for 14 days" where they said "no trial".
  • The README stops calling the repository private. It has been public at
    github.com/ucsandman/legcli; two sentences still said otherwise.

Leg 0.13.1: A one-line fix in the terminal and the marketing site brought up to what 0.13.0 does

Choose a tag to compare

@ucsandman ucsandman released this 19 Sep 06:01

A one-line fix in the terminal and the marketing site brought up to what 0.13.0 does.

  • The terminal's opening line names rungs. next: claude/opus → claude/sonnet → codex → agy, through the same rungLabel every other
    surface uses; it read next: claude → claude → codex → agy on 0.12.0.
  • The site says what is underneath the handoff. legcli.com is reorganised
    around the product as it is now: the sample transcript shows a Fable wall
    answered by Opus keeping the conversation and a login wall answered by Codex
    from the bundle; new sections for the ladder, background cards (End as a
    card, Run in the background, Take over), leg history, the portable
    harness and custom adapters in one compact group, and a Never spent
    without you
    column beside what is never touched. The structured data's
    softwareVersion had been stuck at 0.8.0; the OG image gains Grok; the FAQ
    no longer says a shared board has no TLS.

Leg 0.13.0: The first day on 0.12.0 with three real terminals found five things

Choose a tag to compare

@ucsandman ucsandman released this 19 Sep 06:01

The first day on 0.12.0 with three real terminals found five things. Usage
polling moves out of the terminals into the board, so the endpoint stops
answering 429 and the timeline stops repeating it. Every rung of a ladder can
now name a model from a catalog Leg reads off the installed CLIs, and the new
card form is rebuilt around that. The board holds still under a reader with a
row expanded. The floor is a page you can start work from. And a page whose
files are newer than the process serving them says so.

  • One usage poll per login, in the board. Every claude terminal used to
    ask Claude's usage endpoint once a minute on its own; three terminals plus
    Claude Code's own polling meant a 429 every other minute, and the terminal's
    timeline logged claude usage unavailable: usage endpoint 429: {...} each
    time the answer flipped. The board process now polls each login once per
    LEG_USAGE_POLL_MS (60s), backs off to ten minutes on any refusal and snaps
    back on the first good answer, and writes the two windows onto every active
    session of that login. A refusal is recorded once on the usage record
    (error, error_since, owner only) with one status line, claude usage unavailable since 9:03 AM: usage endpoint 429: rate_limit_error, and one
    claude usage is back; it never erases the measured buckets. Terminals
    poll nothing, so a terminal started before this release keeps its old
    minute-by-minute poll until it is restarted.
  • A model catalog, read from the CLIs you have. GET /api/models lists
    what each provider can run today: claude's aliases (fable, opus, sonnet,
    haiku); codex's models_cache.json entries with visibility: list plus the
    default from config.toml (gpt-6-astra, gpt-5.6-sol, gpt-5.6-terra,
    gpt-5.6-luna, gpt-5.5 on the machine this shipped from); agy models and
    grok models, each run at most once an hour behind the answer and cached
    under <LEG_HOME>/models/. A rung's model is validated by shape for every
    provider and by membership for claude only, so next month's codex model is
    not refused and a saved ladder is never silently reset to the default.
  • The new card form, rebuilt. Task first, then the repository as a picker
    of known repos or a typed path, the branch to cut from, and Who runs it:
    one row per rung with provider, model, permissions, ask before start and max
    turns, reorderable, prefilled from your ladder, with Save as my default
    ladder
    writing it back to Settings. Advanced holds the workflow, scripted
    first agent, leases, merge method, test command and title. The dialog posts
    its chain as one object per rung, so claude/fable then claude/opus is
    finally two legs and not one. Two columns at 900px, one below; it scrolls on
    a short window instead of hiding Create card below the fold.
  • The one-line entry row is never empty. It derives its rungs from
    handoff_order when a preferences file predates ladders, the ladder noun
    opens both the rung and the model selects, and with only metered providers
    and spending off it says so instead of "no agent is configured".
  • The board holds still. With a row expanded, a text selection live, the
    pointer on a row or focus in the list, the needs-you re-sort is computed but
    not drawn until you come out (hover and focus release after 30s on their
    own). The expansion is never detached from the document, so its selection,
    scroll offsets and open confirm row survive every 3-second poll; the
    region's top is anchored against rows above it changing height; the
    timeline appends new lines above you without moving what you read, and a
    status line repeated word for word within a minute folds into one with
    ×N. scripts/board-jump-probe.mjs is the regression harness: it printed a
    205px drift on 0.12.0 and 0px now.
  • The floor is a page you start work from. The four login panels move
    behind the same Capacity and models disclosure as on the board, under
    the same one-line strip; the Run in the background entry sits under it
    and posts exactly what the board's does; Running, Waiting on you, Queued,
    Backlog and Done today are Background-style rows with counts in their
    headings, a queued row says its position and what it waits for, and j/k and
    Enter work as on the board. More settings carries the typed task to the
    board's dialog through /#new-card=. The strip and the entry row live once,
    in src/board/strip.js and src/board/entry.js, loaded by both pages.
  • A page newer than its process says so. /api/health answers with the
    process version; the page files carry theirs. When they differ the board and
    the floor print This board process runs leg 0.10.0 and the page files are 0.13.0. Restart it to match: leg down && leg up, which is the state a board
    left running across an npm i -g lands in, and the state that hid every
    0.12.0 feature on the first morning.

Leg 0.12.0: The board is rebuilt around the two questions you actually open it to answer: which…

Choose a tag to compare

@ucsandman ucsandman released this 19 Sep 06:01

The board is rebuilt around the two questions you actually open it to answer:
which terminal needs you, and which model is about to run out. Usage stops
being a region and becomes one strip plus a property of every row; a hand-off
can now change the model instead of the CLI; and background work comes back as
rows you can read at a glance instead of a ledger you have to dig through.

  • The board top: a capacity strip, and the login panels behind one
    disclosure.
    Under the verdict, one strip prints each login's binding
    bucket rather than whichever window happened to be stored: claude 63% 6:01 PM, codex back Sat 10:11 PM, agy no figure, grok no reading. The
    four login panels are not rewritten, they move intact behind Capacity and
    models
    with every gauge, notch and aria-valuetext they had, and the
    claude panel head grows a model rail whose chips read fable 63% and
    opus 12%. Every branch of verdictLines() is now asserted under
    VERDICT_CH by test/board-verdict.test.mjs, so a 52px headline can no
    longer wrap to three lines. Measured on the seeded board at 1280: the first
    terminal row sits at 536px, where it used to sit at 1382px.
  • A terminal row says what it is doing, on which model. The register reads
    waiting on you baton on main dirty 3 ahead 2 claude/fable, and a quiet
    agent carries quiet 5m. ahead is one git rev-list --count on the
    existing poll; the model is the alias the leg resolved to, updated from the
    transcript for claude so a silent fallback off Fable becomes visible, and
    never a guessed default. Under the prompt the row prints its binding bucket,
    63% of the fable week, and past the warn threshold the rung that would keep
    the terminal.
  • A terminal waiting on a human says so, everywhere. Leg now wires Claude
    Code's Notification hook (permission_prompt, idle_prompt,
    agent_needs_input). The row's status word becomes waiting on you and it
    carries the question verbatim: waiting on you: permission to run Bash(git push origin HEAD), asked 2m ago. That sorts the row first, counts it in the
    region head (4 running, 2 waiting on you, 4 share the claude login) and in
    the tab, which becomes (2) Leg with a dotted favicon. Codex, agy and grok
    publish no such signal, so their rows say quiet Nm and never claim to be
    waiting. permission_prompt fires after about six seconds and mostly when
    you look away, so this is a reliable notice, not an instant one.
  • A hand-off can now change the model, not just the CLI. Destinations are
    rungs of (agent, account, model). The ladder's default is claude/fable,
    claude/opus, claude/sonnet, then each remaining installed agent, and a
    terminal's expansion prints it: now: claude / fable, then claude / opus, then claude / sonnet, then codex, then agy with first eligible now: claude / opus under it. A claude downshift with a known session id starts
    claude --resume <id> --model <alias> and skips the bundle, so the
    conversation survives the switch; every other rung is primed from the bundle
    as before. leg ladder prints and edits the same thing from a terminal
    (ls, set <n> <agent>[/<account>[/<model>]], rm <n>, spend on|off),
    and leg sessions handoff <id> --to claude/default/opus names a rung.
  • The wall is attributed to what it actually walled. A Fable limit walls
    Fable, not the login: walls{} is keyed by model and limited_until is left
    alone, so claude/sonnet keeps working. A session, weekly or spend limit is
    account scoped, and a same-login model rung is then refused as a wasted
    switch with the reason on the row. Wording Leg cannot parse walls the whole
    login and prints the evidence it failed on rather than guessing.
  • Nothing spends money unless you said it could. may_spend is off. An
    automatic hand-off skips any rung that bills credits or a metered balance and
    writes the reason to the ledger; the Settings sentence is A rung that spends
    usage credits or metered balance may be taken by an automatic hand-off
    , and
    today it adds Usage credits are off, so there is nothing to spend through
    the wall.
    rather than offering a dead control. A reserve per login holds a
    floor back from automatic hand-offs only: An automatic hand-off skips a rung
    past the floor; a hand-off you press yourself still takes it, and the picker
    says so.
    Climbing back is a radio with the rule printed under it: Leg never
    interrupts a running turn to climb.
  • Cards are terminals you are not sitting at. Live cards are rows in a
    Background panel directly under Terminals, in the terminal row's
    register with a measured work stat and the same button grid; a running card
    says no message until this leg ends, started 8:15 PM, because -p --output-format json is mute until the leg exits. Finished cards fall into
    one ledger line, 10 finished cards, 5 done, 5 failed, last 8:15 PM, so ten
    done cards are one row and not ten. Starting one is a single field,
    Run in the background:, over an inferred sentence whose nouns are buttons
    (in recruiting-tool on main, with claude/fable then claude/opus then
    claude/sonnet then codex then agy, build only
    ); the old thirteen-field
    dialog is still there as More settings.
  • Two verbs for leaving and coming back. The End confirm row grows
    End, and keep going as a card: it writes the bundle, hands the
    terminal's own worktree to a card with its lineage, and says where the work
    went. On a card, Take over pauses it and hands back the one command a
    browser cannot run, leg claude --resume-card <id>, in an interactive
    terminal primed from the card's bundle.
  • A time figure, or nothing, and never a time without its sample count.
    burn() reads a per-bucket history ring and prints a rate only with at least
    three samples spanning ten minutes inside the current window; the estimate is
    the endpoint slope, capped at the reset, and a flat or falling line prints
    nothing at all. Where it can speak, the row and the verdict say about 2h 40m
    of Fable left, from 9 samples over 4h
    . A reading that comes back unchanged
    more than ten minutes after the last sample is still recorded, so a flat hour
    is a measured zero rather than a starved gate.

Fixes:

  • Fixed: the only live Claude wall Leg ever captured was classified as no
    progress.
    The fixture pattern matched You've hit your Fable limit and
    Anthropic now writes You've reached your Fable limit, so a real wall on the
    headless path scored {outcome: "no_progress", signal: "none"} and the
    terminal sat on a dead login. The pattern takes both wordings, and
    fixtures/limits/claude/claude-fable-limit.json is a new observed-live
    fixture produced from the captured response rather than from the docs.
  • Fixed: the board printed the wrong percentage for a claude login. The tap
    kept only five_hour and seven_day out of the usage response and threw the
    rest away, so a login whose binding bucket was the Fable week was reported at
    the account-wide figure. The whole limits[] array is now read into
    buckets[], and the strip, the verdict and the row all print the bucket that
    will actually stop you.
  • Fixed: a seeded board's rows were live controls on real repositories.
    scripts/seed-wes-board.mjs named real paths, and a click on a seeded row
    cut a worktree in a real checkout. The seed now names C:\Projects-seed\...,
    realistic in shape and impossible in fact. docs/ERRORS.md carries the
    entry.

Migration:

  • preferences.json keeps handoff_order and now derives it from the ladder,
    so validHandoffOrder, requireHandoffOrder and every older terminal keep
    working unchanged. It gains handoff_ladder (rungs of {agent, account, model, when, cost}), may_spend (default false), climb_back (default
    next-handoff, or never), reserve ({agent: percent}),
    notify_terminal (default true) and notify_board (default false). A
    bare handoff_order expands into one model: null, when: always rung per
    agent, so behaviour is unchanged until you edit a rung.
  • Usage records under $LEG_HOME/usage/ gain buckets, walls, history,
    extra_usage and facts. An older Leg ignores them; a record without them
    falls back to the two windows it has always had.
  • Session records gain model, waiting and ahead, all nullable. On a
    shared board all three are dropped from someone else's row (waiting carries
    the verbatim question, model and ahead describe this machine's usage and
    someone else's work). Your own terminal is never redacted, so you keep all
    three on it whichever role you hold.
  • playwright is a dev dependency now, so scripts/board-shots.mjs runs from
    a fresh npm install. It is not shipped in the package.

Still assumed, and marked as such:

  • Whether codex resume <id> -m <model> composes. The resume subcommand and
    the -m flag are each verified from codex --help; putting them together is
    not. A codex rung therefore ships primed from the bundle, and only the claude
    rungs claim to keep the conversation.
  • Whether codex, agy and grok leave an OSC 2 terminal title alone once the
    child starts drawing. That is why naming the terminal tab is not in this
    release: the browser tab badge needs no such assumption, and ships.

Leg 0.11.0: grok is a card adapter, not just a terminal

Choose a tag to compare

@ucsandman ucsandman released this 19 Sep 06:01
  • grok is a card adapter, not just a terminal. --chain grok works. Its
    flags were read from grok --help on grok 1.0.34 (3736acbc8658) and its
    result envelope out of the shipped binary, both on 2026-09-17, replacing the
    guessed field names the unregistered adapter carried. The prompt now travels
    by --prompt-file (a hand-off prompt carries the whole bundle summary, and
    Windows caps a command line near 32k) and --cwd is passed explicitly,
    because grok can run against a shared leader process and a leg must edit its
    own worktree. The probe reached the account and came back with a real wall.
  • A grok terminal at the wall now hands off. An exhausted Grok Build
    account answers 402 Payment Required: Grok Build usage balance exhausted,
    and none of the rate-limit strings the tap looked for appear in it. The
    terminal sat on a dead login instead of moving on. Both the tap
    (src/taps/grok.mjs) and the leg classifier (a new observed-live fixture)
    now read it as a limit. Found by running the probe against an exhausted
    account on 2026-09-17.
  • Any CLI can be a card agent, from a JSON spec and no code. leg adapter template --name muse > muse.json, leg adapter add muse.json, then
    --chain muse,claude. leg adapter check muse prints the exact command line
    a leg would run before one does; leg adapter list|show|rm are the rest.
    Placeholders ({{prompt}}, {{promptFile}}, {{cwd}}, {{mode}},
    {{model}}, {{resume}}, {{maxTurns}}, {{runDir}}) fill the argv, and a
    group in the args is dropped whole when a placeholder inside it is unset, so
    an absent model never leaves a bare --model. The flags that turn a
    supervised agent into an unsupervised one are refused whatever a spec says.
    A broken spec is reported by name and reason, never thrown, so one typo
    cannot stop the board. See docs/adapters.md.
  • Hand off now can name its destination. The button still takes the next
    option in the order; Details → Hand off now to lists every destination
    with the reason a greyed one cannot be picked, and leg sessions handoff <id> --to codex is the same choice from a terminal. A destination that is
    not on the chain, not installed, or at its wall is refused at the click with
    the reset time. If the one you picked walls between the click and the
    hand-off, the work continues down the order and the terminal says which
    agent took it instead, rather than going somewhere else in silence.
  • TLS for a shared board. leg share on --tls-cert <file> --tls-key <file>,
    or LEG_TLS_CERT / LEG_TLS_KEY. Leg issues no certificate: a self-signed
    pair teaches everyone to click through a warning, which is worse than
    plaintext on a network that is already private, so it takes a pair you have
    (tailscale cert <machine>.<tailnet>.ts.net is one command). Half a pair, a
    missing file or an empty one stops the board with exit 3 rather than quietly
    serving plaintext. The loopback companion stays http, because the certificate
    is for the shared name and that traffic never leaves the machine.
  • An operator role, between owner and guest. An operator runs the
    pipeline board — adds, runs, approves, reassigns and kills cards — and their
    own terminals, and sees nothing that describes this machine: no settings, no
    home path, no repository paths, no conversation index, no audit. leg share add dana --role operator. What each role may reach is decided in one place
    (mayUseCards / mayUseMachine in src/share.mjs) instead of per endpoint.
  • An audit trail: who did what, across every terminal and every card.
    Settings → Audit trail on the board, GET /api/audit for a script, owner
    only. Hand-offs, landings, approvals, reassignments and kills, newest first,
    each with the person or agent that did it. It reads what the ledger already
    recorded and stores nothing new, and every answer carries how much it read
    (14 terminals and 3 cards, 812 events read) so an empty trail cannot be
    mistaken for a quiet week.
  • Fixed: a guest's own terminal leaked the owner's reset times. The new
    hand-off destination list carried the exact reset timestamp of every
    account, and a guest owns their own terminal, so it reached them through it.
    A guest and an operator now get the destinations and the coarse reason
    (at its usage limit) without the timestamp. Caught by the share-security
    suite in the same change.
  • Fixed: the board slowed down as soon as a custom adapter existed.
    /api/health asks every adapter where its binary is, and reading the spec
    directory on each of those calls put a readdir, a read and a JSON parse per
    spec on the event loop the terminals lane is pushed from. The parsed list is
    now cached against the directory's entries with a one-second floor, so a
    spec added or removed is still seen at once.

Leg 0.10.0: The board is responsive again, leg starts straight away, and the board opens every time

Choose a tag to compare

@ucsandman ucsandman released this 19 Sep 06:01
  • The board is responsive again, leg starts straight away, and the board
    opens every time.
    One running terminal was enough to saturate the board
    server: it rewrites its record every few seconds and takes a control lock
    about once a second, and the watcher over the sessions directory rebuilt the
    whole terminals view — over a second of git — for every one of those
    touches, including the lock files and the temp files atomic writes leave
    behind. The server spent all its time in that loop, so it answered a
    stylesheet in fourteen seconds and /api/health in seven. Four consequences,
    all fixed:

    • The watcher now drops locks and temp files by name, and checks the rest
      against a stat fingerprint of the files the view is actually built from,
      because a lock taken inside a session directory changes that directory's
      own mtime and arrives as an event naming nothing else. What survives is
      floored to one rebuild per interval. canLand is cached per record
      revision with staggered expiries, so twenty worktrees are never re-read in
      one pass. The terminals view went from 2,000 ms to 115 ms; on a board with
      66 sessions /api/health went from 14.2 s to 2.4 ms and /board.css from
      14.2 s to 1.5 ms.
    • leg claude treated a board too busy to answer a health probe as no board
      at all, spawned a second server that could only die of EADDRINUSE, then
      polled the dead child for fifteen seconds. A listener on the port is now a
      board: leg attaches to it.
    • The board is opened whether or not this terminal is the one that started
      it. Previously the browser was only opened on the path that spawned the
      server, so with a board already running nothing ever opened.
    • One push rebuilt the terminals grid three times (two listeners registered
      for leg:sessions and one for the legacy alias, with the payload parsed
      twice per push and one dispatch outside the staleness guard). Every button
      was destroyed and recreated several times a second. One push now rebuilds
      once, and an open confirm row is left alone until it is answered.
  • Remove, Remove record, End and Land work. Confirming any of them did
    nothing at all. confirmRow clears pendingConfirm and then calls its
    callback, and the callback read pendingConfirm.action — off the variable it
    had just cleared. Every Yes on the Terminals panel threw
    TypeError: Cannot read properties of null (reading 'action') into the
    console and never reached the request. The callback now closes over a
    snapshot taken before the row is built.

  • Every conversation on this machine (leg history, leg worktrees, the
    board's Conversations cell).
    One read-only index over the conversations
    Claude Code, Codex, Grok, Antigravity and Copilot CLI keep in their own
    stores, plus Leg's own sessions, deduplicated (a session Leg started and
    the same conversation in the agent's store are one row, marked leg; the
    rest are external). List, filter by agent, repository or text, open one
    (its last messages, read from the tail of the transcript only when asked),
    and leg history continue <id> to start a supervised leg on it where the
    installed CLI documents resume-by-id (claude, codex, grok, agy; copilot
    lists and reads but is not an agent Leg runs). Nothing in an agent's store
    is moved or written; Leg writes only ~/.leg/history/index.json, refreshed
    incrementally by size and mtime, and never opens a SQLite file. Every
    checkout Leg can see, git's and its own and the ones conversations ran in,
    with owner, uncommitted count, orphaned, stale and missing verdicts, read
    only. On a shared board the whole group is the owner's. Redaction learned
    the shapes another agent's transcript carries (Stripe, Google, xAI, npm,
    GitLab, Hugging Face keys, JWTs, private-key blocks, basic auth, URL
    credentials, password=), and the board token joined the values it strips.
    Support matrix and every file read: docs/history.md.

  • History, after its review. The board's Conversations cell pages past
    150 rows (the total no longer shrank with each cursor page); the board
    refreshes a stale index in a child process instead of its own event loop;
    a cwd on an unreachable network share no longer stalls a refresh; an
    Antigravity retitle or a touched presence lock is seen by an incremental
    refresh, and only that conversation is re-read; an older Leg session id
    still opens the conversation it was one leg of; leg history prints a
    failed refresh whatever the last index still lists, refresh fails cleanly
    when another Leg holds the lock, --limit 0 is a usage error (--all),
    --offset works, a Leg-only row's id prints whole; a malformed id on
    /api/history/<id> is a 400; an account name that is a path is ignored;
    titles are cut to 200 as documented and go through the held-value
    redaction too; the Copilot chip has a colour. Redaction no longer eats
    Basic authentication/authorization, cache_key = ... or
    refresh_token: string, and a value never crosses a line break.