Skip to content

Latest commit

 

History

History
1459 lines (1242 loc) · 81.1 KB

File metadata and controls

1459 lines (1242 loc) · 81.1 KB

Changelog

All notable changes to Katsuobushi are recorded here, newest first. The format follows Keep a Changelog; the project is versioned with Git tags following SemVer. While in 0.x, any release may break — consumer-facing breaking and behavioral changes are detailed in MIGRATING.md.

[Unreleased]

[0.5.1] — 2026-08-11

Closes the ways a dispatch could report success it had not achieved. A drive no longer returns before the agent's terminal report, an armed drive no longer waits forever when a turn ends unreported, and sandbox status no longer shows a stalled turn as making progress. The board stops travelling inside guest branches, and the landing procedure gains a guard that restores it and refuses to continue quietly on an unexpected failure. See MIGRATING.md.

Fixed

  • A dispatch drive returned before the agent's terminal report. The guest pushed TurnCompleted{reported:true} into out.messages ahead of the Report, so an armed host drive left its loop without ever seeing the terminal text — and therefore without journaling it to reports.ndjson. The conclusion survived only in work-state.json, which no skill named. The TurnCompleted push is now deferred until after the Report.
  • An armed drive waited forever when the auto-nudge never fired. Three faults compounded. The is_work_idle guard on GraceExpired deferred nudges until work went idle, and a turn whose carrying duration kept climbing never looked idle — so nudgeCount stayed at 0 against a budget of 5. The host drive then looped on heartbeats with no bound. Observed in practice: a turn ended unreported after eight minutes and the drive waited two hours with no timeout and no notice. Nudges now fire unconditionally on the grace timer, and an armed drive fails with a clear message once the guest's budget is provably exhausted.
  • sandbox status reported a stalled turn as active work. The LIVENESS and WORK columns counted elapsed time rather than activity, so a turn that had already ended read as healthy progress; only turn-state.json's frozen lastActivityAt was honest. render_liveness now reports ended unreported <duration> ago when phase is in-flight and ended_at is set.
  • A guest branch carried the board and merged it back silently. dispatch claims the card (a BOARD.md edit) and then seeds the guest from the working tree via git stash create, so every branch carried a dispatch-time board snapshot that merged back at landing without a conflict — putting a card in the wrong lane inside the commit that shipped its work. The seed is now filtered: board paths are restored to their HEAD state before the mirror push, so the guest's board never diverges from the host's.
  • The landing guard swallowed unexpected git checkout failures. The || true that makes the guest-added-file case work also hid every other failure, so a landing could appear to have guarded the board when it had not. Residue left staged under the board path with a status other than A now aborts the landing with an error on stderr.
  • The landing guard's abort could close the operator's shell. exit 1 in a block pasted into an interactive terminal ends that session. Both recipes are now wrapped in a subshell, so the abort exits only the subshell. git commit and jj git import sit inside it and remain gated by the guard.
  • sandbox status truncated a terminal report to its first line. The detail view now renders the full text, so a multi-line verdict survives a lost terminal.

Changed

  • Nudges fire unconditionally on the grace timer. Previously they waited for the work state to go idle, which meant a turn that ended unreported while a background build ran was never nudged at all. An agent can now be re-prompted while a build is still running — but only once its turn has already ended unreported, and the nudge is queued in the MCP stream and cannot stop the build.
  • The dispatch seed is a single dispatch seed commit. It replaces the WIP on … / index on … pair that git stash create produced, and its tree carries the board at HEAD rather than the orchestrator's uncommitted edits.
  • The guest contract forbids leaving background work running. An agent must finish or explicitly kill every job before it reports, must say which, and its report done must state that nothing was left running. Paired with a note that the guest /nix/store is a thin overlay on a large shared read-only base, so whole-store verification is never appropriate there.
  • project-orchestration gains guidance on not leading the reviewer. Five rules, a worked example pairing a leading directive with a neutral rewrite, and a gotchas entry. The prior advice to list "what should block" is reworded to ask questions rather than assert findings, because it invited the verdict it was meant to test.
  • The sandbox skill's journaling limit names both unjournaled exits. It documented only the killed-process case; a nudges-exhausted natural exit lands in the same place, and the skill now says so and names where to read the conclusion instead.

Removed

  • is_work_idle and the WorkStateIdle / WorkStateActive events. Dead after nudges stopped depending on the idle transition. The work-state coordinator, GuestMessage::WorkStateTransition, and work-state.json are unaffected.

[0.5.0] — 2026-08-10

Rebuilds how work moves between the parties that write, review, and integrate it. Nothing enters the owner's history until a peer review passes, landing creates a commit instead of rewriting one, a resumed instance works against the current tip, and moving a branch between instances is a command rather than a raw git push at a storage path. Also removes progressStallSecs and moves the sandbox fetch destination to refs/remotes/sandbox-guest/<inst>. See MIGRATING.md.

Added

  • sandbox deliver <instance> --branch <ref>. The opposite direction of sandbox fetch: pushes any host ref into a target instance's mirror as refs/heads/delivered/<basename>. A guest sees only its own host directory, so no instance can read another's mirror; this is the supported path between two parties. The delivered/ prefix cannot collide with the target guest's own sandbox/<instance> working branch, and the push is a force-push so re-delivery after new commits simply advances the ref. Inside the guest the mirror is origin, so the branch reads as origin/delivered/<basename>.
  • A resumed instance is refreshed to the current tip. On resuming a named instance, the orchestrator advances refs/heads/base in that instance's mirror and the guest rebases its own branch onto it. The guest rewrites only its own branch, in its own clone.
  • checks.sandbox-verb-coverage. A flake check that fails when the sandbox wrapper's subcommand list and the Rust SandboxCommand variants diverge, in either direction, naming the offending verb and both files. Two verbs had already drifted out of the wrapper when it was added.
  • sandbox deliver and sandbox prune are reachable from the sandbox command. Both existed in katsuctl but were missing from the wrapper's subcommand list, so sandbox prune answered Unknown subcommand.

