CLI

Two layers. The worker script is the entry point a person calls, and tickets.py is a helper that the worker script usually calls from inside.

The worker script (<root>/workers/<name>.sh)

Run tick.sh directly and it refuses with rc=2. Without knowing where the worker file is, it cannot know the root either. Every command below goes through that worker file.

CommandWhat it does
(no argument)One dispatch. The cron entry point - take the worker lock → reap → select → run
listThe state of the open ticket queue (open, assigned, blocked)
dryrunNo claim and no run: prints only what it selected and the prompt it assembled
reapOne pass of reclaiming stale tickets, and nothing else
unassign <hash> [--force]Unassign (clear session_id and drop the in-progress suffix) → back to the queue

list, dryrun, reap, and unassign all look at the whole queue, so any worker file under the same root gives the same answer.

unassign refuses and ends with rc=3 if the session is still alive. Unassigning while the session runs reopens the ticket, and two workers end up on the same one. Unassigning yourself is the exception - if the session making the call owns that ticket, it goes through.

--force cuts that session's pid. It locks the ticket into awaiting answer before cutting, so the freed ticket does not go back to the backlog. It shows up where it waits for a person's answer. If the ticket has no pid, forcing gets you nowhere. There is nothing to cut, so you have to end that session yourself.

tickets.py subcommands

A helper for handling the queue and frontmatter. Most of them the worker script above calls from inside, and the ones a person uses directly are about handclaim and find.

