AGENTS.md
July 26, 2026 · View on GitHub
Development workflow
All code changes go through a branch, worktree, and PR — no exceptions.
- Branch + worktree + PR for every change. Create a fresh branch off latest
main, materialize it as a git worktree under.claude/worktrees/<branch-name>, make changes there, and open a PR. The top-level checkout at~/agentdstays onmain— never edit files there directly. - No direct push to
main. Changes land onmainonly via a merged PR. - Release process lives in
docs/RELEASING.md. Use that guide for versioned releases and publishing prebuilt binaries. - No
Co-Authored-By: Claudetrailer in commits. Don't append model attribution to commit messages.Co-authored-by:for other humans is fine. - Clean up after merge. Remove the worktree (
git worktree remove <path>), delete the local branch (git branch -d <name>), and delete the remote branch (e.g. via GitHub's "delete branch after merge", orgit push <remote> --delete <name>). - After merge, update and build the main worktree. Once a PR is merged and the feature worktree is cleaned up, switch to the top-level checkout (
~/agentd), pull latestmain, and runcargo buildthere (debug profile). This keeps the user's main worktree binaries current so/construct restartcan pick up the latest mergedconstructchanges, especially when operating from a remote-control session. Report the updated main-worktree debug binary paths when relevant. - When the change is testable, build all binaries in the worktree and report paths in the agent response. Run
cargo buildinside the worktree (debug profile — much faster to iterate on than release; the binaries live under.claude/worktrees/<branch>/target/debug/), then print the absolute path of every binary the workspace produces —constructbinary the workspace produces in the agent response so the user can copy and run it. Explicitly call out which binary the PR's code lives in so the user can run the right one without grepping the diff (e.g. "this PR only touchescrates/cli→ relevant binary isconstruct"). - Record a video / screenshot when it helps the reviewer, and post accessible artifacts on the PR. This is a judgment call:
- Sometimes only an "after" recording makes sense (a brand-new pane / popup / view that didn't exist before).
- Sometimes a before/after pair is needed (a tweak to an existing render: a color, a fade rate, a layout shift).
- Sometimes neither is useful (refactors, internal API changes, daemon logic with no user-visible surface). Use Recording the TUI below for the mechanics. Report local artifact paths in the agent response so the user can open them; if posting on the PR, attach or upload the actual media so reviewers can access it. When unsure which of the three applies, ask the user before recording.
Recording the TUI
Use vhs to capture deterministic mp4 / gif clips of the TUI without needing a desktop session or screen-recording permissions. The notes below are the ones we wish we'd had on the first attempt.
-
Build the worktree's binaries. vhs records whatever
constructyou point it at, so make sure the worktree has been built (cargo buildper the workflow above) before recording. For a before/after pair, prepare two worktrees so each side has its own binaries — never re-recordbeforefrom a tree that already has the change applied. -
Isolated daemon. Run vhs against a fresh
CONSTRUCT_RUNTIME_DIR/CONSTRUCT_STATE_DIR/CONSTRUCT_DATA_DIR/CONSTRUCT_CONFIG_DIRunder/tmp/so it doesn't collide with the user's running daemon. Each recording gets its own dir and its own daemon process; tear them down at the end. -
Put the TUI in a state that actually shows your change. This part varies most by change — pick whichever shape fits:
- Specific harness features (a smith tool, codex output rendering, claude resume, …): spawn that harness with a representative prompt, e.g.
construct new --prompt "<task>" smith. Use a prompt whose output exercises the diff (tool calls if you changed tool rendering, long messages if you changed wrapping, etc.). - Minibuffer / keymap / popup / palette: send the keystrokes from inside the vhs tape with
Type,Ctrl+X,Enter,Sleep, etc. — no extra sessions needed if the feature is reachable from a stock TUI. - Session-list / modeline / matrix rain / anything driven by fleet activity: spawn 2–4 sessions producing ambient activity. The most robust pattern is
construct new shell(interactive shell) followed byconstruct send <id> "<command>"pushing a noise loop into each. Don't pass the loop as thenew shellprompt — both bash and zsh observed to fall back to interactive mode under PTY and never actually run-lc <cmd>, leaving the daemon silent. - Single-session views (transcript, scrollback, diff): spawn one session, then trigger the view via tape keystrokes (
C-x zfor zoom, mouse-wheel events, etc.). Whatever the shape, give the daemon a few seconds to settle (sleep 3) after setup so the first frames the tape captures aren't a half-loaded UI.
- Specific harness features (a smith tool, codex output rendering, claude resume, …): spawn that harness with a representative prompt, e.g.
-
Inherit env into vhs, don't
Envit. vhs'sEnvdirective splits on whitespace, so aPATHwith colons errors out and writing oneEnvper variable is fragile. Export the env in the outer shell that invokesvhs; the spawnedttyd/bashinside the tape inherit it. Inside the tape, type the absolute path of the worktree'sconstructbinary instead of relying onPATH. -
Quote every string in the tape. vhs 0.11+ requires quoted values for
Output,Env, etc. Unquoted paths are parsed as command tokens and fail. -
Same script for both sides (when recording a pair). Wrap the recording in one shell driver invoked as
record.sh beforeandrecord.sh afterso the only difference between runs is which worktree's binaries are onPATH. Reference recipe (matrix-rain change — adapt the activity, sleeps, and output names to your case):#!/usr/bin/env bash set -euo pipefail VARIANT="${1:?usage: record.sh before|after}" BIN_DIR="/Users/you/agentd/.claude/worktrees/rain-${VARIANT}/target/debug" DEMO_DIR="/tmp/agentd-rain-demo-${VARIANT}" TAPE="/tmp/rain-${VARIANT}.tape" OUTPUT="/tmp/rain-${VARIANT}.mp4" rm -rf "$DEMO_DIR" mkdir -p "$DEMO_DIR/run" "$DEMO_DIR/state" "$DEMO_DIR/data" "$DEMO_DIR/config" export CONSTRUCT_RUNTIME_DIR="$DEMO_DIR/run" export CONSTRUCT_STATE_DIR="$DEMO_DIR/state" export CONSTRUCT_DATA_DIR="$DEMO_DIR/data" export CONSTRUCT_CONFIG_DIR="$DEMO_DIR/config" export CONSTRUCT_SHELL_BIN="/bin/bash" # adapter discovery export PATH="$BIN_DIR:$PATH" "$BIN_DIR/construct" daemon run >"/tmp/rain-${VARIANT}-daemon.log" 2>&1 & DAEMON_PID=$! trap 'kill $DAEMON_PID 2>/dev/null || true; wait $DAEMON_PID 2>/dev/null || true' EXIT for _ in $(seq 1 50); do "$BIN_DIR/construct" ping >/dev/null 2>&1 && break; sleep 0.2; done SESSION_IDS=() for _ in 1 2 3; do SID=$("$BIN_DIR/construct" new shell | tr -d '[:space:]') [[ -n "$SID" ]] && SESSION_IDS+=("$SID") done sleep 1 NOISE='while true; do printf "Editing src/main.rs Reading tests/foo.rs Running tests\n"; sleep 0.3; done' for SID in "${SESSION_IDS[@]}"; do "$BIN_DIR/construct" send "$SID" "$NOISE"; done sleep 3 # let intensity ramp + the reveal queue prime cat >"$TAPE" <<TAPE_EOF Output "${OUTPUT}" Set FontSize 14 Set Width 1600 Set Height 800 Set TypingSpeed 30ms Type "${BIN_DIR}/construct" Enter Sleep 30s Ctrl+X Ctrl+C Sleep 500ms TAPE_EOF vhs "$TAPE"For a single "after"-only recording, drop the
VARIANTargument and the second invocation. -
Verify before posting. Extract a midpoint frame from each video (
ffmpeg -ss 00:00:15 -i out.mp4 -vframes 1 mid.png) and confirm the TUI rendered the change you're trying to show. If the first attempt missed the moment (the rain panel was idle, a popup was open, etc.) re-record — don't ship a video that doesn't actually demo the diff. -
Attach to the PR. Drop the file(s) into the PR with
gh pr comment <n> --body "..."(or upload via the web UI). For a pair, label thembeforeandafterand link the commit each was recorded from.
Hot-reloading the web UI (dev only)
crates/daemon/assets/index.html + static/* are include_str!/include_bytes!'d into the daemon, so a naive edit needs a recompile + restart + browser reconnect. To skip all that, point a running debug daemon at a worktree's assets directory and it serves them from disk, with a live-reload poller injected (browser auto-refreshes on save):
- From a dev session (any worktree): call the
webui_hot_reloadMCP tool withdir: "<worktree>/crates/daemon/assets"(debug builds only).dir: nullreverts to the embedded assets. - At boot:
CONSTRUCT_ASSETS_DIR=<dir>(honored in debug builds only). - Programmatically: the
dev.set_assetsIPC method /client.dev_set_assets().
Edit index.html → save → the browser reloads itself (the injected poller watches /dev/version, a combined mtime of the served files). No rebuild, no daemon restart. Release builds ignore all of this and always serve the embedded, tamper-proof assets.
Design specs
Use specs/ for durable design decisions that future agents must preserve while changing agentd. These are normative records of intent and constraints, not code navigation notes.
- Add or update a spec when a change creates, changes, or depends on a key design decision. This includes architecture, abstraction boundaries, UX semantics, persistence behavior, protocols/events, harness contracts, operational conventions, and cross-client behavior. Do not add a spec for routine implementation details or one-off bug fixes unless the fix establishes a reusable rule.
- Keep each spec focused on one decision. Prefer many small files over a broad document that mixes unrelated rules.
- Name specs with a stable sequence and slug. Use
specs/NNNN-title-kebab-case.md. Ifspecs/does not exist yet, create it in the same PR as the first spec. - Document intent, not current code. Avoid file paths, function names, line numbers, or implementation anchors that will go stale. Agents should inspect the current codebase separately after reading the spec.
- Update existing specs when behavior changes. Do not leave conflicting design records in place. Mark old decisions as
supersededor edit them when the decision itself has evolved.
Use this format:
# NNNN-title-kebab-case
Status: accepted | proposed | superseded | deprecated
Date: YYYY-MM-DD
Area: architecture | protocol | ux | persistence | harness | cli | tui | webui | convention
Scope: one sentence
## Decision
The rule or design choice that should remain true.
## Reason
Why this decision exists: user need, system constraint, failure mode, tradeoff, or product intent.
## Consequences
What future changes must preserve, what tradeoffs are accepted, and what this decision makes harder.
## Non-Goals
Optional. Boundaries that prevent overgeneralizing the decision.
## Examples
Optional. Concrete behavior examples without relying on current file names or function names.
The minibuffer is just another session
Most TUIs make the bottom command bar a special UI primitive. We don't — it's a regular smith session, persisted on disk like any other. Differences:
- Hidden from the list.
kind: SessionKind::Orchestratorfilters it out oflist_items. - Auto-created.
SessionManager::ensure_orchestrator()runs at daemon start. - Rendered in the bottom strip. Same
ItemHistory::replaypipeline as the main view, just a different Rect. - Specialized system prompt. Smith branches on
CONSTRUCT_SESSION_KINDto act as the fleet dispatcher instead of a worker. - Subscribes to fleet events. A second IPC connection turns other sessions'
Status{AwaitingInput|Errored|Done}andToolApprovalRequestintoOBSERVATION:messages the orchestrator can react to. - Approvals render inline in the PTY. No global minibuffer preempt — the panel is the PTY.
Everything else — slash commands, tool-block expand/collapse, input queue during turns, persistence across daemon restart, automode, resume — works identically to any smith session, because the minibuffer is one. Add minibuffer features as session features.
Rendering across resize and restart
- Resize is instant. No full-history replay. Older content keeps its original line wraps; new content uses the new width.
- History survives daemon restart. When a harness can resume silently, the prior scrollback stays visible. When a harness must repaint itself on resume, the daemon hands it a clean slate instead — partial reuse leaves the terminal half-rendered.
- Sessions come back at the size the user last had. A resumed session must render at the user's current dimensions on the very first frame, not at a creation-time default.