Changed

  • Work enters the owner's history at ready, not at done. The report bridge no longer lands on done: it fetches, verifies real commits landed, and moves the card to needs-review. Landing is the step that moves a card to ready. Three consequences — the owner's history holds only reviewed work, a bounce costs no landing, and "has this landed" has an exact answer.
  • Landing creates one squash commit instead of duplicating and repairing. git merge --squash plus git commit, with the message taken from the card rather than from a commit on the branch. The commit is the owner's by construction, so nothing re-authors it afterwards. One card contributes one commit however many review rounds it took.
  • The board is orchestrator-only state, not host-only. The orchestrator is a role, not a machine; the skills no longer define it as "the host agent". The role runs on the host today and could later run in its own sandbox.
  • The dispatch guard is documented as a backstop. Under squash landing there are no agent-authored commits to re-attribute, so the guard has no routine case to catch. It still catches work landed by another route.
  • A reconciliation is recorded on the card. A branch is reviewed against the tip it was seeded from and integrated onto the current tip. Where the orchestrator had to choose something in a file the branch changed, the card's ## Review notes records what did not apply and whether the card returned to review.
  • sandbox fetch writes to refs/remotes/sandbox-guest/<inst>. The destination moved from refs/heads/sandbox/<inst> (written by 0.4.1) to refs/remotes/sandbox-guest/<inst>. Writing to refs/remotes/ means jj imports the ref as a remote bookmark, so a force-update can never rewrite local history regardless of what the host has built on top. Old refs/heads/sandbox/* refs are left behind and are safe to delete; see MIGRATING.md.

Removed

  • progressStallSecs is removed from lib.sandbox. The argument controlled a host-side "no reports for Ns" notice; both the knob and the notice are gone. Use the WORK column of sandbox status to tell a working guest from a stuck one: Active means the heartbeat is current, Active (Late) means heartbeats stopped, and Idle means the agent stopped without reporting. See MIGRATING.md.
  • Attribution repair is gone from the landing procedure. --reset-author, jj metaedit --update-author, and the rule that re-attribution had to happen before the next dispatch all existed to fix up duplicated commits. A squash landing creates a new commit, so there is nothing to repair and no deadline.

Fixed

  • sandbox fetch guard uses strict descent instead of containment. The

  • sandbox fetch guard uses strict descent instead of containment. The guard previously called git for-each-ref --contains=<old-tip> without comparing objectnames, so any refs/heads/sandbox/<inst> left by the prior fetch scheme (whose tip IS the old tip, not a descendant of it) was treated as evidence of a rebased host commit and the fetch was refused. Every instance that existed before this change migrated successfully on the first post-upgrade fetch and was then permanently un-refetchable from the second onward. The guard now fetches %(refname) %(objectname) and filters out refs whose objectname equals the old tip — migration debris — keeping only refs that strictly descend from it.

  • sandbox fetch error message names the actual recovery. The previous message said to use jj duplicate/git cherry-pick "rather than re-fetching", but re-fetching is the only way to obtain new guest commits. The message now names the offending refs, explains that a stale refs/heads/sandbox/<inst> pointing at pure guest history is safe to delete, and retains guidance for the genuine bad-rebase case.

  • git cherry-pick command in the sandbox skill corrected. The landing instructions used git cherry-pick <name>@sandbox-guest, which is jj notation and is rejected by git rev-parse. The correct git form is git cherry-pick sandbox-guest/<name>, which git resolves via disambiguation to refs/remotes/sandbox-guest/<name>.

  • README inventory brought back in line with the flake. The root inventory listed three of six skills and presented menu as a lib.* output when it ships through the overlay as pkgs.katsuobushi. The sandbox guide documented seven of ten verbs, showed a sandbox status example that predated the WORK column, and never mentioned the report journal (reports.ndjson) or the persisted directive (directive.md / sandbox prompt --redeliver). The project guide omitted project labels and the icebox.

[0.4.1] — 2026-08-06

A fix for one 0.4.0 regression: sandbox fetch's new ref layout was invisible to Jujutsu. See MIGRATING.md.

Fixed

  • sandbox fetch is visible to Jujutsu again. 0.4.0 landed the guest branch in refs/katsuobushi/<inst>, a namespace jj never imports (jj reads refs/heads/*, refs/tags/*, and refs/remotes/*), so on a colocated jj repo the fetched commits sat in the object database unreachable from every revset. The fetch now force-updates +sandbox/<inst>:refs/heads/sandbox/<inst>. Idempotency comes from the leading + — a refetch after a review bounce force-updates the branch rather than failing non-fast-forward — not from the ref's location, so the branch stays where jj can see it. The landed probe reads the same branch, and the success message again names the ref actually written.

[0.4.0] — 2026-08-06

Two design threads land together. The board grows a label-and-icebox model (PDD001): a label is an epic you read through a filter, an iced note is a card with no board entry, and the host is the board's single writer. On top of it come three authoring skills (PDD002): design writes a PDD, plan files its work as cards, and implement drives a sandbox swarm to ready. No spec or instance-state change (specVersion 4 / instanceVersion 2 unchanged) — the sandbox fetch change is host-local. Consumers with legacy design: fields run one project lint --fix; see MIGRATING.md.

Added

  • project status --label=<value> — read an epic as a filtered board view. The match is exact and whole-token. Repetition is AND, and the option composes with --lane and --available.
  • project labels — enumerate the label vocabulary with a card count per label. project labels --json maps each label to its card ids. Archived cards are excluded by default; --include-archived folds them in. Iced notes count.
  • The iceboxproject new --icebox writes a note with no board card, so a filer never collides with the host on BOARD.md. project status --icebox lists the iced notes (id, title, labels) and composes with --label. project status set <id> icebox | todo | cancelled shelves a card off the board, promotes an iced note to the front of To-do, or cancels one straight into the archive.
  • project new --label — the canonical, repeatable label option (the older --labels spelling stays as an alias).
  • Three authoring skillskatsuobushi:design, katsuobushi:plan, and katsuobushi:implement. design runs the PDD authoring ritual (grill, draft, Simplified Technical English, context-free read-through). plan files an accepted PDD's work as atomic cards. implement schedules a per-card implementor and reviewer sandbox to take a label thread to ready.

Changed

  • sandbox fetch lands into a per-instance tracking ref. The guest branch now fetches into refs/katsuobushi/<inst>, a ref the host never rebases, instead of the local sandbox/<inst> branch. A second fetch of an already-landed instance — every review bounce — is now a clean fast-forward rather than a non-fast-forward failure. The guest push target and the sandbox status mirror probe are unchanged.
  • project lint treats an orphan note as the icebox. A note with no board card was a warn: orphan-note; it is now an info icebox inventory line that never fails the gate.
  • The card face drops the design column. A freshly inited board's settings block no longer declares a design metadata-key; the value lives on a label.
  • The host-only board rule is stated as an invariant in the sandbox and project-orchestration skills and in .dispatch-instructions.md: a sandbox guest never writes project/kanban/, and findings return through the report channel.

Deprecated

  • project new --design <ref> — deprecated. It now warns and records the reference as a label. Use --label.

Removed

  • The design: note frontmatter field. Its meaning moves to a label. project lint --fix folds a legacy design: reference into the card's labels, drops the dead key, and drops the design settings column. The migration is idempotent.

[0.3.8] — 2026-08-05

Makes Nix-built Rust artifacts diagnosable — the groundwork for having agents start warm instead of cold-compiling a dependency closure before their first useful action. Two additions to lib.rust (an alignment manifest beside every deps bundle, and a checker that says whether cargo will actually reuse it), plus agent-facing guidance that steers dispatched agents at the project's own menu rather than at habitual cargo invocations. No spec or instance-state change (specVersion 4 / instanceVersion 2 unchanged). Consumers see one deps-bundle rebuild — see MIGRATING.md.

The motivating measurement, for context: seeding a cargo target directory from a crane deps bundle achieves nothing on its own — cargo discards the artifacts and rebuilds — unless the shell also resolves dependencies through the same vendored source replacement the bundle was built against. That is why alignment is worth a tool: every way of getting it wrong is silent, and looks exactly like an honest long build.

Added

  • Alignment manifests beside every deps bundle. lib.rust's deps-only derivations now write manifest.json next to target.tar.zst, recording what cargo folds into every unit hash: the rustc identity, target triple, profile, RUSTFLAGS / CARGO_ENCODED_RUSTFLAGS / CARGO_BUILD_RUSTFLAGS, a content hash of the workspace cargo config the build actually saw, and the vendored source directory. target.tar.zst itself is unchanged and crane ignores the extra file.
  • lib.rust.checkArtifactAlignmentkatsuobushi-check-artifact-alignment [--profile <name>] <manifest.json|bundle-dir>. Compares a bundle's manifest against the environment a live cargo would run under, reproducing cargo's own config resolution (.cargo/config.toml from the cwd upward plus $CARGO_HOME, nearest wins) to find the effective [source.crates-io] replace-with directory. Exit 0 aligned, 1 with the diverged field named and its consequence spelled out, 2 when the verdict is unknown (no manifest, a cargo config that cargo itself could not parse, or a broken check) — an unknown is never reported as a mismatch. Verified against ground truth: a green verdict corresponds to cargo reusing the closure (3 workspace units), a red one to a full rebuild (91 units).
  • A sourceInclude lint. lib.rust warns at evaluation when the project has a .cargo/config.toml that sourceInclude does not carry into the Nix builds. Those flags then apply to the dev shell but not to anything Nix builds, and cargo discards the mismatched artifacts without saying why. The default sourceInclude already includes .cargo; this catches a project that overrode it for an unrelated reason.

Changed

  • The guest contract points dispatched agents at the project's menu. Both the sandbox's guest README and the always-on agent contract now tell an agent to run nix develop -c menu before its first build/test/run command and to prefer the project's own commands — and, when the menu has nothing for the job, to use the raw tool and report the gap, so a missing menu command becomes board signal instead of a silent divergence. The contract is explicit that an agent session starts in a plain login shell, where menu/showMenu are not on PATH until you go through nix develop.
  • The project-orchestration and sandbox skills drop the cold-compile folklore. The reviewer-directive template no longer teaches "first build cold-compiles and takes minutes"; build/test commands are documented as belonging in the menu rather than in .dispatch-instructions.md; and the advice to raise progressStallSecs now asks first why the guest is compiling that long at all.

[0.3.7] — 2026-08-05

Takes the rebuild latency out of the multi-turn review loop and stops the orchestrator guessing at how big a sandbox VM is. Docs-only — the project-orchestration skill; no tooling, spec, or instance-state change (specVersion 4 / instanceVersion 2 unchanged). See MIGRATING.md.

Changed

  • A card's sandboxes stay warm until it is ready. The skill previously read as though a VM were spent once its branch landed, so orchestrators carried the sandbox skill's sandbox stop --remove <name> habit — correct when work is accepted — into needs-review, where it isn't. Every round of reviewer feedback then paid a full cold compile in a fresh instance before it could change a line, which in a project with a 20-minute build is most of the loop. The implementor VM is now paused (sandbox stop card-<id>, no --remove) at needs-review and resumed with the findings — sandbox prompt auto-starts a paused instance, and its scratch volume still holds the built target dir and the cargo/nix caches. The counterpart reviewer is paused the same way between rounds, since it may re-review several times. --remove for both is now tied to the card reaching ready (or being bounced, cancelled, or stalled). The skill spells out the three things a resume forces you to write around: the VM's RAM is wiped so the prompt must stand alone, its mirror is frozen at launch so fixes come back on the old seed, and both full instance names belong on the card's ## Dispatch log so a later turn can find the pair.
  • Fan-out is sized from the project's own lib.sandbox config. The concurrency guidance asserted "each VM is ~4 vCPU", which is only the lib's default — a project that sets vcpu = 8 was silently budgeted at half its real cost. The orchestrator now reads vcpu and mem out of the consuming flake before computing anything, and falls back to 4 vCPU / 8192 MiB only when the call sets neither. RAM joins cores as a bound, since mem often binds first: max VMs ≈ min((cores ÷ 2) ÷ vcpu, (RAM ÷ 2) ÷ mem), read from nproc / free -m. The session-start question to the product owner now has to quote the per-VM size and the VM count it implies, rather than asking for "half the box" in the abstract.
  • The "if you loaded only this skill" section is a pointer, not a summary. It restated three sandbox facts that have since been documented in place; it now just says to load the sandbox and project skills.

[0.3.6] — 2026-08-05

Burns down the 2026-08 post-mortem: a sandbox launch is now diagnosable while it provisions, recoverable when a dispatch dies or a harness wedges, and honest about what it concluded. Alongside it, two silent data-integrity fixes on the project board — a formatter run could blank a card's title and empty its dependency list, and opening the board in Obsidian could fail the Markdown gate.

The consumer-visible behavior changes are the --until-report default flip for sandbox dispatch / prompted sandbox start, a third sandbox status state (provisioning), and lib.markdown's exclude doing something for the first time. No spec or instance-state change (specVersion 4 / instanceVersion 2 unchanged). See MIGRATING.md.

Added

  • lib.project exports markdownExclude. A lib.markdown consumer merges it into exclude to keep the machine-managed BOARD.md out of the Prettier gate: exclude = project.markdownExclude. The glob is derived from boardDir, so it follows a relocated board. Card notes (<boardDir>/issues/*.md) stay gated — they are prose Obsidian does not reformat.

  • progressStallSecs is a lib.sandbox argument, and the progress notice no longer cries wolf. The stall window was a hard-coded internal constant (300s) rendered into the spec but not exposed, and only working/info reports reset its clock — heartbeats are deliberately silent — so an agent inside one long foreground tool call looks idle however hard it is working. A dispatched agent's first act is typically a 15-25 minute cold compile, so the notice fired on essentially every launch and was benign every time. A watchdog that always barks trains operators toward alarm, and in the field their one alarmed response was the destructive one. Two changes: the window is now a consumer knob (progressStallSecs ? 300, so a project with 20-minute builds can set 1500), and the notice escalates instead of asserting trouble up front — the first fire reads "no reports for Ns — normal during long builds …", and only a second fire at three times the window says the agent may be stuck, after which the episode goes quiet. Neither ever breaks or kills the turn, and the default window is unchanged.

  • Report texts are journaled to reports.ndjson. An agent's reports existed only as a transient stream: the guest relayed each one and the driving prompt/dispatch process rendered it to stdout, while host-side persistence (liveness.json) held only heartbeat freshness and the turn counter and guest-side (turn-state.json) only phase and timestamps. A turn could reach ended-ok — the fact of a clean terminal report durable — with the report's content gone if that stdout was lost. In the field a reviewer's report done "VERDICT: …" landed, the operator lost the stream, grepped the whole state dir for "VERDICT", found nothing, and had to re-prompt the reviewer to restate its own verdict. Every relayed report — plus the Stopped/ReArmed lifecycle verdicts — is now appended at the drive sink to reports.ndjson in the instance state dir, one JSON object per line with turnId, status, text, and a host timestamp. (The journal is written by the live drive, so it survives losing that drive's output — a closed terminal, a dead tmux pane — but not the drive process itself; a report landed by a later auto-nudge after the drive is gone is not journaled.) (a multi-line verdict stays one line; a torn trailing line never hides earlier history). The watchdog's stall notice is deliberately not journaled — it is not something the agent said. sandbox status <inst> renders the latest terminal report as a last report: line. Best-effort throughout: a failed append warns and never breaks the drive, and the --json stream is unchanged.

  • A prompted launch persists its directive, and sandbox prompt --redeliver resends it. dispatch claims the card, composes the directive, boots the VM detached, and only then tail-calls prompt to deliver it — so the text lived solely in the launching process's argv. Killing that process left a healthy idle VM, a card marked in-progress, and the directive nowhere on disk; recovery meant hand-recomposing it from the card plus the instructions file, which only works if you know the composition rule. The composed directive is now written to directive.md in the instance's state dir beside instance.json, and sandbox prompt <inst> --redeliver delivers it verbatim as a fresh turn (normal turn-id and delivery-ack semantics; passing text as well is a usage error, and a missing directive fails with the explicit command to run instead). A promptless launch of an instance that has one prints the --redeliver hint. No new secret exposure — a directive is card text already plaintext in the board, and secrets ride fw_cfg. The file's lifetime follows the state dir, so an ephemeral instance's is reaped with it and a named instance's persists across stop/start.

  • Pre-boot provisioning is visible. The whole window between "launch started" and "QEMU is up" — mirror clone, nix DB snapshot, context and secret staging — used to be structurally silent: instance.json is written before the recipe runs while the QMP socket only appears at boot, so sandbox status said stopped, and console.log did not exist yet because only the runner writes it. An operator watching an 11-minute stall had /proc spelunking as their only diagnostic, and killed a live step because of it. Now:

    • the launch recipe tees its own output to provision.log in the state dir from its first step, with a ::: <step> / ::: done (Ns) marker pair per step (the real stdout/stderr are saved and restored before the runner takes over, so interactive mode still hands a true TTY to ssh+tmux);
    • it maintains a phase marker that sandbox status renders as provisioning (<step>) in the detail view and --json, and as provisioning in the list — a live VM still wins, so a stranded marker can never mask a running instance;
    • the guest's Nix-DB seeding service publishes its verdict to the share, and sandbox status <inst> renders it as a store db: line (host-seeded / system-only — host seed skipped / … rolled back). An unseeded guest boots perfectly healthy and can't run a single gate; that is now visible before an agent burns a session discovering it.

Fixed

  • An injected-but-never-accepted turn now has a recovery path. The guest turn machine distinguishes created, injected (the channel notification was written to the harness's MCP transport) and accepted (the harness actually began the turn, evidenced by a hook or first report). Injection success only meant send_notification returned Ok, which says nothing about the harness processing it — so a harness that accepted the write and never started a turn left injected && !accepted, a state the machine recorded but no recovery path consumed: every host resend hit the dedupe arm and vanished, and the only way out was sandbox stop plus a restart. The guest now allows a bounded number of re-injections (2) for a turn that is injected but neither accepted nor ended, carrying an "if you already received turn N, continue it; otherwise begin it now" guard preamble so the small double-run risk degrades to a no-op. Re-injection stops the moment the turn is accepted (unspent budget is kept), and an accepted, ended, or budget-exhausted turn still dedupes exactly as before — an executed turn is never re-run. If the turn still never accepts, the host's verdict now names that specific state and the recovery (sandbox stop / sandbox start --agent --name, then --redeliver) instead of the ambiguous "delivery failed".
  • An aborted launch no longer leaves an instance reading provisioning forever. The pre-boot phase marker was cleared only on the success path, so a launch that failed mid-provisioning — an unreadable secret exits 1, and set -e/Ctrl-C can end the recipe anywhere — left the marker behind and sandbox status reported provisioning (<step>) indefinitely for an instance with no VM. That is worse than the stopped it used to report, and would strand an orchestrator that (per the migration note) treats provisioning as a live launch worth waiting for. The recipe now arms trap 'rm -f <phase>' EXIT INT TERM before the first step.
  • A long labels:/blocked_by: list no longer gains a phantom empty entry. Once a flow list is long enough, Prettier explodes it one item per line with a trailing comma — the common on-disk shape for a real label set. The flow parser read that trailing comma as an extra, empty item, which reached --json, project status, and the Obsidian card face. Items whose raw text is blank are now dropped; an explicitly empty item (written "") still round-trips.
  • Concurrent <name> format / <name> lint runs no longer race their staged ignore file. Both share a configuration name, so they shared one .prettierignore.<name> path and the first to finish deleted it out from under the second. Prettier does not error on a missing --ignore-path — it exits 0 having ignored nothing — so the losing format run would silently rewrite every excluded file, BOARD.md included. The staged file now carries the pid, and a missing one is a hard error rather than a silent no-op.
  • The launch recipe keeps stdout and stderr separate. Provisioning output was teed with 2>&1, which sent the recipe's own failures (a missing secret) to the caller's stdout. Each stream is now teed back to its own descriptor; provision.log still captures both.
  • sandbox prompt <inst> with no text is a usage error instead of delivering an empty turn. --redeliver is the supported way to omit it.
  • The launch-time host Nix DB snapshot no longer livelocks against a busy host. The snapshot used a plain SQLite .backup, whose API restarts from page zero whenever another connection writes the source — and the nix-daemon writes db.sqlite on every derivation registration, so any overlapping host nix build (a second concurrent launch is one) restarted it indefinitely. Observed in the field at 11m32s and 9.1 TB read for a database that snapshots in under a second when quiescent. It is now a VACUUM INTO, which holds a read transaction instead of restarting, wrapped in a 120s timeout so any future pathology degrades to the already-designed fallback — a guest that boots unseeded — rather than wedging provisioning. Failure or timeout stays non-fatal, publishes no partial nix-db.sqlite, clears its temp file, and prints a visible warning naming the consequence. A missing host database is now skipped rather than snapshotted into an empty one (sqlite3 creates a database on open).
  • Prettier-reflowed frontmatter no longer blanks a card. When a frontmatter value exceeds Prettier's print width it is reflowed onto indented continuation lines — valid YAML, and what markdown format considers canonical — but the key's own line then reads as empty. The line-oriented note reader took that as "absent", so a routine format run silently and retroactively blanked card titles (project status and the Obsidian card face both rendered empty) and emptied blocked_by lists, corrupting the dependency graph. get_scalar and get_list now fold the continuation lines back into the value, the way YAML does. Block lists and nested mappings are still never mistaken for a wrapped value, and set_scalar replaces a wrapped value's continuation lines rather than orphaning them. The reader-only fix keeps the editor's byte-for-byte preservation guarantee — nothing rewrites the note, so unknown keys (Obsidian's included) survive as before.
  • project lint flags a card with no readable title (empty-title). Previously a blanked title left the board and the note perfectly consistent, so lint reported clean while the card rendered blank everywhere — the sharpest edge of the reflow bug. It is an error, not a warning: new requires a title, so an empty one is unambiguous corruption.
  • lib.markdown's exclude never actually excluded anything. Prettier applies .gitignore semantics to --ignore-path, which anchors any pattern containing a / to the directory holding the ignore file. Because the generated file lived in the Nix store, a workspace-relative entry like vendor/** resolved against /nix/store/… and silently matched nothing. The format command and the flake check now stage the generated ignore file at the workspace root (the check copies the source tree first, since a store path is read-only) so exclude entries are workspace-relative as documented.
  • Opening BOARD.md in Obsidian no longer fails the Markdown gate. The board has three writers with incompatible serializations — the katsuctl project CLI, Prettier, and the Obsidian Community Kanban plugin — and the plugin's serializer is fixed and cannot be made Prettier-stable, so merely reading the board in Obsidian broke markdown lint. The board is machine-managed structured data rather than prose, so it is excluded from the gate (see markdownExclude above); this repo's own flake wires it.

Changed

  • sandbox dispatch and a prompted sandbox start --agent stay armed by default (--until-report is now the default for them). With the flag off, a drive returned the moment the agent yielded — a TurnCompleted{reported: false} surfaced the "stopped without reporting" warning and broke, instead of staying armed for the guest's auto-nudges or a late terminal report. For an interactive prompt that is defensible; for an orchestration flow it inverts what the command exiting means, since an orchestrator (or a human skimming) reads "the command returned" as "the work concluded". In the field the flag was omitted on both reviewer launches, so a reviewer paused mid-cold-build read as complete and its verdict was judged from a drive that had simply disarmed. Documenting the flag as opt-in made forgetting it the default outcome exactly where it mattered most. --no-until-report restores the old early return; --until-report is still accepted on both and is now a no-op, so existing invocations keep working. Interactive sandbox prompt is unchanged — it still returns on an unreported yield — but its warning now names the two recoveries (re-run armed, or check turn-state.json / sandbox status for a later ended-ok, since the guest's auto-nudges may land a report after the command exits).
  • Loading the sandbox skill is now a hard prerequisite of the project-orchestration skill. The skill previously only described the relationship ("complements … the sandbox skill"), and an orchestrator that read that line and skipped the skill flew its VMs blind. The body now opens with an imperative instruction to load it, the frontmatter description says so at selection time, the dispatch and reviewer sections name it as a prerequisite, and a short "if you loaded only this skill" callout inlines the three facts whose absence has actually cost sessions (where console.log lives, that the no-progress notice is usually benign, and stop-vs-remove).

[0.3.5] — 2026-07-23

Teaches lib.rust to build under an arbitrary Cargo profile, refreshes the default wasm-bindgen-cli to the latest release, and tightens the project status archive window so finished work clears the human view within the hour. No spec or instance-state change (specVersion 4 / instanceVersion 2 unchanged). See MIGRATING.md.

Added

  • Per-helper Cargo build profiles in lib.rust. Every exported build helper (buildCrate, buildWasmCrate, buildTrunkCrate, buildTestArchive) now takes an optional profile (default "release"); pass profile = "dev" for an unoptimized build. The shared workspace-deps bundle is keyed by (target, profile), so a crate and the dependency closure it reuses are always built under the same profile, and buildTestArchive threads the profile into cargo nextest archive (--cargo-profile) so its archived test binaries no longer diverge from that bundle. Release builds are byte-identical to before.

Changed

  • Default wasm-bindgen-cli bumped 0.2.1080.2.126. The hashes shipped in defaultWasmBindgenHashes now track the latest wasm-bindgen release; a workspace whose Cargo.lock resolves a different version still overrides via the wasmBindgenHashes argument.
  • project status clears archived cards after 1h (was 24h). The non-JSON list drops accepted and cancelled cards once their disposition_at is over an hour old, keeping the human view on live work. --json is unaffected — tooling still sees every archived card.

[0.3.4] — 2026-07-19

Fixes the project board writer's formatter-instability churn and the archive corruption it caused, plus a sandbox build that exhausted the guest's RAM-backed scratch. The board writer now emits prettier-stable markdown and anchors the archive on its ## Archive heading, so a CLI rewrite followed by markdown format is byte-identical and duplicate ## Archive sections can no longer accrete. No spec or instance-state change (specVersion 4 / instanceVersion 2 unchanged). See MIGRATING.md.

Added

  • project lint structural-lane checks. Errors on duplicate lane headings (duplicate-lanecards_in reads only the first, so cards in the rest are dropped on the next rewrite), and warns on a card in an unrecognized lane (unrecognized-lane) or a checked - [x] card stranded in an active lane (checked-in-lane, the fingerprint of a card un-archived by separator loss).

Changed

  • The board writer emits formatter-stable markdown. Empty lanes get a single blank line (not the \n\n\n a formatter collapses), and the archive separator is --- (what Prettier normalizes *** to). A freshly inited board and the settings block are prettier-canonical from birth, so the first markdown format over a board is a no-op instead of a drift chore.
  • The nix-daemon build directory lives on the disk-backed scratch volume. lib.sandbox guests point build-dir (and the daemon's TMPDIR) at /scratch/nix-build so a large in-guest build spills to disk instead of the RAM-backed root tmpfs. scratchVolumeSize must now also cover transient build trees; the images are sparse, so a generous cap stays cheap.

Fixed

  • Board format-drift churn and duplicate ## Archive sections. The writer's formatter-unstable output left the board dirty under the markdown format gate after every CLI mutation; once a formatter rewrote the *** separator the parser stopped recognizing the archive and appended a fresh ## Archive, stranding cards in an unreachable lane. Archive parsing now anchors on the ## Archive heading (tolerating any or no separator) and merges duplicate sections back into one on the next write.
  • Trailing human content dropped on rewrite. Foreign content left after the last lane on a settings-less board was consumed as stray lines and lost; it is now preserved verbatim, while mid-board strays are still skipped so the structure after them survives.
  • Leading blank lines on a frontmatter-less board. to_text no longer opens a board that has no frontmatter with the \n\n a formatter would strip.

[0.3.3] — 2026-07-19

Makes host↔guest turn completion event-driven again when a sandbox agent ends a turn without reporting — the failure mode that turned dispatch/prompt orchestration into wall-clock guessing. Three layers: the agent contract now mandates foreground work and a report before every turn-end; the guest auto-nudges an unreported idle agent before giving up; and the host can stay armed across an unreported turn-end. No spec or instance-state bump (specVersion 4 / instanceVersion 2 unchanged) — the new knobs are guest-only env vars and the flag is host-only CLI. See MIGRATING.md.

Added

  • Guest auto-nudge for an unreported turn-end. When a sandbox agent stops without a terminal report done/blocked, the guest server now re-prompts it ("report your real state now") up to KATSU_MAX_NUDGES times (default 3), KATSU_NUDGE_INTERVAL_MS apart (default 30s), before resolving the turn as ended-unreported. This recovers the two common silent-stop cases — the agent forgot to report, or it backgrounded work and yielded — without operator intervention. maxNudges/nudgeIntervalMs are the Nix-side knobs; 0 disables nudging (the prior single-grace behavior).
  • sandbox prompt/dispatch/start --until-report. Keeps the host stream armed across an unreported turn-end (emitting a rearmed note and continuing to wait for a real terminal report) instead of returning with the "stopped without reporting" warning. Pairs with the guest auto-nudges so a backgrounded build that finishes and reports long after the turn ended is still caught live.

Changed

  • Agent-mode turn discipline. The always-on agent contract now mandates running builds/tests in the foreground, never yielding a turn with unfinished work, and always running a terminal report before stopping — baking in the rule dispatched agents previously had to be told ad hoc.

Fixed

  • The project-orchestration skill loads again. A : (colon-space) inside its YAML description broke frontmatter parsing, so the skill was silently dropped from the registry and could only be found by crawling the filesystem. The description is now a colon-free plain scalar.

[0.3.2] — 2026-07-19

Corrects the project-board guidance so agents are pointed at the project menu command rather than the underlying katsuctl project binary, which is meant to be a hidden implementation detail. Docs-only; no spec or instance-state bump (specVersion 4 / instanceVersion 2 unchanged). See MIGRATING.md.

Changed

  • The project skill and board docs reference the project command, not katsuctl. The project skill, the scaffolded project/kanban/README.md (and its generator in katsuctl), and the lib.project README/notes now present project as the interface — katsuctl is an implementation detail behind the menu command, which forwards --json payloads through untouched. Newly scaffolded boards get the corrected README; existing boards are unaffected.

[0.3.1] — 2026-07-19

Makes katsuctl build on non-Linux (macOS): the project domain is cross-platform, and the Linux-only sandbox domain is now conditionally compiled out. Also fixes streamed agent reports being lost from a captured, non-TTY stream. No spec or instance-state bump (specVersion 4 / instanceVersion 2 unchanged). See MIGRATING.md.

Added

  • katsuctl builds on non-Linux. The sandbox domain (and its Linux-only tokio-vsock dependency) is gated behind #[cfg(target_os = "linux")], so on macOS katsuctl compiles with the project board commands and omits the sandbox subcommands. lib.project and packages.<system>.katsuctl are now available on every system; the sandbox library stays Linux-only.

Fixed

  • Streamed agent reports are no longer lost in captured output. sandbox prompt / sandbox dispatch rendered live reports to stdout, which the emitExec menu wrappers capture and which races teardown in a non-TTY/backgrounded stream — so a terminal report done could vanish. Reports now stream to stderr (the reliable channel the other progress lines already use), and gate their color on stderr's TTY-ness; --json streaming stays on stdout.

Changed

  • The shared output/rendering module moved from sandbox::output to a top-level output module — an internal refactor enabling the cross-platform split.

[0.3.0] — 2026-07-18

Adds a file-backed project board and agent orchestration built on it: queue work as Obsidian-Kanban cards (katsuctl project / the project menu command / lib.project), dispatch cards to sandbox VMs for implementation, and peer-review in sandboxes. Ships two new skills. Additive for existing consumers — no spec or instance-state bump (specVersion 4 / instanceVersion 2 unchanged). See MIGRATING.md.

Added

  • project board + lib.project. A lightweight backlog rendered as an Obsidian Kanban board: project init / new / status / status set / prioritize / lint. Six-state lifecycle (to-do → in-progress → needs-review → ready → accepted, plus cancelled), 6-hex card ids, and blocked_by dependencies that clear their dependents at ready.
  • project status — the unified view+mutate surface: bare lists the board, <id> shows one card, --available / --lane filter, --json is machine-readable. A card entering Ready auto-slots into suggested acceptance order (dependencies first, then oldest created) without disturbing a manual order.
  • project status set --accept-all bulk-accepts every Ready card. Terminal transitions stamp a disposition_at timestamp; the human project status list shows only cards archived in the last 24h (--json returns all).
  • sandbox dispatch <card> launches an agent VM to implement a board card, and sandbox fetch now reports whether committed work actually landed (branch tip vs. launch seed).
  • Two skillsproject (board mechanics) and project-orchestration (the implementor / peer-reviewer / product-owner / orchestrator roles, sandbox dispatch, the report bridge, and a host-core concurrency budget).

Changed

  • The Katsuobushi repo's own sandbox allowedOrigins now include the crates.io origins so in-guest cargo builds fetch dependencies normally.

[0.2.9] — 2026-07-09

Fixes a regression from the 0.2.6 command-tree rename: prompting a paused, named sandbox no longer fails trying to resume it. The fix is host-side, so a dev-shell rebuild picks up the corrected controller; no spec or instance-state bump (specVersion 4 / instanceVersion 2 unchanged). See MIGRATING.md.

Fixed

  • sandbox prompt resumes a paused named instance again. The 0.2.6 rename removed the sandbox:start menu binary, but sandbox prompt's auto-resume still shelled out to that name — so prompting a powered-off named instance errored instead of booting it to deliver the turn. Resume is now self-contained: it re-runs the start subcommand and execs the boot recipe that emits, via the pinned katsuctl / bash store paths, depending on no menu command being on PATH.
  • Stale sandbox:* command names in hints. Status, attach, resolve, and stop messages — and the post-launch start echo — still suggested the removed colon-namespaced commands (sandbox:status, sandbox:fetch, …); they now name the current subcommand forms (sandbox status, sandbox fetch, …).

[0.2.8] — 2026-07-06

Adds a built-in menu command to every dev shell, so the command table can be reprinted on demand. Menu-only; no spec or instance-state bump (specVersion 4 / instanceVersion 2 unchanged). See MIGRATING.md.

Added

  • Built-in menu command. makeMenu now always contributes a menu command ("Print this menu.") that reprints the top-level command table. Like any command it shows its own figlet banner + description first, but it does not reprint the hero graphic — that stays a one-time greeting shown when you drop into the dev shell. Define your own menu in commands to override the built-in.

[0.2.7] — 2026-07-06

A menu-polish follow-up to 0.2.6: subcommand banners regained their figlet header, and all menu decoration moved to stderr so it can no longer muddy a command's stdout. Menu-only; no spec or instance-state bump (specVersion 4 / instanceVersion 2 unchanged). See MIGRATING.md.

Fixed

  • Subcommand banners show the full command chain again. 0.2.6's subcommand trees dropped the pretty figlet header other commands show — a branch printed none, and a leaf would at most have shown its own bare key. A resolved leaf now banners with its whole Nix-encoded chain (sandbox status, not status; markdown format), and a bare branch banners with its own name (markdown).

Changed

  • Menu decoration goes to stderr. The dev-shell greeting (hero + command table) and every command's figlet banner now write to stderr instead of stdout, so nix develop -c 'sandbox status --json' | jq — and any captured/piped menu command — keeps clean, pipeable stdout. The greeting still displays on the terminal and still clears the screen on entry; the per-command banner still precedes the command's real output.

[0.2.6] — 2026-07-03

Dev-shell menu commands are now organized as subcommand trees, so a namespace collapses to a single command + menu row instead of one row per verb. The sandbox:* and format:* / lint:* commands are renamed accordingly — the one consumer-facing break in this release. No spec or instance-state bump (specVersion 4 / instanceVersion 2 unchanged). See MIGRATING.md.

Added

  • makeMenu command trees. A menu command may now be a branch — an entry with a subcommands attrset instead of a command — which compiles to one shell application that dispatches on its first argument and recurses to any depth. Both leaves and branches take an optional help string; running a branch bare (or with -h / --help) prints that preamble plus an aligned table of its subcommands. Flat command sets are unchanged, so an existing menu keeps working untouched.
  • Sandbox usage lines read as sandbox …. clap prints its errors and Usage: lines qualified by katsuctl's real path (e.g. katsuctl sandbox --config <CONFIG> attach <INSTANCE>); the menu wrappers now rewrite that prefix back to the command the user typed (sandbox attach <INSTANCE>) in katsuctl's stderr. Only stderr is filtered, so streaming stdout — notably sandbox prompt's live report stream — is untouched.

Changed

  • lib.sandbox menu commands are now sandbox <verb>. The seven sandbox:* entries collapse into one sandbox branch with start, prompt, status, fetch, stop, attach, and screenshot subcommands. Each verb keeps its exact behavior, and nix run .#sandbox is unchanged.
  • lib.markdown menu commands are now <name> <verb>. Each invocation's format:<name> / lint:<name> pair becomes a single <name> branch with format and lint subcommands (default name markdown, so markdown format / markdown lint). The flake check name is unchanged.

Removed

  • The colon-namespaced command names. sandbox:start (and the other six sandbox:*), format:<name>, and lint:<name> no longer exist as dev-shell commands — use the subcommand forms above. Update any script, CI step, or nix develop -c … invocation that calls an old name.

[0.2.5] — 2026-07-01

A hardening release from a full engineering audit of the sandbox feature: shell quoting, secret staging, and teardown fixes on the host; turn-delivery correctness fixes on both sides of the agent-mode channel; guest control-plane hardening; and safe parallel launches. No spec or instance-state bump (specVersion 4 / instanceVersion 2 unchanged) and no config changes — but restart agent instances so the guest-side fixes take effect, and note the tightened eval-time validation. See MIGRATING.md.

Fixed

  • Recipes single-quote host paths. The emitted start recipe double-quoted paths (the git toplevel, XDG-expanded roots, context entries), leaving $, backticks, and \ shell-active; every path is now single-quoted with the same close-escape-reopen idiom the prompt payload already used, so a path containing shell-special characters is inert.
  • fromEnv secrets are born 0600. The credential file was created under the default umask and then chmod'd, leaving a brief window where the plaintext token was world-readable; it is now recreated under a subshell umask 077, matching the install -m 0600 guarantee the fromFile branch already had.
  • sandbox:stop confirms the VM died before removing its state. quit was fire-and-forget: a wedged monitor fell through to recursive removal, deleting the disk images out from under a still-running qemu while reporting success. Stop now polls the monitor after quit and refuses removal (loud, nonzero, nothing deleted) while it still answers; both dir removals are also attempted before an error surfaces, so a partial failure no longer strands a half-torn-down instance.
  • A failed first injection no longer wedges the turn. The guest committed a turn to in-flight before the injection ran, so if the injection failed (the first-turn race) every host resend of that id was dedupe-dropped forever. Delivery is now tracked separately: an undelivered turn re-injects on resend, a delivered one dedupes, and a resend during the stop-grace window no longer creates a fresh turn (which would have executed it twice).
  • The turn-id counter never rewinds. A corrupt (or schema-newer) liveness.json silently reset nextTurnId to 1, and the guest's turn-id dedupe would then drop the next genuinely-new prompt. A corrupt record now fails sandbox:prompt loudly instead, the best-effort heartbeat writers skip rather than clobber it, and unknown fields no longer fail the parse.
  • sandbox:status no longer reports a phantom active stream. The streamActive flag is only cleared by a clean driver shutdown, so a panicked/killed driver left status claiming an active stream forever; the flag is now believed only while the recorded heartbeat is within the watchdog deadline.
  • A stale report cannot end the wrong turn. Both sides applied accept/terminal transitions to whatever turn was in flight, so a late done from turn N could terminate turn N+1 and falsely satisfy its delivery ack; a report naming a different turn now relays without transitioning.
  • Parallel sandbox:starts cannot collide. CID/port selection read sibling instances before either launch had persisted its claim (and a sibling's ssh port is not even bound until its qemu boots, so the bind probe alone could not see it). The planner now skips sibling-recorded ports and CIDs and holds an advisory flock under the project state root across the probe→persist window — swarm launches allocate safely.

Changed

  • The guest bounds and times out its I/O. Inbound lines on the control and report sockets are capped at 1 MiB (the report socket is reachable by the unprivileged in-guest agent, so an unterminated flood was an in-guest OOM), outbound writes to the host time out after 10s and drop a wedged connection instead of freezing the heartbeat behind it, and the turn-state.json persist moved off the async workers so a stalled 9p share cannot pin them.
  • Eval-time validation is tighter. A homeFiles entry with an unknown mode now fails evaluation instead of silently never appearing in the guest, and homeFiles/extraRepos destinations get the same full .. traversal check as workspaceContext (whose /..-suffix form extraRepos historically missed).
  • Ephemeral instance names are UTC-stamped. The timestamp is now formatted in Rust (it was the lone bare-PATH date invocation in an otherwise pinned-tool contract) and uses UTC where the shell used host-local time.

[0.2.4] — 2026-06-29

A packaging hotfix: the sandbox:* menu commands failed for consumers with katsuctl: command not found. They invoked katsuctl by bare name and relied on it already being on the dev shell's PATH — which only Katsuobushi's own dev shell arranged, so a project that wired in just sandbox.menuCommands got commands that could not find their own controller. The instance spec bumps to specVersion 4; see MIGRATING.md.

Fixed

  • sandbox:* commands work without katsuctl on PATH. Every menu command (and nix run .#sandbox) now invokes the controller by its absolute store path, and the agent-mode start recipe self-references it through a new tools.katsuctl spec field instead of a bare katsuctl … prompt tail-call run in a child shell. A consumer that wires only sandbox.menuCommands into a dev shell no longer hits katsuctl: command not found. No PATH manipulation remains in any command.

Added

  • lib.sandbox exposes katsuctl. The host controller derivation (built via lib.rust/crane from Katsuobushi's pinned source) is now returned from lib.sandbox as katsuctl, so a project can put a bare katsuctl on its dev shell PATH for direct use. The sandbox template wires it in for power users; the sandbox:* commands no longer require it.

[0.2.3] — 2026-06-29

A graphics hotfix: in a graphics guest an X11 app — or any tool that probes DISPLAY — failed out of the box because only WAYLAND_DISPLAY was set. See MIGRATING.md.

Fixed

  • X11 apps work in a graphics guest. The guest now exports DISPLAY=:0 alongside WAYLAND_DISPLAY (in both the sshd SetEnv and the login shell) and ships xwayland, so sway's XWayland shim serves X clients on :0. A tool that probes DISPLAY, or an X-only app, now runs with no per-invocation ceremony. Gated on the graphics opt-in; a graphics-off guest is byte-for-byte unchanged.

[0.2.2] — 2026-06-29

Opt-in graphics: a sandbox can now boot a headless compositor and a paravirtual GPU so a browser or Wayland app actually renders. It is off by default, so existing consumers are unaffected; enabling it widens the host-facing attack surface (the GPU command stream is parsed in the host QEMU process), which the README documents plainly. The instance spec bumps to specVersion 3; see MIGRATING.md.

Added

  • lib.sandbox: opt-in graphics capability. A new graphics attrset (enable, default false; gpu role-preference list, default ["integrated" "discrete" "software"]; output, default 1920×1080@60) boots a headless sway compositor on a virtual output and, when a GPU rung resolves, hands QEMU a virtio-gpu-gl device against a host render node — so a browser (WebDriver/Playwright) or a Wayland app can render. The browser/app goes in the existing packages list. Pinning gpu = ["software"] keeps the full original boundary (llvmpipe, no GPU device) at a performance cost. When enabled, sandbox:status adds a graphics preflight row that runs the real GPU resolver against the host and flags a missing render-group membership, and a launch-time notice records the widened attack surface. See lib/sandbox/README.md.
  • sandbox:screenshot <instance|#> [path]. A new menu command that grabs a PNG of the headless-sway output by running grim over the existing loopback ssh — no daemon, no new port. Defaults to a timestamped PNG in the cwd; - streams to stdout. Requires the graphics opt-in; a purely-offscreen workload that never composites a surface screenshots as blank (expected).
  • sandbox:status GRAPHICS column. The instance list now shows the GPU rung each instance launched on — integrated, discrete, software, or none when graphics is off — recorded per-instance in instance.json (and surfaced in the detail view and --json).

Changed

  • The instance spec is now specVersion 3 (carrying the graphics block); a stale v2 spec is rejected loudly. Rebuild your dev shell (nix develop) so the spec re-renders. No config changes are required.
  • instance.json is now instanceVersion 2 (it records the resolved graphics rung). A v1 instance state from an earlier release is rejected on read, so recreate any persistent (--named) instance after upgrading — ephemeral instances are unaffected.

[0.2.1] — 2026-06-28

Sandbox liveness: the host and guest now agree on when a turn started, finished, or silently died — closing the first-turn race and surfacing unreported hangs. An agent-mode VM emits heartbeats and lifecycle edges, and the guest persists turn state to the share so sandbox:status can report it out-of-band, even with nothing attached. No action for devshell users beyond rebuilding (the instance spec bumps to specVersion 2); see MIGRATING.md.

Added

  • Turn/transport liveness machinery. The guest controller runs a per-turn state machine and writes a durable turn-state.json to the share on every transition (idlein-flightended-ok / ended-unreported), plus a periodic heartbeat. A report hook <event> bridge wires Claude Code's Stop, SessionStart, and UserPromptSubmit hooks (managed-settings tier) into that machine.
  • Host drive watchdog. sandbox:prompt now runs a deadline-aware loop: ack-and-resend of an undelivered first turn, a heartbeat-deadline that detects a dead transport, a one-shot progress-stall notice, and a pre-send ready-gate that closes the first-turn race — a prompt to a just-booted instance no longer lands in the arming gap. Heartbeats are silent, so a backgrounded drive is never woken by a tick, and a monotonic, persisted turn_id makes resends safe.
  • sandbox:status liveness line. Status reads turn-state.json (and the host-written liveness.json) to show per-instance turn/transport state with no connection — e.g. turn 3 ended-unreported 14m ago · no active stream — corroborated against QMP.
  • Seven liveness tunables (heartbeatSecs, heartbeatMiss, progressStallSecs, deliveryDeadlineSecs, deliveryRetries, readyGateSecs, stopGraceMs), Nix-driven from one source into both the spec and the guest env.

Changed

  • The instance spec is now specVersion 2 (carrying the liveness tunables); a stale v1 spec is rejected loudly. Rebuild your dev shell so it re-renders.

Fixed

  • The per-instance share root is now guest-writable, so the guest controller can create turn-state.json on a real boot. The mapped-xattr 9p share left the root root-owned; the launch recipe now opens it a+rwX, as it already did for sync.git.
  • liveness.json is written atomically (temp + rename), so sandbox:status never reads a torn record.

[0.2.0] — 2026-06-27

The host side of the sandbox is rewritten from an unmaintainable pile of untested shell into a compiled, tested Rust binary, katsuctl. From a devshell user's perspective this is a no-op — sandbox:start, sandbox:prompt, sandbox:status, sandbox:fetch, sandbox:stop, and sandbox:attach keep their names and behavior — but their logic now lives in katsuctl <domain> <command> with unit, golden-snapshot, and seam-level tests, verified end-to-end against a real KVM boot. The three in-tree Rust crates are also renamed for clarity (breaking only for anyone depending on them directly — see MIGRATING.md).

Added

  • katsuctl host-side controller (katsuctl sandbox <command>) absorbing all the sandbox host logic: instance naming / ssh-port / vsock-CID / seed-commit decisions made in tested Rust, a Nix-rendered instance spec passed via --config, a native QMP client (liveness + quit), a consolidated instance.json per-instance metadata file, an emit-script harness for the start/attach terminal hand-offs, and dual human/--json output with strict color gating. Built reproducibly via the flake (nix build .#katsuctl).

Changed

  • The six sandbox:* devshell commands are now thin katsuctl wrappers — same names and behavior, but every decision is made in tested Rust and the shell that remains is a flat, generated recipe. Secrets are emitted as references, never values, and the start/attach recipes are golden-snapshotted.
  • Rust crates renamed (see MIGRATING.md): the host controller crate katsuctlkatsuobushi-controller (it still ships the katsuctl binary), katsuobushi-protocolkatsuobushi-sandbox-protocol, and katsuobushi-sandbox-controlkatsuobushi-sandbox-guest (its guest channel-server binary renames with it).
  • sandbox:status gains an aligned, color-coded table and a --json mode. The list shows # / INSTANCE / STATE / MODE / PERSIST; the ssh port and vsock CID moved to the per-instance detail view (sandbox:status <name>). A bare status doubles as the launch prerequisite gate — nonzero exit if a declared secret or /dev/vhost-vsock is missing.

Removed

  • The old host-side shellsandboxRunner, the isRunning QMP probe, instanceHelpers, and statusSecretChecks — and the standalone katsuobushi-sandbox-prompt host-client binary, all replaced by katsuctl.

[0.1.10] — 2026-06-26

A sandbox release with one consumer-facing breaking change: the guest's writable scratch is now disk-backed instead of RAM-backed, and the single storeOverlaySize argument is replaced by three sparse-image sizes. Also adds auto-start when prompting a paused instance. See MIGRATING.md.

Added

  • sandbox:prompt auto-starts a paused instance. Prompting a named instance that was stopped (but kept) now restarts it — booting and arming the channel (~30–60s) before delivering the turn — instead of hanging against the powered-off VM. A pause discards the VM's RAM, so the live conversation does not survive it; only the pushed branch does, and the resumed agent begins a fresh session on top of its branch.

Changed

  • Writable scratch is disk-backed, not RAM-backed. The writable /nix/store overlay, the workspace clone (with build artifacts), the relocated cargo/rustup/XDG caches, and the guest Nix database now live on per-instance sparse disk images instead of tmpfs. Capacity scales with host disk rather than a fraction of mem, so a Rust target/ can no longer exhaust guest RAM; the guest root / stays a tmpfs. A named instance keeps these images across a stop/restart, so warm build caches (and host-path registrations) survive a pause; ephemeral instances get fresh images each launch.
  • importHostStoreDb: the guest Nix database now persists and is seeded once. On its own persistent volume, the host-DB snapshot is applied a single time per named instance (gated on a marker) and then accumulates the agent's in-VM registrations — keeping it consistent with the persistent store overlay across a restart, rather than re-seeding every boot.

Removed

  • storeOverlaySize is replaced by storeVolumeSize / scratchVolumeSize / dbVolumeSize. The old single tmpfs-size string is gone; the three new arguments size the disk images (in MiB, sparse). Defaults: 16384 / 32768 / 4096. See MIGRATING.md.

[0.1.9] — 2026-06-26

A sandbox-ergonomics release: instances are now numbered, and there is a one-shot command to attach to a running agent's live session. Purely additive — no consumer config changes; see MIGRATING.md.

Added

  • sandbox:attach <instance|#>. A new menu command that SSHes into a running instance, pins TERM=xterm-256color in the remote session (so terminals like ghostty don't confuse the guest's tmux), and attaches the agent's katsuobushi tmux session — collapsing the ssh-then-tmux attach dance that sandbox:status <instance> used to print by hand.
  • Numeric instance references. sandbox:status now prints a leading # column numbering each instance, and that index is accepted anywhere a name is — sandbox:prompt, sandbox:status, sandbox:attach, sandbox:fetch, and sandbox:stop all resolve an all-digit argument as a 1-based index into the current listing. The numbering is positional (it can shift as instances come and go); names remain the stable handle. Real instance names always carry a - from their timestamp or hex suffix, so a name is never mistaken for an index.

Changed

  • sandbox:status listing gains a # column. The instance table now leads with a 1-based index; anything parsing that table by column position should account for the extra leading field.

[0.1.8] — 2026-06-25

A sandbox release. One default-on behavioral change — the guest now reuses the host's Nix store instead of re-downloading what the host already built — that is transparent in normal use; see MIGRATING.md.

Added

  • lib.sandbox: importHostStoreDb option. A new argument (default true) that makes the guest reuse everything the host has already built instead of re-downloading it. The guest already mounts the host /nix/store read-only, but microvm registers only the guest's system closure as valid, so other host paths (e.g. a nix develop toolchain) were present on the mount yet re-substituted from the network. The runner now snapshots the host's db.sqlite at launch (a consistent SQLite .backup, ~0.5s) into the share, and a guest boot service transplants it over the system-only DB — after microvm's own closure registration — so every host-built path becomes valid with no network and no copying. The transplant is best-effort: a missing snapshot or a host/guest Nix schema mismatch rolls back to the system-only DB, so a sandbox always boots. No new read exposure — the whole host store was already readable over the mount. Set importHostStoreDb = false to opt out.

Changed

  • lib.sandbox (this repo's own config): allowlist static.rust-lang.org. Dropping into nix develop inside the sandbox provisions the Rust toolchain via rust-overlay, which fetches from static.rust-lang.org; that host was missing from the egress allowlist. With importHostStoreDb on, the toolchain is reused from the host offline, so this is only the fallback for picking up a newly bumped rust-toolchain.toml.

[0.1.7] — 2026-06-25

A docs-and-features release; nothing to migrate (see MIGRATING.md).

Added

  • lib.menu.makeMenu: colorizeGraphic option. A new optional argument (default true, preserving current behavior) controls whether the ASCII art banner is run through the colorizer. Set colorizeGraphic = false to print the banner raw while still colorizing the title and command table. Has no effect when no banner is set.
  • lib.menu.makeMenu: graphicFile option. A new optional argument (default null) supplies the banner from a file path that is cated at runtime rather than inlined as a string. This keeps raw bytes — notably ANSI escape (U+001B) sequences in pre-colorized terminal art — out of the shellHook, which nix develop would otherwise reject when serializing the shell environment to JSON. Takes precedence over graphic; pair with colorizeGraphic = false to preserve the art's embedded colors. Katsuobushi's own banner now ships as pre-colorized pixel art (hero.ansi) through this path.

Changed

  • lib.sandbox: sandbox:* menu descriptions trimmed to short summaries. The dev-shell menu entries for sandbox:start / prompt / status / fetch / stop dropped their inline usage hints (e.g. sandbox:fetch <instance>), leaving a one-line summary; full usage lives in the sandbox skill. Command names and behavior are unchanged.
  • lib.sandbox: sandbox:status preflight names the OAuth token fix. When CLAUDE_CODE_OAUTH_TOKEN's host source is missing, the environment: report now appends a run 'claude setup-token' hint alongside the variable to export.

[0.1.6] — 2026-06-25

A skill-and-docs release; nothing to migrate (see MIGRATING.md).

Changed

  • sandbox skill: fan out via sub-agents; refined jj landing guidance. The skill now drives parallel fan-out by giving each task its own sub-agent — each launches and drives its own --named VM to done and returns its branch plus the agent's done summary — while integration stays serial in the orchestrator. The jj landing step now anchors accepted work on the working-copy commit @ (jj new <tip>) and leaves bookmark placement to the user, keeping landed work durable across the git imports the sandbox commands trigger. Touches plugins/katsuobushi/skills/sandbox/SKILL.md and lib/sandbox/README.md; no library change.

[0.1.5] — 2026-06-24

A docs-only release; nothing to migrate (see MIGRATING.md).

Changed

  • sandbox skill: remove an instance once its work is accepted. The skill now directs tearing the sandbox down with sandbox:stop --remove <name> as soon as its unit of work is complete and accepted — both in the branch-landing workflow and in the lifecycle section — since the sandbox/<name> ref is the durable artifact, not the VM. No library change.

[0.1.4] — 2026-06-24

Changed

  • lib.sandbox: a provided --name is suffixed with random entropy. At launch, --name foo now mints an instance named foo-<8 hex> (e.g. foo-a3f9c2d1), so every launch is a fresh, collision-free instance instead of a silent resume of an older same-named branch. The full suffixed name is printed at launch and by sandbox:stop; drive (prompt/status/fetch/ stop) and resume with that full name. A name that already carries the 8-hex suffix is left as-is, so passing the printed name back is safe. See MIGRATING.md.

[0.1.3] — 2026-06-24

A docs-and-internals release; nothing to migrate (see MIGRATING.md).

Changed

  • sandbox skill docs substantially revised — added the branch-landing / integration workflow, conflict-reconciliation-as-delegation guidance, and parallel fan-out notes.
  • lib.sandbox: sandbox:status preflight internals refactored. The preflight now builds its report in a subshell and carries the problem count out via the subshell's exit status (the || errs=$? is load-bearing under inherit_errexit). Observable behavior is unchanged from 0.1.1.

[0.1.2] — 2026-06-24

A docs-only release; nothing to migrate (see MIGRATING.md).

Changed

  • sandbox skill docs reworked — clearer sandbox:status guidance, a note that sandbox:* are dev-shell menu commands (nix develop -c sandbox:status from outside the shell), and Prettier reflow.
  • **Markdown linting now covers plugins/**/\*.md.** Repo-internal; no consumer impact.

