path grammar and glob patterns
the one grammar every surface derives from (segments, paths, patterns, the rand placeholder, loop names, run ids), plus the anchored matching rules, reserved names, and resolution order.
every surface (CLI validation, the SDK, the socket schema) derives from one grammar block. it is defined nowhere else, and no surface uses ad-hoc string matching or SQL LIKE. for the conceptual walkthrough, see identity: paths and uuids.
the grammar
segment = [a-z0-9_]{1,64}
path = segment ("/" segment)* # ≤ 8 segments, ≤ 256 chars total;
# the last segment is the name
abs_path = "/" path # anchored to the root
pattern = path where any segment may # wildcards are whole segments only:
be "*" or "**" # a/b/*, a/*/b, a/**, a/**/b, no
# partial forms (phase_*); bulk verbs
# only; a trailing bare "/" is invalid
{rand} = creation-only placeholder # replaced with 5 random [a-z0-9] chars
# anywhere in --name/--path values
loop name = segment # one segment, mandatory
run id = "r" + 5 [a-z0-9] # r82c9s, generated, never user-chosen
run path = loop_name "/" run_ida segment is lowercase alphanumeric plus underscore, up to 64 characters. a path is at most 8 segments and at most 256 characters total. a depth violation is invalid_request with details.reason: "path_too_deep" (carrying the effective path and count).
anchored matching
patterns are resolved against the caller's scope first (a leading / skips that), then matched anchored against the full path. review/* can never match reviewer/x.
*matches any characters except/: one whole segment.**matches any characters including/: any number of segments (one or more).
both may appear anywhere and more than once (*/review/* is legal). a bare * means every agent at the current level; a bare ** means everything under the scope. there are no partial-segment forms like phase_*. a trailing bare slash (review/) is invalid syntax.
so review/* matches agents directly under review (one level, not nested ones), and review/** matches everything under review at any depth, never the agent named review itself. self plus subtree is two targets: kill review "review/**".
the stored matcher preserves exactly these semantics: no raw string-prefix scans, no SQL LIKE, because _ is both a legal name character and a LIKE wildcard.
where patterns are accepted
| verb | accepts |
|---|---|
wait, kill, ls, top (filter) | patterns and uuids (<pattern|uuid>) |
send, logs, attach | exact target only (<path|uuid>); wildcards rejected |
quote patterns in the shell (kill "review/**") so your shell does not expand them against real files first.
the {rand} placeholder
same path means same agent slot, by definition: there is no silent auto-suffixing. when you want a fresh path each run, use {rand}: anywhere in a --name or --path value, it is replaced with 5 random lowercase alphanumeric characters before validation.
--path "review_{rand}/file_1" → review_u38c8/file_1 this run
review_o90j8/file_1 the next{rand} is creation-only. it never appears in a selector.
reserved level-1 names
some top-level path segments are owned by orcr, and are also rejected as loop names:
| level-1 segment | you may create paths here? | owned by | what it is |
|---|---|---|---|
idle | no | orcr | the parking workspace (GC) |
unmanaged | no | orcr | agents you started by hand, auto-tracked |
| an active loop's name | only from inside that loop's runs | the loop | its runs and their agents |
| an ended loop's name | yes | free again | history stays reachable by uuid |
reserved names apply at level 1 only: inside a scope, --name idle resolving to review/idle is fine. loop protection applies to creation only: observing agents under an active loop (ls, wait, logs, top) always works, and kill "/nightly/**" works too (with the normal confirmation) as the escape hatch for a runaway loop. while loop nightly is active, nothing outside its own runs can create an agent anywhere under nightly/**, whether via a relative path or an absolute /nightly/….
enforcement order
every check runs on the effective path (the relative input already resolved against the caller's scope), in this order:
- parse the input.
- resolve against the caller's scope, unless the path is absolute.
- validate grammar and depth:
invalid_request,details.reason: "path_too_deep". - reserved level-1 check:
invalid_request,details.reason: "reserved_name". - active-loop ownership.
- active path uniqueness:
state_conflict,details.reason: "path_in_use"with the occupying{uuid, path, status}.
steps 3 through 6 happen in one BEGIN IMMEDIATE transaction, which is what makes concurrent spawns safe.
resolution and uniqueness
a full uuid contains dashes, which names never do, so it resolves to its row directly, active or ended. a bare target is tried as a path first (the active agent, else the most recent ended agent with that path); only if nothing matches as a path is it tried as a uuid prefix of 8 or more hex characters.
results say which one you got: JSON carries resolved: "active" | "latest_ended". send, attach, and kill act on active agents only.
a full path must be unique among active agents (any non-ended status, including lost, which reserves its path until reconciliation confirms the terminal is gone). this is enforced by a partial unique index. paths of ended agents are reusable: the uuid is what stays unique forever. for older reuses of a path, use the uuid from ls --all.
error codes and exit codes
the nine stable error codes, each code's details shape and process exit code, the full exit-code table, and the wait-timeout vs agent-timeout distinction.
store schema
the SQLite schema owned by the server: its six tables and columns, key indexes, what lives as files instead of blobs, and the fields that are derived and never stored.