CommandArgumentsWhat it doesWho uses it
handclaim<ticket path> ["<owner>"]An interactive session takes a ticket by hand. claim, plus writing pid, claimed_at, and transcript (for the liveness check)A person (an interactive session)
find<root> <hash>Finds a ticket's path by hashA person (checking a hash exists before writing it into deps, say) or the engine
select<root>Prints unassigned open tickets, highest effective priority first, as path|hash|kind|persona|priority|baseline|effective lines. Ties go by creation date, oldest firstThe engine (tick.sh picking candidates)
wips<root>Prints the tickets in progress right now as path|hash|effective priority|assigned_at|pid|owner lines. The other side of select - who is runningThe engine (picking a victim to preempt)
list<root>The full state table of open ticketsThe engine (the worker's list hands straight over to it)
claim<path>The atomic take, <hash>.md → <hash><in-progress suffix>.mdThe engine
release<path>In progress back to the original name (back to the backlog)The engine
assign<path> <sid> ["<owner>"]Writes session_id and assigned_at (and owner) into the frontmatter. pid is cleared at the same timeThe engine
setpid<path> <pid>Writes pid into the frontmatterThe engine
setinbox<path> <inbox path>Writes inbox (the path of the interject FIFO) into the frontmatterThe engine
askhuman<path> [--if-blocked]Locks the ticket into awaiting answer - appends a ## 질문 n (the question section) to the body and puts one hash that does not exist into deps and awaiting. With --if-blocked it only does so when the last section is a fresh ## 블록 (the block section)The engine (both branches of unassign)
clear<path>Clears session_id, assigned_at, pid, and inboxThe engine (unassign, and the failed-dispatch path)
reap<root>Reclaims in-progress tickets whose session died, back to the backlogThe engine (first thing every tick)

select can be called as it is when you are scripting something together, the same way find can. That said, the places a person touches the queue day to day are covered by the worker script's list, dryrun, and unassign, plus handclaim.

migrate.py - project migration

Moves the registered projects to a new format when an update changes it. The app calls this on its own at launch and shows the progress on a blurred screen (The screen blurs after an update). You run it by hand mostly to apply a skipped project again, or on a machine with no screen. The file is migrate.py in the engine folder; the engine inside the app lives in ~/Library/Application Support/dira/engine/.

CommandMeaningrc
list [--root <root> | --all] [--json]Shows a table of each item's state per project (applied - not applied - failed - skipped - broken - on hold)0
check [--root <root> | --all]Prints a <project> <item ID> line for each project with items left to apply. Skipped items do not count0 none, 1 some
gate <root>Decides whether that project may dispatch. When it blocks, it prints the reason in one line. Skipped items block too0 pass, 1 block
dry-run [--root <root> | --all] [--id <ID>]Lists, for each item to apply, the files it would change, marked present or absent. Changes no file0
apply [--root <root> | --all] [--id <ID>] [--status <file>]Applies. For each project it waits for running sessions to finish, then backs up, applies and checks each item. A skipped item is applied only when you pass its ID with --id0 all applied, 1 failed or skipped items remain, 3 another apply is running
skip --root <root>Records the project's remaining items as skipped. Same as Skip this project on screen0
revert --root <root> --id <ID>Restores that item's last backup and deletes its applied record. Refuses items that cannot be reverted0, 1 refused
promote <ID>Releases an item that first goes to the dira project only to the other projects as well. Until then it shows as on hold elsewhere, and on hold does not block dispatch0

With --root, items that apply once to the whole computer are checked too. --all means every project registered in the app. The applied records, backups and log for each project stay under <root>/migrations/.

dira env - using project environment variables from a session

As of 2026-10-10 the commands in this section and the session start notice are still being built. If running dira env list in a session fails with a command-not-found error, this section cannot be followed yet.

These commands let a session use the values left in Environment variables in settings. How to use the screen and what is protected are in the Project environment variables section of Screens. The commands work on the same values, and they need the dira desktop app to be running.

Worker sessions and project home sessions are told, when they start, the variable names registered for the project and the commands below. They do not receive values or revisions. That list of names is as of session start, so if someone may have changed it since, read it again with dira env list. A home session opened without a project gets no such notice.

CommandMeaning
dira env listPrints name, updated time, and revision. Never prints values
dira env create NAME --stdinRegisters the value read from stdin under a new name. Refuses a name that already exists
dira env replace NAME --revision REV --stdinReplaces the value. Ends in a conflict if REV is not the current revision
dira env delete NAME --revision REVDeletes it. REV is checked the same way as replace
dira env run --keys NAME1,NAME2 -- COMMAND ARGS...Runs COMMAND with only the chosen names in its environment

Values are read from stdin only. No option takes a value as a command argument, and no command prints a value. Registering looks like this.

# register a value copied to the clipboard; the value never appears on the command line
pbpaste | dira env create TEST_API_TOKEN --stdin

# read the revision from list, then replace with a new value
dira env list
pbpaste | dira env replace TEST_API_TOKEN --revision <revision from list> --stdin
sh

Do not write a value into echo or printf and pipe it. The value becomes a command argument and stays in shell history. When a session needs a value only a person has, it should not ask for the value in the chat; it asks the person to enter it on the settings screen. Once a value has gone into a chat or a tool call record, dira does not erase it from that record.

If replace or delete ends in a conflict, another session changed it first. Read the new revision with dira env list and run it again. When two sessions replace with the same revision at the same time, only one succeeds.

run - running with only the chosen variables

dira env run --keys TEST_API_TOKEN,TEST_DB_URL -- python3 scripts/smoke.py --target staging
sh
  • Only the names listed in --keys go in. A registered name you did not list is not passed.
  • If any listed name is missing, COMMAND is not started at all. Deleted names and names that cannot be registered fail here.
  • If the shell already has a variable of the same name, the registered value wins. The environment of the shell that called run does not change.
  • COMMAND is run with its arguments as written, without a shell. If you need a pipe or an expansion such as $TEST_API_TOKEN, call a shell yourself, as in -- sh -c '...'.
  • The exit code is COMMAND's own, and cancel signals such as Ctrl-C are passed to COMMAND.
  • In COMMAND's stdout and stderr and in the progress log, any string identical to an injected value is masked, even across lines or when the output arrives in pieces. A transformed value is not masked.
  • Values are read fresh on every run. A replace or delete on the screen applies from the next run; a COMMAND already running keeps the old value.

If the desktop app is closed or the connection drops, every dira env command ends with unavailable. run does not start COMMAND, and create, replace, and delete save nothing. It does not mean there are no variables; start the app and run it again. How to unlock the OS key storage is in the table of the Project environment variables section of Screens.

dira mcp - connecting the engine to an MCP client

As of 2026-10-11 this command is still being built. If it fails with a command-not-found error, use python3 mcp_server.py from the engine repo instead.

CommandMeaning
dira mcpStarts the engine MCP server over stdio, as line-delimited JSON-RPC. Works with the app closed

You rarely run it by hand. Put {"command":"dira","args":["mcp"]} in your MCP client's config and let the client start it. The tools it offers and how to connect over HTTP are in Engine MCP.

dira remote - connecting to a web service workspace

As of 2026-10-11 this command is still being built.

CommandMeaning
dira remote connect <server> --root <queue root>Connects that project to a workspace with a confirmation code. Registers the runtime and, if the queue has no worker, creates workers/w1.sh and the two crontab lines. rc 1 if the code expires, rc 2 if the project is already connected to another workspace
dira remote disconnect --root <queue root>Tells the server you are leaving and deletes remote.json and that workspace's credentials. Leaves the crontab lines alone
dira remote sync --root <queue root>Exchanges one round with the server. Workers call it before every ticket pick, so you rarely run it by hand. Does nothing and exits rc 0 without remote.json; exits at once if the last sync was under 10 seconds ago

What goes to the server and what does not, and why a job from the web starts late, are in Remote queue.

Next is frontmatter fields.