[0.1.1] — 2026-06-24

Added

  • lib.sandbox: sandbox:status preflight. A bare sandbox:status now prints an environment: block before listing instances, verifying every declared secret at its host source (the fromEnv variable is set, or the fromFile path is readable) and checking for /dev/vhost-vsock. It names the exact host variable feeding each guest secret, so "is this host ready to launch?" is a single command with no project-specific knowledge required.

Changed

  • lib.sandbox: sandbox:status exits non-zero when the preflight fails. Previously the bare command always exited 0; it now exits with the count of missing prerequisites, so its exit status alone is a usable gate. See MIGRATING.md.
  • Docs: clarified that the guest always reads CLAUDE_CODE_OAUTH_TOKEN while secrets.*.fromEnv chooses which host variable supplies it, and documented the agent-harness workaround (a harness scrubs CLAUDE_CODE_OAUTH_TOKEN from its children, so source it from a differently-named host variable, e.g. HARNESS_OAUTH_TOKEN). Touches lib/sandbox/README.md, the sandbox skill, and the sandbox template.

Fixed

  • lib.sandbox: the guest can push to the 9p sync mirror. The per-instance bare mirror is now shared over 9p with security_model=mapped-xattr (was security_model=none), so files the guest creates are recorded as agent-owned. The unprivileged agent could previously never write its receive-pack quarantine dir, so git push failed and no work crossed the sandbox boundary. The mirror's pre-existing directories are also opened so the agent can create entries inside them.

