The full reference
Every tool in the stack.
The seven headline products are how the stack introduces itself — but nothing is held back. This is the
complete index of 71 command-line tools a person actually invokes, grouped by what they do. Search to jump straight to
one, or click any tool to
expand its captured --help — flags, options, and
examples.
/ to focus ↑↓ to move ↵ to jump
Tasks & Coordination
The coordination layer — where agents get work, message each other, and land it.
JAT - AI Agent Task IDE
USAGE:
jat Launch the IDE (production build, default)
jat --dev Launch the IDE with HMR dev server
jat <project> Launch a project's environment (own IDE session/port)
jat demo Launch demo environment
jat list Show all projects
jat init Auto-detect projects in ~/code
Each project's IDE runs in its own tmux session on its own port, so
'jat <project>' never disturbs another project's running IDE. Restart a
specific IDE with jat-ide-restart --session <name> --port <port>.
FLEET VERBS (every verb takes --json and --dry-run):
jat task create|update|close|label|show
jat agent spawn|respawn|retire|nudge|message|resume
jat overseer launch <epic…> [--autonomy L0-L3] [--max N]
jat review show|approve|send-back
jat decide list|show|raise|answer
jat mesh status [--host H]
QUICK START:
jat init Auto-detect and add projects
jat Launch IDE to manage agents
jat --dev Use when editing the IDE itself (hot reload)
PROJECT MANAGEMENT:
init Auto-detect projects in ~/code
add <path> [name] Add a project
join-project <name> --postgres-url <url> [--path PATH]
Join a graduated (Postgres) project
remove <name> Remove project
list Show all projects with status
UPDATES:
update Pull latest JAT updates
update --check Check for updates without installing
update --status Show installation path and version
update --disable Disable automatic update checks
update --enable Enable automatic update checks
DEMO MODE:
demo Launch demo with sample projects
demo setup Fresh setup (cleans & recreates)
demo on/off Toggle demo visibility
SCHEDULER:
scheduler Show scheduler status (default)
scheduler start Start the scheduler daemon
scheduler stop Stop the scheduler daemon
scheduler restart Restart the scheduler daemon
scheduler logs Attach to scheduler session
SESSION MANAGEMENT:
resume <agent> Resume session by agent name (e.g., CrystalTundra)
resume <id> Resume session by ID (partial OK)
resume --list List recent sessions
find-session <agent> Find session ID for an agent
-r Shortcut for resume
-fs Shortcut for find-session
ADVANCED:
cleanup Remove stale agents
config Show configuration
CONFIG: ~/.config/jat/projects.json jt - JAT task CLI
Usage: jt <command> [options]
Commands:
create "title" [--title X] [--type X] [--priority N] [--description "..."]
[--labels "a,b"] [--deps "id1,id2"] [--assignee "X"]
[--files "glob1,glob2"] [--parent EPIC_ID]
[--model low|medium|high|max] [--json]
Run 'jt create --help' for all options
update TASK_ID [--status X] [--priority N] [--assignee X] [--title X]
[--description "..."] [--notes "..."] [--labels "a,b"]
[--files "glob1,glob2"] [--type X] [--reason "..."] [--json]
[--no-reason-comment]
[--metadata "JSON"] [--metadata-unset key1,key2]
[--uat | --no-uat]
--metadata stores a JSON object as task metadata;
--metadata-unset clears top-level metadata keys.
--uat marks the task for human validation: it lands, then
parks at submitted + awaiting-validation until accepted.
--reason is REQUIRED when --status is waiting/blocked or
--labels adds a blocked:* label (see shared/hold-labels.md).
--no-reason-comment records the reason without posting it
as a comment — only for callers that already wrote it to
the task thread themselves (e.g. jat-signal waiting)
close TASK_ID [--reason "..."] [--json] [--approve ACTOR]
[--superseded-by OTHER_ID] (--dup-of alias)
--superseded-by: the task was resolved by ANOTHER task —
legal only when that task is closed/accepted/deployed with
its commits on master, and you are not this task's assignee
accept TASK_ID [--reason "..."] [--json]
UAT verdict: a submitted/awaiting-validation task → closed
reject TASK_ID --reason "..." [--json]
UAT verdict: → open, label cleared, reason posted as an
internal note naming the agent that did the work
escalate TASK_ID --reason REASON [--detail JSON] [--from-session S]
[--from-agent A] [--no-spawn] [--cost N] [--json]
Advance a capability-routed task to its next rung and
record the escalation. Run 'jt escalate --help'.
delete TASK_ID
show TASK_ID [--json] [--project KEY]
Bare ids resolve to their project from the id prefix;
--project pins it explicitly.
list [--status X] [--type X] [--assignee X] [--limit N] [--json]
[--snoozed | --include-snoozed] --status open hides live snoozes
ready [--json] [--limit N] (never lists a snoozed task)
queued [--json] Open tasks an overseer has committed
to run (derived from live claims)
snooze TASK_ID <1d|2bd|2w|tomorrow|mon|YYYY-MM-DD> Hide until the wake date
(workspace timezone); status stays open. Label: snoozed:<wake date>
snooze --wake [--dry-run] [--json] Clear expired snoozes + notify
unsnooze TASK_ID Clear a snooze now
search "query" [--status X] [--type X] [--labels "a,b"]
[--updated-after DATE] [--limit N] [--json]
dep add TASK_ID DEPENDS_ON_ID
dep remove TASK_ID DEPENDS_ON_ID
dep tree TASK_ID [--reverse] [--json]
dep cycles
epic-status EPIC_ID [--json] Ground-truth epic state (DB + signals + tmux)
data tables | schema | query | exec | create | drop | columns
bases list | show | create | delete | attach | detach | render | search
sessions search <keyword> [--project NAME] [--limit N] [--json]
Search Claude Code session history by keyword
spawn [TASK_ID] [--remote] [--local] [--model M] [--attach]
agents [--json] [--project X] [--all] List active agents
logs <agent-name> [--remote] [--follow] [--lines N] Stream agent output
ssh [agent-name] Connect to VPS or attach to remote agent session
audit [--json] [--fix] Check project context injection compliance
graduate [PROJECT] --url <postgres-url> [--dry-run] [--yes] [--force]
Migrate project tasks from SQLite to Postgres
provision [PROJECT] [--admin-user U] [--via-docker TARGET]
Provision a postgres database on the shared
server + role, and mints <project>-postgres-url
(wraps jat-provision-postgres; host-side)
downgrade [PROJECT] [--dry-run] [--yes] [--force] [--confirm PHRASE]
Reverse graduation: Postgres → local SQLite
init [--quiet]
Create the local SQLite task store (sqlite
projects only — refused for postgres-backed or
unregistered projects)
Options:
--version Show version
--help Show this help
Status values: open, in_progress, waiting, blocked, submitted, accepted, deployed, closed, dev
Task types: bug, feature, task, epic, chore jat-send — Write a steering message to an agent's mailbox, or post to a channel.
Usage:
jat-send <agent> "text" # kind=steer (default)
jat-send <agent> --complete # kind=complete
jat-send <agent> --answer "x" # kind=answer (label or 0-based index)
jat-send <agent> --nudge # kind=nudge (wake-only, no text payload)
jat-send --channel <name> "text" # post to a channel room (no DM)
Channel mode (--channel):
Posts the message to the named channel in ~/.agent-mail.db instead of an
individual mailbox. The channel must exist (create with `jat-channel create`).
Sender is resolved the same way as DMs (--from > AGENT_NAME > tmux session).
Agent members of the channel receive a mailbox nudge so they drain and see
the post; the nudge text includes the channel name and author.
Mailbox format: ~/.config/jat/mailbox/<agent>.jsonl
Each line is a self-contained JSON object (never split across lines):
{ "id": "<uuid>", "ts": "<iso8601>", "from": "<sender>",
"kind": "steer|complete|answer|nudge",
"text": "...", # steer/answer only
"taskId": "<id>" } # optional; set via --task or TASK_ID env
Drain-and-ack protocol (consumer side):
1. Acquire a drain lock: flock -n ~/.config/jat/mailbox/<agent>.lock
2. Read all lines from <agent>.jsonl into memory.
3. Append those lines to <agent>.consumed.jsonl (O_APPEND atomic write).
4. Truncate <agent>.jsonl to zero bytes (> <agent>.jsonl).
5. Release lock and process the messages in memory order.
Consumed messages are NEVER deleted — <agent>.consumed.jsonl is forensics.
Exactly-once: the drain lock ensures a concurrent jat-send between steps 3
and 4 lands in the live file and is picked up on the next drain cycle.
Concurrency guarantee:
This tool writes exactly one JSON line per invocation using a single
O_APPEND write (via >> redirection after building the full line in a
variable). POSIX guarantees that O_APPEND writes up to PIPE_BUF bytes
are atomic on regular files — a single line stays intact even under
concurrent jat-send calls. Lines are kept ≤ 4095 bytes to stay inside
PIPE_BUF on Linux (4096) and macOS (512 bytes pipe buf, but filesystem
O_APPEND is page-atomic). Text longer than ~3800 chars is accepted but
the caller should be aware this guarantee weakens on extremely long lines
(>PIPE_BUF) — in practice steer messages are well under this limit.
From identity resolution (--from flag > AGENT_NAME env > jat-* tmux session
name > overseer-<pid>). This must never fail an urgent steer on an
unresolvable identity.
Options:
--channel <name> Post to a channel room instead of a DM mailbox
--complete Send a completion trigger (kind=complete, no text);
--task is required (an approval with no task ID was read
as a blanket approval for every task the agent held)
--reviewed <sha> With --complete: pin the approval to the land-branch
commit it reviewed — mandatory for gated work
--answer <x> Send an answer (kind=answer; x = label text or 0-based index)
--nudge Send a minimal wake token (kind=nudge, no text)
--task <id> Attach task ID to the message (default: $TASK_ID env)
--from <name> Override sender identity
--mailbox-dir <d> Override mailbox root (default: ~/.config/jat/mailbox)
--no-wake Same as JAT_SEND_NO_WAKE=1 (forwarded on a cross-box hop)
--line-b64 <b64> Node-side relay only: write a pre-encoded line directly
--signed-line-b64 <b64>
Node-side relay only: write a pre-signed line (verified
against the oracle for its recipient and kind)
--dry-run Print the JSON line without writing it
--no-verify Skip the post-wake drain WAIT (bulk senders). The
mail-deaf preflight still runs — see below.
--help Show this help
Delivery verification:
Writing the mailbox and firing the wake token proves nothing about DELIVERY.
If the target project does not register worker-inbox-trigger.sh on
UserPromptSubmit, the wake token is executed as a shell command, hits
jat-inbox's bare-invocation HOOK MODE, reads empty stdin and exits 0 — the
message is never surfaced and the sender sees a cheerful "woke <Agent>".
So after waking, verify in two stages:
1. PREFLIGHT (always, instant) — does any settings file governing the
target project register worker-inbox-trigger.sh on UserPromptSubmit?
No => the message can never arrive: exit 3 with remediation. This runs
even under --no-verify, so opting out of the wait never buys silence.
2. CONFIRMATION (opt-out, up to 3s) — poll the mailbox for this message
id. Drained => confirmed. Still queued => the agent is busy mid-turn
and its Stop-hook drain will collect it: report and exit 0.
Environment:
JAT_SEND_NO_WAKE=1 Write the mailbox but never send the wake token
JAT_SEND_NO_VERIFY=1 Same as --no-verify
JAT_SEND_VERIFY_TIMEOUT Seconds to wait for the drain (default: 3)
Exit codes:
0 message written / posted (delivery confirmed, or pending on a live agent)
1 usage error
3 message written but UNDELIVERABLE — target project registers no mailbox
drain hook. The message sits in the mailbox until that is fixed.
Cross-box delivery:
The sender resolves the target's live session; if the agent's pane runs on
another mesh node, the pre-signed message is delivered over SSH on that
box, so no separate nudge is needed or possible — the receiving harness
submits it at its next turn-end drain. An approval (kind=complete) is
additionally recorded on the control plane so a close gate on any node
can see it. Suppress that with JAT_SEND_NO_RECORD=1. jat-inbox — Drain an agent's mailbox and dispatch owner control verbs.
Called by worker-inbox-trigger.sh (UserPromptSubmit hook) when the "jat-inbox"
trigger token arrives. Can also be invoked standalone for testing.
Usage (hook mode):
echo '<hook-json>' | jat-inbox
stdin: Claude Code hook JSON { "session_id": "...", "prompt": "..." }
stdout: Claude Code hookSpecificOutput JSON
Usage (standalone / test):
AGENT_NAME=TestAgent jat-inbox --drain-only
→ drains mailbox, prints messages to stdout, no hook JSON wrapper
Control verb interception:
Any drained message whose body_md or text starts with (or contains) a
recognised !verb is intercepted by control-verbs.sh BEFORE being delivered
to Claude. Owner-only gate: verb from a non-operator sender is logged and
silently dropped. jat-search - Unified search across tasks, memory, files, and sessions
Usage:
jat-search "query" Meta search (all sources)
jat-search tasks "query" [options] Deep task search (FTS5 + vector)
jat-search memory "query" [options] Memory search (FTS5 + vector)
jat-search files "query" [options] File search (ripgrep + filename)
jat-search sessions "query" [options] Claude session history (ripgrep)
Options:
--project PATH Project path (tasks/memory/files: default cwd;
sessions: default ALL projects, narrows to one)
--limit N Max results (meta: 5/source, individual: 10)
--json JSON output (default for meta search)
--summarize LLM synthesis of meta results
--verbose Debug info to stderr
--help Show this help
Examples:
jat-search "authentication" Search everything
jat-search tasks "OAuth timeout" --json Search tasks only
jat-search memory "browser automation" Search memory
jat-search files "searchTasks" Search file contents
jat-search sessions "isolated-index" Find a Claude session by what
you said, then claude --resume
jat-search "auth" --summarize Meta search with LLM synthesis
Sessions search greps every ~/.claude/projects/**/*.jsonl transcript and
returns matching sessions newest-first with a context snippet and a ready
`claude --resume <id>` command — for recovering a session you crashed out of. jat-skills - Skill catalog, installer, and local management
CATALOG:
jat-skills search <query> Search catalog by keyword
jat-skills list-available List all catalog skills
jat-skills info <skill-id> Show skill details
jat-skills refresh Force refresh the catalog cache
jat-skills sources Show source status
INSTALL & MANAGE:
jat-skills install <name-or-url> Install a skill locally
jat-skills install <dir-source> [--id NAME] [--force]
Install a MULTI-FILE skill (whole directory)
jat-skills list List installed skills (warns on broken links)
jat-skills enable <name> Enable an installed skill
jat-skills disable <name> Disable an installed skill
jat-skills uninstall <name> Remove an installed skill
jat-skills update <name> Re-fetch skill from source
jat-skills sync Sync agent links (prunes dead store links)
jat-skills fleet-sync [--check] Apply the fleet's pinned third-party
skill set (run by install.sh)
OPTIONS:
jat-skills doctor Verify every skill link and registry
entry; exit 1 naming each broken one
jat-skills --json Output as JSON (combine with other commands)
jat-skills --help Show this help
EXAMPLES:
jat-skills search github # Find GitHub-related skills
jat-skills install git-commit # Install from catalog by name
jat-skills install https://github.com/user/repo # Install from URL
jat-skills list # Show installed + status
jat-skills disable git-commit # Disable without removing
jat-skills update git-commit # Re-fetch latest SKILL.md
INSTALL SOURCES:
- Catalog name Looks up skillMdUrl from catalog
- GitHub repo URL Fetches SKILL.md from repo root or subdirectory
- Raw URL Fetches SKILL.md directly
SKILL DIRECTORIES (searched by agents):
1. Project-level {project}/skills/
2. User-level ~/.config/jat/skills/
3. JAT built-in ~/code/jat/skills/
INSTALLED SKILLS:
Skills stored in ~/.config/jat/skills/{name}/SKILL.md
Registry at ~/.config/jat/skills/installed.json
AGENT LINKS (auto-synced on install/enable/disable/uninstall/update):
Claude Code ~/.claude/commands/{name}.md → SKILL.md symlink
Pi ~/.pi/agent/skills/{id}/ → skill dir symlink
Other agents receive skill info via prompt at spawn time $ am-register --help
Usage: am-register --name NAME [options]
Register or resume an agent identity in Agent Mail.
IMPORTANT: Agent names are globally unique across all projects.
If an agent name already exists, this command will resume that agent
(updating last_active timestamp) instead of creating a duplicate.
NOTE: IDE-spawned agents are pre-registered by the spawn API.
This command is for manual/CLI use only.
Options:
--name NAME Agent name (required, e.g., BlueLake)
--program PROGRAM Program name (default: claude-code)
--model MODEL Model name (default: sonnet-4.5)
--task TASK Task description (default: "")
--project PATH Project path (default: current directory)
--json Output as JSON
--help, -h Show this help
Examples:
am-register --name BlueLake
am-register --name FairBay --program cursor --model gpt-4 --task "Bug fixes"
am-register --name GoldDusk --json
Database: ~/.agent-mail.db
$ am-agents --help
Usage: am-agents [options]
List all agents registered in a project.
Options:
--project PATH Project path (default: current directory)
--json Output as JSON
--help, -h Show this help
Examples:
am-agents
am-agents --json
am-agents --project /path/to/repo
Database: ~/.agent-mail.db
$ am-whoami --help
Usage: am-whoami [options]
Show current agent identity and context information.
Options:
--agent AGENT Agent name (default: $AGENT_NAME environment variable)
--project PATH Project path (default: current directory)
--json Output as JSON
--help, -h Show this help
Environment:
AGENT_NAME Default agent name (overridden by --agent)
PROJECT_KEY Default project path
AGENT_MAIL_DB Database location
Examples:
am-whoami
AGENT_NAME=Alice am-whoami
am-whoami --agent Bob --json
Database: ~/.agent-mail.db jat commands
The jat IDE workflow — the slash commands and skills an agent runs to pull work, land it, verify it, and hand its context off cleanly.
/jat:compact (alias: /jat-compact) — structured work-context compaction for long agent sessions.
A generic /compact summarizes the transcript and hopes the important parts
survive. jat-compact does the opposite: before the window is truncated it
EXTERNALIZES the session's real working state to durable storage, so nothing
load-bearing depends on surviving as prose.
WHAT IT EXTERNALIZES:
- Tasks touched or created, each with the reason it exists
- Knowledge-base blocks written this session
- People, credentials minted, agents still in flight
- The waiting-on-user list and operational lessons learned
- A short transfer note capturing where things stand
THEN it hands back a one-paragraph RELOAD KEY. Paste that after the
compaction (or in a fresh session) and full working context is restored from
the externalized files — not reconstructed from a lossy summary.
WHEN TO REACH FOR IT:
- A session is context-heavy and approaching its limit
- Before processing a large input (a long transcript, a big document)
- Any time you would otherwise say "compact" or "hand this off"
Runs inside the jat IDE session. Nothing leaves your machine. /jat:start — begin working on a task in the jat IDE.
Orients a freshly-spawned agent before it touches any code:
1. Reads the project's context and conventions
2. Pulls the next ready task off the shared queue (or the one it was
spawned for)
3. Claims the task's file lease so no sibling agent can edit the same files
4. Sets up its workspace and confirms what "done" means for the task
The result is an agent that knows what it is doing and cannot collide with
the rest of the fleet. Pairs with /jat:complete, which closes the same task
out through the verification gate. /jat:complete — finish the current task properly, then end the session.
Rather than trusting "looks done", it runs the full verification gate before
anything is marked complete:
- Builds the project and runs its checks / type-checks
- Drives the change in a real browser where the task touched UI, capturing
console + network evidence for review
- Writes a completion summary and closes the task
- Releases the file lease and ends the session cleanly
If the gate fails, the task stays open and the failure is reported instead of
silently passing. Session ends after a successful completion. /jat:verify — escalatory browser verification of a change.
Opens the app in a real browser and actually exercises the change end to end
instead of asserting from the code alone:
- Navigates the affected flow and interacts with it
- Captures console errors and network failures as they happen
- Escalates through progressively deeper checks until it can confirm the
change works, or surface exactly where it breaks
Produces an evidence bundle (screenshots, HAR, console log) that rides along
for a human or overseer to review. Used on its own, or as the browser leg of
/jat:complete. /jat:ask — ask a question about your own projects, answered locally.
The slash-command front door to jat-ask. It gathers context from your own
tasks, notes, memory, and recent activity, answers the question, and shows
the sources it drew from — all on local inference, so no project data leaves
your network.
USAGE:
/jat:ask what open tasks are blocking the voice feature?
/jat:ask summarize what changed in the billing flow this week
Good for reorienting after time away, or pulling a fact out of a large
project without grepping for it by hand. /jat:tasktree — turn a spec into a structured, dependency-wired task tree.
Feed it a PRD, a requirements doc, or a rough spec and it produces a task
tree the fleet can actually execute:
- Breaks the work into concrete tasks and sub-tasks
- Wires dependencies so nothing starts before its prerequisites
- Assigns priorities and, where useful, file partitions so tasks can run
in parallel without conflicting
The output lands in the shared task queue, ready for agents to pull via
/jat:start. This is how a document becomes a plan the fleet can run. /jat:fanout — spawn a wave of agents partitioned by file.
Given a set of tasks, it launches a wave of agents with the work partitioned
by file, so two agents can never edit the same file at the same time — the
core trick that lets a fleet run wide without stepping on itself.
WHAT YOU GET:
- Many tasks running in parallel, each in its own file lease
- No merge conflicts between wave members by construction
- A single notification when the whole wave has landed, so you wait once
instead of babysitting each agent
Best paired with /jat:tasktree, which produces the file-partitioned tasks a
fan-out consumes. /jat:design-campaign — run a best-in-class design skill across every route
of an app, at fleet scale (alias: /route-campaign).
This is the metaskill pattern jat is built for: take an excellent external
skill and drive a whole fleet of agents through it, with adjudication and
verification wrapped around the outside.
HOW IT RUNS:
- Scans the app's routes and spawns a manager agent per route group
- Each manager critiques its route (via the external `impeccable` design
skill), fixes what it finds, gap-lists to a perfect score, then fans out
file-partitioned builder agents to do the work without conflicts
- The invoking session acts as campaign manager — adjudicating findings,
sequencing stages, and holding the quality bar
COMPOSES: impeccable — jat supplies the orchestration, leases, and
verification; the outside skill supplies the design judgment. jat does not
reimplement the skill, it runs it across the whole codebase at once.
USAGE:
/jat:design-campaign all
/jat:design-campaign /dashboard Web & Browser
Drive a browser — your own consented session or the agent's headless one — down to a record-once API client, plus the domains and deploys around a web app.
pilot — drive YOUR browser from the CLI (consent-scoped, tabex-style)
START HERE
setup [--port N] One-time config + browser relaunch instructions (default port 9223)
status Connection + consent summary
tabs [filter] [-v] Scannable tab list (IDX ID EN HOST TITLE); [filter] narrows
by host/title/url; -v adds full URLs
pick [filter] Interactive fuzzy picker (fzf: arrows/mouse/type/Enter) → enables the tab
CONSENT
enable <tab> Enable a tab (index, id prefix, or url substring)
disable <tab> Revoke it
auto-attach add|rm|list [host] Standing rules: tabs on <host> always enabled
ACT (targeting: --tab ID|IDX, --match-url S, --match-title S, --open-url URL [--wait-timeout MS])
nav <url> Navigate the target tab
element list Visible interactive elements
element click --text "Save" Click by visible text / aria-label (preferred)
element fill --label "Email" --value X
page click|text|html [--selector CSS] CSS-level escape hatch
js "code" Page-context JS (return/await ok) — last resort
key "<combo>" Trusted keypress: "?", "Escape", "Control+K", "Control+Shift+Z" [--repeat N]
type "text" Trusted typing [--focus "Label"] [--delay MS]
wait --text S | --selector CSS | --ms N Block until condition [--timeout MS]
assert --text S | --not-text S | --selector CSS [--count N] Exit 3 if DOM doesn't match
reload | back | forward History navigation
shot [--output F] [--full] [--selector CSS] Screenshot (page or one element)
OBSERVE (consent-scoped, in-process)
console [--filter error|warn] [--duration MS] [--limit N] Console + runtime errors (incl. buffered)
net [--url S] [--failures] [--duration MS] [--limit N] Network activity over a window
snapshot [--selector CSS] [--max-depth N] Accessibility tree
monitor <tab> [--har] [--shots N] [--duration MS] [--no-net] [--url S]
LIVE-TAIL a tab (ssh-friendly): nav/console/errors/network as
timestamped lines + JSONL session log under ~/.local/state/jat/pilot-monitor/
replay <events.jsonl> [--realtime] Re-print a monitor session (paced with --realtime)
CAPTURE
record start|stop|status [--output F] Record traffic to HAR — enabled tabs only (via browser-har.js)
client <file.har> Derive a curl API client (via har2client.js)
Every command accepts --json. Acting on a non-enabled tab exits 2. Usage: browser-element.js list|click|fill [options]
list List visible interactive elements
click --text "Save" Click the element matching visible text / aria-label
fill --label "Email" --value "x@y.z" Fill the field matching its label / aria-label / name
Targeting:
--match-url <substr> Target the tab whose URL contains <substr>
--match-title <substr> Target the tab whose title contains <substr>
--open-url <url> Open a new tab at <url> and target it
--wait-timeout <secs> Timeout for --match-*/--open-url (default: 10)
--agent <name> Tag the tab-ownership registry with this agent
--task <id> Tag the tab-ownership registry with this task id
--port <number> Chrome DevTools port (default: $JAT_BROWSER_PORT or 9222)
--json Machine-readable output
Examples:
browser-element.js list
browser-element.js click --text "Save"
browser-element.js fill --label "Email" --value "jw@example.com" --match-url "/settings" Usage: browser-eval.js "<javascript>" [--json]
Execute JavaScript in the active page of the agent browser and print the
result. Supports multi-statement code with an explicit return:
browser-eval.js "const x = 5; const y = 10; return x + y"
browser-eval.js "return document.title"
Options:
--json Machine-readable result
Note: this tool has no --help flag (its first argument is the JS to run);
usage above is from the global-tools reference. Usage: browser-screenshot.js [--port <number>] [--output <path>] [--json]
Options:
--port <number> Chrome DevTools port (default: $JAT_BROWSER_PORT or 9222)
--output <path> Where to save the screenshot (default: a tmp file)
--match-url <substr> Target the tab whose URL contains <substr>
--match-title <substr> Target the tab whose title contains <substr>
--open-url <url> Open a new tab at <url> and target it
--wait-timeout <secs> Timeout for --match-*/--open-url (default: 10)
--agent <name> Tag the tab-ownership registry with this agent
--task <id> Tag the tab-ownership registry with this task id
--json Print {path: '...'} instead of the bare path Usage: browser-snapshot.js [options]
Options:
--selector <css> Snapshot only this element and its children
--max-depth <n> Maximum tree depth (default: 10)
--include-hidden Include hidden elements
Examples:
browser-snapshot.js
browser-snapshot.js --selector 'main'
browser-snapshot.js --max-depth 5 --include-hidden Usage: browser-console.js [options]
Options:
--follow, -f Follow console output (live stream)
--filter <type> Filter by type: log, warn, error, info, debug
--limit <n> Limit output to n messages (default: 100)
Examples:
browser-console.js
browser-console.js --follow --filter error
browser-console.js --limit 50 Usage: browser-network.js [options]
Options:
--follow, -f Follow network requests (live stream)
--type <type> Filter by resource type: xhr, fetch, document, script, stylesheet, image
--url <pattern> Filter by URL pattern (substring match)
--limit <n> Limit output to n requests (default: 100)
Examples:
browser-network.js
browser-network.js --follow --type xhr
browser-network.js --url 'api.example.com' --limit 50 Usage: browser-wait.js --<type> <value> [--timeout <seconds>]
Wait Types:
--text <text> Wait for text to appear on page
--selector <selector> Wait for CSS selector to exist
--url <url> Wait for URL to change to/contain value
--eval <expression> Wait for custom JavaScript expression to be truthy
Options:
--timeout <seconds> Maximum wait time for the condition above (default: 30)
--match-url <substr> Target the tab whose URL contains <substr>
--match-title <substr> Target the tab whose title contains <substr>
--open-url <url> Open a new tab at <url> and target it
--wait-timeout <secs> Timeout for --match-*/--open-url tab targeting (default: 10)
--agent <name> Tag the tab-ownership registry with this agent
--task <id> Tag the tab-ownership registry with this task id
--json Machine-readable output
Examples:
browser-wait.js --text "Login successful"
browser-wait.js --selector ".user-profile"
browser-wait.js --url "/dashboard"
browser-wait.js --eval "document.readyState === 'complete'"
browser-wait.js --text "Welcome" --timeout 60 Usage: browser-tabs.js <list|release|open> [options]
Commands:
list [--port N] [--json] List live tabs + ownership registry
release <targetId> [--port N] Clear ownership metadata for a tab (does not close it)
open <url> [--port N] [--agent NAME] [--task ID] [--wait-timeout SECS]
Open a new tab and record ownership
Options:
--port <number> Chrome DevTools port (default: $JAT_BROWSER_PORT or 9222)
--json Machine-readable output (list only)
The registry is advisory, not enforced — it's the browser twin of jat-lease-claim.
Other browser-*.js tools update it automatically via --match-url/--match-title/--open-url. Usage: browser-pick.js [--port <number>] 'message' [--json]
Interactive fuzzy picker (fzf) over the agent browser's visible interactive
elements — arrows/mouse/type/Enter — and enable the picked element's tab.
Options:
--port <number> Chrome DevTools port (default: $JAT_BROWSER_PORT or 9222)
--match-url <substr> Target the tab whose URL contains <substr>
--match-title <substr> Target the tab whose title contains <substr>
--open-url <url> Open a new tab at <url> and target it
--wait-timeout <secs> Timeout for --match-*/--open-url (default: 10)
--agent <name> Tag the tab-ownership registry with this agent
--task <id> Tag the tab-ownership registry with this task id
--json Print the picked element(s) as JSON
Example:
browser-pick.js "Click the submit button" Usage: browser-start.js [--port <number>] [--profile] [--json]
Launch a throwaway Chrome with the Chrome DevTools Protocol enabled — for
sessions that must not touch a persistent agent browser. Auto-picks a free
port via the browser registry unless --port is given.
Options:
--port <number> Chrome DevTools port (default: auto via jat-browser, fallback 9222)
--profile Copy your default Chrome/Chromium profile (cookies, logins)
--json Print a machine-readable summary instead of progress lines
Examples:
browser-start.js # Auto-assign port, fresh profile
browser-start.js --port 9223 # Use specific port
browser-start.js --profile # Start with your profile Usage: browser-har.js <start|stop|status> [options]
Record network traffic from the agent browser (CDP) to a HAR file.
Commands:
start Start a detached recorder attached to all tabs
stop Stop the recorder and finalize the HAR file
status Show whether a recorder is running and where it writes
Options:
--output FILE HAR output path (default: /tmp/jat-har-9222.har)
--filter SUBSTR Only record requests whose URL contains SUBSTR
--port PORT CDP port (default: $JAT_BROWSER_PORT or 9222)
--json Machine-readable output (start/stop/status)
Workflow:
browser-har.js start --output /tmp/ide.har
browser-nav.js http://localhost:3333/tasks # drive the app
browser-har.js stop
har2client.js /tmp/ide.har --output /tmp/ide-client.sh Usage: har2client.js FILE.har [options]
Derive a curl-based API client from a recorded HAR — record one browser session,
then skip the browser and call the API directly.
har2client.js /tmp/site.har --output /tmp/site-client.sh
/tmp/site-client.sh # list derived endpoints
/tmp/site-client.sh get_api_tasks 'status=open'
Options:
--output FILE Where to write the generated client script / spec
(default: <har-name>-client.sh)
--base URL Base origin (default: origin with the most API calls)
--filter SUBSTR Only include requests whose URL contains SUBSTR
--format bash Bash client (default); observed auth headers go to a
chmod-600 <client>.sh.env sidecar (client stays secret-free)
--format md Print just the endpoint map
--format openapi Emit an OpenAPI 3.0 spec (paths, params, inferred schemas)
--no-auth Skip writing observed auth headers to the .env sidecar
--all Include static assets (default: API requests only)
Filters out static assets and parameterizes ID-like path segments
(/api/tasks/{id}). Note: this tool has no --help flag (its first argument is
the HAR file); usage above is the tool's own usage output. Usage: jat-web <command> [args]
Web search + full-page reading for agents. Web access IS available even when
your harness has no WebSearch/WebFetch tool.
Commands:
search "QUERY" [--n N] [--answer] [--json] [--topic TYPE] [--days N]
Web search via Tavily. Default: numbered results (title, url, snippet).
--answer also print the synthesized answer
--topic news | general (default general); --days limits to recent news
--json raw result JSON
read URL [--md] [--raw] [--out FILE]
Full-page scrape to clean markdown via Firecrawl.
--raw full JSON (metadata included)
--out FILE write markdown to FILE instead of stdout
crawl URL [--limit N] [--out DIR] [--json]
Multi-page crawl via Firecrawl (default limit 20, max ~50).
Saves pages as NN-slug.md under DIR and prints the file list.
Auth (resolved at call time, never baked in):
TAVILY_API_KEY / FIRECRAWL_API_KEY env overrides, else jat-secret.
Cost note: Tavily and Firecrawl are METERED. Search once, read what you need —
do not loop. Defaults are capped (n=5 results, crawl limit 20).
Examples:
jat-web search "svelte 5 runes migration"
jat-web search "jatstack jat" --n 3 --answer
jat-web read https://svelte.dev/docs/svelte/$state --out state.md
jat-web crawl https://example.com/docs --limit 10 jat-browser - Browser session registry for multi-agent CDP coordination
USAGE:
jat-browser claim <port> --agent NAME --task ID [--pid PID] [--project NAME]
jat-browser release <port>
jat-browser release-agent <name>
jat-browser status [--json]
jat-browser list [--json]
jat-browser available [--json]
jat-browser cleanup
jat-browser flows [--json] [name]
jat-browser help
SUBCOMMANDS:
claim Claim a port for an agent (fails if taken and not stale)
release Release a specific port
release-agent Release all ports owned by an agent
status Show all claims (runs stale cleanup first)
list Probe ports for LIVE CDP browsers (version + tab count)
available Print next free port in 9222-9231 range
cleanup Remove stale entries (dead PID or closed port)
flows Machine-readable recipes for canonical browser workflows
help Show this help
OPTIONS (for claim):
--agent NAME Agent name (required)
--task ID Task ID (required)
--pid PID Chrome process PID (default: auto-detect from port)
--project NAME Project name (optional)
OPTIONS (for status/list/available/flows):
--json Machine-readable output
EXAMPLES:
jat-browser claim 9222 --agent FairBay --task jat-abc
jat-browser release 9222
jat-browser release-agent FairBay
jat-browser status
jat-browser list --json
jat-browser available
jat-browser flows
jat-browser flows record-derive-client --json
PERSISTENT BROWSER:
A systemd user service keeps a persistent agent browser on port 9222.
'jat-browser list' marks it clearly so you don't release/kill it.
REGISTRY:
/tmp/jat-browser-sessions.json (cleared on reboot = clean state)
PORT RANGE:
9222-9231 (10 ports for Chrome DevTools Protocol) Usage: jat-run-workspace.js <start|stop|status|screenshot|list> --task <id> [options]
Per-task evidence workspace: network HAR + console log + screenshots.
Commands:
start Start recording network (browser-har.js) + console for a task
stop Stop recording, finalize the HAR + console log, write summary.json
status Show whether a recording is active for a task
screenshot Save a screenshot into the task's run dir
list List the files captured for a task
Options:
--task <id> Task ID — determines .jat/runs/<id>/ (required)
--label NAME Free-text label recorded in meta.json (start only)
--filter SUBSTR Only record requests whose URL contains SUBSTR (start only)
--port PORT CDP port (default: $JAT_BROWSER_PORT or 9222)
--json Machine-readable output (start/stop/status)
Workflow:
jat-run-workspace.js start --task jat-abc
browser-nav.js http://localhost:3333/tasks # drive the app
jat-run-workspace.js screenshot --task jat-abc --label after-save
jat-run-workspace.js stop --task jat-abc
jat-runtime-verify --task jat-abc # auto-reads the captured HAR jat-deploy - Trigger a Coolify deploy for a JAT project
USAGE:
jat-deploy [PROJECT] [--watch] [--api-url URL]
ARGUMENTS:
PROJECT Project name. Defaults to the current git repo's basename.
OPTIONS:
--watch Poll deployment status until finished/failed, then exit.
--api-url URL Override the Coolify API base URL.
-h, --help Show this help.
SECRETS (via jat-secret):
{project}-coolify-app-uuid Application UUID from Coolify dashboard.
coolify-api-token Coolify API token (Bearer auth).
EXAMPLES:
jat-deploy # deploy current project, fire-and-forget
jat-deploy myapp --watch # deploy myapp and stream build status
jat-deploy --api-url http://other:8000 some-app
The API URL must be set via projects.json (projects.{project}.coolify_api_url) or --api-url. Usage: porkbun <command> [args]
Commands:
ping Test API credentials
check DOMAIN [DOMAIN...] Check availability + price
buy DOMAIN [--yes] [--dry-run] Register a domain (quotes price first;
asks for confirmation unless --yes)
list List all domains in the account
dns list DOMAIN List DNS records
dns add DOMAIN TYPE NAME CONTENT [--ttl N]
Add a record. NAME "" or "@" = apex.
e.g. porkbun dns add example.com A @ 203.0.113.10
dns rm DOMAIN ID Delete a record by id (from dns list)
Environment:
PORKBUN_API_KEY / PORKBUN_SECRET_KEY (auto-loaded from jat-secret
porkbun-api-key / porkbun-secret-key if unset)
Notes:
- Each domain needs "API Access" enabled in the Porkbun dashboard
(or flip "Opt In All Domains" once in Account > API Access).
- buy requires verified account email+phone and payment on file.
- Some TLDs (.us .ca .eu .au) are not API-registerable. Usage: godaddy <command> [args]
Commands:
ping Test API credentials (lists domain count)
list List all domains in the account
dns list DOMAIN [TYPE] List DNS records (optionally filter by TYPE)
dns get DOMAIN TYPE NAME Get specific record(s)
dns set DOMAIN TYPE NAME CONTENT [--ttl N]
Replace record(s) of TYPE+NAME. NAME "@" = apex.
e.g. godaddy dns set example.com TXT @ "v=spf1 ..."
NOTE: GoDaddy PUT replaces ALL records of that TYPE+NAME.
dns add DOMAIN TYPE NAME CONTENT [--ttl N]
Append a record (fetches existing + adds, then PUTs the set).
mail-audit DOMAIN Print SPF / DKIM selectors / DMARC / MX for the domain
export DOMAIN Dump all DNS records as JSON (for migration)
Environment:
GODADDY_API_KEY / GODADDY_API_SECRET (auto-loaded from jat-secret
godaddy-api-key / godaddy-api-secret if unset)
Notes:
- Get keys at developer.godaddy.com > API Keys > Production (needs a
domain in the account; the Production key works on api.godaddy.com).
- GoDaddy's DNS PUT is REPLACE-by-(type,name); `dns add` reads-modifies-writes
to avoid clobbering. Always `dns list` first. Usage: cloudflare <command> [args]
Commands:
ping Verify the API token
list List zones (domains) in the account
dns list DOMAIN List DNS records for a zone
dns add DOMAIN TYPE NAME CONTENT [--ttl N] [--proxied]
Add a record. NAME "" or "@" = apex.
e.g. cloudflare dns add example.com A @ 203.0.113.10 --proxied
dns rm DOMAIN ID Delete a record by id (from dns list)
Environment:
CLOUDFLARE_API_TOKEN (auto-loaded from jat-secret cloudflare-api-token,
then jat-secret cloudflare, if unset)
Notes:
- Token needs Zone:Read for list, Zone:DNS:Edit for dns add/rm.
- Registrar operations (buy/renew) are not in Cloudflare's public API;
use the dashboard for registrations. Media
Generate and edit images, video, and speech — cloud models where they shine, local + unfiltered where privacy matters — plus the first-party ffmpeg engine that assembles them.
$ gemini-image --help
Usage: gemini-image "PROMPT" [OUTPUT] [options]
Generate an image from a text prompt using Gemini.
Arguments:
PROMPT Text description of the image to generate (required)
OUTPUT Output file path (default: ~/Pictures/gemini-TIMESTAMP.png)
Options:
--model MODEL Model to use (default: gemini-3.1-flash-image-preview)
Available: gemini-3.1-flash-image-preview (fast, Nano Banana 2)
gemini-3-pro-image-preview (quality, 4K)
--aspect RATIO Aspect ratio: 1:1, 16:9, 9:16, 4:3, 3:4, etc.
--size SIZE Image size: 1K, 2K, 4K (4K requires pro model)
--help, -h Show this help
Examples:
gemini-image "A sunset over mountains"
gemini-image "Product photo of a coffee mug" product.png --aspect 1:1
gemini-image "Detailed art" art.png --model gemini-3-pro-image-preview --size 2K
Environment:
GEMINI_API_KEY Required. Your Gemini API key.
$ gemini-edit --help
Usage: gemini-edit INPUT "INSTRUCTION" [OUTPUT] [options]
Edit an existing image with a text instruction using Gemini.
Arguments:
INPUT Path to input image file (required)
INSTRUCTION Text instruction for editing (required)
OUTPUT Output file path (default: ~/Pictures/gemini-edit-TIMESTAMP.png)
Options:
--model MODEL Model to use (default: gemini-3.1-flash-image-preview)
--help, -h Show this help
Examples:
gemini-edit photo.png "Remove the background"
gemini-edit portrait.jpg "Make it look like a Van Gogh painting" art.png
gemini-edit product.png "Add a subtle shadow" --model gemini-3-pro-image-preview
Environment:
GEMINI_API_KEY Required. Your Gemini API key.
$ gemini-compose --help
Usage: gemini-compose IMAGE1 IMAGE2 [...] "INSTRUCTION" [options]
Combine multiple images using a text instruction with Gemini.
Arguments:
IMAGE1, IMAGE2, ... Input image files (2-14 images required)
INSTRUCTION Text instruction for composition (required)
Options:
--output PATH Output file path (default: ~/Pictures/gemini-compose-TIMESTAMP.png)
--model MODEL Model to use (default: gemini-3.1-flash-image-preview)
--help, -h Show this help
Detection:
Arguments that exist as files on disk are treated as images.
The first argument that doesn't exist as a file is the instruction.
Examples:
gemini-compose bg.png person.png "Place person on background"
gemini-compose style.jpg photo.jpg "Apply art style" --output styled.png
gemini-compose a.png b.png c.png "Combine into collage"
Environment:
GEMINI_API_KEY Required. Your Gemini API key. $ openai-image --help
Usage: openai-image "PROMPT" [OUTPUT] [options]
Generate an image from a text prompt using OpenAI gpt-image-2.
Arguments:
PROMPT Text description of the image to generate (required)
OUTPUT Output file path (default: ~/Pictures/openai-TIMESTAMP.png)
Options:
--model MODEL Model to use (default: gpt-image-2)
Available: gpt-image-2 (latest), chatgpt-image-latest,
gpt-image-1.5, gpt-image-1, gpt-image-1-mini,
dall-e-3, dall-e-2
--size SIZE Image size (default: 1024x1024)
gpt-image-1: 1024x1024, 1536x1024, 1024x1536, auto
dall-e-3: 1024x1024, 1792x1024, 1024x1792
--quality QUALITY Quality level (default: auto)
gpt-image-1: low, medium, high, auto
dall-e-3: standard, hd
--format FORMAT Output format: png, jpeg, webp (default: png)
--n N Number of images to generate (default: 1, max: 10 for gpt-image-1)
--help, -h Show this help
Examples:
openai-image "A misty Oregon forest at dawn"
openai-image "Brand moodboard card" card.png --size 1536x1024 --quality high
openai-image "Typography specimen" type.png --format webp --quality medium
Environment:
OPENAI_API_KEY Required. Auto-loaded from jat-secret if available.
$ openai-edit --help
Usage: openai-edit INPUT "INSTRUCTION" [OUTPUT] [options]
Edit an existing image with a text instruction using OpenAI gpt-image-1.
Arguments:
INPUT Path to input image (required)
INSTRUCTION Edit instruction (required)
OUTPUT Output file path (default: ~/Pictures/openai-edit-TIMESTAMP.png)
Options:
--model MODEL Model to use (default: gpt-image-2)
--mask PATH Optional mask image (transparent areas = edit region)
--size SIZE Output size: 1024x1024, 1536x1024, 1024x1536 (default: auto)
--quality QUALITY Quality: low, medium, high, auto (default: auto)
--format FORMAT Output format: png, jpeg, webp (default: png)
--n N Number of variations (default: 1)
--help, -h Show this help
Examples:
openai-edit photo.png "Remove the background" clean.png
openai-edit logo.png "Make it look weathered and vintage" vintage.png
openai-edit card.png "Change the background to a misty forest" --quality high
Environment:
OPENAI_API_KEY Required. Auto-loaded from jat-secret if available.
$ openai-brand-image --help
Usage: openai-brand-image "BRIEF" [OUTPUT] [options]
Generate an image grounded in the project's Design Context knowledge base.
The full KB content is prepended to your brief before sending to gpt-image-1.
Arguments:
BRIEF What you want (e.g. "a moodboard card, landscape")
OUTPUT Output file path (default: ~/Pictures/openai-brand-TIMESTAMP.png)
Options:
--kb KB_ID Knowledge base ID to use (default: auto-detect via jt bases list)
--kb-content FILE Use a file as context instead of a knowledge base
--model MODEL Model (default: gpt-image-2)
--size SIZE Size: 1024x1024, 1536x1024, 1024x1536 (default: 1536x1024)
--quality QUALITY Quality: low, medium, high, auto (default: high)
--format FORMAT Output format: png, jpeg, webp (default: png)
--n N Number of images (default: 1)
--dry-run Print the full prompt without generating
--help, -h Show this help
Examples:
openai-brand-image "hero moodboard, landscape, no text" card.png
openai-brand-image "typography specimen card" type.png --size 1024x1024
openai-brand-image "color palette swatch card" --dry-run
Environment:
OPENAI_API_KEY Required. Auto-loaded from jat-secret if available. $ kling-video --help
Usage: kling-video "PROMPT" [OUTPUT] [options]
Generate a video with Kling 2.1 on fal.ai. Submits the task, polls until
complete, downloads the MP4 to OUTPUT.
Arguments:
PROMPT Text description of the video (required)
OUTPUT Output file path (default: ~/Videos/kling-TIMESTAMP.mp4)
Options:
--model MODEL pro (default) | standard | master
--duration SECS 5 or 10 (default: 5)
--image URL Image URL for image-to-video (first frame reference)
--tail-image URL Image URL for the LAST frame (pro/master i2v only)
--aspect RATIO 16:9 | 9:16 | 1:1 (t2v only; i2v follows the image)
--negative TEXT Negative prompt (default: "blur, distort, and low quality")
--cfg N Guidance scale 0-1 (default: 0.5)
--timeout SECS Max seconds to wait for generation (default: 900)
--json Print the final result JSON instead of a summary line
--help, -h Show this help
Examples:
kling-video "model walks toward camera, studio lighting" out.mp4 --image https://cdn.site/ref.png
kling-video "slow dolly-in on product" hero.mp4 --model standard --duration 10
Provider: fal.ai (https://queue.fal.run/fal-ai/kling-video/v2.1/...)
Auth: jat-secret fal-api-key (format: uuid:secret)
Env: FAL_API_KEY overrides jat-secret lookup
Pricing: standard $0.25/5s · pro $0.49/5s (+$0.098/s) · master $1.40/5s
Notes:
- i2v output resolution/aspect follows the reference image (pro = 1080p tier)
- Output moderation is on the lenient end among hosted video models
- Upload a local image to fal storage first:
POST https://rest.alpha.fal.ai/storage/upload/initiate {file_name, content_type}
then PUT the bytes to upload_url and pass file_url as --image
$ hailuo-video --help
Usage: hailuo-video "PROMPT" [OUTPUT] [options]
Generate a video with MiniMax Hailuo 02 on fal.ai. Submits the task, polls
until complete, downloads the MP4 to OUTPUT.
Arguments:
PROMPT Text description of the video (required)
OUTPUT Output file path (default: ~/Videos/hailuo-TIMESTAMP.mp4)
Options:
--model MODEL pro (default, 1080p/6s) | standard (512p/768p, 6s or 10s)
--duration SECS 6 or 10 (standard only; pro is fixed 6s)
--resolution RES 512P | 768P (standard only; default 768P)
--image URL Image URL for image-to-video (first frame reference)
--end-image URL Image URL for the LAST frame (end pose reference; i2v only)
--no-optimize Disable MiniMax's automatic prompt optimizer
--timeout SECS Max seconds to wait for generation (default: 900)
--json Print the final result JSON instead of a summary line
--help, -h Show this help
Examples:
hailuo-video "model settles into an armchair, catalog lighting" out.mp4 --image https://cdn.site/ref.png
hailuo-video "ocean waves at sunset, aerial" waves.mp4 --model standard --duration 10
Provider: fal.ai (https://queue.fal.run/fal-ai/minimax/hailuo-02/...)
Auth: jat-secret fal-api-key (format: uuid:secret)
Env: FAL_API_KEY overrides jat-secret lookup
Pricing: pro $0.08/s ($0.48 per 6s video) · standard $0.045/s ($0.27 per 6s)
Notes:
- Cheapest per second of the three video tools here (vs kling pro $0.098/s,
seedance std $0.30/s) — good default for drafts and volume work
- Generation is slower than Kling (~5-7 min vs ~1 min for a 5-6s clip)
- Upload a local image to fal storage first:
POST https://rest.alpha.fal.ai/storage/upload/initiate {file_name, content_type}
then PUT the bytes to upload_url and pass file_url as --image
$ seedance-video --help
Usage: seedance-video "PROMPT" [OUTPUT] [options]
Generate a video from a text prompt (optionally with a reference image) using
the Seedance 2.0 model on fal.ai. Submits the task, polls until complete,
downloads the MP4 to OUTPUT.
Arguments:
PROMPT Text description of the video (required)
OUTPUT Output file path (default: ~/Videos/seedance-TIMESTAMP.mp4)
Options:
--model MODEL seedance-2-0 (default, quality) | seedance-2-0-fast | seedance-2-0-mini
--duration SECS 5 or 10 (default: 5)
--aspect RATIO 16:9 | 9:16 | 4:3 | 3:4 | 21:9 | 1:1 (default: 16:9 for t2v, auto for i2v)
--resolution RES 480p | 720p (default) | 1080p
--image URL Image URL for image-to-video (first frame reference)
--end-image URL Image URL for the LAST frame (end pose reference; i2v only)
--no-audio Disable audio generation (default: on; use for silent web loops)
--seed N Deterministic seed (default: -1 random)
--timeout SECS Max seconds to wait for generation (default: 600)
--json Print the final result JSON instead of a summary line
--help, -h Show this help
Examples:
seedance-video "slow orbit around a glossy chrome funnel, black studio" hero.mp4 \
--aspect 21:9 --resolution 1080p --duration 10 --no-audio
seedance-video "product spins on turntable" spin.mp4 --image https://site.com/product.png
seedance-video "cut like this reference" out.mp4 --duration 5
Provider: fal.ai (https://queue.fal.run/bytedance/seedance-2.0/...)
Auth: jat-secret fal-api-key (format: uuid:secret)
Env: FAL_API_KEY overrides jat-secret lookup
Pricing: std $0.3034/s@720p · fast $0.2419/s@720p · mini $0.1547/s@720p
("fast" = latency tier, NOT the cheap tier — mini is cheapest)
Notes:
- fal duration buckets: 5 or 10 seconds only (9/10/11+ round to 10; 1-7 round to 5)
- Image-to-video: pass --image with a reachable URL; fal uses it as first-frame reference
- Result URLs are fal CDN — the tool downloads immediately
- Poll interval: 5s jat-video — the single tested source of truth for every ffmpeg primitive in
the jatstack. A fix lands HERE, once; consumers call it, never re-copy
it. Every primitive has exactly one implementation, so a supersample or
encoder fix can't drift across a dozen pasted copies.
Every subcommand DECODE-CHECKS its own output and fails loud (exit != 0) on
any decode error. Heavy renders route through jat-offload (your
jat-appliance) automatically unless --local is passed or the appliance is
unreachable.
Usage:
jat-video <subcommand> [args] · jat-video help · jat-video <sub> --help
Subcommands:
kenburns IN OUT [opts] Ken Burns zoom/pan (supersample-first)
stitch -o OUT c1 c2 … [--xfade S] CFR-normalized concat (hardcut | crossfade)
loop IN OUT [--xfade S] Seamless ping-pong loop (split + reverse)
chain CLIP [--extract-last] [--upload] Actual-frame clip chaining
decode FILE The gate — exit 1 on ANY decode error
cfr IN OUT [--fps N] Constant-frame-rate normalize
poster IN OUT.jpg [--at S] [-q N] Extract a poster frame
encode IN OUT --quality TIER ONE encoder (master | web | archive)
probe FILE --duration Media duration in seconds (the one probe)
scale IN OUT --height H Scale to height, preserve aspect
grade IN OUT [--preset P] Color grade (eq curve presets)
fades IN OUT [--in S] [--out S] Fade-in / fade-out
aspect IN OUT --ratio W:H Aspect-normalize (pad, no crop)
test Fixture: decode-clean + no-jitter
Quality tiers (the ONE place CRF is chosen):
master ≈ crf18 editing master / re-encode source
web ≈ crf23 +faststart delivery; progressive-download ready
archive ≈ crf28 +faststart smallest web-final Usage: jat-image-edit "INSTRUCTION" INPUT [OUTPUT] [options]
Edit a local image on your jat-appliance (the edit runs through ComfyUI).
Uploads INPUT, runs the edit, downloads the result. Fully local — nothing
leaves your network, no content filters.
Arguments:
INSTRUCTION What to do ("remove the hat", "make it watercolor", ...)
INPUT Local image path
OUTPUT Output path (default: INPUT-edited.png)
Options:
--mode MODE kontext (default) | restyle
kontext: Flux Kontext instruction edit — understands
semantic commands, preserves everything else.
Slower but highest quality.
restyle: SDXL img2img — reimagines the whole image
toward the instruction. Fast.
Structure survives, details change.
--denoise N restyle only: 0.3 subtle .. 0.9 heavy (default 0.55)
--steps N sampler steps (default: kontext 20, restyle 30)
--seed N deterministic seed (default random)
--host HOST ssh host running ComfyUI (your jat-appliance's hostname)
--timeout SECS max wait (default 2400)
--help, -h Show this help
Examples:
jat-image-edit "remove the sunglasses" photo.png
jat-image-edit "make the vase a tall straight cylinder" pot.png pot-cyl.png
jat-image-edit "turn this into a watercolor illustration" ref.jpg wc.png --mode restyle --denoise 0.7
Notes:
- Starts the remote ComfyUI server if it isn't running (niced, cache-none).
- Renders queue FIFO with whatever else the appliance is doing.
- For masked/regional edits or multi-image workflows, tunnel the ComfyUI UI
over ssh and open it in a browser (drag any prior output PNG onto the
canvas to reload its workflow). Usage: jat-video-edit "INSTRUCTION" INPUT [OUTPUT] [options]
Edit or generate a video on your jat-appliance (Wan 2.2 via ComfyUI). Uploads
INPUT, runs the render, downloads the result. Fully local — nothing leaves
your network, no content filters.
Arguments:
INSTRUCTION What to do ("the model walks toward the camera",
"make it a watercolor animation", ...)
INPUT For i2v: a still image (png/jpg/webp) to animate.
For v2v: an existing video clip (mp4/mov/webm) to edit.
OUTPUT Output path (default: ~/Videos/<input>-edited.mp4)
Modes (auto-detected from the input type; override with --mode):
i2v Image-to-video: animate a still per the instruction.
v2v Video-to-video: re-imagines an existing clip toward the
instruction — motion is preserved, appearance changes
with --denoise. Audio is dropped.
Options:
--mode MODE i2v | v2v (default: auto from input extension)
--frames N target frame count (default 121 = 5s at 24fps)
--fps N output fps (default 24)
--denoise N v2v only: 0.3 subtle restyle .. 0.9 re-imagine (default 0.55)
--steps N sampler steps (default: 20)
--seed N deterministic seed (default random)
--host HOST ssh host running ComfyUI (your jat-appliance's hostname)
--timeout SECS max wait (default 7200 — video renders are slow)
--help, -h Show this help
Examples:
jat-video-edit "she settles into the armchair" portrait.png
jat-video-edit "gentle camera drift, clouds moving" hero.mp4 --denoise 0.4
jat-video-edit "turn the whole scene into a neon street" clip.mp4 out.mp4 --mode v2v --denoise 0.75
Notes:
- Missing model files on the appliance -> the tool fails with exactly what
it needs.
- Video renders are slow — plan for tens of minutes. Read the source for the full detail.
Inference & Compute
Push heavy compute and private inference to a dedicated jat-appliance — the local box stays for editing.
jat-appliance — turn a spare machine into a private compute + inference node.
jat-appliance is a repo, not a single command: it converges an always-on box
(a Mac, a Framework, a mini-PC, any capable machine you dedicate) into a
dedicated appliance on
your network that serves LLM, speech-to-text, text-to-speech, and embedding
traffic locally, and accepts offloaded compute jobs from the rest of the
fleet.
The model:
- ROM-model compute. The appliance is stateless: jobs rsync in, results
rsync home, nothing precious lives on the box. Recovery = reinstall from
the repo; backups shrink to config only.
- Profiles, not forks. A profile bundles configs proven together on
specific hardware — runtime choice, context sizes, keep-warm strategy,
memory budgets. A new box is a new profile, not a rewrite.
- Boring-reboot invariant. Auto-login on, power settings pinned, services
self-heal in minutes. A bundled doctor verifies all of it.
How you use it:
- jat-offload / jat-render / jat-typecheck send heavy work to the appliance
(falling back to local if it is unreachable).
- jat-ask / jat-local-llm / speak hit its local inference relay — private
LLM, classification, summarization, and TTS that never leave your network.
- Pool several appliances behind one endpoint with jat.run.
Setup, profiles, and the doctor live in the repo:
https://github.com/jatstack/jat-appliance jat-offload — run a compute command in a working directory on a routed
heavy-compute node in your fleet (a jat-appliance) instead of burdening the
local machine. The general-purpose sibling of jat-render: rsync a workdir up,
run an arbitrary command in it (niced so live inference and whatever else the
node serves always wins), rsync the results back. Built for ffmpeg
assembles/stitches/transcodes, benchmarks, and other heavy batch compute that
competes for local memory with the agent fleet.
FAIL-LOUD POLICY: a job whose whole point is WHERE it runs must not silently
become a local job. An unroutable / unreachable / refuse-to-run remote is an
ERROR (exit 2), not a local run. A local fallback is opt-in and named:
Usage:
jat-offload [options] <workdir> -- <command...>
Options:
--local Run <command> locally in <workdir> (skip the remote host),
deliberately.
--allow-local If the remote host cannot run it, fall back to a LOUD local
run naming the primary path/reason. Without this, that case
is an error (exit 2). A remote command that RAN and failed
is never silently re-run locally.
--fast Skip the background-QoS wrapper (nice only) — faster, still
yields to live traffic.
--keep-remote Don't delete the remote job dir afterwards
--exclude PAT Extra rsync exclude (repeatable)
-h, --help
The remote host is routed via the fleet registry (nodes advertise a
batch-compute capability and report capacity; a node that never reported is
never eligible). Threads are capped on both paths so a harness that never set
intra-op threads cannot pin every core. The fleet-global concurrency cap is
held as a slot on the chosen host for the whole run.
Env:
JAT_OFFLOAD_HOST explicit remote-host override (default: fleet-routed)
JAT_OFFLOAD_BASE remote base dir, relative to remote $HOME (default: offload-jobs)
Examples:
jat-offload ./walkthrough -- bash assemble.sh
jat-offload . -- ffmpeg -i in.mp4 -crf 28 -movflags +faststart out.mp4
jat-offload . --allow-local -- python3 measure_endpointing.py jat-render — offload HyperFrames video rendering to your jat-appliance.
Instead of pinning the local dev box for the minutes a HyperFrames render
takes (each Chrome worker is ~256 MB), rsync the project to the appliance,
render it there **niced so inference always wins**, and rsync the resulting
MP4 back.
The appliance typically doubles as your local inference node, so the render
runs under `nice -n 15` AND background QoS — it can never starve the
inference relay's live traffic.
If the appliance is unreachable, jat-render falls back to a LOCAL render — it
never hard-fails on connectivity. `--local` forces the local path.
USAGE:
jat-render <project-dir> [--local] [--fast] [--keep-remote]
<project-dir> A HyperFrames project (must contain hyperframes.json + index.html)
--local Force a local render (skip the appliance entirely)
--fast Skip the background-QoS wrapper (nice only) — much faster
render, still niced
--keep-remote Don't delete the remote job dir after rendering
ENV:
JAT_RENDER_HOST ssh host to offload to (set to your jat-appliance's hostname)
JAT_RENDER_BASE remote job base dir, relative to remote $HOME (default: render-jobs)
EXAMPLES:
jat-render ./episodes/1-intro
jat-render ./episodes/1-intro --local
jat-render "./my project" --keep-remote jat-typecheck — run svelte-check/tsc on your jat-appliance instead of
locally.
WHY: svelte-check on a large codebase can need several GB of RSS. With many
agents completing at once, that burst is what pushes the local box into
memory pressure, where a live typecheck can be killed mid-run and an agent
reads it as a spurious "build broken". Typechecks are the ideal thing to
move off-box.
WHY NOT jat-offload: jat-offload rsyncs the workdir BACK (no --delete). For a
typecheck there is nothing to bring home — svelte-check emits diagnostics on
stdout and writes no artifacts — and a sync-back is precisely the mechanism
that makes offloading a *build* dangerous. This tool therefore never syncs
back, and it never runs a caller-supplied command: the remote command is a
fixed, in-tool string (`svelte-kit sync` + `svelte-check`). It is structurally
incapable of reaching `vite build`.
CONTRACT (this is what makes it drop-in for the build gates):
stdout — svelte-check's `--output machine` lines, verbatim and nothing else
stderr — progress, timings, and any infrastructure diagnostics
exit 0 — svelte-check ran, no errors
exit 1 — svelte-check ran, errors found
exit 2 — could not run LOCALLY (the package is not installed here).
UNVERIFIED, not a pass.
exit 3 — could NOT run remotely (unreachable / sync / install failure).
Callers fall back to a local run. This is never "clean".
exit 4 — remote RAN and died on its OWN heap limit (V8 abort / OOM kill).
Callers must NOT fall back to a local run: locally this check can
peak above the per-agent memory cap, so the fallback would
OOM-kill the agent. Raise JAT_CHECK_REMOTE_HEAP_MB.
Usage:
jat-typecheck [options]
Options:
--package DIR package dir, relative to repo root (default: ide)
--repo DIR repo root (default: git rev-parse --show-toplevel)
--local run locally (no appliance); still honours the output contract
--setup sync + install remotely, then exit (warm the cache)
--force-install reinstall remotely even if the dependency stamp matches
--timing print a wall-clock breakdown to stderr
--gc run the remote workspace-retention rules, then exit
--dry-run with --gc: log what would be removed, remove nothing
--host HOST appliance host override (default: $JAT_OFFLOAD_HOST)
--base DIR remote base dir under $HOME (default: $JAT_TYPECHECK_BASE)
-h, --help
Env:
JAT_TYPECHECK_HOST remote host (default: $JAT_OFFLOAD_HOST)
JAT_TYPECHECK_BASE remote base dir under $HOME (default: jat-typecheck)
JAT_TYPECHECK auto (default) | remote | local | 0 — `0`/`local` skip
the appliance entirely; `remote` disables local fallback
(exit 3 instead), which is what the test suite uses.
REMOTE LAYOUT: everything is placed under
$HOME/$JAT_TYPECHECK_BASE/<repo-name>/
preserving each tree's path relative to the LOCAL repo's parent directory, so
that out-of-repo `file:` workspace dependencies keep resolving remotely
without patching any package.json. jat-ask - Ask a question about your projects using local LLM inference
USAGE:
jat-ask "QUESTION" [OPTIONS]
jat-ask - [OPTIONS] # read question from stdin
OPTIONS:
--sources Show source labels alongside the answer
--projects PROJ,... Comma-separated project keys to filter context
--json Print raw JSON response (answer, sources, durationMs, etc.)
--timeout MS Timeout in milliseconds (default: 60000)
-h, --help Show this help
ENVIRONMENT:
JAT_IDE_URL Override IDE base URL (default: http://localhost:3333)
EXAMPLES:
jat-ask "what is the status of the billing refactor?"
jat-ask "what open tasks are blocking the voice feature?" --sources
jat-ask "summarize recent voice memos" --projects jat
jat-ask - < question.txt
The IDE must be running (jat or jat --dev) for this tool to work.
Answer text is printed to stdout; errors go to stderr. jat-local-llm - Send a raw prompt to your local inference relay
USAGE:
jat-local-llm "PROMPT" [OPTIONS]
jat-local-llm - [OPTIONS] # read prompt from stdin
OPTIONS:
--system TEXT System prompt
--model MODEL Override model (default: gemma4:e2b-voice)
--json Request JSON-formatted output
--stream [stub] Streaming — not implemented in v1; exits 3
--feature FEAT Feature tag for audit log grouping
--timeout MS Timeout in milliseconds (default: 30000)
-h, --help Show this help
ENVIRONMENT:
JAT_IDE_URL Override IDE base URL (default: http://localhost:3333)
EXAMPLES:
jat-local-llm "classify as bug/feature/chore: add dark mode toggle"
jat-local-llm - < notes.txt
echo "summarize: $(cat report.md)" | jat-local-llm - --json
jat-local-llm "Is this PII?" --model gemma4:latest --json
The IDE must be running (jat or jat --dev) for this tool to work.
Response text is printed to stdout; errors go to stderr. Data
Query and inspect connected databases directly.
Usage: db-query "<SQL query>"
Options:
--json Output as JSON (default: table format)
--csv Output as CSV
Examples:
db-query "SELECT * FROM assets LIMIT 5"
db-query "SELECT count(*) FROM users" --json
Safety: Adds LIMIT 100 if no LIMIT specified Usage: db-schema <table_name>
db-schema --tables
db-schema --views
Examples:
db-schema assets # Show assets table schema
db-schema --tables # List all tables
db-schema --views # List all views Usage: db-sessions [options]
Options:
--user <id> Filter by user ID
--brand <id> Filter by brand ID
--recent <n> Show n most recent sessions (default: 10)
--active Only show sessions with activity in last 24h
Examples:
db-sessions --recent 5
db-sessions --user 123e4567-e89b-12d3-a456-426614174000
db-sessions --active Usage: db-connection-test [--latency]
Test postgres database connection
Options:
--latency Measure query latency
Examples:
db-connection-test
db-connection-test --latency Dev, Git & Secrets
Commit safely in a shared checkout, resolve credentials, grow a project's data, and scaffold a new app.
usage: jat-commit -m "<subject>" [-m "<body>"...] -- <path> [<path>...]
jat-commit --resign [<range>] [--dry-run] [--agent NAME] [--quiet]
Re-sign commits whose signature went stale in a CONFLICTED rebase
jat-commit — atomic, path-explicit commit under a shared commit lock.
In a multi-agent checkout every agent shares ONE .git/index. Two hazards:
1) blanket staging (git add -A / . / -u / <folder>) sweeps siblings' files;
2) files left STAGED while you do something slow get swept into whichever
sibling commits next, under their task id.
jat-commit closes both for the committer:
- refuses blanket/dangerous stage specs (explicit paths only);
- under a flock on the commit lock, stages + commits in ONE step so nothing
sits staged across a slow op;
- verifies the index after staging contains ONLY your paths — if a sibling
already had files staged, it ABORTS instead of committing their work.
It does NOT prevent a sibling editing the SAME file (that is the spawn-time
file-lease allocator's job). This is the hygiene layer.
Signed-agent trailer:
When the committing agent has a secret key on record, jat-commit appends a
trailer line to the commit message:
Signed-agent: AgentName sig=<first-16-hex> sigv=1
covering the staged tree hash + agent identity + timestamp — tamper-evidence
and attribution backed by the key holder.
Example:
jat-commit -m "fix: guard empty input" -- src/lib/parse.ts src/lib/parse.test.ts jat-test — run vitest OUTSIDE the calling agent's memory cap, in its own
build-sized cgroup, admitted through the fleet build semaphore.
An agent scope shares one kernel memory ceiling with its harness: a runaway
test (unbounded fixture, a leaked server graph) OOM-kills the whole agent
session, not just the run. jat-test gives the run its own ceiling — if it
blows through that, the TEST is killed and the agent survives and is told.
On exit it reports the scope's measured memory peak. Even a single file with
NODE_OPTIONS=--max-old-space-size=1536 shared the ceiling with the harness —
this tool removes the sharing, which is what fixes it.
The stack (the same build route the fleet's build guard uses):
jat-build-sem --kind test --worktree <repo> admission: how many at once
jat-build-scope -m $JAT_TEST_SCOPE_MB sibling scope: its own ceiling
npx vitest run … (JAT_BUILD_GUARD_ACTIVE=1)
Usage:
jat-test [options] [--] [vitest args / test files …]
Options:
--package <dir> package to run vitest in, relative to the repo root
(default: the current dir if its package.json names
vitest, else ide)
-m, --memory <MB> test scope memory cap (default $JAT_TEST_SCOPE_MB or 6144)
--watch allow watch mode (default is a single vitest run)
--no-peak do not report the scope's memory peak on exit
--print-plan print the command that would run, execute nothing
-h, --help this text
Examples:
jat-test src/lib/server/placements.test.ts # a single file
jat-test --package ide # a package's suite jat-secret - Retrieve and manage secrets in JAT credentials store
USAGE:
jat-secret <key-name> Get secret value by name
jat-secret --project <proj> <key> Get project secret directly
jat-secret --set <key> <value> [OPTIONS] Set a custom key (pushes to vault)
jat-secret --delete <key-name> [--vault] Delete a custom key
jat-secret --list List keys across all tiers
jat-secret --find <pattern> Search key names + descriptions, all tiers
jat-secret --export Output export statements
jat-secret --env <key-name> Get the env var name for a key
jat-secret --whoami Show the resolved agent identity
jat-secret --1p Show 1Password integration status
jat-secret --1p-list List items in 1Password JAT vault
jat-secret --vault-push [key] Push key(s) to encrypted vault
jat-secret --vault-pull [key] Pull key(s) from encrypted vault
jat-secret --vault-delete <key> Remove a key from the vault
jat-secret --vault-status Show vault connectivity status
jat-secret --help Show this help
SET OPTIONS:
--env VAR_NAME Environment variable name (default: auto-derived from key)
--desc "text" Description of what the key is for
--no-push Keep this value on this machine only (skip the vault push)
EXAMPLES:
jat-secret stripe # Get a secret
jat-secret myapp-supabase-service-role-key # From project secrets
jat-secret --project myapp supabase_service_role_key # Direct access
jat-secret --set my-api "sk_xxx" # Set + push to vault
jat-secret --set my-api "sk_xxx" --env MY_KEY # Set with explicit env var
jat-secret --set my-api "sk_xxx" --desc "My key" # Set with description
jat-secret --set local-scratch-dir /mnt/fast --no-push # Machine-local only
jat-secret --delete my-api # Delete locally
jat-secret --delete my-api --vault # Delete everywhere
jat-secret --env stripe # Get env var name
jat-secret --find stripe # Search key names + descriptions, all tiers
eval $(jat-secret --export) # Load all as env vars
PROVIDER KEYS:
jat-secret anthropic # Anthropic API key
jat-secret google # Google/Gemini API key
jat-secret openai # OpenAI API key
jat-secret slack # Slack Bot Token
jat-secret telegram # Telegram Bot Token
jat-secret cloudflare # Cloudflare API Token
jat-secret github # GitHub Token
(+ discord, gmail, vercel, fly, convex, linear, sentry,
turso, upstash, neon, pinecone, resend, twilio, openrouter)
PROJECT SECRET FALLBACK:
Names like "project-secret-key" are resolved from projectSecrets:
1. Find longest matching project prefix
2. Strip prefix + dash, convert dashes to underscores
3. Look up in projectSecrets.<project>.<key>.value
VAULT COMMANDS:
jat-secret --vault-push # Push all local secrets to vault
jat-secret --vault-push my-key # Push one secret to vault
jat-secret --vault-pull # Pull all secrets from vault
jat-secret --vault-pull my-key # Pull one secret from vault
jat-secret --vault-delete my-key # Remove one secret from the vault
jat-secret --vault-status # Check vault connectivity
Lookup chain: credentials.json → vault server → 1Password → error
(vault is consulted automatically with 3s timeout when key not found locally)
DISCOVERY (--list reads the SAME chain resolution does):
A key that resolves MUST appear in --list. The listing covers all three
tiers and says which one would serve each entry:
Provider keys (local) built-in provider ids from .apiKeys
Custom keys (local) .customApiKeys — the local tier wins the chain
Project secrets (local) .projectSecrets, shown in the dashed form
that actually resolves
Vault only in the vault, NOT on this machine; these
resolve through the vault fallback
1Password only in 1Password and in neither tier above
Local entries tagged [local only] exist on THIS MACHINE ONLY — they were
never pushed, so no other machine can resolve them. Push with --vault-push.
(jatweb-*/local-* are exempt by design and never tagged.)
If a configured tier cannot be read, --list says so in-band AND on stderr
and exits 3. It never silently lists fewer keys: a short list that looks
complete is how a present credential gets reported as a missing
integration.
--find <pattern> is the same tier walk, filtered: a case-insensitive
substring match against BOTH the key name and its description (where one
exists), across all three tiers, labelled the same way --list labels
them. Use it instead of `jat-secret --list | grep PATTERN` — piping
through grep only ever matched key names, so a key findable only by its
description (or gated behind the vault/1Password network calls) was
invisible to it. Same incomplete-result contract as --list: an
unreachable tier is reported in-band and exits 3, never silently omitted.
NAMING CONVENTION:
Bare names are reserved for the enumerated built-in provider ids
(anthropic, openai, slack, github, ...). Every other secret is:
<service>-<kind> kind = api-key | token | secret | url | password
-api-key is the default kind for an API credential, so a new DeepSeek key
is `deepseek-api-key`, not `deepseek`.
The other shape is ALIASED, not rejected. When a name misses every tier,
an unambiguous conventional alias is resolved and announced on stderr:
jat-secret deepseek -> note: resolved via 'deepseek-api-key'
Ambiguity is never guessed through — if both `foo-key` and `foo-token`
exist, `jat-secret foo` fails and names both. On any other miss, near
matches are suggested. Set JAT_SECRET_NO_SUGGEST=1 to skip both (scripted
callers that probe optional secrets in a hot loop).
ENVIRONMENT:
JAT_SECRET_NO_PUSH=1 --set never pushes to the vault
JAT_SECRET_NO_SUGGEST=1 no alias resolution / suggestions on a miss
JAT_SECRET_VAULT_TIMEOUT seconds for vault writes (default 10)
JAT_SECRET_VAULT_LIST_TIMEOUT seconds for the --list vault fetch (default 8)
JAT_SECRET_OP_TIMEOUT seconds for the 1Password item list (default 6)
SYNC BEHAVIOUR (--set pushes, --delete does not):
--set pushes to the vault by default, so a secret added on one machine is
immediately resolvable from every other one. The push FAILS OPEN: if the
vault is unreachable or mis-keyed, the value is still written locally, the
exit status is still 0, and a loud stderr warning tells you to run
`--vault-push <key>` later. A failed network call must never eat a
just-typed credential.
Skipped automatically (no flag needed):
jatweb-* the vault's own credentials — it cannot bootstrap itself
local-* naming convention for machine-scoped values (paths, ports,
per-box tokens)
Skip it explicitly with `--no-push`, or set JAT_SECRET_NO_PUSH=1 for a
scripted caller that only ever writes machine-local values.
--delete deliberately does NOT propagate. Auto-pushing a set is additive
and recoverable; auto-deleting is irreversible across every machine in the
fleet. So a local delete stays local — but it is not silent: it checks the
vault and warns when a remote copy survives (which the next --vault-pull
would otherwise resurrect). Use `--delete <key> --vault`, or
`--vault-delete <key>`, to retire it everywhere.
VAULT SETUP (once per machine):
# Generate and share with teammates:
jat-secret --set jatweb-url "https://jat.example.com"
jat-secret --set jatweb-enc-key "$(openssl rand -hex 32)" # share out-of-band
jat-secret --set jatweb-api-secret "$(openssl rand -hex 24)" # share out-of-band
# Apply DB migration (once):
psql "$(jat-secret jat-postgres-url)" -f lib/migrations/vault_secrets.sql
# Push all secrets to vault:
jat-secret --vault-push
# New machine: get the 3 meta keys from a teammate, then:
jat-secret --vault-pull
1PASSWORD INTEGRATION:
jat-secret automatically falls back to 1Password when a key isn't found
in credentials.json or vault. Create a vault named "JAT" (or configure a
custom vault name) and add items with names matching your key names.
Setup:
1. Open 1Password > Settings > Developer > "Integrate with 1Password CLI"
2. Create a vault named "JAT" in 1Password
3. Add items with names matching your key names (e.g., "stripe", "openai")
4. The password/credential field is used as the secret value
Custom vault name (in ~/.config/jat/projects.json):
{ "defaults": { "op_vault": "MyVault" } }
Lookup chain: credentials.json > vault > 1Password > error Usage: jat-provision-postgres <project-name> [--admin-user USER] [--via-docker SSH_TARGET [--container NAME]]
Creates an isolated database + scoped role on the shared Postgres server and
stashes the connection string in jat-secret, so any machine on your network
can graduate the project to Postgres.
jat-provision-postgres my-project
# -> creates my-project_tasks database + my_project_user role
# -> stashes the connection string as my-project-postgres-url in jat-secret
# Then graduate the project:
jt graduate my-project --url "$(jat-secret my-project-postgres-url)" --yes
Options:
--admin-user USER superuser to run the admin steps as
(default: $JAT_PG_ADMIN_USER, else postgres)
--via-docker TARGET run the admin psql INSIDE the postgres container over
`ssh TARGET docker exec`, so no superuser password is
needed (host-side, human-invoked only)
--container NAME container for --via-docker
(default: $JAT_PG_CONTAINER, else the shared postgres container)
Safe to re-run — uses IF NOT EXISTS guards; refreshes the role password if it
already exists. Role names can't contain hyphens, so they are converted to
underscores automatically.
Secrets: the server host (always — it goes into the URL) and the superuser
password (human-only; not needed with --via-docker). Usage: jt graduate [PROJECT] --url <postgres-url> [options]
Migrate a project's task store from local SQLite to shared Postgres.
One-way: exports every task/dep/label/comment from .jat/tasks.db, imports
them atomically into Postgres, flips the projects.json backend flag, and
archives the local db as .jat/tasks.db.sqlite-backup-{timestamp}.
Arguments:
PROJECT Project name or path. Defaults to the current project
(inferred from PROJECT_ROOT or cwd).
Options:
--url URL Postgres connection string (required unless --dry-run)
--dry-run Print the migration preview and exit without writing
--status STATUS Override status for all imported rows (e.g. 'dev', 'submitted')
--target-table TBL Postgres table to import into: 'project_tasks' (default)
or 'tasks' (legacy JAT-owned table)
--mark-internal Deprecated: equivalent to --status dev (kept for compat)
--yes, -y Skip the interactive confirmation prompt
--force Proceed even if the target Postgres db is non-empty
--json Machine-readable JSON output (implies --yes)
--help, -h Show this help
Behaviour:
Idempotent — re-running on an already-graduated project exits 0 with a
message and does not touch either database.
Atomic — on any import error the Postgres transaction rolls back; the
local SQLite db and projects.json are untouched.
Examples:
jt graduate --dry-run
jt graduate --url postgres://jat:pw@host/jat_prod
jt graduate jat --url postgres://jat:pw@host/jat_prod --yes jst — the agent-proven SvelteKit app framework jatstack apps are built with.
A minimal, readable SvelteKit starter: SvelteKit 2 + Svelte 5, Tailwind 4,
DaisyUI 5, jatui, TypeScript, and vitest — with a database (Drizzle + Postgres)
and auth (Better Auth) included but DORMANT. The app boots with an empty
environment; setting DATABASE_URL turns them on.
Small on purpose (~50 files): you can read the whole template in an evening,
and that is the point — build on a stack you actually understand, one that AI
coding agents can navigate end-to-end.
Quick start (use it as a GitHub template, then):
npm install
npm run dev
# visit /notes to see the example module
When your app needs a database (also enables auth + /login):
cp .env.example .env # set DATABASE_URL, BETTER_AUTH_SECRET
npm run db:generate && npm run db:migrate
npm run dev
Source and template: https://github.com/joewinke/jst Agents & Sessions
An agent is a transcript + a task + a worktree — not a process. Split, move, revive, and respawn sessions across machines, checkpoint before compaction, and keep the ledgers that make any session resumable anywhere.
jat-fork — fork a live session into a second, independently-diverging one,
locally or onto another fleet machine.
jat-fork SESSION [--to HOST] [--move] [--name NAME] [--cwd DIR]
[--worktree-pushed] [--dry-run]
[--before PHRASE | --rewind | --line N]
jat-fork SESSION --before "the ADR/matrix work" fork from BEFORE that turn
jat-fork SESSION --rewind pick a user turn to back up to
jat-fork myproj-BoldLedge15 fork locally (same cwd)
jat-fork myproj-BoldLedge15 --to HOST fork onto the remote host
jat-fork myproj-BoldLedge15 --to HOST --move migrate: fork, verify, kill source
--move is also spelled as the dedicated verb jat-move:
jat-move SESSION --to HOST == jat-fork SESSION --to HOST --move
(sibling of this tool — use whichever reads better; use fork for the copy
case, where the source keeps running. --keep-source is accepted below as a
self-documenting no-op alias for a plain fork)
── Why FORK is the primitive, and migration is a mode of it ────────────────
An agent is not a process. It is a transcript + a cwd + a task. `claude
--resume <uuid>` reads a transcript and never consumes it — the file stays
on disk, and the resumed session appends to a NEW uuid. So copying a
transcript and resuming the copy gives two sessions that share one past and
diverge from the fork point. That is a fork in the git sense, and it is the
honest description of what the machinery does.
Migration is then just fork + kill-the-source, which is why `--move` is a
flag here rather than a separate `jat-migrate` tool: a "migrate" that could
not fork would still have to implement every hard part of forking (copy the
transcript, rewrite the uuid, resolve the cwd on the far side, verify the
far end actually came up) and would additionally be unsafe — killing the
source before the destination is proven alive is how you lose an agent.
Here the source dies only after the destination answers.
What this unlocks: live rebalancing (one box at capacity -> push agents to
other machines), speculative exploration (fork an agent at a decision point,
let both branches run, keep the better one), and cheap "what if" reruns of a
long investigation without paying for its context again.
── Rollback mode: fork from BEFORE a detour ───────────────────
A fork usually resumes at the source's present. Sometimes the useful branch
point is EARLIER: after a crash-recovery detour you want a second terminal
continuing from before the crash. --before/--rewind make the fork resume from
just before a chosen USER TURN, and — because the source keeps running — you
can compare the two branches. That is exactly why this is a fork and not
Esc-Esc: Esc-Esc rewinds the session in place, destroying the forward state.
--before "<phrase>" fork before the FIRST user turn containing the phrase
(matched against real human turns, never tool output)
--rewind list the recent user turns with a snippet and pick one
--line N internal escape hatch: fork before the record on line N
Line numbers are deliberately not the interface: they are never surfaced in
Claude Code, pi, or the cockpit, so a human cannot obtain one. --before and
--rewind are the UI; the boundary detection (which `user` records are real
turns) lives in tools/lib/fork-rewind.py and is shared by all three.
Rollback and --move are mutually exclusive, deliberately: --move kills the
source, and the source is the branch still holding the forward state that a
rollback fork exists to keep.
DOCUMENTED WRINKLE: the fork inherits the source's message lineage, so
Claude Code's Remote Control treats it as the same conversation and will not
auto-start in the fork while the source holds the bridge ("another Claude
Code already has Remote Control for this conversation · run /remote-control
to move it"). Harmless — the notice is printed at fork time and
/remote-control moves the bridge. A "perfectly independent" fork would need a
new conversation identity, but that identity derives from the shared history
a fork exists to keep, so it is not something to engineer away.
── Constraints, deliberately ───────────────────────────────────────────────
* The source must be a LIVE local tmux session. Reviving a dead one is
jat-resume's job; fork is about splitting something that is running.
* A local fork lands in the SAME cwd by default. For a worktree agent that
means two claudes editing one worktree — a file-conflict hazard the lease
system cannot see (leases are per-task, and a fork shares the task). It is
allowed, loudly; --move refuses it unless --worktree-pushed attests the
branch is pushed and the worktree recreated on the destination (a
migration must not leave the worktree behind on the wrong machine).
* A remote fork resolves the cwd HOME-RELATIVELY, not as an absolute path:
the fleet shares a ~/code layout but not a home prefix (/home/<you> on Linux,
/Users/<you> on macOS), so a literal path copy can never fork one box->another.
A cwd under $HOME is re-rooted at the destination's own $HOME; a cwd
outside $HOME must exist verbatim there. Worktree cwds under
~/.local/state/jat/worktrees are machine-local and refused — moving an
agent's worktree is a git operation (push the branch, recreate it there),
not a file copy, and belongs in its own task.
* Remote forks are NOT registered with the destination's IDE — the fork
arrives as a standalone claude session. It is recorded in the resurrector's
manual-session map on the destination so it survives a fleet death there.
* Worktree + --move is refused (gates below) UNLESS --worktree-pushed is
passed: the caller attests the land branch is pushed and the worktree
recreated on the destination — exactly the sequence jat-mesh-migrate
drives, so the migration strands nothing.
* On a successful REMOTE --move the placement ledger is
told: event=move, machine_id = the destination's machine id, task/agent
recovered from the source session's open placement row. Best-effort — a
ledger outage must never fail a migration (the IDE writers' rule).
Related verbs: jat-move (canonical relocate spelling of fork+--move),
jat-resume (revive a DEAD session, preserving its spawn flags).
jat-move — relocate a live session to another fleet machine.
jat-move SESSION --to HOST [--name NAME] [--cwd DIR] [--dry-run]
jat-move myproj-BoldLedge15 --to HOST migrate: fork onto the remote host, verify
it is live, then kill the source
move is the human-facing verb for relocation — the intuitive word when you
want an agent on a different machine. It is NOT a separate mechanism: it is
a thin sibling of jat-fork (the primitive, which copies a transcript and
resumes the copy — fork in the git sense). `jat-move S --to H` maps to
`jat-fork S --to H --move`: fork first, verify the destination is alive,
and only then retire the source (killing it first is how you lose an agent).
jat-fork SESSION --to HOST copy — source keeps running
jat-fork SESSION --to HOST --move move — the primitive's own spelling
jat-move SESSION --to HOST move — same thing, spelled as intent
jat-move SESSION --keep-source copy — alias jat-fork accepts too
jat-move SESSION (no --to): refused — a local "move" is
just fork + kill-in-place, rarely
wanted; jat-fork --move owns that case
No inverse flag: `jat-move --fork` ("a move that doesn't move") is refused —
say `jat-fork` (or `jat-move --keep-source`) for the copy case.
Everything else is jat-fork's contract, verbatim (see its header: LIVE
source only, HOME-relative cwd re-rooting on the destination, worktree cwds
refused, destination registered in the resurrector's manual-session map,
--keep-source as a self-documenting alias for a plain fork). This wrapper
only translates argv — it never implements migration itself.
jat-respawn — a fresh session seeded from distilled state.
jat-respawn [SESSION] [--task ID | --tangent "Title"] [--to HOST]
[--narrative-file F] [--rollup-file F] [--blurb-file F]
[--no-distill] [--keep | --kill] [--dry-run]
jat-respawn --blurb-file blurb.md respawn the current session
jat-respawn --tangent "New angle" spawn a tangent, old lives
jat-respawn myproj-BoldLedge15 --to HOST respawn onto another machine
DESIGN (docs/mesh/jat-respawn-design.md): the
transcript is DISCARDED — not copied (jat-fork) or summarized in place
(/jat-compact). State is externalized to systems the fleet already keeps —
the project ledger (project_journal), the checkpoint file + WIP push
(jat-checkpoint), and a respawn-handoff task comment — and a BRAND-NEW
harness session is created through POST /api/work/spawn seeded ONLY from
that distilled state (opening prompt = /<New> <task>; Round 1B
reads the handoff comment). A new session id cannot inherit a resumed
session's leaked provider config (the provider-taint class), and
starts lean instead of carrying the old transcript's bloat.
THE SEQUENCE (each step an existing write path; this tool only orders them):
1. jat-checkpoint --trigger respawn snapshot + WIP commit + push.
Run in the TARGET session's checkout
(pane cwd / its worktree), NEVER the
caller's — step 3b resolves it and
refuses rather than checkpoint the
caller. The respawn
ABORTS if the push fails — a respawn
that strands WIP is the loss class
this epic ends.
2. jat-checkpoint --journal … the distilled ledger row
(only with --narrative-file/--rollup-file)
3. POST /api/tasks/<id>/comments the respawn-handoff brief
(comment_type=note,
metadata.kind=respawn-handoff)
4. POST /api/work/spawn body carries supersedes=<Old> (the
holder-only bypass of the
duplicate-spawn guard) +
worktreeFrom=origin/land/<task>-<Old>
(the successor branches from the
pushed WIP tip). Poll to RUNNING
before touching the old session —
jat-fork's rule: the source dies
only after the destination is alive.
5. DELETE /api/sessions/<Old> ?reason=respawned&successor=<New> —
audit row + placement state
'respawned'. Safe only AFTER the
spawn moved the assignee: the
route's task-release step then
finds nothing to reopen.
6. lease + worktree release the old lease (the
successor's /re-claims
it); remove the old worktree only
if clean AND its tip is on origin.
THE ONE RULE (design §4): same task ⇒ the old session dies (spawn-first,
then kill). New task (--task <other> / --tangent "Title") ⇒ the old session
keeps running. --keep on the SAME task is refused (two agents on one task is
what the spawn guard prevents); --kill on a tangent is refused (a tangent
has no reason to kill its origin).
DISTILL GATE: without --no-distill the tool refuses until distilled state
exists — either --blurb-file (the handoff brief; steps 2-3 then run here) or
a respawn-handoff comment on the task NEWER than the session's latest
`working` signal. --no-distill is the loud, named escape for wedged/tainted
sessions that cannot distill.
--to HOST: the spawn body is POSTed to HOST's IDE (http://HOST:3333 — the
successor's worktree is created THERE from origin/land/…, which is why step
1 is a hard gate). The kill + lease + worktree cleanup stay LOCAL (the old
session runs here), with successor='HOST:<NewSession>' so the placement
ledger points across machines. No ssh-tmux fallback — that would bypass
registration and the sandbox.
Exit codes:
0 respawn complete (or plan rendered with --dry-run)
1 precondition failed (distill gate, push gate, spawn failed…) — the old
session is untouched; nothing was spawned
2 usage error
3 no live local tmux session
Acceptance test: tools/tests/jat-respawn/respawn.sh — DRY-RUN ONLY. Never
respawn a live agent as a test; the overseer runs the first real respawn.
jat-resume — resume a dead/exited JAT agent session WITH its permission flags.
jat-resume [IDENTIFIER] [--here] [EXTRA_CLAUDE_FLAGS...]
With NO identifier (and a terminal), opens the interactive table picker as a
HOME BASE: pick a session, jump in (attach/resume), and when you detach it the
picker reopens so you can jump into the next — a loop you leave with q/Esc.
↑↓ move row · ←→ move the sort column (re-sorts) · Shift+←→ reorder columns
· r reverse · Enter resume · q/Esc cancel. Column layout + sort persist to
~/.config/jat/resume-pick.json. STATE column: run = a tmux session is
alive now · idle = dead but its worktree is present · gone = worktree removed
(resume recreates it). Falls back to an fzf/numbered picker without python3.
IDENTIFIER is any way back into the session:
- agent name BoldLedge15
- {project}-{Agent} myproj-BoldLedge15
- task id myproj-abc12 (also dotted subtasks, e.g. myproj-abc12.3)
- Claude session id 01234567-89ab-cdef-0123-456789abcdef
Agent/session names are matched by name; task ids and session ids resolve via
the IDE's explicit-identifier fast paths (a session id pins the exact transcript
by its own recorded cwd; a task id resolves to the agent that ran it).
Why this exists: when a spawned agent's claude process dies (OOM, crash, API
error), the pane prints Claude Code's own hint — `claude --resume "<name>"` —
which does NOT carry the flags the agent was originally spawned with. A
copy-paste resume silently downgrades the agent from bypass-permissions to
default mode, and it stalls on the first Edit/Bash permission prompt. ALWAYS resume through
this wrapper instead of the raw hint.
The resume command itself — flags, the session-selecting argument, and the
provider env the relaunch needs — is RENDERED BY THE HARNESS ADAPTER LAYER via
`jat-harness render-resume`. This tool never spells `claude
--resume` or `pi --session`: it asks, for whichever harness the resolver says
owns the session, and hands the answer to tmux. Config flags come along
(defaults.claude_flags + --dangerously-skip-permissions when
defaults.skip_permissions — the same rule the IDE's resume paths apply), and
a pi/glm session gets `export OPENROUTER_API_KEY=…` resolved inside the pane
from jat-secret, and an explicit `--model <recorded-model>` pin on the
relaunch — both derived from the session record (the resolver's response, or
the transcript itself via --project-path), which is the fix.
`--dry-run` prints the rendered
launch line and exits before any side effect.
`--resume` needs a real Claude SESSION UUID (an agent name opens
the resume PICKER, which then sits forever with no match — the original
bug), the tmux session must be named `{project}-{Agent}` (a bare agent name
defeats every jat-<Agent> liveness probe in the fleet), and the transcript
for that UUID only resolves under the Claude project dir slugged from the
cwd `claude -r` launches in. These three are one coherent set, not
independent guesses, so they are resolved together server-side by the same
resolveResumeTarget() the IDE's own /resume endpoint uses (see
ide/src/lib/server/resumeTarget.ts), via the read-only
/api/sessions/{name}/resume-target endpoint — not re-derived here in bash.
Worktree recovery: an isolated agent runs in a git worktree that is
REMOVED at land/prune/reap time, while its Claude transcript survives
indefinitely under ~/.claude/projects/{worktree-slug}/. A resume after cleanup
used to report the (definitely-false) "no transcript on disk". Now the resolver
returns HTTP 409 with the branch + repo + cwd, and this tool recreates the
worktree (`git worktree add <cwd> <land-branch>`) before resuming into it — the
one git side effect, kept out of the read-only resolver.
Behavior:
- tmux session {project}-{Agent} alive → send the resume command into it
- no tmux session → create detached {project}-{Agent}
running it, cwd = resolved project
- worktree removed (409) → recreate it, then resume as above
- no transcript on disk (404) → fetch-on-resume:
jat-transcript-fetch pulls the
transcript from the jat-sessions
store into the cwd-keyed path
(cwd key re-rooted for this
host's $HOME), then this tool
re-resolves. Suppress with
JAT_RESUME_NO_FETCH=1.
- --local → resolve against THIS box's own IDE
(jat_ide_local_url) instead of the
control plane. The node-local escape
hatch for when the placement ledger is
stale/missing or the control plane
cannot reach this node.
Env: JAT_RESUME_LOCAL=1
- --here → cd to resolved project, exec here
- --dry-run → print the resolved target + launch line, change
nothing — including a 409 worktree-recovery:
the resolver GET is read-only and the
worktree recreate lives in THIS tool's 409
handler, so a dry-run names what WOULD be
recreated and recreates nothing
- --agent-type overseer|worker → force the sandbox/memory-cap role. Default: resolved
from the signal file, task metadata, then
jat-overseers (pilot / squad leader / wave
driver …); worker when none matches.
The IDE honours a requested overseer only with
jat-exec-token readable (host-side), else it
derives the role from its records
- session lives on ANOTHER box → ssh to that node and run the
resume THERE: resolver's resolvedOn
first, then agent_presence (the
jat-retire/jat-send resolver). Cwd
not here and no node → REFUSED, exit 8
(JAT_RESUME_SSH_OPTS, JAT_RESUME_PRESENCE_TTL)
- live target session names a DIFFERENT agent → REFUSED, exit 7 (never silently place a transcript
into another agent's container)
- claude session id still held by a → resume as a FORK (--fork-session
background job / daemon worker --session-id <new>), loudly; the fork
(claude refuses and exits) is bound to the agent so later resumes
follow it. Post-launch backstop: an
immediate exit is retried ONCE as a fork.
JAT_RESUME_NO_FORK=1 → REFUSED, exit 9
(JAT_RESUME_NO_HOLDER_CHECK,
JAT_RESUME_NO_EXIT_CHECK)
- claude session id with NO transcript → REFUSED, exit 4, before anything is
on this box typed (inside a worker sandbox, where
~/.claude/projects is masked: UNVERIFIED,
resumes). JAT_RESUME_NO_TRANSCRIPT_CHECK=1
- the launch is ATOMIC → from the wedge probe to the audit row,
HUP/TERM/INT are deferred and SIGPIPE
ignored, so a closed stage pane, popup,
ssh or execFile timeout cannot leave a
half-done launch. Afterwards BOTH branches
check the launch ran: text still sitting
at the prompt after 8s is cleared (C-c),
audited and exits 3; no harness is a loud
warning. Audit rows name the invoker.
On a permanent miss (exit 4) the error line says WHICH lookup ran (agent name,
task id, or session id — a lowercase token like 'thinlake' is dispatched as a
task id), then the nearest real sessions follow on stderr with the exact
`jat-resume <x>` to run: substring, then typo-distance, then
shared-prefix matches from GET /api/sessions/index, falling back — loudly —
to GET /api/sessions/resumable. Bounded (4s), never changes the exit code,
never touches stdout. JAT_RESUME_NO_SUGGEST=1 skips it; JAT_RESUME_VERBOSE=1
lists every searched project path instead of the count.
In the two non---here branches this then fires jat-nudge-resume in the
background: `claude --resume` parks at an interactive prompt with
no continuation instruction, so without the nudge a fleet agent is resumed and
then silently sits there forever. Suppress with JAT_RESUME_NO_NUDGE=1.
Every branch also stamps `task.metadata.resumedBy` via jat-spawned-by, so the cockpit's OVERSEER column shows `↺<whoever resumed>`
for a revived worker no overseer claims, instead of the "—" that made a
silently-restarted agent untraceable. The task id comes from the identifier
when you passed one, else from the `{agent}-{taskId}` worktree cwd; a
shared-checkout session has neither and is simply not stamped. Best-effort,
never fatal. Suppress with JAT_RESUME_NO_STAMP=1.
Those branches also re-arm the `jat-heartbeat --loop` presence ticker: recreating the tmux session kills the old ticker, and nothing
else ever restarted it, so every resumed agent ran with a frozen heartbeat —
which presence-reaper reads as death. Suppress with JAT_RESUME_NO_HEARTBEAT=1.
Exit codes: 0 resumed · 2 usage · 3 tmux failure OR worktree recreation failed
OR the typed launch never ran (still at the prompt — cleared)
· 4 no resumable session (includes: the resolved claude session id has no
transcript on this box ) (no Claude transcript on disk AND none restorable
from the jat-sessions store — permanent, don't retry; a transcript survives
but its land branch could not be resolved — also permanent, don't retry)
· 5 resolver unreachable/errored (IDE down or returned something
unexpected — transient, safe to retry) · 6 the shared IDE resolver itself
could not be loaded — a broken install a human has to fix, not a retry
· 7 identity conflict: the target
session belongs to a different agent than the transcript
· 8 wrong box: the session lives on another machine and could not
be resumed there — no node resolvable, the ssh hop failed, or a hopped
invocation found it is not here either. NOTHING was launched on this box.
(When the hop succeeds, the owning node's own exit code is passed through)
jat-graft — pull a Claude Code / pi session running OUTSIDE tmux (a raw
terminal, a stray pane) into a proper {project}-{Agent} tmux session, so it
shows in the jat-cockpit rail and can be supervised.
Ad-hoc sessions aren't in the IDE index, so this does NOT rely on the resolver.
It captures the LIVE process's own argv + cwd + env, PERSISTS a relaunch plan,
builds the replacement tmux session FIRST (while the old copy is still alive),
VERIFIES the replacement harness is running, and only then retires the old
copy — so a failed adopt can never leave a killed session with no replacement. The session id already in the argv (`claude -r <id>` /
`pi --session <id>`) makes the conversation continue seamlessly. Harness-agnostic.
A target ALREADY inside tmux is adopted in place (exit 3): nothing is killed.
After relaunch it registers an identity (am-register) and declares the session
to the cockpit index so it shows in the rail. With --task it also summarizes
the transcript (local gemma4), runs the task analysis (type/priority/labels),
previews the proposed task, and on confirm creates + assigns it. Opt-in:
Usage:
jat-graft <SESSION_ID> graft the process carrying that session id
jat-graft --pid <PID> graft a specific process
jat-graft --auto graft the sole claude/pi running outside tmux
jat-graft --self the RUNNING session grafts ITSELF: register an
identity, create the mailbox, emit the first
signal, declare the cockpit index — identity
backfill for a session born while the fleet
was down. No kill, no relaunch.
The documented front door is / --graft,
which calls this. / hands you a task
and you build context; --self is the REVERSE:
you already have the context, backfill the ID.
[--name A] [--project p] [--model M] [--task] [-y] [--dry-run]
[--program P] --self only: override harness detection
(else the ancestor process chain names it)
[--task ID] --self only: attach to the task/epic id you are
ALREADY working on (in_progress + assignee) —
NOT the no-arg analyze-and-create mode below
--task (other modes) analyze the session and create a jt task (preview → confirm)
--overseer / --oversees ID,ID / --epics E,E declare the session an OVERSEER of those
tasks/epics (the cockpit groups its flock under it; jat-overseers reads it)
--declare-only no relaunch: (re)write the declaration of an already-grafted session
--session NAME with --declare-only: record the LIVE tmux session name (an
in-place graft can keep a name that doesn't end in the agent
name; the cockpit index needs it for liveness)
--model force the relaunch model (else the transcript's last-used model)
jat-checkpoint — snapshot an agent's operational state to disk before compaction.
Claude Code fires PreCompact on BOTH manual (/compact) and automatic
(harness-initiated) compaction. Wiring this tool there turns compaction from a
lossy event we schedule defensively into a checkpointed event: whatever the
summarizer drops, the checkpoint file still holds the operational facts an
agent needs to keep working (task, approach, declared files, git state).
The checkpoint file is BOTH the record and the recovery instructions — its
header tells a freshly-compacted agent what to do with it. In hook mode
(--hook sessionstart) this tool reads the file back automatically when the
session restarts with source=compact.
MODES
jat-checkpoint Write a checkpoint now (trigger: manual-cli)
jat-checkpoint --hook precompact PreCompact hook: read stdin JSON, write, stay silent
jat-checkpoint --hook sessionend SessionEnd hook: read stdin JSON, write, stay silent
jat-checkpoint --hook sessionstart SessionStart hook: on source=compact, print recovery
jat-checkpoint --show Print this agent's checkpoint file
jat-checkpoint --path Print the checkpoint file path
jat-checkpoint --journal Write today's project_journal row (ledger write path,
called by /jat-compact — see PRD §4)
WIP PUSH: on EVERY checkpoint (manual and PreCompact hook) an
agent sitting on its land branch in an ISOLATED worktree gets its unlanded
work committed as a WIP snapshot and the branch pushed to origin, so a moved
or dead agent's work survives as `origin/land/<task>-<agent>` and a new host
can `git worktree add` it. Default ON; opt out with JAT_CHECKPOINT_PUSH=0.
Details and the recovery recipe: docs/mesh/checkpoint-push.md
SESSIONS INDEX: every checkpoint also writes one best-effort
`checkpoint` row into session_lifecycle_audit (harness session id, cwd key,
last write) so the sessions index stays answerable across the fleet.
JOURNAL MODE (the /jat-compact write path onto the Project Ledger):
Every compaction ALSO auto-merges a machine `checkpoint` rollup slice into
today's project_journal row (best-effort, silent) — so a ledger entry exists
even if the full compaction skill never finishes. The curated write is
explicit:
jat-checkpoint --journal \
[--narrative-file F | --narrative "text" | --narrative -] prose; omit = leave stored
[--rollup-file F] JSON object; top-level slices MERGE over stored ones
SHAPE CONTRACT: every curated slice must
be a BARE ARRAY — the reader assigns rollup.<slice>
wholesale to that slice's canonical field (clocks →
calendar, decisions → nextActions, lessons → items).
An object-shaped slice is REJECTED (exit 1, slice
named); unknown keys warn loudly (the reader reports
them degraded and never renders them).
[--project KEY] projects.json key (auto-resolved otherwise)
[--day YYYY-MM-DD] default today (local calendar)
[--task ID] recorded in updated_by (AGENT@ID)
Partial semantics mirror ide/src/lib/server/ledger/journal.ts
(buildJournalUpsert): omitted narrative leaves the stored prose, omitted
rollup leaves the stored slices, supplied slices merge on top. Fails (exit 1)
with a message when postgres is unreachable — the caller (/jat-compact) owns
the fallback. The table DDL is self-healed here (idempotent), so a journal
write never depends on the IDE having booted first.
OPTIONS
--agent NAME Override agent identity resolution
--trigger TEXT Label recorded on the checkpoint block (auto|manual|manual-cli)
jat-resurrect — auto-resurrector: detect a fleet death, revive every agent
from its transcript, and nudge each one back to work.
jat-resurrect run [--dry-run] one pass (what the timer fires)
jat-resurrect arm install + start the systemd user timer
jat-resurrect disarm stop the timer
jat-resurrect status armed? pending bundles? breaker state?
jat-resurrect reset clear the crash-loop + IDE-restart breakers
Why this exists: the tmux fleet server has died more than once. Every
revival was a HUMAN loop: notice (often hours later), jat-resume each agent,
nudge each pane. This tool is that loop, encoded. An agent is a transcript +
a task + a worktree — processes are cattle. Fleet MTTR goes from
"someone notices" to one timer tick.
How it decides WHO to revive: jat-tmux-watch's death bundles. Each bundle's
units.txt is a snapshot of every live agent-*.scope at the moment of
death — the manifest of what was alive. Sessions with a standard
{project}-{Agent} name are revived via jat-resume (flag-preserving,
memory-capped, audited — never raw `claude -r`). Sessions
with non-standard names (manual resumes, forks) are revived from the
manual-session registry at $STATE_DIR/manual-sessions.map
(lines: session_name<TAB>session_uuid<TAB>cwd[<TAB>harness]; the harness
column defaults to claude-code). jat-fork appends to it. The launch line for
those is rendered by `jat-harness render-resume`, never
spelled here.
The nudge is NOT optional: a resumed claude sits idle at its prompt forever — the pane even shows stale pre-crash output that looks alive.
Validated protocol from the incident: send the text and the Enter as TWO
send-keys calls (a combined call can sit unsubmitted as pasted text), then
verify the transcript mtime actually advanced. When
jat-nudge-resume lands on PATH it is preferred over the inline protocol.
Crash-loop breaker: if something is actively killing the fleet (that is how
all seven deaths happened — an unfixed killer plus eager revival equals a
kill/revive thrash loop), revival makes things worse, not better. After
MAX_WAVES bundles processed inside BREAKER_WINDOW seconds this tool
alarms (journal + notify-send) and refuses to act until `reset`.
SINGLE-INSTANCE: overlapping passes were part of a fleet kill chain (four live agent scopes died while two resurrect ticks and
their unserialized jat-tmux-ensure children raced). Three layers prevent it:
the in-script `flock -n` (two CLI/timer passes can never run at once),
systemd's own coalescing of timer re-triggers against an active oneshot, and
`RefuseManualStart=yes` on the service unit so a stray manual `systemctl
start` can never run beside the timer's pass. Re-run `jat-resurrect arm`
after upgrading to install the hardened unit.
Fail modes, deliberately: jat-resume exit 4 (no transcript) is permanent —
recorded, never retried. Exit 5 (IDE unreachable) is transient — the bundle
stays unprocessed and the next tick retries; if the tmux server is up but
the IDE is down (it dies with the server — it lives in a pane), this tool
restarts it via jat-ide-restart, the sanctioned serve-existing-build path
(never `jat`, whose launch path may kick off a multi-minute build).
IDE revival is a THREE-WAY branch: (1) a build lock
HELD (flock, not file existence) means a build is mid-flight — log "build
in flight since T" and let it finish; (2) the build verifiable means
serve-existing-build, with NO timeout around it; (3) the marker MISSING
with nothing building means nothing will ever write it — delegate to a
DETACHED jat-ide-restart --build in its own systemd scope (a plain
background child of this oneshot would be killed when the timer pass
exits).
AND IT NEVER LOOPS ON A STALE BUILD. In one incident: a wave's landings merged into the checkout after the build, so
jat-ide-restart's source-freshness gate refused every minute; because the
refusal was swallowed, the tick re-called the restart forever and each
attempt could fork a tmux server inside the timer's transient scope, taking
an IDE-launched worker with it (8+ dead). revive_ide now (1) runs
`jat-ide-restart --check` BEFORE any restart — a guard that reaps and
restarts nothing, so a stale build can never cost a worker; (2) on a
build-fixable refusal escalates to ONE detached `--build`; and (3) trips a
dedicated IDE-restart breaker on repeated IDENTICAL failures, exactly like
the crash-wave breaker, so a build that keeps failing halts auto-restart and
notifies a human instead of storming builds. `jat-resurrect reset` clears it,
and a healthy IDE clears it automatically.
AND IT RECORDS THE WHY, DURABLY. in one incident the IDE died and every jat-ide-restart attempt failed while agent staging builds ran — with the reason swallowed. The signature
log below keeps only counts, and stderr only reaches the timer's journal,
which rotated away, so the root cause stayed unknown for weeks. revive_ide
now appends the FULL refusal output of every failed check/restart to
$STATE_DIR/ide-restart-failures-detail.log (bounded to its last 2000 lines,
and deliberately NOT cleared by `reset` — it is an audit trail, not loop
state).
jat-transcript-push — push agent session artifacts to the MinIO `jat-sessions`
bucket so any box in the fleet can resume any session (Phase A portability).
WHAT gets pushed
~/.claude/projects/<slug>/<sid>.jsonl Claude Code transcripts
~/.pi/agent/sessions/<slug>/*_<sid>.jsonl pi transcripts
~/.config/jat/checkpoints/*.md jat-checkpoint snapshots
<repo>/scratchpad/TRANSFER-*.md session-transfer briefs, per
projects.json root and worktree
WHERE (object key layout)
jat-sessions/<machine_id>/<session_id>/<home-relative source path>
e.g. jat-sessions/<machine>/abc123.../claude/projects/--home-you-code-jat--/abc123....jsonl
jat-sessions/<machine>/_global/jat/checkpoints/<agent>-<session>.md
A file with a derivable session id is keyed under it; otherwise it lands
under `_global`. The key suffix is always the sanitized home-relative
path, so restore is a plain prefix strip.
HOW (idempotent, resumable, safe)
- Local state file ~/.config/jat/transcript-push/state.json records
(size, mtimeMs, etag) per source path. Unchanged files are skipped, so
the 5-minute timer is a no-op while nothing moved. A failed upload is
simply retried whole next run — S3 PUTs are atomic, so there is never a
partial object on the server to resume *into*; resumability means
"nothing already-uploaded is ever re-uploaded, and nothing half-done
is ever visible".
- Never a writable share: the tool never sets a bucket policy or ACL and
all access is SigV4-signed. The bucket stays private.
- Hook mode (--hook precompact | sessionend) stays silent, exits 0 on any
failure (a compaction/session-end hook must NEVER block its harness)
but logs the reason to stderr.
- flock-protected so the timer and a hook never double-run.
MODES
jat-transcript-push push everything recent (timer / manual)
jat-transcript-push --hook precompact PreCompact hook (stdin JSON, silent)
jat-transcript-push --hook sessionend SessionEnd hook (stdin JSON, silent)
jat-transcript-push --session <sid> limit to one session id
jat-transcript-push --dry-run list what would be pushed
jat-transcript-push --ensure-bucket just create the bucket (idempotent)
jat-transcript-push --round-trip upload+download a canary, verify bytes
jat-transcript-push --verify report session ids recoverable from remote
OPTIONS
--max-age DAYS only push files modified within N days (default 7; timer keeps
the window small, hooks push regardless for the current session)
--force re-upload even when the state file says unchanged
--bucket NAME default jat-sessions
jat-transcript-fetch — restore an agent session's transcript from the
`jat-sessions` object store into the LOCAL cwd-keyed path its harness reads
(fetch-on-resume).
`claude --resume <sid>` / `pi --session <sid>` read a LOCAL transcript file
keyed by the cwd the harness launches in. When a session started on another
fleet box (one fleet box → another), or its local file was removed after being
pushed, jat-resume would report "no transcript on disk" even though the
session is fully recoverable. This tool closes that gap:
1. look the session up in the sessions index via the IDE's read-only
GET /api/sessions/<name>/resume-target/fetch — machine, harness session
id, recorded cwd / cwd key, pushed object key (when the transport
pusher reported one);
2. locate the transcript object(s) in the jat-sessions bucket, keyed
jat-sessions/<machine_id>/<harness_session_id>/<home-relative path>;
3. download and place each transcript at the cwd-keyed path THIS host's
harness reads, normalizing the cwd key: the transcript's own recorded
`cwd` is re-rooted at this host's $HOME when the source home differs
(/home/user on the source → /Users/user here). A worktree cwd that does
not exist here is still placed under its re-rooted slug — jat-resume's
worktree-recovery path (409 → git worktree add) then resumes into it.
The IDE never touches the object store; this tool is the muscle behind the
pointer. jat-resume and jat-fork call it when the transcript is not on
local disk.
USAGE
jat-transcript-fetch IDENTIFIER [options]
IDENTIFIER — any of:
agent name BoldLedge15
{project}-{Agent} myproj-BoldLedge15
task id myproj-abc12 (also dotted: myproj-abc12.3)
harness session UUID 01234567-89ab-cdef-0123-456789abcdef
OPTIONS
--dry-run list what would be restored, change nothing
--list list the transcript objects available remotely, exit
--force overwrite local files that already exist
--all also restore non-transcript session artifacts (checkpoints,
TRANSFER briefs) by plain prefix strip under $HOME
--host URL IDE base URL (default: the local IDE; set JAT_IDE_URL for a remote one)
--bucket NAME object-store bucket (default jat-sessions)
--quiet only errors and restored paths
EXIT CODES
0 transcript restored (or already present locally)
2 usage error
4 nothing fetchable — no session with transcript evidence in the index
5 transport error — IDE or object store unreachable, or the IDE refused
the lookup for lack of a bearer token (401/403)
6 nothing restorable found in the store for this session
Docs: docs/mesh/fetch-on-resume.md
jat-sessions-index — who was doing what, on which machine, resumable where.
The read side of the sessions index. One query over
session_lifecycle_audit, served by GET /api/sessions/index — one row per
session: agent, task, last lifecycle event, machine, harness (claude/pi)
session id, transcript object key, cwd key, first/last transcript write,
and whether fetch-on-resume has what it needs to pull the
transcript from the jat-sessions bucket and resume it on THIS box.
USAGE
jat-sessions-index # last 100 sessions, table output
jat-sessions-index --agent <name> # only sessions for that agent
jat-sessions-index --machine <id> # only sessions that ran on that machine
jat-sessions-index --task jat-abc # only sessions attributed to that task
jat-sessions-index --q <substring> # session-name substring filter
jat-sessions-index --resumable # only sessions with harness id + cwd key
jat-sessions-index --limit 500 --json # raw rows for scripts
jat-sessions-index --host <url> # IDE base URL (default: the local IDE)
The write side is automatic: spawns/ends are recorded by the IDE's signal
ingest, checkpoints by jat-checkpoint's hook, transcript pushes by
jat-transcript-push (all via POST /api/sessions/<name>/audit). See
docs/mesh/sessions-index.md.
set -uo pipefail
JAT IDE base URL via the one shared resolver.
. "$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]:-$0}")")" && pwd)/../lib/ide-auth.sh"
IDE_URL="$(jat_ide_base_url)"
jat_ide_auth_warn_if_needed "$IDE_URL"
jat-placements — the placement ledger, from any seat.
The read (and report) side of the placement ledger. One
durable Postgres row per placement — task, agent, session, machine,
transcript key, land branch, state — written on every spawn and updated on
resume/move/end. This is how any seat answers "where is this task running,
on which box, on which land branch?" and how the self-healing reschedule
finds work stranded on a machine that stopped reporting.
USAGE
jat-placements # current placement of every task
jat-placements --active # only open placements (the reschedule read)
jat-placements --task jat-abc # one task's current placement
jat-placements --task jat-abc --history # full placement history (every move)
jat-placements --agent <name> # placements for one agent
jat-placements --machine <machine_id> # placements on one machine
jat-placements --planning # include planning sessions (task_id NULL)
jat-placements --limit 500 --json # raw rows for scripts
jat-placements --host <url> # IDE base URL (default: the local IDE)
REPORTING (for tools that move or end a placement out-of-band — jat-fork
--to HOST, a cell shutting down):
jat-placements report --session <name> --event move --machine <id> \
--actor jat-fork --reason "box draining" [--task T] [--agent A] \
[--land-branch B] [--state S] [--host <url>]
events: spawn | resume | move | end | state. machineId defaults to THIS
machine's ~/.config/jat/machine-id — pass --machine explicitly when
reporting on behalf of another box.
set -uo pipefail
Want the guided tour instead?
The docs cover the seven
headline products in depth — jat, jst, jat-pilot, jat-secret,
jat-appliance, jatstation, and jat.run — with the story of how they fit
together. This page is the complete lookup for when you already know what
you're after: click a tool, read its real --help.
Every panel is the tool's own --help output.
Source lives under github.com/jatstack.