The JSON surface agents depend on. Every schema here is generated from the Codable types in Sources/DirectaKit and locked by golden-file tests; changing a field is an API change and updates this document in the same commit. All timestamps are ISO-8601 UTC with milliseconds. Timeouts are seconds.
Success: the command's result object on stdout. Failure with --json: {"ok": false, "error": {"code", "message", "hint"}} on stdout; hint is the literal remediation command when one exists.
Stable error.code values: already-exists, config-invalid, daemon-starting, daemon-unreachable, internal-error, not-found, not-trusted, port-drift, port-held, project-still-exists, request-too-large, resource-locked, resource-mutated, spawn-failed, usage, version-mismatch. (Grows append-only.)
A request line the daemon has not yet seen a newline for is capped at a fixed size (a request is always small; the largest legitimate one, a whole devservers.json through project.writeConfig, is nowhere close). Exceeding it answers request-too-large on that connection and closes it, with no hint: the cause is a client writing raw NDJSON to the socket without a newline, and there is no directa command that fixes someone else's framing bug. This never fires for the CLI or the app, which always send one small, complete, newline-terminated request at a time; it only guards a client writing directly to the socket. A daemon response carries no such cap, since directa logs without --tail can legitimately answer with tens of megabytes.
Exit codes: 0 ok · 1 operation failed (crash, timeout, conflict) · 2 usage · 3 daemon unreachable · 4 named server not found. Unnamed status in an unconfigured project exits 0 with {"servers": []}. A project-scoped command run from a current directory that no longer exists (a removed worktree) fails usage (exit 2) at once, saying the current directory is gone, with hint run: directa status --all; it never walks a relative path. lock refuses the same way even with --project, since its command runs in the caller's directory. Session hooks and statusline stay silent instead (Antigravity's hook writes its empty envelope). --project must name an existing directory; a project name is refused usage (exit 2) rather than answered as an empty project.
Every --timeout and --acquire-timeout takes a number of seconds from 0 to 86400. Anything else (text, inf, nan, a negative number, a larger number) fails usage (exit 2) with a message naming the flag, the value, and the range, through the envelope above under --json, before any request reaches the daemon.
DIRECTA_SOCKET: the daemon socket every command talks to.DIRECTA_DATA_DIR/DIRECTA_LOGS_DIR: the CLI's local data and logs directories, the counterparts ofddirecta --data-dir/--logs-dir. WithoutDIRECTA_SOCKET, the socket followsDIRECTA_DATA_DIR(<data dir>/daemon.sock), matching a daemon started with--data-diralone. An empty value counts as unset.daemon.inforeports the answering daemon'sdataDirandlogsDir, anddoctoruses those over the local ones.- Any of the three set turns auto-bootstrap off: a command never installs or starts the background agent for a layout the agent does not use.
daemon install|start|restart|uninstallanduninstall(with or without--purgeor--agent-only) manage that agent, which runs only the default locations, so with any of the three set they refuseusage(exit 2) before touching anything, naming the variables to unset, with hintrun: env -u DIRECTA_DATA_DIR -u DIRECTA_LOGS_DIR -u DIRECTA_SOCKET directa <command>.daemon stopasks whichever daemon the resolved socket reaches to shut down, which is how a daemon started by hand with--socket/--data-dir/--logs-diris stopped.
Filled in per phase as each lands; golden tests lock these schemas as exact strings.
-
directa status [name] [--all] --json→{servers: [ServerStatus], trusted?}. Named lookup that matches nothing exits 4 (not-found); unnamed always exits 0. Anot-foundfor a name the resolved project lacks (from every command that takes a server name, CLI or daemon side) looks for the name in the other registered projects: none has it → hintrun: directa status --all; exactly one → the message names that project and the hint is the same command with--project '<path>'; several → the message names every project path that defines it and the hint isrun: directa status --all <name>. A name is never resolved into another project on its own. The linked-worktree case keeps its own wording: copying devservers.json into the worktree is the fix that keeps the worktree's own server, and--projectwith the main checkout's path acts on the main checkout's server instead, which the message says.--allis machine-wide (every registered project) and auto-prunes checkouts whose path is gone;trustedis present only on a scoped project query. Human mode: an unnamed scoped query that finds no servers while other projects on the machine have some namesdirecta status --all; the JSON is unchanged. -
directa register --name N --cmd word --cmd word … [--port P] [--cwd D] [--write [--force]] --json→{server: ServerStatus}.--cmdrepeats per argv word and accepts dash-prefixed values. The spec is validated the same way a committed one is (a name carrying::, an empty command, an out-of-range port or healthcheck value failconfig-invalid), so a specconfig checkwould reject can no longer be registered.--writealso appends the server to devservers.json, merging so every other entry survives; an existing entry of the same name needs--forceand otherwise failsalready-exists. -
directa unregister <name> --json→{}. Refusesnot-foundfor a name that is not registered ad hoc (including one declared only in the project's committed devservers.json). If the server has a process (running, starting, or failed for its port with the process still up), unregister stops it first through the normal stop path and waits for it to exit before dropping the registry entry and the log directory. Whatever the stop does, the server is left with no restore-at-launch intent, so a committed server of the same name does not come back on the next daemon launch. A stop that does not finish in time keeps the log directory fordoctorto report later; a process that had not appeared yet is stopped the moment it does, and one still running at the next daemon launch is stopped then, provided its start time matches the recorded one (a process that merely reuses the number is left alone). Arestartof the same server that is in flight when unregister lands does not start it again; its result carriesreason: "stopped". -
directa start|stop <name> --json→{server: ServerStatus}.startreturns at spawn withphase: "starting"; health promotion torunningfollows (usewait/ensureto block).stopon an already-stopped server exits 0. -
directa ensure <name> [--port P] [--timeout 60] --json→{reason?, server}. Optional--portoverrides the committed port for this run (flows through the same effectivePort pipeline as sibling rebind anddirecta.local.json). State matrix: running and the latest health check for this process passed → no-op; running while that check is failing → wait without restarting; starting → joins the in-flight attempt (single-flight, two concurrent ensures cannot double-spawn); unhealthy → restarts it as one daemon-side stop-then-spawn (the restart's port re-check included) and blocks until healthy, the stop recorded with detailrestarted by ensure: unhealthy; stopped/crashed/failed → start and block until healthy. Fails fast the moment the phase turns crashed/failed.reason: "crashed"|"failed"|"stopped"|"timeout"; reason present ⇒ exit 1, withlastExit/spawnError/recentLogTailforensics inserver. Sibling port conflict auto-rebinds and still returns ok withserver.portConflict.state: "rebound". -
directa start <name> [--port P] --json/directa up [<name>] [--only a,b] [--port P] [--timeout 60] --json: same optional--portoverride as ensure (applied to each started server underup). -
directa wait <name> [--healthy|--stopped] [--timeout 60] --json→{reason?, server}.--healthy(default) rides through non-terminal transitions (another session's restart) and fails fast on crashed/failed/stopped. It resolves only once a health check has passed for the process that is up now: a check from the process it replaced does not count, and while the latest checks are failing the wait keeps blocking even thoughphasemay still readrunninguntil enough failures accumulate to mark it unhealthy.--stoppedresolves once the server has no live process: stopped, crashed, or failed with its process gone; a server failed for its port whose process still runs keeps the wait open, and a human-mode timeout on one namesdirecta stop <name>. Human mode: a--healthywait that falls short on crashed or stopped namesdirecta ensure <name>as the next step. A timeout while the server is stillstarting(fromwait,ensure,restart,up) still exits 1, and its human line says the server is still starting, not that it failed, and namesdirecta wait <name> --timeout <seconds>to keep waiting. -
Port pre-check on every start-shaped path (
start,ensure,up,switch, a lock resume, boot recovery): resolveseffectivePortfirst. Held by a managed sibling (same git common-dir) → auto-rebind and continue (status carriesportConflict.state: "rebound"): the rebound block (primary, span, named offsets) has no listener and clears every port a live managed server holds, a declaredportSpanreserving its whole block rather than only the ports it listens on. Held by an unrelated managed server →port-heldnaming that server and project. Held by an unmanaged listener →port-heldwith pid + command when lsof can name it. After first-healthy, ifobservedPort != effectivePort→port-drift(Vite silent bump is not healthy). The managed-holder scan sees both live supervisors and holders recorded in state whose pid is still alive; a server holds its whole claim whilerunning,starting, orunhealthy(never whilestopping, since it is releasing its ports), and a server failed for its port holds the port it was seen listening on while its process lives. A listener on a port the target itself holds is the target's own run, not a conflict. The listener probe tries both loopback families and runs before the holder scan, so a run another request spawned a moment earlier (a concurrentensureof the same server) is found as managed rather than reported as an unmanaged listener. Runs only when the target is not already up. Underup/switchan unhandled held port fails the whole rollout withport-held. A transition that stops a run and spawns the next one in one step (restart, the watch sweep's restart,start/ensureover a port-failed run) checks again between the stop and the spawn: it waits a bounded time for the server's claimed ports to have no listener that belongs to the run just stopped, cannot be attributed, or sits on a port that had no listener before the stop (a listener outside that run on a port that was already listening before the stop is the shared or foreign case the pre-check let through, and it does not block the spawn), and a listener still there is named in the server's sys log (pid, command, and whether it belonged to the run just stopped) and fails the transition withport-heldnaming it and hintrun: directa why <name> --project '<path>', so the new run never answers its first health check through a process it did not start. A transition refused this way leaves that server stopped, and a multi-namerestartends at that entry: earlier entries stay restarted and later ones are untouched, their spec, port, and url exactly as before the request (preparing a spawn never alters a live run's reported spec). A watch-sweep restart refused this way records astoppedevent whose detail names the held port, so status, events, and the hooks show why the server is down. -
Ownership check after first-healthy: a passing healthcheck proves something answered, not that this server answered. Every pid listening on
effectivePortis compared against the server's process tree. Listeners both inside and outside →portConflict.state: "shared", phase untouched, since this server is serving and only the port is ambiguous (a v4-only listener beside a v6 wildcard bind, for instance). All listeners outside the tree →portConflict.state: "foreign", and the phase changes tofailedonly when one of those pids is another server this daemon supervises, which is what makes theft provable. An unattributable outside listener is annotated and left running: a container-backed or daemonizing server has its socket held by a process directa never parented, and that is indistinguishable from theft by lsof alone. No listeners reported → nothing is recorded, because lsof can be unavailable and an empty result is not evidence; when lsof did answer and named no listener oneffectivePort, the server's sys log says so, since a pass that nothing was listening for afterward means some other process answered it. A server failed forport-driftor a proven foreign owner keeps its own process running:stop,unregister, and forgetting its project stop that process,start/ensurestop it before spawning the next run,lock --pausestops it like any running declarer and a non-pausinglocklists it as live, and a status read leaves the settings it was spawned with (an overridden or rebound port, the url built from it) in place. -
Latent port conflicts are annotated on every status read, including the machine-wide sweep and
why, so a stopped server names the holder keeping it down instead of reporting only "not running". -
directa.local.json(project root, gitignored by convention): partial per-server overlay merged after committeddevservers.json(port, portEnv, portSpan, ports, command, cwd, env, url, host, heads, healthcheck, shell). Never committed.portSpanclaims a consecutive block from the effective primary without naming each secondary;portsnames secondaries as{ "offset": N, "env"? }(moves with sibling rebind) or{ "port": N, "env"? }(absolute, machine-singleton). OverlappingportSpanand named offsets is a config error. -
errorSummaryis directa's own count of the last run's stderr lines with the first and last timestamps, captured when a server turns crashed, failed, or unhealthy. It is arithmetic over the log, never the lines themselves, so the session-context hook can surface that errors piled up without emitting attacker-influenceable child output; the agent reads the actual lines withdirecta why. -
terminalEvidence/recentLogTailon status: short out/err/sys lines captured at terminal transitions (and persisted across ensure truncate / daemon rehydrate) sowhystill sees a refusal after a retry. -
Healthchecks: explicit
healthcheckblock wins; else a declaredportimplies a TCP probe; else healthy = alive past a 2s stabilization window.ServerStatus.healthchecksays which ("http"|"tcp"|"none") so agents know whenrunningis unverified.unhealthyAfterapplies only after first-healthy: a slow boot staysstarting.timeoutMsbounds each probe in full (an HTTP answer that arrives inside it passes however slow it is); a probe falls back from IPv4 to IPv6 only when the IPv4 connection was refused, never after a timeout. -
directa logs <name> [--follow] [--tail N | --head N] [--all] [--since 5m|ISO] [--since-mark <id>] [--grep RE] [--stream out|err|sys|mark] --json→ one{at, stream, text}object per line on stdout.--head Nkeeps the oldest N matching lines of the window (the read-what-was-skipped shape:--since <ts> --head 200) and failsusagewith--tail,--all, or--follow. With none of--tail/--head/--since/--since-mark/--follow/--all, the request carries an implicittail: 200(client-side; the wire itself is unchanged, so this applies against any daemon version) rather than answering a long-running server's whole history.--allasks for that full history outright and failsusageif combined with--tail(naming incompatible amounts of output); combined with--since/--since-markit is a no-op on the tail, since those already leave it unbounded on their own.--follow's own initial-backlog default (tail: 50) is separate and unaffected.--grepis the Swift Regex dialect. A pattern that will not compile, or one that nests an unbounded quantifier inside another (the(a+)+catastrophic-backtracking family, which Swift's regex engine runs for minutes on a single line), failsusagerather than running.--followpolls past the daemon's cursor (restart-safe): the first answer seeds it even when no line matched, and each poll returns exactly the lines appended since, same-millisecond lines included, with stdout flushed after every poll. UnderCLAUDECODE=1with stdout not a terminal,--followprints one stderr line pointing atdirecta monitor <name>and the Monitor tool.--headand--followagainst a daemon whose answer lackscursorfailversion-mismatch(exit 3, hintrun: directa daemon restart) rather than read an unbounded window. Structured lines are sanitized: ANSI/OSC escapes stripped, NULs removed, CR spinner rewrites collapsed to the final frame; timestamps are per-file monotonic. -
logs.querywire fields (all optional, append-only both ways; an older client's params and an older daemon's result still decode):LogCursor{at, count, position?}: every record beforeat, plus the firstcountrecords stamped exactlyat(all streams, file order), lie behind it.{"at":"1970-01-01T00:00:00.000Z","count":0}is the cursor of a family with no records.position{file, offset}is present oncecountreaches the daemon's threshold (a large same-millisecond run, which is every record written after a backward clock step until the clock catches up):fileis the inode number of the log file holding the newest record behind the cursor (a rotation's rename keeps it) andoffsetis the byte just past that record. A poll past a positioned cursor resumes at that byte rather than counting through the millisecond again, and the next cursor'scountadds what arrived to the one the caller sent. A position that no longer names a record ending there stampedat(its file rotated out of the family, or a made-up value) is ignored andcountapplies, so a cursor stays valid without it and callers pass the cursor back exactly as received.LogStreamCounts{err?, mark?, out?, sys?}: one number per stream, always an object.LogStreamTotals{err, mark, out, sys}: the same object with all four fields present.- Params:
after: LogCursor(exclusive lower bound),head: N(oldest N matches),tailByStream: LogStreamCounts(newest N per stream, so one stream's burst never crowds another out; nil leaves a stream untrimmed, 0 excludes it),maxLineCharacters: N(each text cut to N characters, the last being…). Refusedusagewith a hint:afterwithsinceorsinceMark;tailwithtailByStream;headwithtailortailByStream; any negative count (after.count,head,tail, atailByStreamfield);maxLineCharactersbelow 1. - Result:
cursor: LogCursoron every answer, even an empty one (the position after the newest record in the family when the query ran; the nextafter);totals: LogStreamTotalsonly when the query carriedafterortailByStream(records matched per stream before trimming, sototalsminus the returned lines is exactly what the trim withheld). Aheadquery withoutaftercarries nototals, and the daemon stops reading once it holds the N lines.
-
directa monitor <name> [--lines-per-minute N] [--lines-per-arm N] [--errors-per-minute N] [--errors-per-arm N] [--tick S] [--json]: streams a server's output shaped for an agent's own streaming tool (Claude Code's Monitor tool, Grok Build's monitor tool). Attaches with a start marker (checkout path, phase, pid, last exit, effective budget,directa logs <name> --tail 200for earlier output), then pollslogs.querypast the daemon's cursor once per--tick(default 2, range 0.5-60 seconds), applying repeat suppression and per-stream budgets to what returns;server.statusevery 10s feeds a health line only on change. Budget flags, each refused by the argument parser outside its range (exit 64, the message naming the flag and the range, as for--tick):--lines-per-minute(default 120, range 1-1200),--lines-per-arm(default 600, range 1-20000: the total for the run, permanent, re-arm by running the command again),--errors-per-minute(default 30, range 1-600),--errors-per-arm(default 300, range 1-5000, independent of stdout's so stderr keeps flowing after stdout is spent). Attaches with atail: 0query (the end cursor, without reading the log's history) and gates on itscursor, the same gatelogs --head/--followuse, failingversion-mismatch(exit 3) rather than degrading; the first tick gates ontotalsthe same way, ending the run with a line naming the message and exit 3. A name unknown to the resolved checkout isnot-found(exit 4) before any output; the same condition discovered after attaching instead ends the run gracefully (ended (server unregistered)), never a nonzero exit. Onlydaemon-unreachable,daemon-starting, andconfig-invalidare waited out, each reported with one line when it begins, retrying with backoff up to 10s; an answer the client cannot decode is reported the same way. Unreachable and undecodable answers share one clock: 60s of them exits 3 (unreachable) or 1 (undecodable) before attaching, or ends gracefully after. Any other error exits through the failure envelope before attaching, or after attaching ends the run withended (<message>; <hint>)and the exit status that code maps to. Monitor never starts the daemon: watching must not change what it watches, since a daemon may be stopped on purpose (directa daemon stop) and an agent re-arms its monitor in a loop, so an absent daemon is waited out and reported instead. After 29 minutes (below Claude Code's 30-minute Monitor-tool kill) the run reads past its cursor once more, prints what arrived, and ends naming the command to run again anddirecta logs <name> --since <final cursor time> --head 200for anything that lands before the new run attaches. Everydirecta logs <name> ...hint names the server only when the name is shell-inert (ASCII letters, digits,-,_,., at most 64 characters) and says<name>otherwise. Each tick asks for at most 300 lines each of out and err and 50 each of sys and mark; lines the daemon matched past those caps are never dropped silently: they get asuppressedline naming the count, the stream, the cap, and adirecta logs <name> --since <cursor time> --stream <stream> --head 200command, except out or err lines trimmed while that stream is already over its per-minute budget, which count toward its resume line instead. A read-what-was-skipped hint starts where the skipped lines start: the oldest suppressed repeat for the repeated-lines summary, the cursor the query read past for lines the daemon trimmed. A reprinted multi-line block (a stack trace) collapses only lines already seen; the first new line ends it and shows. Every child line shown, a(repeated xN)collapse included, spends the stream's budget. Human-mode lines use one of two namespaces:<label> out|/<label> err|for child output (and its(repeated xN)/(again, Nth in <window>)collapses),directa <label>:for everything else (the start marker, lifecycle, marks, health, budget/transient/suppressed-summary lines);<label>is<name>in a main checkout or<name>@<worktree label>in a linked worktree.--jsonemits oneMonitorEventobject per line (NDJSON) instead of the human line:{at, count?, kind, label, stream?, text},kindone ofattached|line|repeated|suppressed|lifecycle|health|budget|transient|ended(append-only),countpresent only forrepeated/budget/suppressed(the repeat count, the crossed budget cap, or the suppressed total),streamone ofout|err|sys|markpresent online/repeated(picks the namespace) and on any kind that names a specific stream. -
directa mark <name> <text> [--label L] --json(or--all <text>) →{marks: [{at, id, server}]}. The id feeds--since-markonlogsandevents, so no clock agreement is needed. Marks flow through the same append path as process output; ordering is exact. -
directa events [--all] [--since …] [--since-mark <id>] [--tail N] --json→{events: [{at, detail?, kind, project, server}]}with kindsstarted|stopped|crashed|failed|healthy|unhealthy|marked|registered|unregistered.kindis append-only: a client must tolerate a value outside that list (a newer daemon can add one) rather than fail the whole response, and must round-trip an unrecognized value unchanged if it relays events rather than interpreting them. Scoped to the current project unless--all. A negative--tail(on the wire,events.querytail) is refusedusagewith a hint, as a logs query refuses one. This is the "what happened while I was compacted" answer. -
directa why <name> --json→{findings: [{server, phase, summary, evidence[]}], rootCause?}. Diagnoses over the merged project view (committeddevservers.jsonplus ad-hoc registry), the same set asstatus/ensure. WalksdependsOnto the deepest broken dependency; evidence prefersrecentLogTail/ persistedterminalEvidence, else a structured-log window (out+err+sys). Exit 0 after a short life notes that controlled refusals are common. A running server is called healthy only when its latest health check passed; one whose latest checks failed reads as running with that many failed health checks in a row and the latest failure's detail. Agent session context never embeds raw child lines; it points here. -
directa up [<name>] [--only a,b] [--timeout 60] --json→{results: [{reason?, server}]}. Wave-parallel in dependency order (Kahn waves); a positional<name>is shorthand for--only <name>(both together failusage);--only/<name>pulls in transitive dependencies; a wave with a failure stops the rollout (later waves depend on it); anyreason⇒ exit 1.waitFor: "started"on a server launches it without blocking on health; the default blocks until healthy.directa down [<name>] --jsonstops the whole project in reverse waves, exit 0 always; a named server stops only that server, never its dependents, through the sameserver.stopevery other single-server command uses (nevergroup.down'sonly, so every daemon version understands it) rather than a group teardown, and exits 4not-foundfor a name that does not exist. -
directa trust --json→ approves the project's committed config. Trust is also recorded implicitly by an explicitstart/ensure/up/restarton a file-sourced server, and byswitch(which records it before its lifecycle playbook runs); the invocation is the approval. The SessionStart hook advertises only trusted projects.status --jsoncarriestrustedwhen project-scoped. -
directa open <name> [head]→ opens the server's URL in the default browser, or a named head of a multi-headed server. URLs derive ashttp://<host>:<port>/from the projecthost(default<slug>.localhost), overridable per server (hostorurlkeys). Aheadsmap (display name → URL) models one server fronting several surfaces (a Host-routing dev proxy); heads appear inServerStatus.heads, the agent context block, and the app's row/detail menus. A head is written either as an absolute URL or as a root-relative path (/admin), which resolves against the server's own effective base so it follows a rebind; the same two shapes apply tohealthcheck.url. Anything else, and a root-relative value on a server that declares neitherportnorurl, is aconfig checkerror. -
directa link <verb> <name> [head] [--json]→ prints adirecta://URL for the cwd project (directa://ensure/<slug>/<name>, etc.). Verbs:open,ensure,stop,why.--json→{url}. For Raycast/Shortcuts/docs; the menu bar app handles the same URLs via Launch Services. -
directa x-url <url> [--json](hidden): dispatches adirecta://URL through the sameDeepLinkRunnerthe app uses (no Launch Services). Smoke/CI entry. Success →DeepLinkRunResult{verb, projectPath, detail?}; bad URL / unknown slug → usage/not-found. -
directa config check --json→{errors, host, servers, warnings}from the daemon's own validator (cycles, unknown dependencies, and unresolvableheads/healthcheck.urlvalues are errors; duplicate declared ports, unknown versions, an http healthcheck with no url, and bare-loopback hosts and heads are warnings); errors ⇒ exit 1. Name servers after the project (not a genericweb) and give each a<project>.localhosthost, not barelocalhost: the subdomain keeps browser cookies/storage/service workers isolated per project. -
directa restart <name> | --all [--port P] [--timeout 60] --json→{results: [{reason?, server}]}. Stops and re-ensures as one daemon-side transition, so no other session'sensurelands in between and a refusal (a held resource, a paused server, a config that no longer parses) arrives before anything stops rather than after. The stop is non-retiring, so resume-on-boot survives it. A barerestartwith neither a name nor--allis a usage error; anyreason⇒ exit 1, and human mode prints each server that fell short with its reason, the same lineupandswitchprint. The CLI readsserver.statusfirst, then sendsserver.restartonce and never repeats it blindly: when the connection drops before the answer (the daemon crashed, was killed by the system, or was restarted), it says so on stderr, waits for a daemon to answer again (the restart's--timeout, at least 30 seconds, waiting outdaemon-starting, with each status read given only what is left of that wait, so a daemon that accepts the connection and never answers cannot stretch it), and decides per server from a fresh status read against the pids it read first: the same process still running means the restart never reached it, and only those servers are restarted again; no process, with none running before or with the serverstopped, is finished withserver.ensure(the same--port); anything else (a new process, or a new run that crashed) is waited on withserver.wait --healthy, so its result is what the restart would have answered. A connection lost again repeats that decision, a bounded number of times. The result has the same{results}shape. A daemon that never answers again failsdaemon-unreachable(exit 3) with hintrun: directa statusand a message saying the restart may already have happened. -
A server may declare
watch: project-relative files it reads at boot and does not reload itself ("watch": ["vite.config.ts"]). A change to one restarts that server. A server whose framework already reloads its own config declares nothing and behaves exactly as before. Paths are literal, relative to the project root, and may not exist yet (their appearance is a change); a glob, an absolute path, a path outside the project, or a directory is aconfig checkwarning and is ignored. The baseline is taken after the server has been up for a settle window, so a config the server writes during its own boot does not bounce it, and a change must hold still for a quiet window, so one save touching several files is one restart. A restart is refused while a resource the server declares is held, and the pending change fires when that hold releases rather than being dropped. Repeated restarts inside a short window suspend the watch with asyslog line naming the paths, since directa cannot tell a server rewriting its own file from a person saving repeatedly; an explicitdirecta restartre-arms it.DIRECTA_NO_WATCH=1disables the sweep for the whole daemon. -
directa config init [--dry-run] [--force] [--host H] [--name N --cmd word … [--port P]] --json→{check: CheckResult, content, notRecovered?, path, written}. Writes devservers.json from the servers the daemon already knows, which is the way back from losing a gitignored file. Refuses an existing file withalready-existsunless--force;--dry-runreturns the content and writes nothing. The projection writes only what the file declares: an effective or rebound port, a materialized url, an absolute icon path, and the port and host keys directa injects into the environment are all dropped (a worktree label host from state an older build persisted is dropped too), so the file stays portable to another checkout on another machine.lifecyclehas no runtime counterpart and cannot be recovered; it is named innotRecoveredrather than silently lost. The written file is indented, unlike every wire frame. -
directa config check --jsonalso reportsworktree(the linked git worktree's display label) when run inside a worktree, plusserverHostsfor servers whose effective host differs from the project's; only adirecta.local.jsonoverlay produces a difference today (effectiveHost/effectiveHostReasonat the top level are populated only by an older daemon). A linked worktree deliberately keeps the declared host unchanged: every*.localhostname resolves to loopback, so a label subdomain never disambiguated a bind, while a third-level subdomain breaks any origin the app itself pins (an auth callback, a CORS allow list, an API key referrer, a cookie domain). A worktree checkout reportsconfig ok. -
directa doctor [--fix] --json→{findings: [{detail, kind, severity}]}: daemon/launchd state, captured-PATH staleness, the host:port signature table with conflicts, cross-project port collisions (port-collision: two unrelated projects declaring one port, which no host:port signature can catch because the hostnames differ while the bind does not; sibling worktrees are excluded since they rebind by design), unmanaged listeners on managed ports, and registry entries whose project path no longer exists (the daemon auto-prunes those at boot, on machine-wide status, and on a timer sweep, once a path has been observed missing continuously for a full sweep interval rather than on the first miss, so a network blip or a slow unmount does not cost a project its trust or log history and the servers of a genuinely discarded checkout stop themselves within about a minute;--fixcallsproject.forget(params{project}) for a leftover the automatic sweep has not yet caught, which stops its supervisors, drops its registry row, trust, locks, and state, and removes its log directory, refusingproject-still-existsif the checkout has reappeared since; each attempt reports its ownfixedorerrorfinding rather than assuming success). Also reportsharness-hookfindings (a detected harness with no directa hook, or a hook pointing at a path that no longer exists); aninstall-shadowfinding when a second directa copy shadows the Homebrew install (amake installCLI in~/.local/binahead of brew's bin on PATH, or a foreign/Applications/directa.appHomebrew could not overwrite, either of which lets a baredirectaor the daemon keep running an old version after an upgrade); anorphan-log-dirfinding for a directory named the way directa names a project's log directory (<slug>-<8 lowercase hex>) under the daemon's own logs dir (daemon.info'slogsDir) that matches no project the daemon claims (daemon.info'sclaimedProjects: every registry project plus every project with a resident supervisor, not the narrower set with a currently readable devservers.json, so a trusted project mid-edit on an invalid config keeps its log directory), naming its apparent size anddirecta doctor --fixas the command that removes it (unregistering a project's last server, or the daemon forgetting a missing one, already removes its log directory; this catches one that predates that cleanup or was orphaned some other way, since onlydirecta uninstall --purgeremoves the whole logs tree automatically).--fixlists the unclaimed directories without sizing them and asks the daemon to remove each one by name throughlogs.removeOrphan(params{directory}, a single directory name inside the daemon's logs dir, never a path; result{outcome, path, reason?, remedy?},outcomeone ofremoved|refused|failed). The CLI never deletes a log directory itself. The daemon checks each name against the projects it claims at that moment, with no other request running in between, so a project started while doctor ran keeps its directory. It removes the directory only when it sits directly inside the logs dir (both paths compared with links resolved, so a/varand a/private/varspelling agree), is a real directory rather than a symbolic link, has directa's log directory name shape, and is claimed by no project; anything else is left in place, and each attempt reports its ownfixedorerrorfinding, a refusal naming a next step where one exists and never a deletion command. Against a daemon that does not knowlogs.removeOrphan,--fixremoves nothing, reports the leftover directories as it would without--fix, and adds oneerrorfinding saying the daemon is too old, withdirecta daemon restartas the fix. A second daemon started on another socket with the same logs dir is outside this check: its projects are not in the answering daemon's claimed set. A symbolic link, or a folder without that name shape (a logs dir shared with other apps holds theirs), is never reported. The finding,--fixincluded, is skipped entirely against a daemon whosedaemon.infopredatesclaimedProjects, never guessed from machine-wide server status; and, when a newer release is available, anupdatefinding read from the shared update-check cache (refreshed only when stale). Doctor reads and writes the files a daemon owns (the update-check cache,agent.path, the logs tree) in that daemon's owndataDirandlogsDirfromdaemon.info, and in the local layout (the Environment section) only when no daemon answers. Ajetsam-coalitionwarning fires when a live server still shares the daemon's jetsam coalition, the condition that lets memory pressure kill the daemon instead of that server. Ajetsamwarning fires when the agent last exitedOS_REASON_JETSAM(macOS killed ddirecta as the largest compressed process; KeepAlive then relaunches it, and recover adopts each server whose launchd child job is still running rather than restarting it, bouncing and restarting only a server it cannot adopt, and leaving a running server alone when launchd will not say which jobs it has). Aleftover-jobwarning fires when one-shotdev.quantizor.directa.job.*labels remain registered with no live server: a jetsammed daemon skips the spawn-pathbootout, and recover reaps them. Without--fixevery finding is report-only and names the exact command to run.--fixchanges only directa's own state: it forgets stale registry entries and removes unclaimed log directories under directa's own logs dir, and never edits a file the user owns outside that. -
directa switch <branch> [--no-fetch] [--timeout 120]→ clean-tree guard (refuses dirty; never stashes), fetch, group down,git switch(remote-tracking fallback), validate the branch's devservers.json, record trust for the project (a failure warns on stderr and switch continues; running the playbook is the approval, and recording it first keeps the state coherent if the playbook dies midway), then the project'slifecycle.switchplaybook (argv arrays run sequentially from the project root; failures stop withdirecta upas the resume hint), then group up. The branch's devservers.json is validated after the checkout and before the playbook runs, so a configconfig checkwould reject (including an empty lifecycle command, which the validator now covers) failsconfig-invalidinstead of feeding argv to the shell. Playbooks live in devservers.jsonlifecycleand are agent-configurable. -
Config extras: project-level
icon(project-relative path, per-server override) feeds Spotlight thumbnails; every server and head is indexed in Spotlight as<project> · <head>with subtitledirecta · <url>(best-effort; not a Top Hit launcher); a worktree server's entries lead with the main project (myproj · review · web) and carry it as a keyword so a typed "myproj" still lands;headsand pins surface in the menu bar app. -
directa lock <resource> [--acquire-timeout 300] [--pause] [--timeout 120] -- <command…>→ runs the command holding a project resource exclusively. By default the servers that declare the resource in theirlocks(devservers.json) keep running: the lock serializes access against other holders and the fingerprint guard below flags a live server corrupting the state.--pausestops those declarers for the command and re-ensures them on release (even on command failure), for state a running server holds open (a local database). The command starts only after every paused declarer's whole process tree has exited, its descendants included; a declarer whose stop gives up with processes still running fails the lock withresource-locked, naming that server and hintrun: directa why <name>, resumes whatever was already paused, and never runs the command. The failing declarer itself is not resumed, since its processes may still hold the port. The CLI fails at once on aresource-lockedrefusal that names no holder, rather than retrying it until--acquire-timeout: whenlock.statusshows none after a refusal, the CLI retries the acquire once at once (the holder may have just released) and fails only if that retry is refused with no holder again. The command's environment addsDIRECTA_LOCKS, the comma-joinedcanonicalPath::resourcekeys held around it, appended to any inherited value, so a tool with its own lock for the same resource can tell it is already serialized.directa lockitself reads it: a command whose inheritedDIRECTA_LOCKSalready lists the key it asks for runs the command at once without a second acquire (re-entrant), so a guarded script that locks the same resource again cannot wait on its own parent. A resource name containing,or::is refused (usage), since the joined list could not be split back apart. The key is reserved from the moment an acquire begins pausing declarers, so astartorensureof a declarer, or a second acquire, that lands during the pause is refused as contended rather than slipping in. A paused declarer whose stop was already in flight counts as stopped only once that stop's whole tree has exited.ensure/startof a declaring server is refused (resource-locked, naming the holder pid) while a live holder owns it, with or without--pause. Locks are path-scoped (canonicalPath::resource); they do not pause other checkouts. Locks persist across a daemon crash: a dead holder auto-releases and resumes the paused set; a still-live holder keeps them paused so the harness stays exclusive. A server the daemon would bring back at boot (or resume on another lock's release) that declares a resource a live holder owns joins that holder's paused set instead of being refused: it readsstoppeduntil the release, then starts the way a--pausedeclarer resumes, and a server declaring several held resources waits on each in turn. Boot restore marks a servercrashedonly when its process died with the daemon; a server the daemon stopped on its way down readsstoppedif its restore is refused for any other reason (trust, config). Exit status is the command's. The--is required and directa's own options go before it; everything after--is captured verbatim, so a nested--, a dash option, and an empty string all reach the command untouched. A missing terminator or an unknown option is rejected by the parser at exit 64 rather than being passed through. The command runs in the caller's own current directory, never the resolved project root (they can differ: a monorepo subpackage below the root, or an explicit--projectpointing elsewhere), so a relative file argument or a tool that finds its own config by walking up from cwd sees what it would running unwrapped. A contended acquire writes the holder's pid, how long it has been running, and what it paused or left running to stderr, then repeats a still-waiting line every 15s, so a wait is never silent;--acquire-timeout 0makes exactly one attempt and fails immediately withresource-locked. All of lock's own output is stderr: stdout belongs to the guarded command.A
locksentry is written either as a bare name ("d1") or as an object naming where the resource's state lives ({"name": "d1", "path": ".wrangler/state/v3/d1"}); both forms parse and the bare form re-encodes bare. With a path declared,lockfingerprints that state before and after the command (device and inode, size, and a SHA-256 over a bounded manifest) and reports a change: with a declaring server still running (the default) it is aresource-mutatedfailure, because that server holds the old state open and can write its cached pages back over the change, and under--pause, with the declarers stopped, it is a note on stderr. Two servers declaring one resource with different paths is a config error rather than a guess. What the check cannot catch: it flags the risk window, not the damage, since the flush that corrupts can land after the command exits; it cannot see state outside the declared path (a sibling-walfile whenpathnames only the.sqlite), divergence that never reaches disk, or a change that reverts to byte-identical state inside the window; a directory stops hashing file contents past a byte budget and reportsexact: falsewhen it did, so a change confined to a file past that budget is missed; and it never names which process wrote. A resource that is a single file is hashed whole at any size, so a middle-only rewrite that preserves size and mtime is caught. A bare-named lock (nopath) has no state to fingerprint, so the default hold cannot detect a change at all; when such a hold leaves a declaring server running,lockwarns on stderr that the guard is off and names the exact fix: which declaring server to edit and the object form to give it ({"name": "d1", "path": "<path to d1's state>"}), or--pause. -
directa context: the harness-agnostic session context: a fenced<directa-servers>plain-text block (server phases, effective URLs, log paths, latent/rebound port-conflict warnings, the ensure/wait/why/logs/lock cheat-sheet) for the cwd's project. A linked worktree gets a banner naming its display label. Silent (exit 0) when the project is unregistered or untrusted or the daemon is down; never bootstraps; never contains raw log lines or command strings. -
directa daemon status --json→{daemon?, launchd, reachable}.reachableis whether the daemon answered over the socket, and it is the field to branch on:launchdreportingrunningonly means a job is loaded, so a loaded-but-not-listening daemon prints a reassuring launchd line withreachable: false.daemonis present only when reachable. Exit stays 0 either way, because the launchd half is still a useful answer.daemon.restoringis present and true only while boot restore is running, and absent otherwise; a client waiting for a usable daemon wantsreachableand norestoring, since the daemon accepts before restore finishes and declines work until it does. -
Boot restore is a window, not an outage. From the moment the socket accepts,
daemon.infoanddaemon.shutdownanswer and every other method is refused withdaemon-startingand the hintrun: directa daemon status, rather than the daemon being unreachable while it works. The distinction is what a client must branch on:daemon-unreachablemeans nothing answered and starting a daemon is the fix,daemon-startingmeans one is already coming up and starting another is wrong. The CLI waits outdaemon-startingfor up to 30 seconds, saying on stderr what it is waiting for, then fails with exit 3 if the daemon never finishes. After restore, a server whose surviving process restore could not yet decide to adopt (launchd's job list did not answer) keeps that refusal for start-shaped and stop-shaped requests naming it (stop,down,unregister, a pausing lock) and forwait, with a message naming the server and its pid, until the daemon reads the job list again; its status shows the recorded phase and pid meanwhile. A deferred survivor whose process has exited needs no job list and is settled at once. Once the retry window has passed with the job list still silent, the refusal namesrun: directa daemon restartas the fix. A refusal for a deferred survivor is not a daemon still starting: the CLI fails at once with its message rather than waiting it out as unreachable. A drain stops deferred survivors too. The session-start hook does not wait: it talks to the socket directly and stays silent, because a session must not block on a daemon. -
directa uninstall [--agent-only] [--purge] --json→{actions: [string], agentOnly, purged}. The one uninstall verb. Stops every running server: the daemon drains on its way down, since nothing would supervise them afterward.--agent-onlydrains too, so a Homebrew cask upgrade restarts running servers. Removes the background agent (and any legacy home LaunchAgent) and Start at Login, then agent hooks from every harness, then the CLI and daemon binaries directa itself installed at~/.local/bin(a Homebrew install keeps its CLI in brew's bin, so those paths do not exist there and this is a no-op; the cask removes brew's symlink). A non-Homebrew/Applications/directa.appis moved to the Trash; a Homebrew cask's bundle is left for brew. Data, logs, preferences, and caches are kept unless--purge.--agent-onlystops after the agent and never touches Start at Login, hooks, binaries, or data: it is what the Homebrew cask calls on every upgrade, so it must stay narrow.purgedin the result is true only when data was actually removed (never under--agent-only). -
directa daemon uninstall [--purge] --json: deprecated alias fordirecta uninstall --agent-only(with--purgemapping to data removal). Kept working because the CLI JSON contract is a public surface; the deprecation notice goes to stderr so--jsonstdout stays clean. Removal is a future major. -
directa daemon install|start|stop|restart|status: launchd lifecycle.stopdrains and writes a deliberate-stop marker that auto-bootstrap honors;restartandinstall(upgrade) both capture running servers, bounce the daemon, and re-ensure them by name. Under the background agent they hand off rather than drain: the old daemon exits without stopping its servers and the new one adopts them, so servers keep running across the bounce and the re-ensure is a no-op for each adopted one; a daemon run with--foregrounddrains instead, and its servers bounce and come back. The handoff is the wire requestdaemon.shutdownwith params{"handoff": true}(handoffoptional, absent or false meaning drain); a daemon that predates the field drains. The menu bar app hands off the same way before it re-registers the agent (a daemon older than the app, or one it could not reach). A handed-off daemon answers the SIGTERM that follows without draining, and an adopted server's structured log resumes from the byte the old daemon last read, so output written during the bounce is kept. The new daemon accepts beforerecoverAtStartupand refuses real work withdaemon-startinguntil it returns, so that re-ensure waits the window out rather than racing a half-finished restore.installalso stages-and-renames the daemon binary and captures the login-shell PATH into the agent plist. Reboot recovery: the LaunchAgent runs at load; starting a server records resume-on-boot; a machine shutdown drains without clearing it;recoverAtStartupresolves specs through the merged config+registry view (so committeddevservers.jsonservers come back, not only ad-hocregisterentries) and restores those servers one at a time so sibling port claims observe each other. Restore is autonomous, so it honors the trust gate: a committed server whose project was never approved (no explicitstart/ensure/uprecorded trust) is not spawned, which keeps a cloned repo's devservers.json from running itself after a reboot. A deliberatedirecta stop/downclears the intent. Renamed or deleted servers leave orphan state rows that recover drops. -
directa hook install [--harness antigravity|claude|cursor|grok|opencode] [--cli-path PATH] [--statusline]: idempotently wires a session-start hook into the harness's settings (antigravity: PreInvocation in ~/.gemini/config/hooks.json. Antigravity has no session-start event, and PreInvocation fires before every model call, withinvocationNumstarting again at 0 on each message. The hook emitsinjectStepswithephemeralMessageonce per conversation, and again wheninitialNumStepsdrops (the harness shortened the conversation). Later calls, and PostInvocation after tools, follow the tool-change rule below; when that rule quotes err lines, PostInvocation setsterminationBehaviortoforce_continueso the model can deal with them, at most three times per conversation. The conversation record, including Antigravity's step count, is one file per agent under$TMPDIR/directa-hook-snapshot(DIRECTA_HOOK_SNAPSHOT_DIRrelocates the directory); invocations on one record take turns under a file lock, and one that cannot get the lock within a few seconds goes ahead without it rather than stall its agent. A missing step count counts as one past the recorded count. An empty project still records the conversation, so later messages do not ask the daemon again; claude: SessionStart with matcherstartup|resume|clear|compactin ~/.claude/settings.json, emittinghookSpecificOutput.additionalContext, and silent when the payload carriescursor_versionwhile directa's Cursor hook is installed, because Cursor also runs~/.claude/settings.jsonhooks by default and its own hook already carries the block; PostToolUse with matcher*under the tool-change rule below; cursor: sessionStart in ~/.cursor/hooks.json, emitting{additional_context}, plus postToolUse under the tool-change rule below; grok: PostToolUse with no matcher in ~/.grok/hooks/directa.json (the event whose additionalContext Grok delivers after the tool result; it follows the tool-change rule below). Install removes this command from any other event (a leftover SessionStart, PreToolUse, UserPromptSubmit, or Stop), and writes a managed ~/.grok/rules/directa.md standing instruction to rundirecta contextbecause home rules apply to every project and cannot hold a live snapshot; opencode: a managed ~/.config/opencode/directa.md with that same standing instruction, wired through theinstructionsarray of the winning global config (opencode.jsonc preferred over opencode.json) because OpenCode has no session-start injection point to pipedirecta contextthrough, and entries effective from the losing config file are carried into the array the winner holds so they do not go inactive). Omitting--harnessinstalls every harness detected on this machine (present, whether or not a hook is installed yet), printing one line per harness installed and one per harness skipped as not detected;--harness Xalways installs exactlyX, detected or not, the same as every prior version.--cli-path(absolute) overrides the path recorded in the hook command, which the menu bar app passes as the CLI owner's path so a Homebrew install records the stable shim in brew's bin rather than the internal bundle path this binary resolves to. After a successful install it also prints a one-bullet discovery tip for the project's CLAUDE.md/AGENTS.md (wired to the nearest devservers.json's first server when one exists); the tip is printed for a human to paste and is never auto-appended to those files. Adding a harness: CONTRIBUTING.md. -
Tool-change rule (every after-tool hook, and every Antigravity call past a boundary): the full summary is pasted only at a boundary (Claude Code and Cursor session start, compaction included; Antigravity's first call and a dropped
initialNumSteps), which also drops that agent's pictures of every other project, or when the conversation has no record yet for the project the call resolves to (hooks installed mid-session, Grok's first call, the first call from another checkout). A conversation is one agent: a Claude Code subagent, whose payload carriesagent_idbeside its parent'ssession_id, keeps a record apart from its parent's, and a record keeps one picture per project, so an agent moving between checkouts compares each against that checkout's own last picture rather than reading the switch as servers removed and new. An untrusted project counts as having no servers, so its servers read as new once trust is recorded. Any other call writes nothing unless a server changed in one of these fields: phase, displayed port, url, port-conflict state,specStale, last exit code or signal, spawn errno, worktree label, or the server appearing or disappearing. Stderr growth and a new err tail alone never speak;directa monitoris the stream for that. When the rule speaks, the<directa-servers>fence holds one line per changed server and nothing about unchanged ones:<name>: <phase> (was <old phase>)when the phase moved, else<name>:, followed by;-joined clauses for each other changed field (port <n>orport cleared,url <url>orurl cleared,port conflict <state>orport conflict cleared,config changed, restart to applyorconfig applied,exit <code>orsignal <n>,spawn errno <n>,worktree <label>orworktree cleared; an exit or spawn errno that clears updates the record silently, and a line with no clause is never written); a new server reads<name>: new, <phase>; url <url>, a gone one<name>: removed. Every value on a line is sanitized like an err line. A server now crashed, failed, or unhealthy ends its line with; run: directa why <name>(the name bare only when shell-inert, else<name>). A server that entered crashed, failed, or unhealthy in this delta is followed byerr: <line>for each line of its last 5 err lines not already shown to this conversation, sanitized (a line once shown is never requoted, even after a restart; a failed log read still speaks the line, with no quotes); no other call reads the log. Only these quotes count as new err lines for Antigravity's pull-back. -
directa hook uninstall [--harness antigravity|claude|cursor|grok|opencode] --json: idempotently removes directa's session hook from a harness's settings, leaving everything else the file holds untouched (an entry that held only directa's hook is dropped whole; a foreign hook alongside it is kept). Omitting--harnessremoves from every detected harness.--jsonsuccess is{}, matchinghook install. -
directa statusline: reads harness statusline stdin JSON (workspace.current_dir or cwd), printsmyproj:3000 ok · api crashedfor the project, empty otherwise.
{ "blockedOn": "interactive-auth", // present when the last runs died the way an interactive credential prompt blocks: self-exit, nonzero, bounded tens-of-seconds lifetime, repeatedly, without ever passing a healthcheck. Heuristic, in directa's own words; clears when a run dies differently or a healthcheck passes "declaredPort": 3000, // committed / ad-hoc declaration "effectivePort": 3742, // what this run binds (override, overlay, sibling rebind, or declared) "errorSummary": { "count": 3, "firstAt": "…", "lastAt": "…" }, // stderr tally for the last run; present once it turns crashed/failed/unhealthy "heads": { "admin": "http://admin.myproj.localhost:3742/" }, // multi-headed server: display name → URL (follows effective host/port) "healthcheck": "http", // "http" | "tcp" | "none" "icon": "assets/icon.png", // absolute path after config validation, when the config declares one; feeds Spotlight thumbnails "lastExit": { "at": "…", "code": 1, "signal": null }, // exit of the run that just ended (crashed, failed, or stopped). Absent on the run that replaced it, including while that run is still starting "lastHealthAt": "…", // when the current run last passed a health check; absent until it does, and cleared when the run ends "lastHealthCheck": { "at": "…", "detail": "HTTP 500", "failures": 4, "passed": false }, // the current run's latest health check; `failures` counts the failed checks in a row ending at it (0 when it passed), `detail` says why it failed (absent on a pass). Absent until the run's first check and on a server with healthcheck "none"; cleared when the run ends "locks": ["d1"], // resources this server holds while running; `directa lock <resource> -- …` gets exclusive access without stopping it "logPath": "~/Library/Logs/directa/myproj-a1b2c3d4/web/current.log", "observedPort": 3742, // from post-healthy listen scan; must match effectivePort or the server fails with port-drift "phase": "running", // stopped|starting|running|unhealthy|stopping|crashed|failed "pid": 4242, "portConflict": { // present when a conflict was handled or is latent / fatal "declaredPort": 3000, "effectivePort": 3742, "holder": "web@/Users/me/code/myproj", "message": "port 3000 held by sibling; rebound to 3742", "state": "rebound" // "rebound" | "held" | "drift" | "foreign" | "shared" }, "ports": { "admin": 3743 }, // resolved named secondaries when the spec declares `ports` "project": "/Users/me/code/myproj", "recentLogTail": ["…"], // crashed/failed only "server": "web", "spawnError": { "errno": 2, "message": "…" }, // failed only "specStale": false, // the merged config differs from the spec the current run started with; a run started by restart, ensure, or start is never stale until the config changes again "terminalEvidence": ["…"], // short out/err/sys lines persisted across ensure truncate / daemon rehydrate, for `why` "uptimeSec": 123, "url": "http://myproj.localhost:3742/", "worktree": "review", // present only when the server runs from a linked git worktree: display label, host unaffected "mainProject": "myproj" // with worktree: the main checkout's slug, so displays read "myproj · review" }