[0.1.0] — 2026-06-23

The first tagged release. Highlights below; consumer-facing migration notes for everything tracked on untagged main up to this tag are in MIGRATING.md.

Added

  • lib.sandbox — a new library that assembles a microvm.nix guest which boots into a working dev environment where an agent harness (Claude Code by default) runs with its blast radius bounded by a real VM. Provides apps.sandbox (nix run .#sandbox), the sandbox:* menu commands (start, prompt, status, fetch, stop), checks.sandbox, and nixosConfiguration. Scaffold with nix flake init -t github:cdata/katsuobushi#sandbox.
  • sandbox template and sandbox agent skill for the above.
  • rust template for scaffolding Rust projects.
  • Transitive infra dependency inheritance. Katsuobushi now owns crane, nix-filter, rust-overlay, and microvm, passing them through to consumers so a lib.rust consumer flake collapses from six inputs to two.
  • lib.rust: target argument on buildCrate / buildTestArchive for cross-compiling to arbitrary triples; sourceInclude argument for crates that do not live under rust/.

Changed

  • lib.markdown now uses Prettier instead of rumdl, which mishandled GFM tables. Scope is now include / exclude glob lists plus a name label (replacing docsDir); settings takes Prettier options; outputs and menu commands are namespaced per invocation (format:<name> / lint:<name>).
  • lib.rust input arguments renamed to match nixpkgs vocabulary: buildInputsnativeBuildInputs (build tools) and librariesbuildInputs (link libraries); both now default to [ ].
  • lib.rust wasm-bindgen version is derived from Cargo.lock rather than hard-pinned, failing fast with a copy-pasteable fix on a mismatch. Default hashes ship for 0.2.108.
  • lib.rust crate version is derived from Cargo.toml instead of a hardcoded 0.1.0; derivation name prefix derives from projectId.

See MIGRATING.md for the full upgrade details.