Every capability Collie has in herdr is also a command, because both front doors are thin adapters over the same services (ADR-0003). This page is the contract for driving Collie from a script or an agent.
--json is the machine-readable surface. Everything else Collie writes — run directory
files included — is internal mechanics that can change without notice; if you need it,
there is a command for it.
For the vocabulary, see CONTEXT.md. For what each workflow is for, see
Workflows.
Put these before the subcommand:
| Flag | What it does |
|---|---|
--json |
Emit one machine-readable envelope instead of text. |
--workspace <id> |
Scope to that herdr workspace, and root the run at its directory. |
--log-level <lvl> |
all, trace, debug, info, warn, error, fatal, none. |
collie --json --workspace wT run start plan --input goal="ship it"A run is rooted at the directory Collie is standing in. To run against another repository, pick one of:
cdinto it.- Set
COLLIE_CWD=<path>— the working directory for the picker and the agents, and the root of a CLI run. - Pass
--workspace <id>, which re-roots the run at that workspace's directory as well as scoping to it.
collie run list lists every run, unless the selected workspace is a task workspace — then
it lists that Task's. See Tasks.
The environment is the source of truth for which workflows exist and what they take — a user or project layer may have overridden any of them.
collie --json workflow list
collie --json workflow show implement
collie --json persona list
collie --json persona show implementer
collie --json workflow check # validate every layer, without starting a runA workflow is a TypeScript module — see the SDK — and list returns those under
modules, each read as show reads one: what you are shown is what would start. A Markdown
workflow an older Collie left in your layer is neither listed nor run; collie doctor names
it.
workflow show <id> gives the module's public id, the layer and file it came from, each
input with its strategy and its schema, the names the host settles beside your input, the
schemas of its result and its failure, and what its definition declares, under metadata:
{
"ok": true,
"data": {
"workflow": {
"id": "implement",
"title": "implement — build the plan, review it, fix until nothing blocks",
"layer": "shipped",
"path": "/home/you/.collie/workflows/implement.workflow.ts",
"inputs": [
{
"name": "plan",
"required": true,
"strategy": "work-source",
"schema": { "type": "string" },
"limits": []
}
],
"options": [{ "name": "branch", "meaning": "Branch selection for mutating work, …" }],
"success": { "schema": { "type": "string" }, "limits": [] },
"error": { "schema": { "$ref": "#/$defs/WorkflowErrorEncoded" }, "limits": [] },
"metadata": { "hints": { "plan": "work-source" }, "selectable": ["feature", "…"] },
"broken": null
}
}
}limits names each place the drawn JSON Schema constrains less than the module's own
schema does — a projection limit, not an invalid schema. broken is the sentence a module threw
when it was constructed, and null when it did not.
The same reading answers collie_definitions over MCP and Pi, and fills the needs_input
refusal you get from run start when a required input was not given — so every door names
the same file, the same schemas and the same diagnostics.
workflow check [<workflow>] loads each module, constructs it and typechecks it against
the SDK declarations, without starting a run, taking an agent or opening a worktree. It
reports three different things and keeps them apart: problem(s) is what stops it running,
drawn without: is a projection limit, and ok, not typechecked means no compiler was
installed in that directory — never silence. Where the effect installed there is not the
host's, as after an upgrade, it provisions the toolchain again before typechecking. It
exits non-zero for problems, so it fits a pre-commit hook.
collie --json workflow create tally # your own layer
collie --json workflow create tally --layer project # this project's
collie --json workflow fork implement --layer user --name oursBoth write <id>.workflow.ts where a run will find it, return its path, and never write
over a file that is already there. create writes the smallest definition that runs;
fork writes one that spreads the original's definition under the new id, so everything it
does not name is still the original's. Both then provision the authoring setup beside the file —
package.json, tsconfig.json, collie.d.ts — merging what it needs into a
package.json or tsconfig.json you already have, and installing the toolchain with the executable's own embedded Bun. With no network on a
first use the answer says toolchain_unavailable: the module still runs, and nothing was
typechecked.
Then: edit it, collie workflow check <id>, collie run start <id> --input ….
collie --json run start <workflow> --input k=v [--input k=v …]
collie --json run start <workflow> --inputs-json '{"goal":"ship it"}'| Flag | What it does |
|---|---|
--input k=v |
Repeatable. The names come from workflow show, plus branch (below). |
--inputs-json |
Every input at once, as one JSON object. |
--decide s=t |
Refused: a Run asks its questions when it reaches them. |
--task <id> |
Continue that Task instead of starting a new one, as task list prints it. |
--continue-task |
Continue the Task whose workspace this is; needs_input outside one. |
--here |
Keep the Run in this workspace, as its Task, instead of opening one. |
--verify <json> |
Repeatable. A check Collie may run to prove it, as one verify.json entry. |
--harness <h> |
The harness this Run's agents run on, over the workflow's own preference. |
--model <m> |
The model this Run's agents run on, over the workflow's own preference. |
--effort <e> |
The effort this Run's agents are asked for, over the workflow's own. |
--attach <file> |
Repeatable. A file to give the Run — see Attachments. |
--request-id |
Idempotency key — see Retrying safely. |
--harness, --model and --effort reach every agent the Run starts, and its children,
without the workflow doing anything for them; a piece of work that names its own still
wins. They are resolved together before anything starts, so a model the harness does not
take is invalid_input naming what it would take. Which agent does the
work is the whole order.
collie --json run start plan --input goal="the picker is cut off" --attach shot.png
collie --json run steer <run-id> "this is what it does now" --attach after.png
collie --json run action <run-id> carry-on --input work="still cut off" --attach log.txt--attach names a file to give the work, and is repeatable on run start, run steer
and run action. A relative path is resolved against where the command runs; the CLI
sends the absolute path and uploads nothing, so the file has to be on the host's Machine.
The host copies each into the Run's own runs/<id>/attachments/ — a copy, never a link —
before its first step, or for a steer before the message is delivered, and every step's
prompt lists the Run's attachments with their paths
(sdk.md). A steer's message gains an
Attached: <path> line for each file it brought. A Run started from another — a
follow-up, a child, an implementation of a plan Run — starts with copies of that Run's
attachments. Two files of one name and different content are both kept, the later under a
short sha256 prefix.
A path that does not exist, is not a regular file, cannot be read or is larger than
100 MB is invalid_input naming it, before anything is claimed: no Run, Task, worktree or
workspace is made. The files are part of the request: at the host, the same request id with other
attachments is RequestConflict, and with the same ones it is the same Run. The CLI
answers a --request-id it already has a receipt for from that receipt, before the host
is asked, so there it is the first Run whatever is attached. They are not
an Input, so the Workflow's own input, and with it the execution, is the same either way.
The operation's line in the Run's operations.jsonl names each file under asked, by the
name the Run keeps it under and the path it came from. Chat's start, followup and
deliver actions take the same paths as attachments
(ADR-0046).
An id a TypeScript module claims is that module's, and run start starts it on the local
host rather than as an orchestration of agents — no flag says which, because what an id
runs is what is saved for this project (sdk.md is where
that is). An id whose module will not load is refused by that file, and an id no module claims is a
workflow this installation does not have.
A module's inputs are schemas rather than text, and they are settled before a Run, a claim or an execution exists — so a value one of them refuses costs nothing to refuse:
--input k=vis what a human typed. It is tried as text first and read as JSON only where the schema will not take text.--input count=3is the number3for a number,--input labels='["a","b"]'is the list for a list, and--input ref=12is the string"12"for a string-or-number union, because text wins where both would do.--inputs-jsonis typed, and settles what text cannot.false,0,[]andnullsurvive it;--inputs-json '{"ref": 12}'is the number where the text was the string.- Missing is absent, not empty. An input nobody gave is one the module never sees, so
an optional one stays optional and a required one is named back to you rather than
quietly becoming
"". - What it refuses names the field: a value its schema will not take is
invalid_inputsaying which input and what it takes, and an input the module needs and nobody gave isneeds_inputcarrying each missing field's schema — fill them in and retry with the same--request-id, and it is still one Run. - The host's own names never reach a module's input.
branch,task,workspace,repo,outcome,risksandpreviousare options Collie supplies; a module may not declare one, and asking for an outcome a module always proves is refused rather than recorded as something it will not deliver.
collie verify --run <run-id> -- <command> records against a module's Run as it does
against any other: the same collector, the same binding to the tree the command ran on, and
the same refusal for a directory that is not that Run's. A module can ask for one itself,
but only for a command its approved set named when the Run started, or one granted it since
with run intent verification or chat's set_verification.
A module may be made of other modules. A child is a Run of its own — run list has it,
run show says whose it is, and the Task it belongs to is its parent's — and it is started
by the same lookup a front door uses, so a project that overrides that module overrides
what its parents run. sdk.md is how one is
written.
The Run it starts is shown, listed and waited on by the same commands as any other, and
--request-id deduplicates it the same way — the claim goes to the host, so the retry is
the same Run there too. --goal and --constraint seed the Run's Intent;
--decide is refused rather than accepted and dropped, since a module asks its questions
when its work reaches them.
The picker asks for exactly what the module declares, in the way its own schema allows: a closed set is a menu of the values it takes rather than a text box, and an input the module attached a strategy to is worked out from this checkout and its Task before anyone is asked for it. ADR-0018 and ADR-0019 are why each of those is the way it is.
run show says what a module's Run is waiting on and what is set over it, and the ordinary
commands do the rest — whichever door you come in, it is the host that settles it:
collie --json run answer <run-id> yes --decision keep --request-id "$(uuidgen)"
collie --json run hold <run-id> --request-id "$(uuidgen)"
collie --json run release <run-id> --request-id "$(uuidgen)"
collie --json run stop <run-id> --request-id "$(uuidgen)"
collie --json run resume <run-id> --request-id "$(uuidgen)"
collie --json run steer <run-id> "check the migration too" --request-id "$(uuidgen)"--decisionnames the question where a module is waiting on more than one; leave it out and the one open question is answered, and being asked for is refused where that is not exactly one. A value the question does not take is refused with what it does take; one it takes is taken in any case, soYESanswersyes.- One answer settles it. A second is refused with what the Run already has; the same
--request-idagain is the same answer rather than another. - A control reaches one Run. Its siblings and the host carry on. A control over a Run whose module is not loaded here is recorded and says so, naming the file to repair, rather than reporting a success nothing can stand behind.
run stopparks the Run and stops its agents. Their panes are closed, and those of the Runs it started; the workspace keeps its own tab.run resumeclears the stop and picks the Run up again, giving work whose agent has gone to a new one. On a Run that has already finished, a stop only closes its live agents' panes: no control is set, the status stays what it finished with, and with nothing alive it says nothing was running.- A Run parks itself when its agent's pane will not take a prompt. herdr answering
agent_blockedfor ten minutes leaves the Runsuspended, withparkedin its view — and inrun show— saying what held, for how long, and where the prompt is.run resumehands that prompt to the same agent rather than starting another. - A Run with nothing approved to prove it parks before its first agent, where its
outcome needs the approved set.
parkednames the repair:run intent verificationgrants this Run a command through the host, andrun resumecarries it on. - A Run's agents live in its Task's workspace, opened by the start that made the Task.
A stop closes only its agents' panes there. If herdr has closed that workspace by the next launch, it
is reopened on the Run's checkout and the Task records the new id; with the checkout gone
too, the Run parks with
parkednaming both and the way back.run resume --workspacedoes not move a Run:--workspaceonly chooses where the command looks, and the resume says so. run steersays something to the Run's agent through the one sender, with the same incarnation and harness-capability checks as every other delivery, and tells you whether it was delivered rather than that it was accepted for sending: anything not sent isoperation_failed, with why. A finished Run's live agent takes one as a running Run's does, whether or not its module is still loaded. It carries out nothing:collie steeris still the only thing that proposes an action, and a proposal still names its exact payload to be confirmed.
ADR-0021 is why each of those is the way it is.
A module that has an agent do its work opens a tab and starts one on the harness, model and
permissions this installation is configured for, with the compaction controls every agent
Collie starts gets. What it was actually sent and what it wrote are files under the host's state
directory — agents/<run>/<operation>.prompt.md and .json, with the repair beside them —
so a Run that ended output-unusable can be read rather than reconstructed. An Output the
workflow's schema refuses buys exactly one rewrite from that same agent, and a second
unusable one ends the Run with the reasons named.
ADR-0020 is why.
A Task is the work itself, and the Runs it takes: a plan, the implementation it chains into, the review of that. A fresh start about a branch an open Task's checkout has out — the branch it is placed on, or the one its diff target names; never merely the branch the caller is standing on — is that Task's, and opens in its workspace wherever it was started from. Any other fresh start is a new Task, and gets a herdr workspace of its own, created and never focused. The default branch names no one piece of work, so a start on it is always new. Chains, follow-ups and resumes stay in the Task they came from.
Continuing anything else is explicit. --task <id> names one; --continue-task means the
Task whose workspace this command was run in, and is needs_input anywhere else rather
than a prompt. Neither the workflow's name nor a similar label ever continues a Task on
its own.
--here keeps the work where you are: this workspace becomes the Run's Task — the one
already kept here, or a new one — and its agents open beside you. A mutating workflow
still gets its own worktree; only the workspace a fresh start would open is not. A start
proposed from chat does the same with here: true.
collie --json task list
collie --json run start review --task task-1a2b3c4d --input target=worktreetask list gives each Task's id, its label, its workspace and the Runs it owns. The label
is inferred when the Task is made — <Project or theme> | <what this work is>, from the
work and from the names already live in your herdr session — and is display only:
renaming a task workspace by hand changes nothing about what belongs to it, and Collie
does not rename it back. See Using Collie.
Runs are scoped by Task, not by workspace. collie run list inside a task workspace lists
that Task's Runs; anywhere else it lists every Run, and a Run is reachable by id from the
workspace it was started in. --workspace <id> picks which workspace that is.
Checkouts are unchanged: a mutating run still gets a worktree of its own, keyed by its branch. A task workspace groups the work; it does not isolate files or branches.
--input branch=<name> is the one input no workflow declares, and workflow show lists it
beside every module's own inputs as one of the names the host settles. It names the branch the run works on, and so which worktree it gets. Nobody is ever asked for one: a branch nobody named is resolved in this order:
--input branch=<name>, which wins over everything below. A follow-up is given its parent's branch this way.- The branch the reviewed work is already on, for a run fixing a review.
- The
<name>of abranch:<base>...<name>target you gave (--input target=takes a bare ref too, and turns it into one) — so the checkout and the review target agree. A target Collie inferred does not count: it names the branch you are standing on, and building that would hand the run your own checkout.
Those three are branches that already exist, or that you named, and are used exactly as
they came. Everything else is new work, and gets a new branch called
<your GitLab login>/<the task>:
--input task=<slug>, which is also whatplanandarchitectureforward when you choose Implement now, and what a fan-out gives every repository run.- The plan directory's own name, for a run given a plan directory.
- A slug of the work itself — the description, or the issue id.
- Otherwise a slug of the run's own name.
The login is GITLAB_USER_LOGIN where the environment sets it, and otherwise whoever
glab is logged in as for this checkout's host. Neither is the merge request's assignee
and neither is your OS username. Where there is no login to be had the run does not start
and says to log in — it is an authentication failure, not a question about branches.
A name too long for the cap, or with nothing in it to slug at all, is cut to fit and given
a short digest of the whole of what it stands for: two plans under one directory, and two
runs with no name of their own, would otherwise clip to the same branch, and the branch is
what keys the worktree. The digest is of the work and not a counter, so the same work asked
for twice is the same branch — a retry reuses the checkout rather than opening a second
merge request. Every generated name is put to git check-ref-format before it is used.
The task half of the branch also names the run itself — its slug, and so its agents, its
tab and its row on the board — so what run show calls the run and what the checkout is on
always say the same thing. The login is left out of it: it is the same on every branch you
generate, and spending the slug's length cap on it would make two long plans one row.
It is ignored by a workflow that changes nothing.
An agent's start names everything. run start infers and routes nothing: that is the
Launch flow's, for a human. Run inside a git checkout, the start has named that
checkout. Anywhere else it has to carry --input workspace=<absolute path> or
--input workspace=projects-root, and without one it is needs_input listing the
checkouts under the Projects root to choose from. Every Input the workflow
declares has to be given too, an optional one as an explicit empty string
(--input plan=): the prompt then says it was not given rather than leaving it out.
--input workspace= is the host's, not the workflow's, and it says where the checkout
comes from. new asks herdr for it, so the run gets a worktree workspace of its own
instead of living in its task's; a fresh task takes that workspace as its own rather than
opening a second one. An absolute path is an existing checkout the run starts from.
projects-root roots the run at the Projects root, which is not a repository: a workflow
that makes no checkout, like plan, works there, and one that cuts a worktree, like
implement, is refused because there is nothing to cut it from. It is decoded before
anything exists: any other value is invalid_input naming it, and so is new for a
workflow that makes no checkout. Nothing chains it on: an implement that a
plan, an architecture or a review starts is placed by its own declaration, in the
same task
(what a run does to your repository).
--workspace <id> is a different thing: it roots the run at that workspace's directory.
A workflow that builds on a worktree of its own, started from a directory that is not a
git checkout, is refused with invalid_input naming that directory, before a run, a
worktree, a workspace or an agent exists.
Examples:
collie run start review --input target=https://gitlab.example.com/acme/app/-/merge_requests/2 --input plan= --input proves=
collie run start review --input target=worktree --input plan= --input proves=
collie run start implement --input plan=ENG-123
collie run start plan --input goal="one registry" --input workspace=projects-rootA start that leaves anything out comes back as needs_input, one entry per missing field:
what it means (the field's own description, else what its strategy is) and the facts that
would fill it — the target inference would pick in this checkout for a diff target, the
plan directories of finished Runs for a work source, and the checkouts under the Projects
root for workspace:
{
"ok": false,
"error": {
"code": "needs_input",
"message": "review needs \"target\", \"plan\", \"proves\". Nothing is inferred for an agent's start: give every Input, an optional one you leave empty as \"\".\n- target: …",
"details": {
"workflow": "review",
"inputs": [
{
"name": "target",
"meaning": "The change to review: mr:<iid>, mr:<group/project>!<iid>, branch:<base>...<head>, or worktree",
"facts": ["mr:acme/app!2 (open merge request !2)"],
"question": "review — an MR, a branch diff, or the working tree — target?",
"required": true,
"schema": { "type": "string" },
"limits": []
}
],
"requestId": "33c306ee-…"
}
}
}Retry with the same --request-id once you have the values.
collie --json run wait <run-id> --follow
collie --json run wait <run-id> --timeout "10 minutes"
collie --json run wait <run-id> --until attention
collie --json run show <run-id>
collie --json run list
collie --json run output <run-id>Without --follow, run wait prints one envelope when the run has ended — complete or
failed. A stopped or held run is suspended rather than ended, so a plain wait waits
through it. --timeout takes a spelled-out duration — 30 seconds, 10 minutes, 2 hours — and
comes back as the timeout error code. Abbreviations like 30s are refused as
invalid_input. A wait whose host is replaced under it reconnects and keeps waiting, and
one interrupted before it has an answer still prints an envelope: operation_failed,
"interrupted before it finished".
A Run is watched from the host's own stream: every update
is the whole current state rather than a change to apply, so a wait started long after
the work — or resumed after the last one was interrupted — reads where the Run is instead
of waiting for a notification that has already been and gone. run show names the module file the Run is on, and a
Run whose module has been deleted still reads, pending, with that file named: its history
is Collie's rows, and none of them went anywhere. run list shows those Runs beside the
rest; where the host will not start it says so and still lists the others.
A run that stops to ask a question is not ended, so a plain run wait waits straight
through it. --until attention is the wait that does not: it returns as soon as the run is
suspended — at a question, held, stopped or parked — or has ended, whichever comes first,
and returns immediately when that is already true. --until terminal is the default
spelled out; any other value is invalid_input.
Answer what the wait returned with run answer <run-id> <option> --decision <name> — see
Answer a question.
--follow streams instead of returning one envelope: one newline-delimited
{"type":"status","run":…} line each time the run's status changes, carrying the same
run object as run show. Failures still arrive as a normal envelope — a timeout, or a
run the host could not watch — so a consumer reads each line as JSON and branches on
whether type is present.
run show is the Run as the host has it. Its run object carries:
| Field | What it tells you |
|---|---|
runId, workflow, project |
Which run this is, of which workflow, for which project. |
task, parent |
The Task it belongs to and the run that started it, or null. |
entry, registration |
The module file it was admitted on — named even once it is gone — and the name the engine registered it under. |
input, provenance, strategies |
What it was given, where each value came from, and which inference each input carries. |
options |
The host's own launch options: branch, task, workspace, repo, outcome, risks, previous. |
cwd, branch, workspace, worktree |
Where it works: its own worktree on branch, or the checkout it started from; a workspace of its own, or its Task's. |
outcome |
What it has to prove. |
created, status |
When it was admitted, and where the engine says it is: pending, suspended, complete with its value — the result as the workflow's success schema encodes it — or failed with its reason. |
waiting |
Every question it has been asked, oldest first: name, prompt, options, and answer, null while it is open. |
controls |
A hold or a stop someone set over it. |
parked |
Why it parked its own work and what picks it up again, or null. |
diagnostic |
Why the engine could not be asked about it — a module that is missing, with the file named — or null. |
collie --json boardThe first snapshot of the board the host serves
(ADR-0038): every Task on this Herd's
board, in the order the Home draws them: Needs you first, then Waiting on you (work
that ended and has not landed, what is ready to release first), then Working (active
and quiet), then Finished (landed), and inside each whatever changed last first. state: blocked is what puts a Task in Needs you,
and it means one of two things: a decision to answer, or an agent waiting for you in its
own pane — a harness dialog herdr will not answer, or a run that parked because its agent's
pane would not take a prompt. The sentence says which, and for the second kind it says which pane. It is the same model the pane renders, so an agent
reading this and a human reading the board cannot be told two different stories about one
Task.
Herd-wide, and never narrowed by which workspace you typed it in: one board per Herd
(ADR-0009). A Run belonging to no Task is a
Task of its own; a Repo run of a fan-out is its parent's children rather than a Task beside it.
| Field | What it says |
|---|---|
id |
The Task, or the Run's own id where it belongs to no Task. |
name, project |
The two halves of the task workspace's label. No herdr ids. |
herd |
The Herd the Task's workspace is in: an id in herds while that session runs. Absent for a Task recorded before it was kept. |
state |
blocked, active, quiet, failed, stopped, abandoned (a Run nothing drives and no agent works on) or done. |
steps[] |
The pipeline across the Task's Runs, each done, active, blocked, failed or todo. A step that loops is one entry. |
sentence |
What is happening, in one plain sentence — no step names, counters or glyph codes. It names the round, the wave and the answer it resumed with. |
age, at |
How long it has been going, and when it last changed. |
drift, held |
The one line each carries, or null. |
heldBy |
Who set the hold that stands — the front door it came through — and the reason given with it, or null. |
decision |
The question, proposal or gate waiting on you, or null. One of the two ways into Needs you. |
agents[] |
The live agents on it, its Repo runs' included. |
children[] |
Its plan's repositories, in wave order: repo, its run (null until it starts), its state (done, active, blocked, failed or todo) and its mr. |
mr, mrState, branch |
What it is building, where it can be read, and what the forge last said about the merge request. |
disposition, landed |
What became of the work, where a person recorded it — never inferred from a merge request — and whether it needs nothing more, which is Finished. |
ended |
When the leading Run ended, or null while it has not. |
planReady, offer |
A finished plan nobody has implemented, and the offer its card's first action invokes. |
checks |
What checked an open merge request: passed or failed (with name) at revision at, running, or unchecked; else null. |
ready |
Ready to release: the leading Run succeeded, its merge request is open, and its checks passed. |
check |
The check Collie is running for the leading Run, as collie run checks gives running (with its log and lastLines), or null. |
reopened |
A finished Run whose agent took a steer after it ended: the delivery, the agent, the first line it was told and its status now; else null. |
run, runs[] |
The Run a card acts on, and every Run of the Task. |
A gate is a Run parked at its evidence gate with nothing approved, listing the checks its
checkout's .collie/verify.json (or your config's verify.json) offers. Answer it with
collie run answer <run-id> approve --decision evidence-gate, or approve:<name>,<name>
for part of the list: the host grants those, as run intent verification does, and takes
the Run up again. It is not skipped, since with nothing approved no check could prove it.
A fan-out's own Run never parks, so where neither it nor any of its Runs asks anything, the
first of its Repo runs in wave order parked at its gate is the Task's decision. That gate's
run is the Repo run, which is the one an answer releases, and its repo names the
repository, as the sentence does. A gate whose checkout and config offer no checks says so,
and names how to grant one: chat's set_verification, or
collie run intent verification <run-id> --name <name> -- <command>.
A fan-out finds its plan from its work source even where the kind was never recorded with
its input, as for an implement Run started from a plan's end menu: a plan directory is the
plan, so children lists every repository it names, those not started as todo.
A run waiting on a question reports status: "suspended" and lists it in waiting with
answer: null. Relay the question and its options as written, then send the answer,
naming the question:
collie --json run show <run-id> # read waiting
collie --json run answer <run-id> "Implement now" --decision next-1 --request-id "$(uuidgen)"
collie --json run show <run-id> # confirm it took--decision names the question you are answering, so an answer that arrives after the run
has moved on cannot land on another one: a question already answered is refused with what
the run already has. Leave it out and the one open question is answered; with more than one
open, that is refused. The answer is one of the question's options, in any case — Yes,
yes and YES are the same answer — and one it does not take is refused with the ones it
does. A refusal is invalid_input where your answer was the reason, and otherwise
operation_failed with the host's own sentence.
collie --json run stop <run-id> --request-id "$(uuidgen)"
collie --json run resume <run-id> --request-id "$(uuidgen)"stop parks the run where it is — at its next boundary, or inside the wait it is in — and
closes its agents' panes, and those of the Runs it started, which is what stops them; the
workspace keeps its own tab. Only the process each launch recorded is closed, never another
under the same name, and a pane that will not close fails the stop and names the agent
still running. resume
asks the host to pick a suspended run up again. It re-enters the workflow's current code
and reuses every Activity already done, so completed work and its Outputs are kept and
never redone, and an agent still live is reattached to rather than started a second
time; one a stop closed is started again with the prompt it had. It clears a stop first, hands a run parked on a pane the same prompt, and registers
the modules as they are now — so a run whose module was missing and has been put back is
carried on without restarting the host. On a run the engine is already working it changes
nothing.
A workflow edited in a way that changes its shape has no promise of a seamless resume, and
not every such edit can be detected: begin new work where one will not carry on. A run an
older Collie recorded is read-only and is refused with what recorded it; run start begins
the same work again.
A run's Intent is what it is for, what its work must respect, and what Collie may do about it without asking; everything Collie says about drift is a comparison against one.
Every Run has one from the moment the host admits it, written before any of its work
runs, and a Run started from another — a child, or a follow-up offered on a card —
inherits that one's as it stands. run intent reads and amends it; run intent verification grants the run a command through the host (below).
collie --json run start implement --input plan=./plans/steering \
--goal "land steering behind the existing envelope" \
--constraint "rule:protected_paths:src/**,test/**" --severity block \
--constraint "no new dependencies"
collie --json run intent show <run-id>
collie --json run intent set-goal <run-id> "<goal>" --request-id "$(uuidgen)"
collie --json run intent add-constraint <run-id> "<text>" --severity block
collie --json run intent remove-constraint <run-id> <constraint-id>
collie --json run intent authority <run-id> auto_correct=true --propagateVersion 1 is the workspace's defaults, then what the work source itself asks for, then
what --goal and --constraint named — later beating earlier where they name the same
constraint. For a plan directory the work source's ask is read from its SPEC.md: the
bullets under a heading matching Requirements, Success criteria, Boundaries,
Constraints, Out of scope or Done when become warn constraints carrying the file,
heading and line each bullet starts on; an out-of-scope bullet reads Out of scope: <bullet>.
A constraint is the whole bullet: it runs until a blank line, a heading or the next bullet at
its indent or shallower, and its wrapped lines and the bullets nested under it are joined
into it with single spaces. An Intent already seeded keeps what it stored, unless it
follows its own plan/SPEC.md as below. In a
Run on a worktree of its own, its plan/SPEC.md, written by its planner, is read the same
way at every work boundary: new bullets are added, dropped ones are removed, and a
constraint anyone but the plan removed stays removed. No text ever grants authority —
not a plan, not the repository, not a prompt.
--severity pairs with the --constraint in the same position; a constraint given
without one is warn. A block constraint stops work; a warn one is reported.
A constraint spelled rule:<kind>:<args> is one Collie checks itself; anything else is
judged. The spellings:
| Rule | What it holds the run to |
|---|---|
rule:protected_paths:<glob>[,<glob>…] |
Changes stay inside these paths. |
rule:branch_is:<branch> |
The run works on this branch. |
rule:mr_target:<project>[:<iid>] |
Its merge request targets this. |
rule:output_field:<step>:<path>:eq|ne:<v> |
A step's Output field reads this way. |
rule:command_exit:<name>:<code> |
A named verification exits this way. |
authority takes k=v pairs: auto_correct, now_allowed, interrupt_allowed,
stop_allowed and exclusive_steering are true/false; max_corrections_per_constraint
is a whole number. auto_correct is on by default and auto_correct=false turns it off for one
run; every other grant is off by default. On by default does not yet mean corrected: no
harness has proven attribution, so a correction also needs exclusive_steering until an
operator records it (ADR-0037). A key that is not one of these is refused rather than stored. There is no model-call quota among them: how many calls a run
or the Herd makes, and what they cost, is recorded under herd/<herdKey>/budget.jsonl as
usage, and never used to refuse the next one. An Intent written by an earlier build with
model_calls_per_run still reads; the number decides nothing.
The one grant that is not a k=v word is a command Collie may run itself:
collie run intent verification <run-id> --name unit -- bun test
collie run intent verification <run-id> --name unit --removeIt is bound argument for argument — bun test and bun test --bail are two different
permissions — and --cwd is worktree or a path relative to the run's cwd. A granted
verification whose name a rule:command_exit constraint refers to is run once, at the
run's finish, so that rule is checked against a result rather than against nobody having
looked. Everything else on steering/verifications.jsonl is written by an agent calling
collie verify, and is recorded by: agent.
--propagate applies the amended Intent to every child run that is still going: the
child's own constraints are kept, the parent's are replaced, and anything the two disagree
about is reported rather than resolved. A child inherits its parent's constraints and goal
at birth, never its authority — a grant is per run.
collie --json run intent defaults show
collie --json run intent defaults add-constraint "<text>" --severity warn
collie --json run intent defaults remove-constraint <constraint-id>
collie --json run intent defaults set-authority max_corrections_per_constraint=3Defaults are per workspace: every run started there begins with them.
The Home's right-hand pane is an ordinary Claude Code (or Pi) session with Collie's role and Collie's tools — that is where questions about the flock are asked, and it is docs/using.md. From a terminal:
collie --json chat status
collie --json chat harness pi
collie chat status-line
collie chat status-line --install
collie chat context
collie --json tools list
collie --json tools call collie_herd
collie --json tools call collie_run --input '{"run":"<run-id>"}'chat status says what is running and, separately, what is chosen for next time.
chat harness sets that preference: it never stops, replaces or summarises a conversation
already running, and it changes nothing about the harnesses your runs use. Claude Code is
the default, on an existing installation as much as a new one.
chat status-line prints board selection: <name>, or board selection: none · whole herd, which is what Claude Code shows under the prompt so that chat and board agree on
what "it" means. --install is what setup.sh runs: it configures statusLine in your
own Claude Code settings — never prepare.sh, exactly like the keybindings — and leaves a
status line you already have alone, naming it instead. collie doctor reports which of
those it found. Outside a herdr session the command prints nothing, so a Claude Code you
opened somewhere else carries no line about a board it is nowhere near.
chat context is the same fact for the conversation: the Home's Claude Code runs it as a
UserPromptSubmit hook, so each message you send carries one line naming the open card,
and nothing at all while none is open. You never type it, and it is wired per launch —
nothing in your own Claude Code settings changes. A second hook beside it, chat heard,
hands each message you send to Collie's tool host, so what collie_do carries out that
turn is recorded with your words; the same command on Claude's Stop hook forgets them
when the turn ends, and a message from any other Claude session is not used.
tools is the same contract native chat is given, and it is the whole of what chat can
reach. Claude gets it over a local MCP server (collie mcp, which you never type) and Pi
through a generated extension; this is the third way in.
| tool | What it does |
|---|---|
collie_herd |
Every run in the Herd, bounded, saying how many it left out |
collie_run |
One run: its goal, constraints, the Choice it waits on and how to answer it, its answers with who and when, its result, the Runs it started, its merge request and branch, plan, findings, disposition, last log lines, cards and open drift |
collie_workspaces |
The workspaces this session has (the Home marked as no checkout), the Tasks whose workspace is open, the Projects root, and each workflow with its Inputs |
collie_receipts |
What one run waits on the human for — its Choice, an evidence gate, proposals — and what state each message to its agents actually reached |
collie_definitions |
The Workflows and Personas there are; one resolved and checked, or one Persona's body |
collie_installation |
What Collie needs, which workspace the Home is, what a cleanup would close, the defaults |
collie_news |
What this conversation has not been told; reading it settles those items for it |
collie_hold |
Hold a run, or every unfinished run in a workspace, until someone releases it |
collie_do |
Carry out, at once, a board action or decision on a named run the human asked for |
collie_propose |
Carry out the rest of what the human can ask for, with a request id that makes retries safe |
The reads are Herd-wide and are never narrowed implicitly: no read is filtered by what
the board is showing or which card is open. The selection is an input a tool may be
given instead (ADR-0012).
Chat is told it at each prompt, and collie_run, collie_receipts, collie_hold and
collie_do act on it when you name no run — saying which run that was, in the answer.
collie_installation is the one read that is not read-only: the installation checks
include a bounded git fetch, and it says so rather than letting a client assume.
Three of them write, and every one carries out what the human asked for in the conversation, at once (ADR-0011): chat may do what they could do on the board themselves, because sending them to the UI for it is chat obstructing the person it serves. What Collie wants of its own accord never comes through these: the evaluator's proposals wait on the board, and chat asks in words.
collie_do takes the board's own actions on a named run — stop, resume, release,
answer, deliver, followup and start — through the same closed union, the same
last-moment admission check and the same executors a confirmation runs. It also takes the
board's decisions, which are not actions on a run: confirm a waiting proposal by its id
and the hash collie_receipts lists beside it, decline one by the same two, and disposition to record
what became of a finished run's work. It answers a line per action saying what each one
came to. A kind outside that set is refused with the name of the tool that does take it:
amending an Intent, forking a definition, changing the defaults, keeping a run's checks for
its repository, a cleanup and an upgrade are collie_propose's.
A start names its workspace by id, label, a checkout's path on this Machine, or a
repository's directory name under the Projects root (or projects-root). A path that is no
directory here is refused as that, with the Projects root; any other name that matches
nothing says what was searched. A workspace inside Collie's state directory — the Home's —
is refused as Collie's own namespace, not a checkout
(ADR-0033).
collie_propose takes the same closed action set a steer produces — stop, resume,
hold, release, answer, deliver, start, followup, update_intent,
set_verification, remember_verification, clear_override, navigate, update_defaults, fork_definition, home_cleanup,
upgrade — through the same validate, the same proposals journal and
the same executors. It executes the request immediately, with no separate confirmation.
Supply request_id and reuse it on retries to return the original receipt rather than
repeat the action. There is no arbitrary shell-command action for chat to run — but
set_verification, carried out in the same call with no yes, names a command Collie
spawns itself at the gate, outside any agent's permission rules, and it may be sh -c
anything. The merge request lists each check with its command, and that is where the human
reads it (ADR-0011).
update_defaults names the workspace whose new runs it changes, for the reason start
names one: defaults are filed per workspace, and the board carrying out a confirmation is
in the Home. Its text adds a constraint in your own words, or names the id of the one to
remove — prose there matches no id, and is refused rather than reported as applied. A
proposal about the installation names no run, so the board draws it whichever row is
selected.
Settling is not in that set: there is no action kind that confirms, declines or
reconciles, so a proposal can never contain its own yes. Settling is a separate act, and
chat takes it as the human does — collie_do confirms against an id and hash, or
declines. Everything else a human can type, chat does too. The checks that prove a Run have tools of
their own: a start carries the ones chat chose as verify, set_verification adds or
withdraws one, and remember_verification keeps a Run's set for its repository's later
Runs. What no tool covers — setting a
Run's or the Herd's authority, reconciling, collie verify, chat harness — it runs
with the collie CLI, through the same validation and executors
(AGENTS.md, invariant 1). test/chat-parity.test.ts walks the command
tree itself and fails on a command with no route, so this list cannot quietly fall behind.
A run it names that does not exist is refused rather than retargeted; an agent the run
does not have comes back as a question for you rather than being dropped. start takes a
workspace, so a launch asked for from the Home lands in the repository it is about
rather than in Collie's own namespace directory — and it needs no existing run.
Requests retain their origin (chat:, cli:, or the board). A collie command is a
human's (cli-tty) only at a terminal, in a pane herdr does not report as an agent's or
outside herdr; anywhere else it is cli. Attribution is an audit record, not an approval requirement. Launches into Collie's Home state directory return
needs_input: choose the project with --workspace or COLLIE_CWD before starting work.
Legacy Runs rooted there cannot be resumed into the state directory; start a new Run
against the project instead.
collie --json steer "why is this on main?" --target run:<run-id>
collie --json steer "hold it and look at the branch" --target run:<id> --dry-runA steer carries out your request about one run and returns execution results. A question
asking for an explanation is answered without changing the run. Use --dry-run to preview
actions without executing them. Suggestions triggered by background events remain proposals.
--target is required. Without one the call is refused with target_required: Collie does
not guess which run you meant from what you typed.
--from <card> binds the proposal to that card's revision, so confirming it after the
tree moved is refused rather than applied to different work. --dry-run prints what
Collie would propose and records no proposal.
collie --json confirm <proposal-id>
collie --json decline <proposal-id>
collie --json proposal reconcile <proposal-id> <index> --as applied|not-appliedconfirm executes an existing proposal and decline refuses one. Optional
--hash <content-hash> checks that you are addressing those exact contents. Expired or stale-target proposals are still rejected.
Ordinary chat requests and steers do not need this command.
Actions run in order, each one re-checked immediately before it runs and journalled on
both sides. The first failure stops the rest. An action whose kind this build cannot carry
out is skipped: executor_missing. A failed or skipped action makes the envelope ok: false
and stops the sequence, instead of reporting success or running dependent actions.
Multiple Intent edits in one request use the same initial base_version; execution
advances that version for its own successful edits. Changes made by another request still
invalidate the stale snapshot.
An action that started and never settled makes the next
confirm refuse with reconcile_required until you say what happened to it.
collie --json run deliveries <run-id>
collie --json run deliveries <run-id> --reconcile <delivery-id> --as sent|not-sentWhat has been sent to a run's agents. Explicitly settle an unknown delivery, or an
unobserved one a Run parked on, once you have checked whether it arrived; a timeout
alone never authorizes a resend. A parked step's prompt settled not-sent is sent once
more by run resume — see
Delivery.
A finished run's status is never rewritten and its Workflow is never re-entered, but its
live agents still take run steer and run stop
(ADR-0041). New work with steps of
its own is a follow-up, and so is a request to an agent whose pane is gone: a steer to one
fails and names this route. What a Run offers to do next is its own declaration, so
carrying on is one of its offers:
collie --json run actions <run-id>
collie --json run action <run-id> follow-up --input plan="the docs change is still outside src/"A follow-up carries on its parent's work, so it runs on the parent's branch — in the
worktree that branch already has — and updates the parent's merge request rather than
opening another. --input is only what is left to say, under whatever name the follow-up's
workflow gives it (plan for implement); the board's Follow up asks for those words and
sends them there.
collie --json run disposition <run-id>
collie --json run disposition <run-id> --as merged --ref "cego/collie!43"
collie --json run disposition <run-id> --as superseded --ref <other-run-id> --note "restarted clean"A run's status says how its execution ended, and it is never rewritten. When the work it
was for lands by some other route — a person finishes it by hand and merges — that is a
different fact, recorded beside the status rather than over it, with what backs it up.
This is not a Delivery, which is one message to one agent; run deliveries is that.
The board records the same thing: Mark merged and Mark abandoned in a finished Task's drawer, with its merge request as the reference. See the Control Plane.
Without --as the command reads. With it, --as is merged, abandoned, or
superseded, and there is deliberately no value meaning the run succeeded after all.
run show then says both: failed · merged cego/collie!43 by mk. Nothing is inferred —
Collie does not decide that a merge request it did not open was this run's work, so a
person says so and the record says who and when.
A correction is a new record rather than an edit, so what was believed before is still
readable. The most recent one is what run show reports.
This is integrated here and not yet on the board: the Home's rows and Live region do not read it, so a Run whose work shipped still reads as a plain failure there.
collie --json run actions <run-id>
collie run action <run-id> <offer-id> --input k=vWhat to do next is the Workflow's own declaration — a module's actions and followUps,
a definition's offers: — never something Collie knows about a particular workflow. An
offer is listed when the Run's facts meet what it needs: a finding left open, a branch, a
merge request, tickets it wrote, something it was pointed at. The first eligible one is
marked as the obvious thing to do, and one that cannot be made is listed with the reason
rather than hidden.
An offer says where its inputs come from — the Run's own directory, its plan, what it was
pointed at, its branch, its merge request — and Collie fills those in, so --input is for
what only you know. What you pass wins where both have an answer.
Everything is decided again when you invoke one. The declaration is re-read from the code as it is now, its eligibility is asked about the facts as they are now, and the arguments are decoded by the workflow it starts — so an offer edited away, one whose facts have moved and arguments the child will not take each start nothing at all. The board's keys carry the same offer ids into the same operation, so both doors refuse in the same words.
Once somebody has recorded what became of the work, its follow-ups are no longer offered. Actions are given that fact and decide for themselves: looking at what was merged is still worth offering.
collie --json run drift <run-id>What this run has drifted from, and the evidence for each. Two kinds, and the difference matters: a rule constraint is one Collie checks itself — which paths changed, which branch, which exit code — with no model involved at all; a semantic one is judged, and judged against a capped diff of the files it names rather than a summary of them.
A rule check never passes on an absence. A command_exit rule whose verification nobody
ran is a breach, not a pass; so is an output_field rule against a step that wrote
nothing, and an mr_target rule with no merge request. "Nobody looked" and "it was fine"
are different answers.
run drift reads what those checks recorded. An unresolved report there shows on the board
and as attention drift_unresolved, and a cross-run check that was owed and never made as
cross_run_pending.
collie run start implement --input plan=<source> --input outcome=bugWhat kind of result a run has to prove, and so what evidence closes it:
outcome |
What closes it, beyond the approved verifications passing on the final tree |
|---|---|
| (empty) | nothing more. Unclassified work is not a feature by default. |
feature |
the tickets it built are named, every check a ticket's **Checks:** line promised has a passing verification, and the review says the agreed scope was met |
bug |
a regression recorded with --expect fail before the fix, and passing after |
refactor |
the review says behaviour was preserved |
investigation |
a conclusion, references inside the run, and whether there is a patch at all |
docs |
the documented commands, run as written, each with a passing verification |
migration |
migrate-up and migrate-down (or rollback) both passing |
The gate runs before the merge request, which is where the claim is made. Collie runs the
run's own approved set itself at the tree as it stands, then says what is missing. A check
that failed runs once more; gaps a check could still close go to the implementer for up to
four fixes, a reviewer's judgement or an Output's claim goes straight to the merge request
since no fix moves it, and whatever is still unproved is named in the merge request, which opens anyway: the
human verifies before it lands. Only the latest of Collie's results on that tree counts, so a
pass a later fail contradicts is not one. In implement, a check that fails at the gate is run once more,
before any fix, where the branch leaves the default branch, in the run's own checkout. One that
fails there too is named in the merge request as failing before the run's changes, with that
revision, and is not handed to the implementer. A run with nothing approved is told so rather than passed —
an empty set would make the gate say yes to anything — and a run whose outcome needs the
approved set is refused at run start when it has none (see What Collie may run
itself). A grant withdrawn while the run works parks it at its
gate; collie run intent verification <run-id> --name … -- <command> and then collie run resume <run-id> carry it on.
An investigation that concludes there is nothing to change skips the merge request with a note and finishes. That is a real outcome, and nothing is invented to have something to merge.
plan settles the outcome during its interview and forwards it, so a chained build is not
asked again. A value that is not one of these is refused at the front door rather than
discovered at the gate.
plan and review runs have a kind of their own that nobody chooses — plan proves it
wrote tickets (issues_dir in its Output), review that it wrote a summary a human can
read — and neither opens a merge request, so there is no gate to stop them. Their row is
read once when the run finishes, and what is missing is recorded on the run and shown on
the board and in run show.
collie --json run metrics <run-id>What a Run actually did, from its own journal: time to the first collected verification, how many passed, failed or were unstable and how many Collie ran itself, how many slices landed, how much rework (fix rounds and halts), and the largest context sample any of its agents reported. Plus the outcome it is being held to, what the evidence gate found missing, and the obstacle where there is one.
Terminal activity is not on this list, on purpose. A changing pane says an agent is alive; it does not say anything was produced. The pane clock is liveness; this is progress.
Nothing here is a limit. Counts, tokens, costs and timings are recorded and shown; none of them refuses anything, and there is no quota to exceed.
Obstacles. Where one command fails several times in a row the same way — same exit,
same last line — Collie says so: on the record, in run show, and in the next prompt, so
the agent can change approach rather than repeat itself. It is a sentence, not a stop. A
counter reaching a number is not evidence that work cannot be done, and what prevents a
false claim of success is the evidence gate reading collected results.
collie --json run checks <run-id>Every check Collie ran for the Run, oldest first, and the one it is running now
(ADR-0042). Each says its pass —
gate, baseline, recheck, fix with its round, finish, or a plain check — so
three runs of the same suite read as what each was for.
| Field | What it says |
|---|---|
running |
The check running now, or null: name, pass, round, the revision it runs at, elapsedMs, usualMs (the median of its last five runs in this repository, null for none), others (checks this host is running beside it), base (the default branch a baseline runs where the branch left), the sentence the card says, its log and lastLines. |
done[] |
Each finished one: name, pass, round, result, seconds, the revision it ended on, when it ended (at) and its log. |
Each check's output is written to a log in the Run's evidence directory as it arrives —
both streams, in the order they came — and kept after it ends, up to 8 MiB; past that the
log says it was cut. running.log, running.lastLines (its last 40) and each done[].log
say where it is.
collie run checks <run-id> --follow--follow prints the running check's output as it is written and exits when the check
ends, with a line saying how it ended; with no check running it says so and exits. Under
--json the output goes to stderr and the envelope carries what the check ended as. The
board's Open check output opens a pane in the Task's workspace running exactly this.
collie --json run report [--since <iso-date>]Where Runs end, across every Run the host knows, whatever workspace is selected. For each
workflow: how many Runs completed, failed, are suspended or pending, how many recorded a
merge request, how many a human stopped, how many decisions were answered or are still
open, and the rework and verifications summed from each Run's metrics. Then each Run that
failed, with its reason, each that completed, with its result, and each that was stopped.
--since keeps only Runs admitted at or after that date.
Like metrics, the report is data: no number in it is a threshold, and it refuses nothing.
collie --json verify --run <run-id> -- bun test
collie verify --run <run-id> --name typecheck --cwd <path> -- bun run typecheckRuns the command, watches its exit, and records the result against the run's tree — the
commit it was on and a digest of everything not committed, taken before and after.
That pair is the point: a pass on a tree that changed while the command ran says nothing
about either tree, so such a result is unstable and never pass. So is a tree too large
to fingerprint at all, because two unmeasured trees are not one tree.
What the command prints is shown as it runs — its stdout on stdout and its stderr on
stderr, or both on stderr under --json, where stdout is the one envelope — and the exit
status is the command's own, so a failure says why the first time.
| Flag | What it does |
|---|---|
--run |
Required. The run this is a verification of, and whose tree is snapshotted. |
--cwd |
Where to run it. Must be inside that run's own checkout; defaults to it. |
--name |
What to call it on a card. The executable by default. |
--expect |
What a pass looks like: pass (default), or fail for a reproduction. |
--expect fail is how a bug is proved to exist. A regression test that exits non-zero on
the tree before the fix is the evidence, so that record is a pass; the same command
succeeding is the reproduction failing, not the bug being fixed. Stability comes first
either way — a tree that moved under the command is unstable whatever was expected.
collie verify --run <run-id> --name regression --expect fail -- bun test test/bug.test.tsAn agent may collie verify anything; Collie runs only this run's
approved set, matched argument for argument. The set is settled when the run starts — the --verify
entries given with it, else .collie/verify.json in the project, else the checks remembered
for the checkout's remote (~/.collie/user/verify/<host>/<path>.json, by origin, then
upstream), else ~/.collie/user/verify.json, whichever is found first and taken whole — and copied into the run's Intent as its run_verification grant. Editing the file afterwards changes the next run and never a
running one. From then on the Intent is the set: run intent verification adds to it or
removes from it, and an Intent whose list has been emptied is a run Collie may run nothing
for — the seed is not put back behind the human who removed it. A Run of a workflow module
keeps its set with the host rather than in an Intent, and the same command amends it there.
[{ "name": "tests", "executable": "bun", "argv": ["test"], "cwd": "worktree" }]A file that is there and does not decode is an error naming it, never an empty set, and
the start is refused. So is a start of a workflow that declares
verifies when its outcome needs the set and nothing is approved: nothing could prove it,
so it is invalid_input with the repair, and no Run, worktree or Task is made.
collie run start implement --input plan=… \
--verify '{"name":"tests","executable":"bun","argv":["test"],"cwd":"worktree"}'Everything after -- is the executable and its arguments, spawned directly. There is no
shell: what was written down is what ran. The command's own exit status is passed
through, so anything wrapping collie verify -- bun test behaves as it would around
bun test.
Results go to the run's steering/verifications.jsonl. An agent's Output saying the tests
passed is a claim and is shown as one; only a collected result is a verification.
A repository's checks are remembered from a Run that already has them:
collie run intent remember <run-id> [--replace] [--request-id <id>]It writes the Run's current set, whole, to the file for its checkout's remote, and every
Run started in that repository later begins with it. It refuses a file that is already
there unless --replace is given. Chat does the same with remember_verification
(ADR-0035). Edit the file
to change the set, or delete it to withdraw it.
collie --json run hold <run-id> --request-id "$(uuidgen)"
collie --json run hold --workspace <workspace-id> --request-id "$(uuidgen)"
collie --json run release <run-id> --request-id "$(uuidgen)"hold stops a run taking on new work; whatever is already running carries on. The run
reads it at its next boundary and parks there, so work in flight finishes rather than
being cut off, and run show lists the hold under controls. It lasts until release:
nothing lifts a hold at a time.
--workspace holds every unfinished run of the Task that workspace belongs to instead of a
single run. Each run takes its own hold, so releasing one lifts that one and leaves the
rest held. --reason "<why>" is recorded with the hold, and the board's heldBy says it
beside who held it.
collie --json workflow fork implement --layer user --name ours
collie --json persona fork reviewer --layer project --name strict-reviewer| Flag | What it does |
|---|---|
--layer |
user (where you save workflow modules) or project (this project's .collie/). |
--name |
The id the fork takes; it wins over the one it forked from. |
--request-id |
Idempotency key. |
A workflow is forked by importing it: the fork imports everything it does not name, so there is nothing to merge and no step to pick. A persona is Markdown and is copied whole.
An existing file at the target path comes back as target_exists rather than being
overwritten. See Authoring for what the resulting file means.
With --json, every command prints exactly one line: a success or a failure envelope. That
holds for a crash too — a defect is caught and reported as operation_failed rather than
leaving you an empty stream and an exit status. The exceptions are
run wait --follow, which streams events and ends without an envelope when
the run finishes, and onboard, which prints a line per step
before its envelope. The examples on this page are indented for reading; Collie writes each on
one line.
Success:
{"ok":true,"data":{ … }}Failure:
{
"ok": false,
"error": {
"code": "run_not_found",
"message": "Run \"nope\" was not found.",
"details": { "run": "nope" }
}
}details is command-specific and always an object. Without --json, a success prints its
human line and a failure prints error.message.
| Status | Meaning |
|---|---|
0 |
Success. |
1 |
The operation failed. Something happened, or tried to. |
2 |
The command line was wrong: invalid_input, needs_input or workspace_required. Nothing happened, so there is no receipt to replay. |
| Code | When |
|---|---|
workspace_required |
The command needs a workspace and none could be determined. |
workspace_not_found |
The --workspace id is not a workspace herdr has. |
workflow_not_found |
No layer defines that workflow. |
persona_not_found |
No layer defines that persona. |
run_not_found |
No run with that id, or none by that id in the Task this command is scoped to. |
task_not_found |
--task named a Task that does not exist. |
target_exists |
A fork would overwrite a file that is already there. |
needs_input |
Inputs are missing; details.inputs says which, with their questions. |
timeout |
run wait --timeout gave up. |
invalid_state |
The run, or the installation, is not in a state where that makes sense — resuming one that already succeeded, or upgrade --to on a development checkout. |
invalid_input |
A flag or argument was wrong. |
operation_failed |
Anything else, including a caught defect. |
Every mutation — run start, run answer, run stop, run resume, workflow fork,
persona fork — accepts --request-id. Collie writes a receipt under its state directory
keyed by that id, so retrying the same id returns the original result instead of repeating
the effect. Generate one per intended action and reuse it for every retry of that action:
id=$(uuidgen)
collie --json run start implement --input plan="$PWD/plan" --request-id "$id" \
|| collie --json run start implement --input plan="$PWD/plan" --request-id "$id"A receipt is reserved before the action starts. If execution is interrupted before its
result is saved, retrying reports outcome: unknown instead of running the action again.
Check the Run and its recorded effects before submitting a new request.
A failure carries the request id back in error.details.requestId, including when the id
was generated for you — so a failed send always leaves you something to retry with rather
than forcing a fresh id, which would be a second run rather than a retry. A needs_input
or invalid_input rejection writes no receipt, so the same id is free to reuse once you
have fixed the command.
Native chat returns Request: <id> with its result; pass that value as request_id to
retry. A partially applied sequence keeps its receipt even if its last action asks a
question, so retrying cannot repeat the earlier actions.
run start and run answer are the two worth being careful with: without a request id, a
retried run start is a second run.
collie settings list
collie settings set <key> <value> [--request-id <id>]list prints every one of Collie's settings as this Machine has it, with its default.
set sets one through the host, refused as the TUI's Settings refuses it; an empty value
unsets it. It is an edit made now, so a Desktop that shares settings with its Flock gives it
to every Machine: the latest edit of a key wins
(ADR-0043). Remembered answers and
anything else in config.json are not settings and are refused.
collie cleanup
collie cleanup --apply [--request-id <id>]cleanup lists exactly what the host's sweep would remove now: one line per item with its
kind, what it is, its size and why it goes, then one line per thing Collie made and keeps,
with the one condition that keeps it, then the total a sweep would free. --apply sweeps
now, through the host, and prints what it removed and what it freed. There is no
confirmation: the host sweeps the same way on its own every ten minutes. What each kind
keeps, and why, is ADR-0045;
Cleanup says what a sweep covers.
Under --json, both answer with the same data:
{
"remove": [
{
"kind": "generation",
"target": "/home/me/.cache/collie/entries/generations/…",
"bytes": 81920,
"reason": "unused for 8 days"
}
],
"keep": [
{ "kind": "worktree", "target": "/home/me/.herdr/worktrees/…", "reason": "uncommitted changes" }
],
"bytes": 81920
}remove is what a sweep would remove, or, with --apply, what it removed, each with the
bytes it freed; bytes is their total. A thing a sweep could not judge — herdr not
answering, say — is in keep with that reason, never removed.
collie upgradePulls first where the installation is a checkout (--ff-only), then runs prepare.sh —
the one routine every entry point ends in, so this brings the plugin link, the runner and
shim, the operator skill and the skills up to date together rather than replacing the
runner alone. A pull it cannot do is reported rather than installed over. A runner
install.sh downloads replaces the one there only once its signature checks out
(Build and release). Otherwise an install with bun builds the runner from
source, and one without keeps the runner already there and the runner step fails.
The report names what moved: the commit range where the checkout advanced, and one line
per preparation step saying whether it was done, was already in place, or was skipped —
so "nothing to do" reads differently from "the runner updated but the skills step could
not run". A skipped step is not a failure: upgrade still succeeds. Under --json the
same steps are in data.steps.
The last step, desktop, brings Collie Desktop on this computer
to the version upgrade moved to, where this user has a released Desktop installed that
is older. It downloads that release's Desktop update and stages it where Desktop's own
updater would ($XDG_DATA_HOME/dk.cego.collie.desktop/stable/self-extraction/, else under
~/.local/share), only once its tar verifies against the release key, as Desktop's own
updates are checked. A Desktop that is running is never replaced underneath it: the step
says the update "applies when you restart Desktop", and Desktop announces it as ready within
a minute. Otherwise it installs when Desktop next starts. An update already staged that is
newer, by Desktop's own download or an earlier upgrade, is never replaced by an older one,
and one it replaces has its tar removed. A download that does not verify is not
staged, and the step is failed with the reason. A Desktop already at the version is
already in place, so Desktop upgrading this computer to its own version changes nothing.
There is no desktop step where no released Desktop is installed, where Desktop runs from
a checkout, or on a platform Desktop is not released for (only linux-x64 is).
collie upgrade --to 0.27.0Moves a released install to exactly that version (fetching tags, then resetting to the
tag), and reports the same way, with data.version. A development checkout — on a branch
other than master, detached on a commit that is not a release, with uncommitted changes,
or ahead of its remote — is refused as invalid_state with the reason, and its checkout is left as it was.
A host's identity names such a checkout's build as development: "<version>+<sha>".
collie onboard [--to 0.27.0] [--secrets-stdin] [--skip helle] [--skip linear] [--gitlab-host <host>]Takes a Machine from bare to a working Collie host, and repairs a half-onboarded one: every
step looks before it acts, so running it again does only what is missing. --to is the
release to install, this runner's own version when it is not given. Nothing runs sudo.
Its GitLab is one host: --gitlab-host, else GITLAB_HOST, else the
gitlab_host setting, gitlab.cego.dk by default. The token
page, the glab login, the push and the closing doctor all name that host.
| Step | What it does |
|---|---|
system |
Checks for git, curl and openssl, or the one COLLIE_OPENSSL names (which checks a downloaded runner's signature; any version, LibreSSL included, will do). Any missing stops here with needs_root and the exact command |
collie |
Clones https://github.com/cego/collie.git (or COLLIE_REPO) into ~/.collie (or COLLIE_DIR) and resets master to the tag, so a plain collie upgrade can still pull; or moves a released checkout to the tag as upgrade --to does |
herdr |
curl -fsSL https://herdr.dev/install.sh | sh, if there is no herdr |
claude |
Anthropic's user-level installer, curl -fsSL https://claude.ai/install.sh | bash, if there is no claude |
path |
Adds ~/.local/bin (and COLLIE_BIN_DIR) to PATH in the shell's profile (~/.bashrc, ~/.zshrc or ~/.profile) |
plugin |
prepare.sh: the plugin link, the runner and shim, the operator skill and the skills. A runner install.sh downloads is installed only once the release key's signature (<asset>.p256.sig) checks out |
claude-login |
claude auth login in this terminal, if claude auth status --json says Claude Code is not logged in; without a terminal, needs_human with that command |
gitlab |
glab auth login --hostname <host> --stdin with GITLAB_TOKEN, if glab is not already logged in there, or is logged in with another token than the one given. Without a token: needs_human, with the token page and its api and write_repository scopes as url |
push |
Generates ~/.ssh/id_ed25519 if there is none and registers it with glab ssh-key add, unless the Machine can already push (over HTTPS with glab's login, or with its own key). GitLab's host key is trusted on first use, and a key GitLab already has counts as registered |
helle |
Writes HELLE_API_TOKEN, and HELLE_API_URL=https://helle.cego.dk for other Helle clients, to Helle's credentials file, owner-only; with no token given and no file, needs_human, saying how Slack's /helle token makes one |
linear |
claude mcp add --transport http --scope user linear-server https://mcp.linear.app/mcp, then claude mcp login linear-server in a terminal of its own, whose URL is streamed: the one it prints, or the one with its callback port it hands $BROWSER, which a shim writes down |
doctor |
collie doctor; onboarded means it is ready |
A development checkout — the one this runner belongs to when that is a checkout, or
COLLIE_DIR — is judged as upgrade --to judges one, and gets the checks
only and the logins: nothing is installed and its checkout is not moved.
Secrets are never arguments. --secrets-stdin reads KEY=value lines from stdin —
GITLAB_TOKEN and HELLE_API_TOKEN — and the token reaches glab on its
stdin too. Helle and Linear count toward onboarded unless --skip names them; a skipped
step reports skipped. Doctor's own checks for them stay optional.
Each step prints a line when it starts and one when it ends. Under --json each is a JSON
line, then the envelope:
{"event":"start","step":"system","title":"Checking for git, curl and openssl"}
{"event":"result","step":"system","status":"needs_root","detail":"git must be installed as root; run the command, then onboard again","command":"sudo apt-get install -y git"}A step that needs the human in the middle of it streams a human line first, as the Linear
login does with its URL and the local port its redirect comes back to:
{
"event": "human",
"step": "linear",
"detail": "open this to let Claude Code reach Linear",
"url": "https://mcp.linear.app/authorize?…",
"port": 62074
}In a terminal the login also takes a pasted redirect URL. Chat can propose an onboarding
too (the onboard action, with an optional skip); nobody is watching that stream, so its
logins are left as needs_human steps rather than started.
status is done, in_place, skipped, needs_root, needs_human or failed; a
result may carry the command to run by hand and the url to open. The envelope's
data.ready is true only when every step ended done, in_place or skipped, and
data.steps repeats every result; otherwise it is an operation_failed that lists what is
left, and the exit status is 1. Re-running is the retry.
collie doctor
collie doctor --json
collie doctor --gitlab-host gitlab.example.comEvery prerequisite in one pass, each with the command that fixes it: herdr present and at
least the min_herdr_version the plugin manifest declares, and a running herdr server, when
there is one, at least that too; the plugin linked from this
installation; the runner built and the collie shim on PATH (installed-but-not-on-PATH is
its own reported state); every skill and every harness
the loaded workflows and personas name; whether the checkout is behind its remote; the
Projects root and its source — projects.root, GITTE_CWD, or the home directory, the last
a ! warning that never fails the run; Claude Code logged in, from
claude auth status --json, where claude is on PATH; and, for the one GitLab host Collie
works against, glab present and logged in there, the token's expiry — a ! warning within
14 days of it and a failure once it has passed — and whether this Machine can push there,
over SSH with its own key, or over HTTPS where glab configured git to use it. An agent sshd
forwarded into the session does not count, since it goes when the computer it came from
sleeps. That host is --gitlab-host, else GITLAB_HOST, else the
gitlab_host setting. Any other host glab knows is named in one
other gitlabs note, unchecked, and never fails the run.
herdr names what upgrading does to the running panes. From a herdr before 0.9.0, the
running server must stop once, which ends every program in its panes: herdr update, then
herdr server stop, then herdr, when nothing is running there, or the experimental
herdr update --handoff. From 0.9.0 on, herdr update leaves the running server and its
panes alone, and a herdr server line fails while the server still runs an older herdr,
until it is restarted when nothing is running. Each fix also offers the package manager
that installed herdr. Collie never runs herdr update or herdr server stop itself.
Disk covers each filesystem holding the state directory, ~/.cache/collie, herdr's
worktrees and the temporary directory, once each. One with less than 10% or 5 GiB free is a
! warning naming it, with what collie cleanup would free where a host has
served this state directory, and collie cleanup --apply as the fix. It never fails the run.
Two more are optional, and reported rather than required. Helle, where a loaded
workflow waits on it (renovate does): the credentials file the Helle MCP wrapper sources,
~/.config/helle/env (or HELLE_ENV_FILE), whose token is tried against
https://helle.cego.dk's /me; a HELLE_API_URL line in it is ignored. A
Linear MCP server in Claude Code, where a workflow routes a step to Claude: user and
local scope in .claude.json, project scope in the project's .mcp.json, matched by name
or URL. Each has three states, and the glyph says which: ✓ there and working, a note
with the setup command when it is not set up at all, and ! — set up and not working, a
file without a token, a token Helle refuses, a settings file that is not JSON —
with the file to look in. Neither ever fails the run: the bundled implement and review
need neither. A run that does need one is refused at collie run start with the same
detail and fix, before any tab opens.
The skills and harnesses come from the loaded definitions rather than a list in the code, so a forked workflow naming a different skill is checked against that one. Every executable it looks for has to be executable, not merely present: a shim without its bit set fails with permission denied at the point of use, which is the confusion this command exists to end. It fetches before answering whether the checkout is behind, because you are waiting on it.
It exits non-zero when a prerequisite is missing, so it can gate a script of your own. A
checkout that is merely behind its remote is not one of those: it is reported, with
collie upgrade under it, and the run still passes — the same line the Control Plane
shows rather than sends. --json carries the same checks as data. setup.sh ends by
running it, and exits with its status: the last word of an install is either that
everything is ready, or what is missing and how to fix each one.
collie host --dir <state-dir>The process durable work runs in. Nobody is expected to type this: a client that needs a host and finds none starts one, detached, and leaves it running — so the work outlives the command, the board or the chat turn that asked for it. Typing it is how you watch one in a terminal.
One host owns one state directory, and the pid lock beside its SQLite decides which. Start four clients at once and they converge on one owner; the three hosts that lost the lock exit without touching anything. A lock left by a host that crashed is broken and taken over — by its recorded start time, so a pid that now belongs to an unrelated process is never adopted and never signalled.
Clients talk to it over a unix socket in that same directory, with Effect's own RPC: the
same schemas at both ends, and nothing listening off this machine. It answers identity,
discover, load, registrations, status, run, runs, watch, recover, offers, grant and steer — one
registry, in front of as many clients as ask. run and runs are the read model the front
doors show; watch streams it, current state first and a whole state each time; recover
registers what current files now allow and hands over what is outstanding. One fiber in the
host asks the engine about the work it has not finished, on a schedule every client shares,
and speaks up when anything a run shows has changed, so watching costs the same whether one
client is looking or the whole board is. Closing a client cancels nothing it started;
stopping the host with kill leaves suspended work suspended, and the next client starts a
host that picks it up. A stopped host gives running steps five seconds to finish and then
exits regardless, letting go of its lock; a step it cut short runs again under the next.
The operations above are HostRpcs: internal, and a Collie client of another build stops
before sending them anything. Beside them on the same socket is FrontDoorRpcs, the door
any front door uses whatever its build or computer, declared with its Schemas in
src/board-model.ts. board streams the board: a Snapshot (the state directory's
installation id, build, development — <version>+<sha>, only where the host runs a
development checkout — protocol, the herds — every running herdr session, by Herd
id and herdr's name — and every TaskView), then an Upsert or a Remove keyed by Task
id for each change, each with a seq higher than the last. A client that reconnects gets a
fresh snapshot. The host builds one board for every client, so a client that subscribes is
told the latest at once (or once the first build is done), and it builds again when
anything under its state directory is written, when herdr pushes an event from any of its
sessions (a pane opening or closing, or an agent's status changing), and every five
seconds: one build at a time, with what changes meanwhile folded into the next. The
installation id is written once, by the first host to own the directory, and survives
restarts and upgrades.
The host also runs what nobody has to have a pane open for: the merge watch, which asks GitLab about each waiting merge request every 5 minutes and records a merge; each Herd's News, which it also supersedes once an item's cause no longer holds; and the cleanup sweep, every ten minutes.
The operations that change a Run are on FrontDoorRpcs too: start, answer, control
(a hold or a stop, set or cleared, and every watcher hears about it), resume and
invoke (an offer). Two reads tell a front door on another computer what those take:
offers, what a Run offers to do next as its module decides now, with each offer's
arguments as JSON Schema; and workflows, what may be started in a project and the Inputs
each asks for. start then takes what a human typed in text, and the host settles it
against the workflow's own schema. cleanup reads what a sweep would remove and keep, and
sweep sweeps now, recorded with its Actor in the state directory's cleanup/operations.jsonl
as well as in cleanup.jsonl. grant and steer stay on HostRpcs. Each takes a request id, and the same id twice is one operation. A
channel first sends declare with its front door, and the host stamps every operation on
it with that, as a line in the Run's operations.jsonl: the operation, the request, the
Actor and what came of it (ADR-0039). A
channel that declares nothing is cli. declare may also carry session, the herdr
session socket the front door runs in: a confirmed, asked or proposed action looks its
workspaces, panes and Herd up there. A channel that names none has them looked up in the
session the host was started from. A chat channel may carry conversation and said,
the human's message that turn as its tool host heard it; the host puts both in the Actor of
everything done on that channel, the executors' own operations included.
collie bridge --as <board|desktop|chat> [--client <name>]A front door on another computer reaches the host through this, run on the Machine — over
SSH for Desktop, or spawned without it for Local. It starts the host if none runs, handing
it the state directory and the herdr session socket (asked of herdr status server when a
login shell sets none), declares the channel as the front door it was started as, prints
collie-bridge-ready on a line of its own, and from then on copies bytes between its stdio
and the host's socket. Whatever came before that line is a login shell's and is discarded.
The declaration is the bridge's: a declare of another front door on the channel is
refused, and the client at the other end never starts, stops or signals a process.
desktop is a human, as board and cli-tty are, and its Actor carries from: the
--client name and the SSH client address the bridge saw.
So are the ones that write anything else: confirm (a proposal's id and content hash) and
decline (its id and content hash too), recorded in its Herd's proposals journal under the Actor; dispose,
what became of a Run's work; steerAbout, which has the evaluator turn free words about a
Run into actions and carries them out; followUp, which starts the follow-up the Run's
Workflow declares; propose, which records what chat was asked for as a proposal and
carries it out; act, which carries out the board's own actions on a Run (stop, resume,
release, hold, answer, deliver, followup, start) with no proposal, anything else
being refused as propose's; reconcile, which settles a proposal step nobody can account
for; settleDelivery, which does the same for a message to an agent; focus, which focuses
a Run's newest live agent, or only the agent it names, else its workspace, on the host's
own herdr and answers with the session, workspace and tab it is in, and the focused agent's
pane id and herdr name where it found one (an older host's reply has neither, and is read
as naming no pane); setSettings, which writes the Flock's settings into
the Machine's config.json, each with its own edit's time and only where that is later than
the Machine's own last edit of the key, records when each was set in settings-set.json
and, when Desktop asks, that a Desktop gave them, and refuses the whole batch over any value the TUI's Settings
would refuse (ADR-0043); settings
reads them back, each with when it was set; and news, which
hands one conversation its Herd's pending News, each item with its Significance, and
records it read, sent or uncertain for that conversation alone. Given keys, it hands
over every pending item and records only those keys: what Desktop's Flock chat actually
gave the model, out of a batch it put together across Machines. Actions travel as
JSON and the host decodes them. collie confirm, decline, steer, run disposition,
proposal reconcile, run deliveries --reconcile and chat news --sent, the board and
chat's tools all go through these, so the host is the only writer of what they record
(ADR-0040).
read answers collie_run, collie_receipts or collie_workspaces — and no other tool —
for the asking channel exactly as the Machine's Native chat would, with no board selection
standing in for a Run nobody named. It records nothing. Desktop's Flock chat reads through
it.
runDetail streams one Run's details while a drawer is open — intent, plan, review,
log tail, verifications, metrics, steering cards, the files it kept as evidence, its diff
and its merge request — current first, then again whenever they change. The diff is the
Run's branch against its merge base with the default branch, per file: the checkout as it
is while the Run works, the branch's commits once it has ended. The host keeps that in the
Run's directory as soon as the Run ends, with the branch head it was taken at, so a merged
branch or a pruned checkout does not lose it and a resumed Run that committed more is read
again. An untracked file reached through a link, or that is not a regular file, is listed
without being read. The review's findings come as
a list. The merge request is what the merge watch last read, asked again after 5 minutes or
when refreshMr is set. Large items are fetched by reference with runFile: log,
review, diff:<path>, evidence:<name>, verification:<id>, plan:<file>, attachment:<name> (one of the Run's attachments), file:<path> (read
only, from the Run's checkout) and pipeline:<url> (the status glab reads for that pipeline), text as it is and anything else as base64. Each answer is
at most 4 MiB from offset (or length bytes where asked) and says the item's whole
size, so a long log or a video is read in parts. A part of an item is base64 whatever it
is, so a character split across two parts is whole once they are joined. A reference is refused where it leaves
the directory it belongs to, links followed, or where it is not a regular file.
Five more answer a front door about the Machine's own files, every path absolute; the Flock
chat's file tools use them (ADR-0011).
readFile hands over a part of a file, at most 4 MiB from offset, as base64 with its
media type and whole size. glob answers the files a pattern matches under a directory,
newest first, at most 100 and how many it omitted. grep takes Claude Code Grep's
arguments that make sense on a host (glob, type, outputMode, ignoreCase,
lineNumbers, before, after, context, headLimit, multiline), runs ripgrep where it
is on PATH and grep -r otherwise, and answers at most 100 lines. writeFile writes a
whole file, making its directory, and editFile replaces oldString with newString,
refusing one that is missing or, without replaceAll, not unique. Both take a request id,
the same id twice being one operation, are recorded with the Actor in
files/operations.jsonl under the state directory (its newest 1000), and are refused where
the file's real path is inside the state directory. The Snapshot carries files: true
from a host that has these; a client sends files to no host without it.
upload takes a file sent to this Machine in parts: its name, size, sha256, an
offset and one base64 part of at most 4 MiB. The host appends the parts under
uploads/<sha256>/ in its state directory, checks the size and the digest on the last, and
answers the file's path, which a start, an invoke or a steer can then name as an
attachment; before the last part it answers null. A digest it already holds whole is
answered with its path at the first part, so the rest is never sent, under the name it was
asked for, and a part sent again is the same part, so a retry needs no request id. Parts are
taken one at a time. A mismatched digest or size removes what arrived and is refused, as is
a part at an offset other than what arrived (send it again from the start) and a file over
100 MB. Each upload is recorded with its Actor in uploads/operations.jsonl. Being asked
for a held digest renews its age, and the host's cleanup sweep removes an upload a week after it
was last asked for; Desktop trusts an upload's path for a day before it asks again. A chat channel's declare may also carry attachments, the names of the files
the human's message carried, which the host records beside said.
protocol is an integer, also in identity. An optional field, a new operation or a new
kind of message does not change it, and a client reads a kind it does not know as
Unknown and skips it. A removal or a change of meaning bumps it, and from then on the
host serves its current version and the one before; version 1 has none before it.
Version 2 changed two meanings: a chat channel's repeated declare replaces the words it
speaks with rather than being ignored, and news with keys settles only those. A version
1 client repeats no declaration and sends no keys, so it is still served as before.
Desktop's Flock chat writes through no host older than version 2, and says so.
discover and start name the project asking, because one host serves the machine and a
project's own .collie/workflows is its own: two projects can run different implementations
of one public id at the same time, each on its own registration.
sdk.md is where a module is saved and when an edit takes
effect.
start carries the author's input in two halves — input for values that already have a
type and text for values as a human typed them — and options for the host's own names
beside them. The module's own schemas settle all of it there, because that is where the
module was loaded; what they refuse is invalid_input naming the field, and the row keeps
what they settled rather than the text it arrived as.
start also names the caller's request — its own id for the work it is asking for — and
answers with the run that claim became. Sending it again is that same run rather than a
second one, whether the first attempt was answered, lost, or interrupted by a host that
died mid-start; sending it with other arguments is RequestConflict rather than a quiet
change of mind. A host that died after asking for the run's checkout or workspace, and
before recording what it got, cannot tell whether one was made: that start is refused
with what may be left behind, and keeps its claim, so the same request never makes a
second one. A start the engine does not take within thirty seconds is refused saying so,
and stays recorded: the next host start hands it over under the same claim. The rows behind
that are in the same SQLite file as the engine's own, and
ADR-0017 is why each of them is there.
It says which build it is, and which installation it serves. A client newer than the host,
from the same installation (after collie upgrade), stops it and starts itself in its
place; a host still there five seconds past its stop grace is killed, and its work is
recovered by the one that replaces it. A host that does not answer is replaced the same way
when the build it recorded in host.build is older. Any other client of another build is told which build is running and which pid to
stop, and sends nothing else. That includes a checkout under development, which is pointed
at a state directory of its own rather than replacing the installed host. Such a
checkout's host also says development: "<version>+<sha>"; a release's does not.
A host that cannot be started at all, or that takes a connection and does not answer within
five seconds, is HostUnavailable, with whether anything owns the directory. ADR-0015 is why each of
those is the way it is.
COLLIE_HOST names the command a client starts a host with — one path, or a JSON array of
the executable and its arguments. Unset, it is this executable.
COLLIE_HOST_STOP_GRACE is how long a stopped host waits for running steps before it exits
anyway, as a duration (5 seconds unset).
COLLIE_HOST_CRASH_AT=admitted|executed is for the recovery proof alone: the host kills
itself in one of the two windows a start has — with the run recorded and the engine not yet
told, or told and the receipt not yet written — so that recovery is demonstrated rather
than argued. Nothing else sets it.
workflow create and workflow fork write package.json, tsconfig.json and
collie.d.ts beside the module, and install the toolchain with the executable's own
embedded Bun — so typechecking a module needs neither Bun nor Node on the machine. A
package.json or tsconfig.json already there keeps everything of yours: the effect and
typescript it lacks and the collie path mapping are added to it. One that is not plain
JSON is left alone, and the answer says what to add. workflow check typechecks each module and
reports each diagnostic with its file and line; an error in one module says nothing about
the one beside it. With nothing installed to check with, that is ok, not typechecked
rather than a module reported as fine.
The effect the toolchain pins is the one the host runs, and at runtime the executable
serves its own effect and collie to the module it loads, so what an author typechecks
against and what executes are the same Effect.