Tiller CLI Command Surface
Scope. This document defines every command the tiller CLI exposes: the operations that read the artifact files, the operations that mutate them under the lifecycle rules, and the command-backed gates it runs. It is a layer over the on-disk model in artifact-schema.md, which stays canonical for the files themselves. Where this document and the schema disagree about a file’s shape, the schema wins and this one gets fixed.
Status. Current reference for the CLI, maintained alongside the shipped behavior. Where the two disagree, the code is authoritative and this document gets corrected with the change. Unbuilt commands and design options are labeled explicitly; the roadmap lists remaining scope and tiller-commands.md lists the shipped commands. The design history records the reasoning behind this contract. The current schema version is 12.
Tiller generates no Markdown status board: there is no gen command group, the sync result has no board field, and source mutations carry no board-only warning or repair remedy.
What the CLI is
Section titled “What the CLI is”The tiller CLI is the typed surface over .tiller/. It does exactly three kinds of work:
- It reads state and prose, for the human and for the agent.
- It mutates state under the lifecycle rules, validating every transition and recording who made it.
- It runs the command-backed gates (build, tests, evals) and records their results.
What it refuses to do is as load-bearing as what it does, and the refusals are the design:
- It does not reason. Decomposition, speccing, task breakdown, and drift fixes are the agent’s work, done through skills. The CLI gives the agent the structural operations to write its reasoning down, plus the gate that checks the writing-down is allowed. The reasoning never lives here.
- It does not orchestrate git or the PR host. It computes and records branch names and PR numbers, and reads local Git to observe what happened. The runtime does not query a PR host; the caller verifies host identity and publication through ordinary host tools. It never creates a branch, opens a PR, pushes, performs a merge, or commits state. Explicit state-commit conveniences remain unbuilt design; ordinary Git is the current path.
- It does not run review tooling. The optional review gates are set by attestation. Tiller never invokes a reviewer.
The agent in its loop and the human at a terminal share this surface. tiller serve renders the working tree for the human in the loop and is currently read-only. The designed write buttons would map one-to-one to CLI commands, but console writes are not built.
The principle underneath is the one the whole tool serves: Tiller holds the structure, the gates, and the state; the agent does the reasoning; the human approves at the gates. In this document that split is physical. It shows up in who may run which command, which the lifecycle table at the end makes explicit.
Grammar
Section titled “Grammar”Every command reads tiller <verb> <noun> <node> [flags].
tiller approve feature F-79dh3qtiller check feature F-79dh3qtiller submit feature F-79dh3q --pr 41The grammar is verb-noun, with a required, validated noun. Every node command is tiller <verb> <noun> <node>, with the noun present and validated against the node it names. The noun is redundant with the node’s typed id, and that redundancy is deliberate: it is a checksum. tiller approve epic F-79dh3q is a hard error, because F- is a feature, not an epic. A caller that has lost track of what it is operating on gets caught at the boundary instead of corrupting state. The redundancy that reads naturally to a human doubles as error detection for an agent.
A small, named set of project-level commands take no node: init, join, check readiness, sync, drift, guide, next, readout, queue, search, query, serve, upgrade, and migrate. tiller status is the one node command with a bare form: with no node it is the project summary, while tiller status <noun> <node> is the typed oracle for a single node.
Four shapes bend the verb-noun rule in disciplined ways, and the grammar names them so they are not read as errors. First, a verb that targets exactly one node type takes the bare id, and Tiller validates the prefix against that one type: amend only ever operates on an amendment (tiller amend A-d6w3f9 ...) and archive only on an epic (tiller archive E-j8m4q2). Second, add creates rather than addressing an existing node, so its argument after the noun is a new node’s key or statement (tiller add feature form-engine/certification-form-loading, tiller add question "<statement>" --title "<short title>"). Third, tiller show plan <file> and tiller ls plans address plans by filename, because a plan is a semi-ephemeral file rather than a node with an id or key. Fourth, configure takes one of project, epic, or feature as its own first argument rather than a fixed noun, and only epic and feature then take a node: tiller configure project --review-grain <g> needs no second argument, since a project has no node, while tiller configure epic <key> --review-grain <g> and tiller configure feature <key> --review-grain <g> each need one. A bare tiller configure --review-grain <g> refuses, exit 2, naming the three targets, because the omitted-target reading would be the widest possible scope and the CLI refuses rather than guesses.
The high-repetition verb takes more than one node. tiller approve <noun> <node>... applies the same move to several nodes of the noun in one invocation. Resolution is all-or-nothing: every address must resolve before anything mutates, so one bad id exits 2 with nothing applied. Application is then per node in argument order, and a blocked node does not stop the rest; the batch exits 1 with per-node outcomes when any node was blocked, 0 when every node transitioned or was already in the target state. tiller approve task --feature <key> approves every proposed task on that feature in dependency order; zero proposed tasks is a plain success, and the flag is mutually exclusive with positional nodes. add stays single-node: adds are content-heavy, and the batching pain was approval.
Approval validates supplied flag presence before any specialized dispatch or mutation. --review-grain is accepted only for epic and feature approval, never for task, shelf, design, or amendment approval, even when its value is empty. --feature requires a nonblank feature address, applies only to task approval, and excludes positional nodes; an explicitly empty selector is misuse rather than an absent selector.
The path-addressed design noun belongs to link, unlink, and approve. A design doc is not a work node and has no id or lifecycle status. Unlike work-node approval above, approval of several named design docs is one atomic batch; its contract is below.
A node is addressed by id or by key. The id is canonical and stable (F-79dh3q); the key is the path-derived label and more convenient to type (form-engine/certification-form-loading). Both resolve to the same node, and the noun is validated against whichever is given. Ids are type-prefixed collision-resistant tokens, not counters; the artifact schema defines the compact random token rule. The prefixes are fixed:
| Prefix | Noun |
|---|---|
E- |
epic |
F- |
feature |
T- |
task |
Q- |
question |
A- |
amendment |
ADR- |
adr (unbuilt design) |
B- |
bug |
CH- |
chore |
TW- |
tweak |
The id/key/path model itself, the id regex, the path layout, and which nouns carry a file of their own versus living as ledger items, is defined canonically in artifact-schema.md. This section only states how the CLI resolves an address to a node:
- Id lookup resolves any addressable node.
- Key lookup resolves only keyed nouns (
epic,feature,task,amendment,bug,chore,tweak;adris unbuilt design). The one ledger item,question, has no key and is addressed by id only. - A feature’s canonical key includes the epic segment:
<epic-key>/<feature-key>(form-engine/certification-form-loading). A task’s adds its own:<epic-key>/<feature-key>/<task-key>. A bug’s, chore’s, and tweak’s prepend the shelf:bugs/<bug-key>,chores/<chore-key>,tweaks/<tweak-key>. - A bug, chore, or tweak also resolves by its bare local key when the noun selects the shelf.
bugs,chores, andtweaksare reserved epic keys, so shelf canonical addresses cannot collide with tree addresses. - An ambiguous key lookup exits
2with the candidate matches in the JSON error, so a caller can disambiguate by id. - The amendment noun narrows a colliding key instead of refusing it, because an amendment key legitimately outlives its holder: a duplicate key is refused only among
proposedamendments, so withdrawing one reshape and opening another under the same key is a supported move and an approved amendment keeps its key forever. Resolution runs two tiers, the open proposals and then everything else, and the highest non-empty tier decides: exactly one candidate there resolves, several stay ambiguous among themselves. So a key names the live proposal whenever there is one, which is what a caller almost always means by it, and a key no open amendment holds still names a single retired holder. What a key cannot do is reach past a live proposal to a retired one, or choose between two retired ones; those are read by theA-id. Nothing else gets this treatment, because two epics or two features sharing a key has no legitimate reading and stays an ambiguity. - A noun that mismatches the resolved node exits
2, the same checksum failure as a mismatched id prefix.
Resolution is the input side. The output side has one convention of its own: when human-readable output or human-facing prose names a node, it renders the key path with the id in parentheses. The path says where the node sits, the id says which node it is, and the id rides in the parentheses because keys are renameable while ids are stable. Per noun:
- Epic:
form-engine (E-j8m4q2). - Feature: the canonical epic-qualified key:
form-engine/certification-form-loading (F-79dh3q). - Task: the canonical three-segment key:
form-engine/certification-form-loading/schema-transcription (T-k3p9w6). - Question: never keyed; the id plus its short title:
Q-h2n7m5“Audit selection unit”.
Bare ids stay fully valid in command position, and for questions they are required there; the convention governs how Tiller talks about a node, not how a command addresses one. Scanning surfaces render a question as id plus title. When a question itself is the subject, show question and the question detail surface retain its complete statement. The CLI renders every one-line human-facing reference through one shared label helper, so a flat surface never shows a bare id where a key path resolves. The one deliberate exception is tiller tree, whose nesting already carries the path: each row shows only its local key segment with the id.
Commit subjects authored during task execution may use the bare task id, following the host project’s commit style. PR titles and bodies use full human references: the title names the node the PR represents, and the body names its parent feature at task grain or lists the tasks and, at epic grain, features it carries.
The verb set is small and semantic. Each verb names the work it records, rather than setting an opaque status:
| Verb | What it does |
|---|---|
add |
Create a node and mint its id (epic and feature in draft, task in proposed, plus question and amendment), including a bug, chore, and tweak on their shelves (add bug <key>, add chore <key>, add tweak <key>, with --attach). add adr, add design, and add evidence remain unbuilt design. |
approve |
Human approval of a proposed plan, the start gate (epic decomposition draft to ready; a feature plan specced to ready, its initial tasks flipping proposed to ready in the same act; a later-added task proposed to ready; amendment proposed to approved, which applies its operations), or refreshes a node’s accepted basis after human review (--refresh-basis), or reviews an answered assumption into the node (--reviewed-assumption <Q-id>), or records the review grain in the same act as an epic or feature start gate (--review-grain <g>, see Review grain). |
configure |
Records review_grain outside a gate, at the project, epic, or feature home (see Review grain). Not a lifecycle transition: it moves no status. |
spec |
Agent asserts a feature’s second-stage output is complete (draft to specced); Tiller validates the specification, the contract, at least one task with its implementation spec, and, for a replace or migrate feature, at least one recorded surface path. It also warns, without blocking, about a preserve criterion the required tests gate already covers. |
start |
Begin a claimed task, bug, or chore, fixing its owner permanently. First task start atomically activates its ready feature and initializes applicable aggregate identity, or adopts an existing ref at the derived aggregate name with --adopt-aggregate. |
check |
Validate active basis, run and record a node’s gates, reusing a gate already passed on unchanged code; --rerun forces real runs and --gate <name> runs only the named gates. On green, a task, bug, or chore advances to check. |
submit |
Validate active basis and unreviewed assumptions, record the PR that represents a task, and move it to in_review. In a repo with no git remote, or on a task whose resolved review grain is feature or epic, --pr may be omitted (or is refused, on a task under a coarser grain) and the current branch and head commit are recorded as the review ref instead. On a feature or an epic, records that node’s own PR or review ref as the reviewed unit instead, record-only, leaving its status untouched (see Review grain). |
attest |
Human attestation for an attestation-backed gate, a manual criterion, or a required eval criterion whose rubric output has been recorded as evidence. |
answer |
Record the answer to an open question and expose any dependent nodes as answered_question drift candidates; approve --reviewed-assumption is the act that reviews the answer into each node. |
claim / release / reassign |
Take, release, or hand off a live unstarted task, bug, or chore reservation. No ancestor claim or cascade exists. |
transfer |
Explicit human-directed same-scope replacement of started unfinished execution work; preserve original attribution and create an approved ready successor. |
link / unlink |
Record structural facts learned after add time. link --depends-on adds resolved dependency ids and unlink --depends-on removes them, directly only before affected work starts; a repeated add or absent-edge removal is a no-op. link --surface <path> unions existing-code paths onto an epic or feature and is a separate arm; unlink --surface <path> removes recorded paths, existing or not, before the node is done. --attach and --detach set or clear a bug’s, chore’s, or tweak’s attached_to at any state. On a question, link --blocks <target> widens and unlink --blocks <target> narrows what an open question blocks. |
block / unblock |
Unbuilt design: toggle the orthogonal blocked flag, with a reason. |
resume |
Re-enter work after a PR review requests changes (task, bug, or chore in_review to in_progress), with a reason. Keeps the recorded PR; stales gate results whose recorded code identity no longer matches. |
return |
Reverse an erroneous human gate before work starts, with a reason. |
cancel |
Terminate an epic, feature, or task, or close an open question that needs no decision (cancel question, which records the reason as cancel_reason). A --reason is required and recorded on the event. A bug, chore, or tweak also records --resolution wontfix|invalid|duplicate, and duplicate names the survivor with --duplicate-of. |
resolve |
Record a bug’s, chore’s, or tweak’s carrier: the epic, feature, or task whose merge fixes it (--by), or clear it (--clear). Moves no status; sync crosses the node, basis carried, when the carrier’s done lands. |
archive |
Move a done epic to archived. |
intake |
Land an external feedback document into .tiller/feedback/, fingerprinted, and open a proposed amendment carrying it as source, with nothing staged. |
amend |
Stage one operation onto an open amendment (revise, correct-eval, supersede, cancel, move-feature, add-epic, add-feature, add-task, add-question), or union design doc paths into its amends_design, or both in one call. Records a proposal; executes nothing until the amendment is approved. |
withdraw |
Abandon a proposed amendment, with a required reason. |
rename |
Change exactly one axis on an epic, feature, or task: display metadata through --title at any state, or a proposal-safe canonical key and path through --key, always preserving id. |
remove |
Permanently erase an unapproved proposal: a proposed task, a task-free draft feature, a draft epic whose draft feature subtree is task-free, or a proposed bug, chore, or tweak, with a required reason and atomic reference cleanup. |
commit state |
Unbuilt design: explicitly commit reviewed .tiller/ state changes, refusing mixed dirty trees and never pushing. |
show / ls / status / queue / search / query |
Read. query traverses recorded relationships; search finds keyword matches. |
Unbuilt command design. doctor, commit state, block, unblock, add adr, add design, and add evidence are not registered in the current CLI. Their intended behavior is retained where described below, not promised as available. The proposed standalone mutation flags --commit and --dry-run are also unbuilt. Onboarding and binary lifecycle contracts live in distribution.md.
The actor model
Section titled “The actor model”Every mutating command records one identity: the owner it is attributed to, resolved entirely from inside the repo: the --contributor flag, then TILLER_CONTRIBUTOR, then the gitignored .tiller/identity.yml that tiller join wrote on this checkout, and looked up in the manifest’s contributors. Tiller is self-contained in the project repo: there is no configured profile, no global, user, or machine-level config, and no machine-default contributor, so it depends on nothing on the system beyond the binary on PATH. If none is set, or if the resolved contributor is absent from the project, the command refuses rather than guess and points back to tiller join --as <you>. There is no separate agent identity. A developer’s agent runs as that developer, so a claim, a proposal, and a finished task all attribute to the human, and the event stream records the same. New execution units remain unclaimed: creation and approval attribution is separate from the task/bug/chore/tweak owner claim. First start fixes the unit’s owner permanently. Event actors use contributor; two humans remain distinct contributors, while a human and their agent share one identity.
Every command that resolves an acting contributor, not every mutating command (init, join, and add contributor write to the repo too but carry no acting-contributor concept at all), says the resolution aloud when the winning source is a fallback rather than an explicit --contributor: the human report carries one added line, acting as <id> (<source>), and the --json envelope carries a top-level actor field, {"id": "<id>", "source": "<source>"}, always present regardless of which source won. An explicit --contributor prints no echo, since it is already on the command line the caller typed, but the JSON actor field still names it. A silent fallback could stamp the wrong identity onto every node a command touches; naming the fallback in the output the command already prints makes that visible at once.
Upgrade and migrate resolve an actor only for a real source migration. Binary-only work, outside-project upgrade, and artifact-only refresh do not select an actor, even when --contributor was supplied. Maintenance never selects a different checkout identity or creates a missing one; it only renames a legacy owner: key in an existing identity file to contributor:, preserving its value, and unjoined checkouts can refresh. A migration uses the normal registered-contributor precedence and preserves that resolved actor in the maintenance result’s project.actor through handoff and partial failures, rather than inferring automatic top-level attribution from the flag. Migrating and upgrading owns the rest of the maintenance contract.
The roster is contributors: in the manifest and checkout-local identity is contributor: in .tiller/identity.yml. Roster events are contributor.add and contributor.remove, and new event actor attribution uses contributor:; historical events that use owner: remain readable without rewriting their bytes. Execution and question owner fields and scalar ownership filters keep the name owner. No command, flag, environment variable, or manifest key aliases the legacy owner names for the roster or acting identity.
This is the actor model the non-coercion principle produces. The human-only transitions, approve, attest, cancel, archive, withdraw, sync --accept-fast-forward, and the merge that crosses to done, are not enforced by Tiller refusing the agent, because on one machine the agent and the human share an identity and any such refusal would be theater. They are guidance: the skills and the generated .tiller/orientation.md tell the agent to drive to the edge of done and stop, and the boundary that genuinely holds is the merge, which the human performs. Human-only means the human’s decision, not the human’s keyboard: at the start gates the agent may run approve on an explicit human instruction naming the nodes, stating what it will record and reporting what it recorded, while the terminal crossings, attest, sync --accept-fast-forward, and the merge, stay human-typed even on instruction, the agent handing over the exact command. Tiller records workflow evidence locally; neither CI nor merge eligibility may depend on Tiller or its attestation records. The onboarding that sets a machine’s identity and the managed-project independence contract are in distribution.md.
Proposal correction commands
Section titled “Proposal correction commands”The direct correction grammar is explicit and copyable:
tiller rename <epic|feature|task|bug|chore|tweak> <node> --title "<display title>"tiller rename <epic|feature|task|bug|chore|tweak> <node> --key <canonical-key>tiller unlink <epic|feature|task|bug|chore|tweak> <node> --depends-on <target> [--depends-on <target>]...tiller link <bug|chore|tweak> <node> --attach <target>tiller unlink <bug|chore|tweak> <node> --detachtiller remove <epic|feature|task|bug|chore|tweak> <node> --reason "<why this proposal is removed>"rename requires exactly one of --title and --key. A title rename changes only the YAML title; it is legal at every lifecycle state and never edits the node’s Markdown. A key rename is narrower because it moves directories. An epic must be draft, with only draft, task-free child features. A feature must be draft and task-free; it may reparent only when both source and destination epics are draft. A task must be proposed and remain under its current feature. A bug, chore, or tweak must be proposed and stays on its own shelf, and its --key is the local segment alone, the way add bug takes it, since the canonical key’s shelf prefix is not the caller’s to change. Every key move preserves the node id, id-backed references, regular-file permission bits, and the permission bits of moved directories that the destination transaction creates. A feature reparent also removes that id from the source epic’s soft order. Equal values are exit-0 no-ops with changed: false and no event.
Rename grammar failures are misuse, exit 2. Stable reasons include missing_rename_axis, multiple_rename_axes, invalid_title, invalid_key, rename_destination_exists, task_reparent_not_supported, and key_rename_locked. The last reason is deliberately misuse rather than a state-transition block: a caller asked the proposal-move command to rewrite accepted or historical structure. The remedy is the correction route for that change class, not a retry of the same rename.
unlink uses link --depends-on’s resolution and scope rules. Epic and feature edges may target an epic or feature; a task edge may target only a sibling task; a bug’s, chore’s, or tweak’s edge may target only another bug, chore, or tweak, named by id or shelf key, never a tree node. Every requested target resolves before a write, and surviving dependencies retain their stored order. Missing --depends-on, a noun or scope mismatch, or an unknown target is misuse, exit 2. A requested edge already absent is a successful no-op even after work has started.
Questions. cancel, link, and unlink also take the question noun; none is an amendment operation. tiller cancel question <Q-id> --reason "<why>" moves an open question to cancelled, records the reason as cancel_reason, and releases every hold the question had, exactly as an answer does, without writing an answer or a revision. It prints one line (question <Q-id> ("<title>"): "<statement>" is now cancelled.), then a line for each feature whose assumption_basis names the question (<feature> assumed <Q-id>; its plan stands as built.), then the cancel-question readout (headline Question cancelled), which reports the nodes the cancel cleared of every open question and the open questions remaining. Cancelling an already cancelled question is a no-change success (exit 0, readout still printed); cancelling an answered or resolved question refuses question_closed (exit 1) and writes nothing, since the recorded answer stands. --resolution and --duplicate-of apply to no question, so they are bad_args. The JSON payload carries cancel_reason, assumed_by, and, on add question re-asking a cancelled statement, reasks (all omitted when empty).
tiller link question <Q-id> --blocks <target> unions epic, feature, or task targets (id or key, repeatable) into an open question’s blocks, in order; tiller unlink question <Q-id> --blocks <target> removes them and keeps the order of the rest, and removing every target is legal. Every target resolves before anything is written. A target already present on link, or already absent on unlink, is a no-change success. --blocks is the only axis a question takes, and it belongs to no other noun (bad_args otherwise). An answered or cancelled question refuses question_closed. The success payload is {noun, id, blocks, changed}, with blocks always present. Cancel and unlink are the human’s moves, and an agent runs them only on the human’s instruction; an agent may link to widen. The events are cancel.question, link.question, and unlink.question; a no-change call writes none.
The attachment arm is separate and unguarded by state. link --attach <target> records a bug’s, chore’s, or tweak’s attached_to, one epic, feature, or task naming where the work was found, and unlink --detach clears it; both are legal at any state, including after done, because attachment is provenance rather than a lifecycle fact, and a repeated attachment or an already-absent one is a no-op. Attachment is single-valued, so a second --attach replaces the first. A blank or whitespace-only --attach is a usage error (exit 2, reason attach_target), alone or beside --depends-on, never read as a detach or as no attachment requested; unlink --detach is the only way to clear one. add bug|chore|tweak applies the same rule: an explicitly passed blank --attach refuses attach_target with nothing written, while omitting the flag means no attachment. Attachment may share a call with dependencies: link --depends-on <target> --attach <target> and unlink --depends-on <target> --detach validate every requested fact, including the dependency lock, before one commit records both, so a refusal on either axis writes neither field and no event. The success payload reports the resulting depends_on and attached_to for each requested axis; unlink reports the surviving depends_on and the removed ids even when it only detached.
A real dependency change is direct only while the affected work is still open to correction. A task may be proposed or ready. A feature may be draft, specced, or ready and must have no branch. An epic may be draft or ready, must have no branch, and must have no descendant feature branch or descendant work past the unstarted states. Both the dependency arm of link and unlink enforce this boundary. Once it closes, the command returns exit 1 with reason dependency_change_locked, the current noun, id, key, status, and a detail sentence naming the edge rule, never an amendment. One arm stays open: a started task may add (never remove) an edge whose target task is done (status exactly done, not cancelled). The locked payload never advertises return: every changed operation that reaches this guard is already outside the direct pre-work return window.
Beyond the done-target arm above, there is no active-edge escape hatch. Amendment has no link, unlink, or order operation; revise re-pins accepted prose without changing dependencies or unlocking the target. Reviewed move-feature relocates only eligible unstarted feature subtrees while preserving effective prerequisites and question holds. In-place hard-edge edits on unchanged started work and active reparenting are unsupported. Genuine replacement can use cancellation/supersession plus fresh eligible work with its own prerequisites, never a manufactured replacement solely to evade the lock. A fresh proposed task under an active feature may still receive its own legal sibling prerequisites.
remove is not a lifecycle transition or an alias for cancel. It accepts only a proposed task, a task-free draft feature, a draft epic whose child features are all draft and task-free, or a proposed bug, chore, or tweak. Its reference cleanup crosses the shelf boundary in both directions: removing a tree node clears any attached_to or resolved_by that named it, and removing a shelf node clears any depends_on or duplicate_of that named it, each cleared node named in the answer. A blank reason is misuse, exit 2, with remove_reason_required. Unsafe deletion is blocked, exit 1; every blocked JSON answer carries reason, detail, and a generated amendment remedy, but its optional identity and status fields depend on the reason:
| Reason | Payload identity and detail |
|---|---|
remove_state_locked |
noun, id, key, and status identify the removal target. detail names the violated proposal rule, such as a task not proposed, a feature not draft or not task-free, or an epic/child subtree outside the draft task-free window. Only a task whose target status is ready also receives return_remedy. |
proposed_amendment_reference |
noun: amendment, the amendment id and key, and status: proposed identify the open amendment containing the reference, not the proposal the user tried to remove. detail names the operation reference. Human prose tells the caller to edit or withdraw that amendment reference first. |
remove_lineage_reference |
noun, id, and key identify the feature or task whose actual supersedes or superseded_by field carries the surviving lineage edge; status is omitted. detail says that a surviving supersession edge references a node being removed. The payload does not rewrite or flatten the lineage fields. |
non_regular_remove_entry |
Node identity and status are omitted. detail carries the offending path and says the entry is not regular under removal’s regular-file-or-directory rule; there are no separate structured path or kind fields in this payload. |
Concurrent or on-disk changes continue to use the environment reason mutation_stale, exit 3.
Rename, link’s dependency arm, unlink, and remove each resolve and record the ordinary acting contributor. They are not themselves human-only gates: explicit human correction feedback authorizes the agent to run the mapped direct command. Any later approve, approve --refresh-basis, reapproval after return, or approve amendment keeps the actor and delegation rules of that gate.
Human output distinguishes a change from a no-op and renders key plus stable id. JSON title-rename output carries noun, id, key, old_title, title, and changed; key-rename output adds axis: "key", old_key, the new key, moved paths, and changed. Unlink returns the complete surviving depends_on, the exact removed ids, and changed. Remove returns noun, id, key, reason, every removed id, and every changed file, then tells the agent to rerun the current proposal readout. A blocked answer prints its copyable remedy rather than requiring a caller to infer one from prose.
Design baseline commands
Section titled “Design baseline commands”These commands mutate only .tiller/design/baseline.yml and events, never the human-owned Markdown. The artifact schema owns the stored fields and inventory policy.
tiller link design <doc> --describes <path> [--describes <path>]... [--from <source-path>]...tiller link design <doc> --from <source-path> [--from <source-path>]...tiller unlink design <doc> --describes <path> [--describes <path>]... [--from <source-path>]...tiller unlink design <doc> --from <source-path> [--from <source-path>]...tiller approve design <doc> [<doc>]...<doc> is a canonical Markdown path relative to .tiller/design/, such as as-is/editor.md. Escaping paths, symlink components, special files, and the reserved baseline.yml target refuse. --describes and --from are repeatable literal paths, not comma-separated lists. Link and unlink require at least one of these axes. Flags for other nouns are misuse here, including explicitly supplied empty or false values: dependency, surface, attachment, task-selection, basis-refresh, assumption-review, and review-grain flags do not apply. Approval requires named docs and has no --all.
Link unions described paths in existing order. Each must exist and equal or lie beneath a declared valid code root; a path that merely contains or overlaps a root refuses. Reference roots are allowed, including declared external roots. The schema’s anchored path policy prevents escaping the selected root; canonical declaration aliases do not authorize additional symlink traversal. A real describes change clears confirmation; linking the same paths again changes nothing and preserves it.
--from resolves a recoverable regular-file blob at this project’s explicitly observed HEAD. Missing or unborn project Git and a source without a HEAD blob refuse; untracked source hashes and external-repository blobs are not provenance. A working-tree retirement is allowed when HEAD still holds the file. Commit exact uncommitted inputs before translation, then record provenance before retiring them. A from-only request needs an existing describes registration. Provenance upserts by source path: same blob is a no-op, a new HEAD blob replaces the prior ref for that path, and other refs stay. A provenance-only edit does not clear code confirmation.
Unlink subtracts paths and keeps survivor order. It can repair entries after doc, source, or described code paths vanish; --from removal needs no Git lookup. Removing the final describes path removes the ledger entry, not its prose. Describes-only link and unlink do not require Git.
Approval validates every named registration, existing regular Markdown doc, and described path before writing. It records the actor, one current project HEAD and one clock instant for the batch, and fingerprints current working-tree code under the schema’s inventory policy. It never substitutes project.adopted or an external HEAD. Dirty, added, and removed code is observed; recording HEAD does not assert committed or clean code. An absent or unborn project HEAD refuses the whole approval.
All three verbs resolve the contributor using the normal precedence against a fresh manifest. They guard fresh ledger and manifest snapshots and commit their planned state and per-changed-doc events in one generation-checked source transaction. A failure validating any approval target writes none of the batch; a stale generation or source failure uses the existing mutation refusal and rollback rules. Safe project-local prose snapshots can be read guards, but this is not a lock on arbitrary editors, external trees, or Git history.
Results use the normal envelope and actor, path identities, and actual changed state, without invented work-node ids. Approval distinguishes newly recorded confirmations from unchanged docs; unchanged contributor, project HEAD, and fingerprint facts can be a no-op despite a fresh observation time. No-op docs write no event. The approve readout reports these facts, not a move to ready; a later readout failure remains nonfatal after committed source.
Approval is the human’s decision. An agent may record it only on an explicit instruction naming the docs. One confirmed doc clears the existing-code startup gate; remaining unconfirmed docs are not a global blocker.
Output and exit codes
Section titled “Output and exit codes”This is the contract that makes the surface drivable from an agent loop, and none of it depends on the grammar.
Output is human-readable by default and machine-readable under --json, which every read and mutate command accepts. The human-readable side names every node by the reference convention above, the key path with the id in parentheses, while --json output is unchanged by it. The keys are stable. A mutation prints the resulting node state as JSON, so the agent reads the new state back without a second call.
A confirmation names what was recorded. Task start reports its recorded branch and base_branch; parent activation and applicable aggregate initialization commit in that same guarded transaction. A green check says state is now check.
Exit codes are a small, meaningful scheme, so the loop branches on the code without parsing prose:
| Code | Meaning |
|---|---|
0 |
Success. |
1 |
Blocked. A gate is red, or a transition is not legal from the current state. An expected outcome the agent reasons about. |
2 |
Misuse. Bad arguments, an unknown node, a noun that mismatches the id. |
3 |
Environment. Not a Tiller repo, git unavailable, a declared path missing, or a recoverable persistence condition. Stable persistence reasons include mutation_locked, mutation_stale, mutation_recovery_needed, and evidence_unwritable. |
The split between 1 and the rest is the point. A 1 is a normal business answer (“not ready yet”); a 2 or 3 is something to fix before retrying.
Two more guarantees serve the agent. Mutations are idempotent: a transition whose target equals the current state is a no-op success, so retries are safe. And transitions are validated with a recoverable error: an illegal move is refused with the current state and the legal next moves named, so a stuck agent can correct itself without a human.
The breadcrumbs those refusals carry, and help text generally, name only in-project destinations or tiller commands, never a doc of the Tiller source repo: a client project does not carry those docs, and the project’s resolved binary may be a different version than a local source checkout even where they happen to exist.
The read side of that recovery is tiller status <noun> <node>, the oracle: it reports the node’s state, its recorded gate results, and the transitions legal from here, with nothing executed. It is cheaper than running a gate and more useful to an agent than a static --help page, because it is lifecycle-aware. The proposed --dry-run write-side preview, showing file changes without writing them, is not built.
The explicitly requested design-brief skill uses this read side without entering a mutation loop. A failed read permits safe read retries and reporting unavailable inputs, not running refresh, gates, source repair, or lifecycle commands to make an export succeed. The skill contract owns this narrow recovery exception.
tiller queue [--kind <queue-kind>] [--noun <noun>] is the read-only enumeration behind the
readout’s attention summaries. It applies its optional filters to the current entries from the
same Queue projection the console consumes, preserving Queue order and eligibility exactly. It
does not reconstruct eligibility from lifecycle status and never mutates the tree.
The accepted queue kinds, in the exact order the CLI and Queue projection use, are:
| Kind | Noun rows |
|---|---|
approve_amendment |
amendment |
approve_epic |
epic |
approve_feature |
feature |
approve_tasks |
feature |
approve_bug |
bug |
approve_chore |
chore |
answer_question |
question |
carrier_cancelled |
bug, chore |
attest_gate |
task, bug, chore |
attest_criterion |
feature, task |
feature_check |
feature |
workflow_closeout |
feature, epic |
merge_epic |
epic |
merge_feature |
feature |
The accepted noun filters, in code order, are amendment, epic, feature, task, bug, chore, and question. Shelf approvals are ordinary Queue entries. carrier_cancelled is derived by gen.WorkActivity; its copied command is tiller resolve <noun> <node> --clear, and the human may instead re-point the node with tiller resolve <noun> <node> --by <carrier>. There is no merge_bug or merge_chore: an uncarried shelf node in in_review is already a ranked human merge wait.
Queue includes shared agent closeout as well as human decisions. workflow_closeout represents assembly, checks, rubrics, acceptance, submit, or sync once per applicable feature/epic through the shared workflow classifier; Actor/Phase identify human versus agent work, never an owner claim. Final task-grain feature checks use feature_check; submitted aggregate reviews use merge_feature/merge_epic. Participating feature asks are deduplicated beneath an epic closeout. Operative questions, bases, pauses, and retired ancestors suppress commands known to refuse; status still reports their conditions. An approve_feature row is hidden while an open question holds the plan, and an approve_tasks row only when every proposed task on the feature is held; the question’s own answer_question row stays, so routing names the answer rather than the approval. Approval asks remain individual, while eligible unclaimed task discovery stays a ranked next and pool concern.
Node status carries question relationships in both useful directions. Question status prints its
durable blocks: targets in recorded order even after answer. Epic, feature, and task status print
only operative blocked by questions: rows from the generated open-question blocker projection.
Feature status honors that feature’s assumption_basis. Task status deliberately does not inherit
its parent feature’s assumptions, so a feature assumption can suppress the feature’s reverse row
without hiding an operative hold at task grain.
The JSON envelope. Every --json output, success or failure, is the same stable envelope, so an agent or the console parses one outer shape and branches on it rather than learning a layout per command. There are three top-level keys: ok, command, and exactly one of data or error.
A success carries ok: true and a data block with the command’s payload:
{ "ok": true, "command": "status", "data": { } }A blocked answer (exit 1: a red gate, an illegal transition, an unready project) carries ok: false and a data block, never an error block. The negative answer stays structured: a transition refusal puts the current_state and the legal_next moves in data, so the agent corrects itself without a human. This is what keeps “not ready yet” distinct from “the tool broke.”
A tool failure (exit 2 misuse or 3 environment) carries ok: false and an error block:
{ "ok": false, "command": "show", "error": { "code": 2, "reason": "noun_mismatch", "message": "F-79dh3q is a feature, not an epic", "candidates": [] }}code is the process exit code. reason is a stable discriminator the loop branches on, such as not_found, noun_mismatch, ambiguous_key, not_a_repo, git_unavailable, eval_contract_invalid (a malformed acceptance contract), or worktree_unreadable (exit 3: a changed working-tree path that exists could not be read while fingerprinting it); a new failure mode is a new reason, not a new outer shape.
Every project command finds its project by walking up from the working directory to the nearest ancestor holding .tiller/tiller.yml. Only observed absence keeps walking: no .tiller entry, a regular file named .tiller, or a .tiller directory without tiller.yml. A .tiller that cannot be inspected, such as one without read or search permission, refuses with project_marker_unreadable (exit 3) and writes nothing, rather than redirecting the command to an outer project. A symlinked marker selects its directory and the load refuses there. message is the human line. candidates appears only where it helps, as on an ambiguous key, carrying the node ids to disambiguate by. The envelope is additive: retain these fields and exactly one outer data or error block.
Maintenance adds optional typed error.partial_result for known results on exit 2 or 3; unrelated errors omit it. Success and exit-1 business refusals carry the same typed maintenance result in data, including complete and phase failure details. A nonzero response can establish that binary replacement or source migration completed. A parent must decode valid nonzero child envelopes, including error.partial_result, and validate protocol, exit/envelope consistency, executable identity, and project identity before composing its result. It must not emit both outer blocks, discard known completed stages, or interpret lost/malformed output as proof of no project writes.
A batch invocation (approve over two or more nodes, or approve task --feature) keeps the same envelope and carries two fields inside data: results, an array with one entry per node in argument order, each entry holding the same fields the single-node data block holds plus a per-node ok; and summary, the outcome counts {total, changed, unchanged, blocked}. The top-level ok is true only when no node was blocked. A single-node invocation is unchanged: its data stays the single-node block exactly, so existing callers never see a shape shift.
The agent readout
Section titled “The agent readout”Every lifecycle readout seam boundary and human gate ends in one markdown block, rendered by Tiller and reproduced verbatim by whichever agent is driving, so a developer’s read of a decompose or an approve does not depend on which harness or model ran it. The explicitly requested design-brief export and code-review review are not lifecycle seams or gates; each ends with its own deliverable instead, without a terminal readout. The block is emitted text rather than agent-composed prose so that every harness reports the same account of recorded work, evidence, and the next action.
tiller readout <seam> prints the block for a seam boundary: one of orient, derive-baseline, decompose, specify, execute, or amend. It takes the seam name, not a node, so it belongs with the no-node commands above. An unknown seam is misuse, exit 2, naming the accepted seams. The amend seam surveys proposed amendments, staged operations, and their live preconditions. mend-drift has no seam of its own here because its edge of done is a drafted fix. A readout reports state rather than gating on it. It is the final act of the seam’s choreography, and the agent reproduces its block verbatim.
There is no design-brief readout seam or export command. tiller guide design-brief delivers instructions, not a generated brief; the agent synthesizes the document from existing reads without changing Tiller state. Likewise there is no code-review readout seam or review command; tiller guide code-review delivers instructions and the agent writes the review file itself.
The derive-baseline block exposes every recorded source-revision-to-doc mapping, grouping by source path and blob identity, plus every ledger doc’s described paths and confirmation facts. This baseline section is not subject to the ordinary produced-entity cap. Linkage and confirmation counts are recorded facts, not proof that a translation preserved every source section. Open questions counts current open questions, not questions opened by this run. The agent’s separate run notes name dropped, merged, or unplaceable material and questions actually opened; the ledger cannot derive a dropped count, even zero.
Baseline full and correction blocks keep this complete review map. Design correction events name document paths and their describes, provenance, or confirmation changes, never dependency edges or lifecycle moves. When docs await confirmation, the completed baseline readout has one HUMAN next action to review and approve named docs, not another derive instruction. Guide and orient explain pending docs without asking the agent to regenerate them. Pending baseline review has an explicit presentation even when Queue is empty; no fake approve_design queue kind or console write action is added.
Missing or unsafe registered prose has a separate repair action, even if every doc was previously confirmed. Full baseline blocks identify the affected docs; ordinary readouts retain a brief repair summary. Historical confirmation stays recorded and does not reopen the startup gate, but the readout cannot also claim all-clear.
The full execute block uses the neutral heading Execution status. Its project-wide population is tasks and uncarried bugs, chores, and tweaks in in_progress, check, or in_review, regardless of owner. Ready, proposed, done, cancelled, and live carried work is excluded. It distinguishes incomplete checks, recorded green checks awaiting submission, and current submissions. Gates green with evidence requires a nonempty applicable required-gate set with passing, non-stale results and evidence. Missing, failed, stale, or unevidenced results do not verify a node.
Current submissions recorded uses the same denominator and counts only included nodes in in_review with the appropriate recorded PR or branch-and-commit review ref. An old PR retained after resume does not count in in_progress or check. A coarse task ref is labeled as riding the resolved feature or epic review; it does not establish that a coarse PR exists. Checked entities lead the capped list, followed by in-progress and in-review entities, retaining existing order within each group. Counts cover the full population, including the accurately reported withheld entities. A separate resolved-review summary retains feature acceptance and feature or epic formal-submission status even when riding tasks exceed the list cap. No active work produces a neutral empty report, not a successful verification claim. Next is the ranked list for the acting contributor. JSON readout text and human rendering remain equivalent.
tiller readout <seam> --since <sha> prints the correction block instead: the lighter block a correction or other small action inside a gate closes its reply with, while a seam exit keeps the full one. <sha> is the commit the previous block reported, its committed at clause. The correction block keeps the headline in the same voice (Decomposition corrected for decompose, Specs corrected for specify), with the commit named under the same clean-tree rule and (since <sha>) after it; it replaces Produced, Verification, and Needs attention with one **Changed** section; and it keeps the queue line and the ranked **Next:** list exactly as the full block renders them, from the same code. Changed comes from the event stream, not from the agent. The event files git names as added under .tiller/events between <sha> and HEAD are exactly the mutations the correction recorded, and each renders as one line in stream order, verb first, the node named by key path with its id: added <ref>, added question <Q-id> ("<title>"); blocks <ref>, renamed <ref> title from "<old>" to "<new>" (or key from <old> to <new>), removed <ref> naming the node from the event’s own record since the tree no longer holds it (and N nodes beneath it for a cascade), linked N edges (M cross-epic) on <ref> and unlinked likewise, the cross-epic count present only when every end still resolves as a feature, ordered N features under <ref> (tasks, bugs, chores likewise), and anything without a sentence of its own as its type and targets, so no recorded event is dropped. With nothing recorded since the sha the section says nothing recorded since <sha>. It reads committed history, so the correction is committed first, as the skills already say. A <sha> git cannot resolve, or one that is not an ancestor of HEAD, is a misuse, exit 2, naming the flag and what it expects; an unknown seam keeps its own exit 2. --json carries the block in the same readout field, byte-identical to the piped output, plus a since field naming the sha as given.
Five human gates print the block themselves, after their existing confirmation and with nothing for the agent to remember to ask for: approve, answer question, cancel question, attest, and sync. Starting a task, bug, or chore is the owner’s own move, not a human gate (see the actor model above), and carries no block. A blocked gate (exit 1) carries no block at all: the transition did not happen, so there is nothing new to report, and the existing confirmation lines, which on a batch approve are the per-node record of what transitioned and what was already at the target, are unchanged and printed first; the block never replaces them.
Completing commands print the ranked list; beginning commands do not. The five gates above keep their full block, whose Next: is the list. submit (task, feature, epic, bug, chore, and tweak) and a passing check gain a short block after their own confirmation: a blank line, **Up next:**, and the ranked lines for the acting contributor, with no heading, headline, verification, or needs-attention section. A blocked or failing check (exit 1) prints no Up next block. start, claim, and resume print nothing new. tiller readout <seam>, its --since form, and bare tiller guide always carry the list. When the list cannot be computed after the mutation landed, the command still succeeds and simply omits the block.
The block itself is markdown, with no outer fence. Produced entities remain a capped list. Pending
work is one **Needs attention** section whose contiguous bullets contain only an exact Queue count
and an inline tiller queue --kind ... drill-down, with --noun where one Queue kind spans feature
and task rows. Every Queue kind has a bucket, including the aggregate merge_epic and merge_feature review asks, each counted on its own, so a Queue holding only a merge ask still renders the section. It contains no concrete item rows and no mutation commands. An empty Queue renders
Nothing needs you right now. only when neither pending baseline review nor registered-prose repair needs presentation. The public readout remains one rendered string, including in the
additive JSON readout field.
Every full and correction block ends on **Next:** on its own line followed by the ranked next list for the reader, the same lines tiller next prints without its heading: at most five numbered rows, a count line naming tiller next --all when more moves are legal, and any hint. A caught-up checkout still gets the label, followed by Caught up: nothing is legal to do right now. The list is the block’s only next-step guidance. Each waiting feature plan and epic is its own row rather than a merged count, there are no separate order, continuity, or execution notes beside it, and the orient block carries no route verification line because Next: already gives that answer.
--json carries the same text additively: a readout field alongside every existing field, on the five gates and on readout itself, with no existing field changing shape. submit and a passing check add a next field beside their existing data instead, the shortlisted payload tiller next --json returns. The block itself, not the surrounding output, is byte-identical between the two: on readout, where the block is the whole of the human output, terminal and JSON match exactly; on a gate, the human side prints the existing confirmation first and the block after it, so the JSON readout field is that same block alone, without the confirmation line in front of it. Either way, a harness calling with --json reads exactly the block the human output carries. One presentation caveat: when stdout is a terminal, the human side renders the block’s markdown for a person (bold instead of the ** markers, fenced commands as indented lines, the Needs attention label in the loud ink, suppressed under NO_COLOR), while piped human output and the JSON field always carry the raw markdown source an agent reproduces verbatim. The block is one text with two renderings, and the wire format is the markdown.
Working-tree posture
Section titled “Working-tree posture”Tiller mutations edit files in the working tree and do not commit by default. The contributor is responsible for making state durable; an authorized execute run performs that work through ordinary Git commands. Execute commits actual check and evidence deltas even when pending required acceptance makes check exit 1. It publishes implementation and recorded checks before the human review handoff, then commits, pushes, and reads back formal-submission state after acceptance. Task-grain post-merge closeout commits and publishes check and sync state on the integration branch, not the merged review branch, without creating a state-only PR. The generated orientation and skill protocol carry the choreography. The planned --commit and tiller commit state conveniences remain unbuilt; neither would replace publication.
Writes are atomic per file by writing to a temporary file and then renaming it. The folder-per-epic-and-feature layout reduces merge conflicts between contributors on disjoint subtrees, but it is not the runtime concurrency strategy. Every source mutation commits its complete state-file and event-file batch under the repository-level Tiller mutation lock in .tiller/.cache/, after checking both the generation captured when the tree loaded and the content snapshots captured when the mutation was planned.
The persistent .tiller/.cache/generation token makes readers and writers detect overlap beyond the lifetime of the lock file. Readers such as serve, status, and sync read the generation before loading, read it again after loading, and refuse the snapshot if the token changed or if a lock or recovery journal is present. A successful source commit advances the generation. Generation-checked derived writes, including orientation and harness wiring, use the same lock, journal, generation precondition, and destination snapshots but do not advance the generation because they do not change source state.
Multi-file commits journal their intended file set and preimages in .tiller/.cache/journal.yml, apply the complete batch, and clear the journal durably. A failed write rolls the batch back; a rollback or durable journal-clear failure retains the journal and blocks later reads and writes with mutation_recovery_needed. Long-running gates execute outside the mutation lock. Their state and event batch commits only if the input generation and destination snapshots are still current, otherwise the command refuses with mutation_stale. Reads take the mutation generation, and mutations take their planning snapshots, without the lock, so a read that races another writer’s replacement of .tiller/.cache/generation or of a file being planned over also refuses with mutation_stale, and a retry is safe. A command gate result is not recorded unless its command-output evidence file was written successfully; evidence persistence failure returns evidence_unwritable and leaves the node’s prior gate state unchanged.
Bootstrapping a project
Section titled “Bootstrapping a project”tiller init --name "Acme Platform" --key acme --kind greenfieldtiller add contributor robin --name "Robin Hale"tiller join --as robintiller check readiness
# or bring design inputs in at scaffold time, from a folder living elsewhere:tiller init --name "Acme Platform" --key acme --kind greenfield \ --design ~/dev/acme-discovery/docstiller add contributor robin --name "Robin Hale"tiller join --as robintiller init scaffolds .tiller/: a commented, human-owned tiller.yml, the skeleton directories, and .tiller/orientation.md. It seeds a missing repo-root .gitignore with root-anchored generated, identity, and local-settings patterns and leaves an existing file untouched; join upserts all Tiller-owned groups, including skill shims. Preserve /artifacts/tiller/, /.tiller/.cache/, /.bin/, /.tiller/identity.yml, /.tiller/local.yml, and /.claude/skills/tiller-*/, without broadening evidence ignores to all artifacts. Existing covering patterns stay unchanged. --capture-usage also writes usage: { capture: true } into the new manifest, turning on agent usage capture; without it init writes nothing about usage and capture stays off. Like the rest of the manifest, the flag applies only to a first scaffold.
First scaffold uses the requested kind. Greenfield starts with inputs.code: [] and no adopted anchor. Brownfield and legacy-migration start with inputs.code: [{ root: ., mode: surface }]; legacy also receives a commented reference example because init cannot guess the old system’s location. When HEAD can be observed, these two kinds record optional project.adopted with git:<short-sha> and a UTC RFC3339 observation time. This is this repository’s HEAD, not a clean-worktree assertion or the revision of an external reference system. Non-Git and unborn repositories still initialize, with no anchor. Reruns preserve the human manifest, including kind, roots, prose, and any original or absent anchor, and identify the project from that manifest rather than passed flags. join and migrate do not backfill it either.
The roster starts empty (contributors: []). Human Next guidance names design preparation, contributor registration with tiller add contributor <id> --name "<name>", then tiller join --as <id>. Greenfield continues to readiness and tiller guide decompose. Brownfield and legacy-migration use tiller guide derive-baseline before readiness, then plain tiller guide, which names decompose only when a to-be design doc exists; an import does not confirm a baseline. Move in-repo docs into .tiller/design/ when ownership moves to Tiller, or write a pointer page when another consumer needs the source location. Legacy guidance separately requires an existing mode: reference root in .tiller/tiller.yml, even with --design. Gate configuration remains outside readiness: greenfield wires build and tests once a stack exists, while existing-code projects can use current stack commands. Evals is already active at feature grain. Init never registers an owner, joins, derives docs, approves, or decomposes.
tiller join --as <id> selects identity and wires this machine’s harness; its full behavior, the Claude Code adapter included, is in distribution.md. Identity has two halves, both kept in the repo. The roster is contributors: in tiller.yml: committed and shared, the set of humans who can hold work, maintained by tiller add contributor <id> --name "<name>" and tiller remove contributor <id>, deliberate human-gated acts. Both replace only the contributors entry, whatever its YAML layout (indented or indentless block sequence, blank or comment lines between entries, flow sequence), leave every other manifest byte intact, and refuse without writing state or events when the rewritten manifest would not parse to exactly the requested roster with all other values unchanged. Who you are on this checkout is .tiller/identity.yml (contributor: <id>): gitignored, per checkout, and never committed, written by join; a fresh clone or Git worktree has none, so every checkout runs its own join, and the contributor_unset refusal says so. --as is the owner id every action attributes to, resolved against the roster: an id absent from it is refused (contributor_unknown) naming the roster and the add contributor path, never registered. join carries no --name; a selected owner’s display name already lives on the roster. --contributor and TILLER_CONTRIBUTOR still override the identity file for CI and one-off attribution, so a corrupt file is irrelevant whenever either is set. A present-but-unreadable .tiller/identity.yml refuses too, distinct from an absent one: identity_unreadable, naming the file and pointing at join --as <id> to rewrite it, rather than the contributor_unset refusal an absent file gets. A file that still holds a legacy owner: key refuses the same way and names tiller upgrade --project-only, which renames the key in place; a bare join selects that ID and rewrites the file. A read-only command degrades to no acting contributor on the same corrupt file instead of refusing.
--design <dir> copies the source directory’s contents recursively and verbatim into .tiller/design/ on the first scaffold, preserving subfolders and recording absolute source path and import time in inputs.imported_from. External sources are read, never changed or deleted. Use a clean folder of design inputs because init does not decide which files matter. Missing, non-directory, and empty sources refuse before scaffold writes, as does a source that overlaps .tiller/design/: the checkout itself, any ancestor of it, the same directory reached through a symlinked ancestor or another spelling, or a folder inside .tiller/design/. Overlap is compared by filesystem identity, and init refuses it with invalid_design rather than silently importing a filtered subset. Existing malformed-source validation, overlap included, also applies on reruns. The copy operation init shares with tiller intake refuses on its own to walk a directory source that contains or sits inside its destination. A single-file source cannot recurse, so tiller intake .tiller/feedback/archive/review.md lands the document at .tiller/feedback/review.md like any other. A rerun over a checkout whose orientation a newer binary generated refuses with orientation_newer (exit 3) and changes nothing; same- and older-generation reruns refresh orientation. A valid rerun over an existing manifest succeeds, imports nothing, and prints an explicit skipped-import notice in human output. It does not change the JSON envelope, imported content, or provenance. Later docs belong under .tiller/design/, using the move-or-pointer rule for in-repo sources, not another init import.
A provenance-recording re-import that preserves a human-edited manifest remains unbuilt. Copy or move later design inputs explicitly rather than expecting an init rerun to import them.
On a fresh scaffold, a root-level source entry named baseline.yml, matched case-insensitively for portability, refuses before any writes. Import a prose-only directory instead: the ledger is machine state, and another project’s confirmation must not release this project’s gate. This check does not change an existing project’s skipped-import rerun.
An import clears the empty-design gap, not all readiness. A bare init succeeds while missing_contributors and missing_design remain; legacy-migration also needs a reference declaration. Contributor registration, valid existing code roots, and any required reference root still apply with --design.
tiller check readiness is a mechanical gate available before any nodes exist. It checks non-empty design/, project kind, contributors, and existence of every declared code root. Every root must explicitly carry surface or reference; empty or unknown modes produce invalid_code_root_mode, naming the root, value, manifest, and legal values. Legacy-migration without an explicitly tagged reference root produces no_reference_root; a declared but missing path still produces the existing missing-code-root gap. Greenfield and brownfield do not require a reference root. Independent gaps remain visible together, and readiness failure exits 1. Fix invalid modes in the human-owned manifest; no migration rewrites them. requires_tiller remains advisory. On a ready project the human output closes with Next (tiller guide <skill>): <reason>, the same suggestion bare tiller guide prints from the ranked list, never a fixed seam.
Readiness reports no_confirmed_baseline on exactly brownfield and legacy-migration when the loaded ledger has no confirmed doc. missing_design_doc applies to each missing ledger prose file in every kind, regardless of confirmation. A baseline.yml alone does not satisfy non-empty design; the existing check is not expanded into recursive prose validation. Other gaps remain independent. Readiness neither fingerprints described paths nor detects their drift, judges document richness, or grants human confirmation. The agent records unresolved questions rather than inventing answers. One confirmed doc clears only the confirmation gap; ordinary routes resume even with other unconfirmed docs.
Registered-prose presence checks stay anchored under .tiller/design/ and refuse intermediate as well as final symlinks. Unsafe or unreadable prose produces the same repair gap as missing prose; another directory’s file cannot satisfy it.
Readiness compares the on-disk .tiller/orientation.md stamp with the running binary and names both generations on mismatch. Behind the binary, the remedy is tiller upgrade --project-only; ahead of it, tiller upgrade. Refresh refuses higher on-disk generations rather than overwriting them. A missing or unrecognized stamp retains the existing readiness carve-out; maintenance can still repair missing artifacts.
Recording surfaces and dependencies
Section titled “Recording surfaces and dependencies”tiller link <epic|feature> <node> --surface <path> unions cleaned paths, relative to the Tiller project root (the project directory in a nested project, not the repository root), into surface.paths. Repeat --surface for several paths or combine it with --depends-on. A surface names existing code, including a greenfield project’s own shipped code. modify may omit it; tiller spec feature requires a non-empty surface for replace and migrate, regardless of project kind.
Every requested path is validated before writing, even if already recorded. Files and directories must exist with no final or intermediate symlink. Reference roots are compared by path segments in the repository’s absolute coordinate system, checking both declared and resolved identities. A candidate cannot equal or lie beneath a reference root, or be a directory containing one: app and . refuse if app/legacy is reference. Absolute declarations, relative normalization, reference aliases, and repository-parent reference roots receive the same overlap checks. legacy-old is not beneath legacy, and a surface root cannot override a reference root. Existing untracked paths outside all declared roots remain legal when disjoint from reference code; recording requires neither Git tracking nor a HEAD fingerprint. Persisted paths remain cleaned and project-relative.
Containment also compares filesystem identities along both ancestor chains. Changing path casing on a case-insensitive filesystem, or using a hard link to a reference file, cannot bypass the reference boundary. These checks inspect metadata only, without enumerating directories or reading code contents.
| Reason | Exit | Refusal and local remedy |
|---|---|---|
surface_path |
2 | Unsafe requested path, including a symlink component. Name an actual project-relative path without traversal or symlinks. |
surface_missing |
2 | Missing leaf or intermediate directory. Select an existing path, or create the intended code before recording it. |
surface_reference |
2 | Reference overlap in either direction. The refusal names the request and declared root; choose disjoint writable paths rather than a containing directory. |
invalid_code_root_mode |
3 | An empty or invalid declared mode. Correct the named entry in .tiller/tiller.yml to surface or reference. This check runs at the writer without requiring readiness first. |
surface_unavailable |
3 | Unexpected metadata inspection or reference-resolution failure, not nonexistence. Inspect the named path and cause, repair access or the reference declaration, and retry. An unresolvable reference root is not treated as writable. |
These checks protect the recorded relationship, not external filesystem writes. Unrelated old surface entries are not revalidated during an addition or retroactively removed. A later deletion remains missing_surface drift.
tiller unlink <epic|feature> <node> --surface <path> is the one route that removes recorded surface paths. Requested paths are cleaned with the same hygiene as link (surface_path for an empty, absolute, or escaping path) and matched literally against the recorded list; existence is not checked, so code the work moved or deleted can still be named. A path not recorded is a no-op success. The surface is history once its node is done (the completion record pins its fingerprints) or its epic archived, so a changing removal there refuses surface_locked (exit 2). A specified replace or migrate feature (past draft and not cancelled) cannot drop its last path, the same rule spec feature enforces; that refuses surface_required (exit 2), and the fix is to link the code’s current location first. --surface applies to epics and features only (surface_target), and cannot combine with --detach. It combines with --depends-on in one source transaction: every arm validates, the dependency lock included, before anything is written. A changed surface emits unlink.surface.<grain> with the surface before and after and the removed paths; JSON adds surface and surface_removed when the call named --surface.
This is the repair for a replace or migrate feature whose own work moves or deletes its surface: sync fingerprints every recorded path at the integration commit, so a moved path resolves to nothing and the node’s earned crossing is skipped. Sync’s skipped-node report marks such a path as a recorded surface (surface: true in JSON) and names the tiller unlink ... --surface remedy to run once the code’s current location is linked.
A combined dependency/surface request validates both arms, reads one fresh node snapshot, and commits both field unions and their events in one journaled source transaction. Validation refusal or a rolled-back source write leaves neither arm recorded. Events remain distinct: link.<grain> for changed dependencies and link.surface.<grain> for changed surfaces, one event per changed axis. Repeating an unchanged request records no events. Dependency lifecycle guards apply only to a real dependency change, so an already-present dependency does not block a legal new surface after dependency edits lock. Surface-only lifecycle behavior is unchanged, and task, bug, and chore dependency behavior is unchanged; surface flags remain epic/feature-only. This contract does not atomize unrelated attachment requests.
Dependency targets resolve all-or-nothing and store ids: epics and features may name epics or features; a task names sibling tasks; shelf dependencies stay among bugs, chores, and tweaks. An unknown target on add --depends-on refuses the add without creating a node. Forward edges therefore require a tiller link sweep after the decomposition tree exists. Dependencies guide rendering and routing, and task start enforces unfinished task and ancestor prerequisites as described under The work loop.
Decomposition
Section titled “Decomposition”Decomposition is the clearest instance of the reasoning-versus-structure split, and it reads as a dance between the agent and Tiller:
# the agent's decompose skill reads the inputs and proposes the tree,# laying every node down in draft:tiller add epic form-engine --title "Form Engine" --source design/04-form-engine.mdtiller add feature form-engine/certification-form-loading \ --change-type create --source design/04-form-engine.md
# the human reviews the draft tree in git or serve, then approves it# in a single decision:tiller approve epic form-enginetiller add epic and tiller add feature are the structural operations. Each mints a stable, type-prefixed, collision-resistant id, creates the folder and its .md plus .yml, and records the source provenance back to the input doc. Tasks use the same id rule; they are associated with their parent feature by where their folder sits, not by embedding the feature’s number in the task id. Every node lands in draft. The approval is the human’s decision.
There is no separate proposal artifact, and that is on purpose. The draft-state tree is the proposal. The git diff (or the tiller serve view) is the review surface. tiller approve epic <key> approves the decomposition as the right tree and moves the epic to ready, but it does not skip feature specification: freshly added features remain in draft until the second stage runs, tiller spec feature validates its whole output to specced, and the human approves each feature plan to ready. This keeps the review in the working tree without turning decomposition approval into approval of acceptance criteria the human has not seen. The node’s state disambiguates the gate, so no flag is needed: a draft epic can only approve forward into ready.
Parallel decomposition needs no scoping mechanism: epics are the lanes. Two engineers decompose different source slices into different epics, which the folder-per-epic layout already keeps from colliding, and the human approves each epic’s tree as it lands.
Specifying a feature
Section titled “Specifying a feature”tiller spec feature F-79dh3qThe second decompose stage runs per feature, just in time, on the developer’s go. The agent writes the specification into feature.md, the acceptance contract into feature.eval.md, and the task explosion under tasks/, each task a folder holding the task.md implementation spec, then asserts the whole with tiller spec feature. Tiller validates the full stage-2 output: feature.md and feature.eval.md exist and are more than stubs; the contract is well-formed, carrying at least one criterion, every criterion a unique id, a statement, a recognized binding type (test, command, manual, or eval), the field that binding requires (a command needs a run, a test a ref or run, an eval a rubric, and a required preserve criterion a baseline); and at least one task exists whose task.md is more than a stub. A missing or stub file is the “not written yet” blocked answer (exit 1), whose message names the unwritten files, the feature folder they live in, and tiller guide specify for the walkthrough, because spec asserts the written output and writes none of it; a file that is written but malformed (no criteria, an unknown binding, a criterion that is just prose) is a misuse (exit 2) whose message names the offending criterion or task and points at tiller guide specify for the shapes, never a silent accept. On success it moves the feature from draft to specced, and prints one Warning: line (a warnings list in the JSON envelope) for each preserve criterion the tests gate already covers. An epic has no specced state; it goes from draft to ready directly at tree approval.
The stage-2 output can change after spec, so a specced feature is validated again whenever it is asserted. Re-running tiller spec feature on a specced feature applies the same checks: a still-valid output answers “already specced (no change)”, and anything else gets the same refusal the first spec would give, with the feature left specced; the preserve warnings below print on that no-change answer too, still exit 0.
The preserve warning is non-blocking and fires only where the manifest proves the redundancy, never on a name match. It needs a required tests gate whose on list includes feature, since that is where a criterion runs. A preserve criterion, required or optional, is covered when its binding’s run equals the tests gate’s run after trimming surrounding whitespace, or when it is a test binding with no run, which resolves to the tests runner plus its ref, a subset of what the gate runs on every check. A preserve criterion with its own targeted run never warns, and with no required tests gate nothing does. The text names the criterion id, the polarity, and the reason, and the remedy depends on the case. A criterion that runs the tests gate’s own command is given a targeted check (one that exercises the at-risk behavior against its baseline, not a re-run of a spec the suite already holds), dropped, or kept with the reason in the run notes. A ref-only test binding is dropped or kept with the reason in the run notes, since rewriting it as an explicit run of the same spec is still a repeat; the warning offers no targeted check there. A kept criterion warns again on every re-spec. approve applies the same stage-2 validation but does not repeat the warnings.
Approving a feature approves the plan. tiller approve feature <node> (specced to ready) is one gate over the whole of stage 2. It first applies the same stage-2 validation as spec feature and refuses an output that no longer passes, before any write. It then refuses while an open question holds the plan: an open question naming the feature, its epic, or any of its tasks not cancelled, deduplicated by question id in first-seen order. That refusal is a blocked answer (exit 1, reason open_question_blocker, the same data block start and submit emit) that writes nothing; its human line reads Blocked: <noun> <ref> is held by <n> open question(s): <handles>; answer them with tiller answer question <Q-id> --answer "...", or cancel one that no longer needs a decision with tiller cancel question <Q-id> --reason "...", fold the answers into the plan, and retry. and never names --assume-question, since a plan gate has no assumption escape. A broken spec is reported before a held one. Then it moves the feature to ready, flips every initial proposed task to ready in the same act, and records the active-work contract, capturing the current feature.md and feature.eval.md in the feature’s accepted_against and each task’s task.md in that task’s own, all as git_blob RevisionRefs over working-tree content, so the basis is pinned the moment the human approves, before any commit. Each record also pins a commit RevisionRef at HEAD: not the state files, which are usually uncommitted at that moment, but the code baseline the plan was approved against, the anchor a drift candidate’s since-when and an amendment’s diff range read from. The task breakdown is part of what is reviewed, deliberately: fourteen tasks under one feature is evidence about the specification that a spec-only review would miss. A task added mid-flight lands proposed and is approved individually, tiller approve task <id> or the batch approve task --feature <key>, pinning its own basis the same way. That approval is the task’s own plan gate and refuses the same way while an open question names the task or an ancestor; the --feature batch reports per task, a held task blocked with its questions and the unheld ones approved, exiting 1 if any was blocked. The question refusal covers only those two plan gates: --refresh-basis, --review-grain, --assume-question, the no-change re-approve of a ready feature, approve epic (whose questions each feature’s plan gate inherits), and bug, chore, or tweak approval do not check it.
When a human edits feature.md, feature.eval.md, or an approved task’s task.md after approval, Tiller does not pretend the active work still matches the old contract. The comparison is against the current working-tree content, so the edit is detected immediately, before it is committed. status, serve, and drift report active_basis for that node, and start, check, and submit refuse it with the active_basis blocked answer (exit 1). After reviewing the edit, the human runs tiller approve feature <node> --refresh-basis, or approve task <id> --refresh-basis for a task spec, to re-record the current revisions in accepted_against without changing the lifecycle state. If the edit changes the shape of the work rather than clarifying it, the agent drafts the proper fix: a spec/eval diff for a small reconciliation, or an amendment for a structural reshape.
The work loop
Section titled “The work loop”This is the hot path, where the agent spends nearly all of its time.
tiller claim task T-k3p9w6 # reserve this task only; --from requires explicit human handoff
# the tasks arrived ready with the approved feature plan; work them in dependency order:tiller start task T-k3p9w6 # ready -> in_progress
tiller check task T-k3p9w6 # run the gates; on green, -> checktiller submit task T-k3p9w6 --pr 41 # record the PR, -> in_reviewtiller claim task|bug|chore|tweak <ref> claims one unclaimed execution unit or no-ops on the actor’s existing claim. Taking another pre-start reservation requires --from <holder> and explicit human handoff. release clears only the actor’s pre-start reservation. reassign task|bug|chore|tweak <ref> --to <contributor-id> --reason <text> is human-directed, delegable only for named work, recipient, and reason. First start atomically sets durable started: true and permanently fixes ownership; resume, check, review, cancellation, and completion never reset it. Terminal owners are immutable even after cancellation before start. Creation/approval assign no tasks, and claims never cascade. Start/resume/task-or-shelf submission require the unit’s own claim; judgment verbs retain existing actor/human-gate rules without owner exclusivity.
Transfer. tiller transfer task|bug|chore|tweak <ref> --to <contributor-id> --reason <text> requires an explicit human decision naming work and recipient, started unfinished work, a different owner, and unchanged approved prose/applicable parent contract. One guarded transaction cancels the original with its ID/key, owner, prose, branch, review, evidence, and proposer intact; mints a same-noun successor with distinct ID/key, recipient claim, copied prose and a fresh accepted-basis path; approves it ready without starting it; and writes reciprocal lineage plus an event naming both IDs/owners/reason. No branch, PR/review ref, green check, attestation, evidence result, or completion basis carries forward.
Preserve prerequisites/live order, redirect unstarted dependents with deduplication, and retain terminal dependents’ history. A started unfinished dependent refuses the whole transfer. Keep direct question history and carry its hold to the successor; preserve ancestor holds, manual pauses, shelf attachment and both directions of carrier relationships. Do not grant assumptions or reviewed answers, globally rewrite Markdown/events, or substitute duplicate_of. An unsafe relationship refuses before writes; repeated transfer identifies the existing successor. The response names original/replacement refs and owners, changed relationships, preserved review facts, recipient start command, and external continuation.
Tiller changes no refs or host APIs. At task/shelf PR grain close the old PR without merging and open a new one. At feature/epic grain preserve the unmerged aggregate PR, assemble the replacement, and rerun affected checks/rubrics/human acceptance. Cancellation neither removes old commits nor revokes host approvals. Report incomplete Git/host work separately from committed source. An already-merged review requires sync and forward work, not transfer.
Recorded review history must be observable through its local branch or a usable submitted head. If neither exists, transfer_review_unobservable refuses before writes; restore/fetch history, sync, and inspect before retrying. Observed merged or squash-matched submitted history refuses as transfer_review_merged. No host API is consulted. The response’s exact start_command and follow_through list describe the recipient and external continuation; an external failure does not roll back the already-committed source transaction.
Feature assumptions. tiller approve feature <ref> --assume-question <Q-id> --reason <text> records a feature-only assumption without a lifecycle move. It is human-directed; task start does not silently inherit that assumption and must satisfy its own operative holds.
Every task start records a separate branch from its full canonical task key under git.branch_pattern; resume never changes it. Task-grain features record no branch. Feature aggregates interpolate reviews/features/<feature-key> and epic aggregates reviews/epics/<epic-key>; riding features retain the epic’s recorded aggregate. The agent creates independent task branches from integration. Coarse dependent tasks may start from the aggregate after submitted prerequisites, including submission-state commits, have been ordinary-merged into it. Task-PR dependencies wait for integration; stacked task PRs are not the protocol.
Shelf start applies the same new-branch safety checks before recording started and freezing ownership: invalid patterns, reserved names, existing refs, and prefix collisions refuse without writing. A shelf item with a live resolved_by carrier also refuses independent start; deliberately run tiller resolve <noun> <ref> --clear first to make it independent. Missing, cancelled, or archived carriers retain the ordinary fallback to independent execution.
Task start also checks the parent feature’s and epic’s dependencies, even before the feature’s first task starts. An ancestor prerequisite must be done, cancelled, or archived. A task prerequisite must be done or cancelled, unless the coarse-grain assembly rule above applies. Refusals retain the task_dependency_unfinished JSON reason. When no aggregate branch applies, the response omits branch and offers only the wait for integration, never assembly into an unnamed branch.
A prerequisite recorded with started_grain: task remains an independent review after its parent acquires a coarser grain. Its offline review_ref is as authoritative as a task PR. Ranked eligibility waits for that review to merge and sync, and task, feature, and console reads do not classify it as an aggregate rider or report its aggregate review missing.
Task start returns base_branch as well as branch. It may stamp a missing aggregate after a safe grain change. A reopened coarse round gets a fresh aggregate, canonical aggregate name plus -<new-task-id>, rather than reuse a merged ref. The agent reads back these results before creating branches and preserves terminal history.
Starting new work beneath a still-done coarse review refuses with aggregate_round_closed. Run tiller sync from integration to reopen the parent and clear its consumed review, then retry start to record the fresh aggregate.
Adopting a hand-built aggregate. When a local ref already exists at exactly the derived aggregate name and the feature or epic records no branch, a plain task start refuses with aggregate_unrecorded. The message says the branch exists but is not recorded, says to review its history with git log <integration>..<branch>, and names --adopt-aggregate; otherwise rename the ref aside with Git. Tiller will not delete refs. tiller start task <task> --adopt-aggregate records that existing ref as the aggregate in the same transaction as an ordinary first start, then runs the ordinary dependency and inclusion checks against it. Agents may pass the flag after inspecting the history; it is not human-gated, and the coarse review PR is the backstop. The success output lists the commits the ref holds beyond integration (adopted_commits in JSON). The flag refuses with:
aggregate_adopt_grain: the task resolves to task grain, or the noun is a bug, chore, or tweak.aggregate_adopt_recorded: the feature or epic already records an aggregate branch, PR, or review ref.aggregate_adopt_task_branch: the derived name is some task’sbranchorreview_ref.branch. A task branch is never reinterpreted as an aggregate.aggregate_adopt_missing: no local ref exists at the derived name.branch_collisionorbranch_ref_conflict: the name conflicts with integration, another node’s branch, or a ref that prefixes it or that it prefixes. Only the exact-name ref is exempt.
Done and recovered reviews still refuse aggregate_round_closed first. When a plain start would name an aggregate it has just minted, so no ref exists and nothing records it, and the unmet prerequisite is a submitted (in_review) task in the same feature, the dependency refusal says so (aggregate_unrecorded: true in JSON) and asks for the aggregate to be built with the prerequisite’s complete branch and the start rerun with --adopt-aggregate, rather than pointing at a branch that was never written. A prerequisite that started at task grain is excluded: it never enters the aggregate, so the refusal says to wait for its PR to merge and sync, and JSON carries dependency_started_grain: task without aggregate_unrecorded. Any other unmet prerequisite keeps the ordinary wait-for-integration refusal.
The agent owns routine branch creation, task-to-aggregate merges, mechanical conflict resolution, gates, and maintenance of one aggregate PR. This assembly is authorized execution, never the final human-only integration merge. No cherry-pick or squash assembly. Requested changes resume the task’s own branch, check/resubmit there, merge its new tip into the aggregate, rerun affected feature checks, and update the same PR. Retained legacy prefix refs are diagnosed with both names and a safe remedy: choose a non-conflicting configured branch pattern for unstarted work or have the human deliberately archive/rename the collision. Tiller never deletes or renames history or remote refs automatically.
During assembly preserve nonoverlapping .tiller/ records, evidence, and event files through Git merges. A source-state decision conflict is not permission to hand-edit machine state; semantic conflicts go to the human and are recorded through Tiller.
Evidence accrues as the work proceeds. tiller check attaches command and test evidence on its own; a manual add evidence verb for a diff, a test reference, or a link is not yet built (check tiller-commands.md for what the binary actually carries). Evidence is what lets a closed task survive scrutiny later, verifiable rather than asserted.
tiller submit is record-only: it records an existing review surface, never pushes a branch or creates a PR. At task grain, submit task <node> --pr <actual-number> moves checked work to in_review and records its actual PR number. A repo with no Git remote permits submission without --pr, recording the current branch and head commit as review_ref; pr remains null and local review and merge still apply. In a remote-backed repo, task-grain submission requires --pr and never silently falls back. Its misuse (exit 2, reason pr_required) names the grain and where it came from: this repo has a remote: submit with --pr <number> (the PR is the review surface); task <ref> resolves to review grain task (<source>), where the source is started at task grain, feature <ref> in this checkout, epic <ref> in this checkout, project setting, or default. Feature and epic submits keep the text before the semicolon alone. Under feature or epic grain, a riding task instead records a review ref without --pr, even with a remote; its resolved feature or epic records the single PR at the existing coarse finish line. Shelf nodes always own their review surface and ignore tree review grain. Task, bug, chore, and tweak submission reads the latest recorded result of every required gate scoped to the unit, not its check status alone: check is re-runnable from check, so a failed recheck records its failure and evidence while the unit stays at check. If any required result is not pass, submit refuses with the blocked answer (exit 1, reason gates_not_green, the gates named in missing) and writes nothing; a later passing check restores submission. Optional review gates never block it. This is a submission rule only: sync still records an actual human merge of red work, preserving the gate as a policy violation.
Task submission requires the current branch to equal the task’s recorded branch. Coarse submission requires the current branch to equal the recorded aggregate and verifies that every in-review riding task’s recorded review head and current task-branch tip are ancestors of the aggregate. Missing, divergent, or unassembled provenance refuses submission; task submission-state commits must be merged too. Run feature checks and prepare the coarse PR only after assembling these tips.
Shelf submission has the same recorded-branch guard as task submission. The current branch and any supplied offline review ref must match the shelf item’s recorded branch and current head. A mismatch refuses without changing state or events; the stored review head comes from the verified branch.
The executing agent, not this command, performs authorized Git and host operations. For a coarse review it commits implementation and riding task records, runs feature check, inspects every result, and commits actual evidence deltas even on exit 1. Machine failures and ungraded or stale rubrics remain agent work. Once only human acceptance remains, it pushes and creates or reuses the matching draft PR, verifying its repository, base, head, open/draft state, and published tip through host tools. It reports that verified URL and publication evidence in Notes from this run, then stops for the named human decision without formally submitting early. After acceptance it completes required rechecks, reuses the same PR, makes it ready when appropriate, formally submits, and commits, pushes, and reads back submission state. Requested changes reuse the established review. No-publish instructions, host failures, and Git conflicts are reported rather than bypassed; offline work retains its local branch-review handoff. Approval, attestation, merge, and sync --accept-fast-forward keep their human boundaries.
Recording the PR is not the last word on a task’s review state. When the task’s recorded branch carries state its pushed remote-tracking counterpart lacks, submission, task status, and execute readout report the unpublished-state warning. At coarse grain the resolved aggregate is the PR branch, and riding task provenance is not proof that an aggregate PR exists or is current. The agent must verify host identity and published tip, and include submission-state commits in the review surface; a pure read establishes neither push nor host state.
Recovering an aggregate review recorded after merge
Section titled “Recovering an aggregate review recorded after merge”tiller submit <feature|epic> <node> [--pr <number>] --recover-merged \ --head <historical-reviewed-commit> --merge-commit <integration-result-commit> \ --reason <explanation>Recovery is human-directed bookkeeping with explicit authorization for the named review. A merge report alone does not authorize inventing historical identity. It runs from configured integration with relevant manifest/work/prose inputs committed, not from the merged aggregate. A configured remote requires a positive PR number; no remote permits omission and retains a branch/head review_ref. --head and --merge-commit accept hex commit IDs, resolve to full immutable OIDs, and reject revision expressions and non-commit objects. Recovery requires both and a nonblank reason. Explicit recovery-only flags without enabled recovery, including empty values and --recover-merged=false, refuse as misuse. Task and shelf recovery refuse; ordinary task assumptions and aggregate submit remain unchanged.
The caller verifies the host repository, PR number, merged state, base branch, head repository, historical head branch/OID, and resulting commit with ordinary host tools. The binary validates local Git only and labels the PR association caller-supplied, not host-observed. No network access or host write is required.
The named result must be on integration’s first-parent history. A merge result must carry the reviewed head through a non-first-parent side (ancestry). A squash result must exactly match the historical branch-point-to-head net patch, with both stable patch-ID and whitespace-faithful digest equality (content); the explicit result is not restricted to ordinary sync’s fifty-commit scan. Contained work without a local merge witness is recorded as contained, not observed done. It remains no_merge_witness until the human uses the existing fast-forward assertion. Unsupported/rebased history, missing objects, unrelated results, and partial or whitespace-mismatched patches refuse without assertion fallback.
Recovery binds the intended riding tasks to historical submitted-state blobs in the reviewed tree, including owner, branch, review head, and accepted task scope. Each unfinished rider must still be in_review, with its submitted head and any retained task tip included in the historical aggregate. Deleted task or aggregate branches are allowed when immutable objects and submitted state remain. Unfinished independent epic overrides and task-owned PRs cannot be absorbed. A changed feature acceptance contract is recorded separately, never substituted for a different task scope.
The operation writes only the aggregate binding and one recover_review.feature or recover_review.epic audit event. It preserves status, ownership, current gates, accepted bases, and all existing events. It does not require green historical/current acceptance and never approves or clears it. A matching retry, even after sync completion, is a no-write success preserving original actor/time/reason; changed identity or witness refuses. First recovery requires live started work and an eligible in-review rider.
Commit the actual recovery delta on integration, run ordinary sync, inspect crossings/current acceptance/historical policy independently, commit actual sync state, publish when authorized, and read it back. Recovered sync uses the frozen head/result/riders and re-proves the result is still on configured integration. An alternate --into cannot reinterpret it. Refusal JSON names a stable reason, node, offending task/ref where applicable, and concrete prerequisite; human output gives the same remedy under existing exit classes.
tiller check and tiller submit are the last two steps to the edge of done, and they get their own sections next.
Gates, evals, and attestation
Section titled “Gates, evals, and attestation”Three commands cover acceptance, and they nest cleanly because the schema’s required evals gate runs tiller eval as its command.
tiller eval <feature|task> <node> runs the feature-grain acceptance contract. A feature reads its own feature.eval.md; a task reads its parent feature’s feature.eval.md while recording results on its own task.yml. It runs the test and command bindings, reports manual bindings as pending-human, and emits a rubric packet for eval bindings. With --emit-rubrics, it writes those packets for the agent to grade. The CLI does not grade them. Each packet entry names the output_path the agent must write its graded result to and the rubric guidance; the packet shape (tiller.rubric_packet.v1) and the graded result shape (tiller.rubric_output.v1) are pinned canonically in artifact-schema.md, “The rubric handoff”. The packet is the --json data block, and --record-rubric-output <path> is the return path: it validates the graded result file, records a failing grade as fail or a passing grade as needs_attestation, and stores the result as evidence. A command or test criterion that already passed on unchanged code under an unchanged resolved command is reused rather than run, as check reuses a gate (below). A default run prints Reused: C1, C2. naming them, and --rerun forces real runs. --rerun is refused alongside --emit-rubrics or --record-rubric-output (eval_mode_conflict), since those modes run nothing.
tiller eval reports passed honestly: a default run is passed: true (exit 0) only when every required criterion is satisfied, meaning an explicit pass or a human-attested criterion. A required criterion that is pending, needs_attestation, stale, or fail leaves the default run unsatisfied (passed: false, exit 1), so a recorded-but-unattested rubric, an unverified manual criterion, or one staled by a reopen never reads as a green acceptance. A feature with no feature.eval.md yet (unspecced) has no contract to run, so eval has nothing to evaluate and reports a clean no-op pass. A contract file that exists but carries no criteria is not that case: it is an invalid contract and refuses eval_contract_invalid (exit 2), as does a repeated criterion ID. The --record-rubric-output mode is different: recording the agent’s graded output is a successful mutation, so it exits 0 even though the criterion it records is only needs_attestation. The not-yet-clean state lives in the data (passed: false, recorded: <id>), never in a failure exit, so “recorded” never reads as “failed”.
tiller check feature F-79dh3q is the workhorse. It runs and aggregates all of the node’s gates: the command-backed required gates (build, tests, and evals), plus the recorded status of the attestation-backed optional gates. It writes the schema’s gates: block into the node’s .yml, one entry per gate carrying status, kind, run_by, run_at, the against revision it ran against, and an evidence reference. The block’s shape is defined canonically in artifact-schema.md; this command writes it and does not redefine it. against is an inline RevisionRef, also defined there: the HEAD git_commit when the working tree is clean, and a sha256 working-tree fingerprint when it is dirty (the usual case for check, where the code under test is uncommitted), so the result is pinned to what actually ran rather than to a commit that does not contain it. A changed or untracked path that exists but cannot be read while computing that fingerprint (a permission-denied file, for example) refuses with worktree_unreadable (exit 3) before any gate runs, recording nothing, rather than fingerprinting the path as deleted; tiller eval reads the same fingerprint and refuses the same way. This honors the schema’s own framing that state files carry gate results. check runs and records, and it is re-runnable: an agent that just ran the test suite in its loop can hand the result over to avoid a redundant run, and Tiller can always re-run to verify a result it does not trust.
check returns passed: true, ok: true, and exit 0 only when every applicable required gate is pass. An unsatisfied required gate, including an evals gate with a pending, stale, or unattested required criterion, returns passed: false, ok: false, and exit 1 with its recorded results in data, not an error block. Recording those results does not mean acceptance passed. A task, bug, or chore does not advance to check until its required gates pass; a feature’s lifecycle state remains unchanged. Optional criteria and optional gates do not block this result. The separate advisory sync policy is unchanged.
check does not rerun a gate that already passed on this exact code. A required command gate whose recorded result is a pass, with the same code identity and the same config hash (its kind and resolved run) as now, is reused: its command is not shelled, and the record keeps the original run’s run_by, run_at, against, and evidence and adds reused_at. The code identity, its blind spots, the sources reuse reads (the node’s own record and, for a feature check, its tasks’ records), and the stored fields are defined in artifact-schema.md, “Gate reuse” and “Code identity”. Only a pass is reused, a gate declaring reuse: false never is, and an edit to the code or to the gate’s command reruns it. Tiller’s own state commits do not move the code identity, so running check, committing the state it wrote, and running check again reuses the first run. A run with no code identity, as without Git, reuses nothing.
Human output names a reuse as Reused: <gate> (run <YYYY-MM-DD HH:MM UTC>), or <gate> (<source node>, run ...) when a feature check rides a task’s record. In the JSON data each entry of gates carries reused, reused_from (the source node, when it is not this one), and not_run where they apply, and warnings lists any advisory text. The check event’s after carries reused, listing gate names and evals/<criterion> for criteria, so a later analysis can count reuse.
check --rerun forces every command gate and criterion to run for that invocation, for a suspected flake or a changed environment. check --gate <name> (repeatable, names trimmed) runs only the named gates, with reuse unless --rerun is also given; evals is a valid name. A blank name is refused as blank_gate and a name no applicable gate carries as unknown_gate. Every other applicable gate is never shelled: it counts only when its recorded result is current under the full reuse key (code and config, and for evals every passed command or test criterion’s own key), and under reuse: false or --rerun an unnamed gate never counts. A gate left alone is reported Not run: <gate> (excluded by --gate). and marked not_run in the JSON data, and when its recorded result is a pass that does not count, that result is marked stale, keeping its evidence and provenance, so submit, sync, and the readout never read it as a pass over the current code. Inside a held evals gate only the command and test criteria a full check would rerun go stale; a manual or rubric verdict a human settled is kept, as a full check keeps it. The node advances only when every required gate is green for the current code identity, so --gate speeds iterating on one failing gate without letting a partial run advance work. check readiness refuses both flags (readiness_flag).
check and eval narrate a command-backed gate while it runs, not only once it finishes. In human mode, a gate prints one line when it starts (gate build: running (make build)...), a heartbeat every 15 seconds it is still in flight (gate build: still running (45s)), and one line when it finishes naming the status and total elapsed time (gate build: pass (2m10s)), so a multi-minute build or test run reads as alive rather than as a terminal gone quiet. The lines are narration only: they land on stderr, never stdout, and never touch what gets recorded, which stays exactly the gated command’s own output regardless of whether anything narrated it. --json refuses every one of them outright: no start, heartbeat, or finish line reaches either stream, so a harness parsing the machine envelope never has to filter narration out of a channel it reads byte for byte.
A passing check ends its stdout on the ranked list after the confirmation line: a blank line, **Up next:**, and the ranked rows for the acting contributor, the same lines tiller next prints without its heading. --json carries the same shortlist as a next field beside the check data. A check that leaves a required gate unsatisfied (exit 1) prints no Up next block, since the work did not move. submit closes the same way (see The agent readout).
The evals gate is special and check never treats it as a bare command. It is the same criteria-aware evaluation tiller eval performs: check reads the node’s *.eval.md, evaluates the criteria (preserving any human-settled status, so an attested criterion is not downgraded), and records the per-criterion criteria map plus a status derived from it (pass when every required criterion is satisfied, fail when a required criterion failed, pending otherwise). This is what keeps a check after a passing eval and attest from clobbering the green evals result back to pending with an empty map. The contract lives at feature grain only; an epic carries no eval file and no gates of its own.
Which gates a grain carries follows the manifest. check selects the required gates whose on list names the node’s noun, and the evals gate obeys the same scoping: a task under evals: on: [feature] records no evals gate and computes passed from its applicable gates alone. That is the seeded default: the acceptance contract gates the feature, tasks gate on build and tests, and task gates catch a broken task before dependents build on it, at every review grain. Recorded results follow the manifest the same way. A recorded gate that is no longer applicable to the node’s noun, after a rescope or after the gate leaves the config, is dropped on the next check of that node, so a manifest rescope plus a re-check unblocks it and no pinned required: true outlives the config. A bug, chore, or tweak runs the gates whose on: list names task, so a manifest that scopes build and tests to [feature] alone silently leaves shelf work ungated; check then succeeds with the warning no build or test gate applies to this <noun>: bugs, chores, and tweaks run the gates whose on: list names task in .tiller/tiller.yml, and none does (the warnings list in the JSON data, and a Warning: line in human output). An attestation-backed gate the manifest does not know, recorded by tiller attest, is preserved. A required attestation gate is never run: check carries its settled verdict (pass, fail, or stale) forward unchanged with its provenance, so a passing human attestation plus green mechanical gates lets the unit advance, while a failing, stale, or never-given one stays blocking. A record written under a different kind, such as a gate that was command-backed when it ran, is not inherited and the gate records pending. Every record one check run writes shares one against baseline, apart from a carried attestation and a reused result, which each keep their own.
Workflow advice reads the current manifest’s applicable required gates, authored contract, recorded criteria, and attestations together. Missing mechanical records outrank a human-only wait even when task gates are green. An unread, unavailable, or invalid contract remains explicit uncertainty, not acceptance. Required rubric preparation and recording are agent work; graded rubrics awaiting attestation and required manual criteria name the human action. Optional criteria do not block. A manual attestation can itself be evidence; no command-output artifact is invented for it.
Use check for the guarded mechanical workflow, not a redundant default eval/check loop. Standalone eval does not enforce check’s complete basis, question, and parent guards. Rubric emission and recording are separate operations. The protocol requires humans to review rubric evidence before attestation; this is not a claim that criterion attestation validates every prerequisite.
A blocked check names every unsatisfied required gate inline, including pending gates, so the reader does not need a follow-up command. An evals gate lists only its unsatisfied required criteria (for example evals: C2 (fail), C4 (needs_attestation)), and a command gate names its actual status and evidence path. Each criterion awaiting attestation includes the exact tiller attest <noun> <node> --criterion <id> --status pass command, explicitly for the human to run after reviewing and accepting it. An agent must not interpret that command as permission to attest.
Before start, check, or submit moves work forward, Tiller checks the active basis and the structural blockers. The structural check runs first and includes the parent plan gate: a task whose parent feature is still draft or specced refuses approve, start, check, and submit with a blocked answer, reason parent_plan_ungated, naming the seam that is missing, tiller spec feature under a draft parent and tiller approve feature under a specced one. A cancelled parent is the same structural check with a different remedy: the feature is terminal, so the doing verbs refuse with reason parent_feature_cancelled and the blocked answer names retiring the task too (tiller cancel task <task> --reason) or re-adding the feature. The seam this answer carries is a template rather than a finished command: cancel demands a reason, and the reason is the human’s to write, so missing_seam carries the literal placeholder <why> where --reason’s value would otherwise go, unlike the ungated arm’s seam above, which is printed ready to run. cancel task itself is not a doing verb and stays open under any parent state, so the named remedy is always reachable; add task, sync, and approve --refresh-basis are unchanged. add task is deliberately open under a draft feature, because the specify stage cuts the task explosion as part of writing the plan, and sync still observes and records whatever actually merged. The walk continues past the feature to the epic above it: a cancelled or archived epic refuses the doing verbs at both grains, a task checking its own feature first and its grandparent epic second, a feature checking its own epic, archived treated exactly like cancelled, and the deepest actionable node named first, so a task under a cancelled feature under a cancelled epic is refused for the feature, not the epic, since the feature is the node the caller can actually act on. That rule inverts in exactly one case: when the parent feature’s own state is merely pre-gate, draft or specced, rather than cancelled, the pre-gate seam is a one-hop dead end if the epic itself is retired, since the remedy it would name, tiller spec feature or tiller approve feature, itself refuses parent_epic_retired the moment it runs. There the epic is checked first and its answer wins, naming parent_epic_retired instead of parent_plan_ungated; a pre-gate feature under a live epic is unaffected and still answers parent_plan_ungated as above. The reason is parent_epic_retired, with epic_state on the payload naming which, and the remedy is the same shape one level out: retiring the node too (tiller cancel <noun> <id> --reason "<why>") or re-adding the epic and planning the work anew. The same retirement also renders as a held annotation ahead of any refusal, a held: line on tiller status <noun> <node>, and a Held chip in the console, while legal_next stays the lifecycle’s own answer, untouched by any of it. For a feature, or for a task whose parent feature is active, the current working-tree content must still match the recorded accepted_against revisions, the feature’s spec and contract and the task’s own task.md; and every open question whose blocks list names the node or an ancestor must either be answered or explicitly recorded in assumption_basis with --assume-question <id> --reason <text>. A mismatch is a blocked business result, exit 1, not a tool failure. The same guard runs before submit, so stale or question-blocked work cannot move to review silently. submit alone adds the assumption review: an assumption_basis entry whose question has been answered and not yet reviewed in refuses the task, checking the submitting task and its parent feature through the same walk-up, with approve --reviewed-assumption <Q-id> as the remedy the blocked answer names. start and check deliberately do not, so the escape hatch keeps its purpose: work may proceed under a recorded assumption, it just may not ship unreviewed.
tiller attest records the human judgments Tiller cannot produce itself:
tiller attest <feature|task|bug|chore|tweak> <node> --gate code_review --status passtiller attest <feature|task> <node> --criterion C3 --status passThe first form sets an attestation-backed optional gate (code, security, performance, or accessibility review). It applies to a task, bug, or chore with its own PR review surface, and preserves the existing feature behavior. It is the only way those gates change, and Tiller never runs the reviewer behind them. The gate key is the manifest’s own: tiller init seeds code_review, security_review, performance_review, and accessibility_review. A key the manifest does not declare is still recorded, since unknown gates are preserved, but the success carries a warning naming the declared keys (warnings in the JSON data), because a misspelling otherwise records a second gate while the declared one stays unattested. The second form remains feature/task-only and signs off a single criterion that tiller eval reported pending: a manual criterion a human verified, or a required eval-kind rubric whose input/output evidence the human reviewed. Bugs, chores, and tweaks have no criteria, so attest bug|chore|tweak --criterion refuses without writing. A required eval-kind criterion is never satisfied by the agent’s rubric result alone. tiller attest appends the judgment to the node’s attestations list, recording the target criterion or gate, the pass/fail status, the owner who signed off, the timestamp, and an evidence reference, and a passing attestation on a criterion satisfies it. A node is clean only when every required gate is green and every required criterion is satisfied, which check, eval, and human attestation together establish and record. tiller attest --gate evals is refused as misuse (exit 2, reason derived_gate), naming --criterion instead: the evals gate has no status of its own to attest, since it is derived from its criteria map. Any other gate the manifest declares with kind: command, such as build or tests, is refused the same way (exit 2, reason command_gate) without writing: its result is the evidence check records from a run, and an attestation would replace a recorded failure and flip the stored kind.
tiller attest feature also refuses to run off the coarse aggregate branch that carries the feature’s acceptance. A feature at feature or epic review grain with a recorded aggregate branch writes its attestation into its own files, and those travel to review only when committed on that aggregate, so an attest run on integration, on another branch, or on a detached HEAD leaves state the PR never sees. The refusal is a blocked answer (exit 1) with reason attest_branch_mismatch, carrying the aggregate as branch and the current branch as current_branch, and it writes nothing. The exact remedy depends on where the aggregate is: when it is checked out in a linked worktree, worktree carries that path and the message reads run this attest in the worktree at <path>, then commit there before submit; when it exists locally but is checked out nowhere, check out "<branch>", run this attest there, and commit before submit; and when the local ref is missing, ref_missing is true and the message reads run git fetch && git switch "<branch>", run this attest there, and commit and push before submit. Tiller never creates, moves, or deletes the ref. The guard exempts task grain, a feature whose aggregate is not yet recorded, a feature whose owning node (the feature at feature grain, the epic at epic grain) is done or cancelled, and a feature carrying a recovered review. Attests on a task, bug, chore, or tweak are never guarded.
The done crossing
Section titled “The done crossing”Only the final merge into integration crosses a task to done, and that merge is human-only. The agent may ordinary-merge submitted task branches into their recorded aggregate, but that assembly crosses no lifecycle gate. Task-grain sync observes each task’s own branch and review; coarse sync observes the submitted aggregate and crosses its riding tasks together. Bugs, chores, and tweaks use their own branch and review. A carried shelf node instead crosses with its carrier under done_basis: carried. Feature rollup requires at least one live task, all live tasks done, and feature gates checked; epic rollup follows its live features. Both recompute when new work is added. Feature and epic completion records retain spec/eval/surface revisions, inventory, and observed commit for drift. Human merges with non-green required gates still record done plus historical policy violations, not a concealed refusal.
tiller sync observes recorded reviews through local Git only; it does not query a PR host. At task grain its local branch is task.branch, never the feature’s or a sibling’s. At coarse grain it is the recorded feature/epic aggregate, unless an immutable recovered_review supplies the historical head and exact result. Ordinary branches resolve explicitly as refs/heads/<branch> so a same-named tag cannot substitute. Integration resolves from --into, then git.integration_branch, then the checkout branch for ordinary observation. Comparing a review branch to itself refuses; a missing ordinary branch is not evidence of a merge.
Sync then resolves that integration name once, to one full commit, before observing anything; a plain branch name resolves as refs/heads/<name> first. Every ancestry, first-parent, and content read, recovered evidence validation included, and every completion record uses that commit: the observed commit on each crossing and policy violation, and the feature/epic spec, eval, and surface fingerprints and inventories. A sync run from a scratch checkout therefore records integration’s revision, never the checkout’s HEAD or its scratch-only commits, and integration moving mid-run cannot mix two revisions into one sync. The integration name stays the identity that guards, recovery records, and results compare and report. A path the integration commit lacks is a reported skip, never read from the checkout instead, and an integration name that resolves to no commit refuses integration_ref_missing (exit 1, with into and source in the JSON data). That refusal is eager: sync resolves integration before it looks for any work in review, so an unborn repository (no commit yet) or a checkout without a local copy of the integration branch refuses even when nothing is in review. Fetch or create the branch locally, or pass --into <branch>. State writes still land in the invoking checkout’s working tree; sync switches no branch and moves no ref.
Ancestry requires containment in integration and a tip off integration’s first-parent line, witnessing a merge rather than an unchanged branch or fast-forward. Content fallback compares the branch’s net patch against the newest fifty first-parent integration commits, then verifies byte-for-byte equality with whitespace intact; this observes squash merges without treating patch-id alone as proof. Preserve local review refs until sync. Deleted refs, post-merge branch commits, multi-commit rebase merges, or mismatching squash patches may remain unobserved and must be reported truthfully. Intermediate aggregate assembly never uses squash, even though the final human integration merge may.
The human may assert an already-contained, unobservable fast-forward through sync --accept-fast-forward, naming the task at task grain or the recorded coarse review node at feature/epic grain, or a bug/chore/tweak. Assertions require a local review ref, contained work in review, and the manifest’s configured integration branch; a different --into refuses into_not_integration_branch. Record human_attested, with event contributor and timestamp, never ancestry evidence that does not exist. The agent hands over this command and never runs it. A local ref still pointing at pre-rebase commits is not contained and cannot be asserted.
Sync reports still-in-review nodes, reopened and skipped work, policy violations, and feature checks_due. A live feature whose tasks are all done but a required feature gate has no recorded result is awaiting check, with the exact feature-check command. Once all required gates have results, the same sync rolls up the feature; it does not leave awaiting sync as an unexplained success. Under advisory enforcement, failed, stale, or pending required evidence, including missing human acceptance, records a violation at the observed crossing. Managed-project CI remains independent of Tiller.
An absent aggregate record is reported as review_missing: “Aggregate review is not recorded; sync has not inspected its merge state.” It remains in the stranded readout. Guidance names ordinary submission if unmerged and explicitly authorized historical recovery if already merged, never guesses missing PR/head/result. Recovered work recommends sync rather than branch assembly, another submission, or a human merge wait, even while independent current acceptance is outstanding. Contained recovery instead names the human fast-forward decision.
Recovered crossings append historical nonpassing required-gate observations alongside current crossing-time violations. Each historical warning retains its source revision, unresolved required criterion IDs, and existing evidence references. Missing required results are pending; unknown policy is never presented as all-clear. Source/node/gate deduplication prevents repeated sync or delayed rollup from appending the same warning. A later approved contract change or pass never rewrites historical acceptance. Completion fingerprints still describe the integration checkout at sync. Reopening clears current recovery along with PR/review-ref; the event preserves the old round.
Node, status, and list JSON expose recovered_review as full historical provenance and review_missing when submitted work waits on an unrecorded aggregate: the task itself is in review, or the feature or epic holds a task in review without its own PR. Draft, unstarted, and in-progress work never reports it. Status projects historical source_revision, unresolved_criteria, and evidence_refs in policy_violations. Sync retains its existing gate-name policy_violations and adds structured historical_policy_violations separately; current and historical results are never merged into one all-green claim.
Recovered workflow advice also carries recovered_crossings_pending and per-feature current_acceptance facts. Its historical_violations counts source-pinned historical warnings, while crossing_violations counts source-less crossing-time warnings independently of current gate status. Ordinary unrecovered advice retains its existing stored-warning count semantics. An epic retains every participating feature’s acceptance and outstanding items, independent of its common sync action. After contained riders cross on human attestation, any remaining feature checks or rollup receive integration closeout advice, not another ineligible fast-forward assertion. Missing-review diagnosis and recovery guidance remain visible alongside basis/question blockers and partial rider readiness.
At a successful feature done crossing, sync also compares that feature’s recorded surface paths
with every baseline doc’s describes. The crossing’s optional touched_design JSON field contains
sorted, unique design-relative doc paths, confirmed or not. One human line after all crossing lines
names their deduplicated union: as-is docs touched: design/as-is/editor.md; run tiller drift.
Task and epic crossings, skipped rollups, reopenings, and no-op runs do not produce this notice.
The comparison observes path overlap in either direction, on segment boundaries; it does not
change the ledger, grant confirmation, or decide whether a doc is a drift candidate.
At the same feature crossing, sync compares the eval blob the completion record pins
(verified_against.eval, read from the integration commit) with the feature’s
accepted_against.eval (the working-tree contract check, submit, and acceptance expiry
judged). When both exist and differ, for example because the eval was refreshed or amended in the
working tree and never committed to integration before the merge, the crossing still happens and
nothing extra is persisted; the feature’s crossing carries an advisory eval_contract_mismatch
object (accepted, verified, both git:<blob>) in JSON, and the human output adds one block,
Eval contract differs from the accepted one (advisory):, naming each such feature with both
blobs. A feature with no accepted eval, or whose two blobs match, adds neither.
A not_contained refusal from --accept-fast-forward is not always a dead end. When the named node’s pushed remote-tracking tip is itself contained in the integration branch and only the local tip carries a commit that never reached it, the shape a state commit left unpushed before its PR merged produces, the refusal prescribes the honest fix rather than only naming the problem: the merged remote tip and the unmerged local tip, both named by short sha, then three numbered commands in order, preserve the local tip on a rescue branch, realign the working branch onto the pushed upstream, and rerun the assertion. The realignment step reads the working tree it is about to act on rather than assuming one shape: it prescribes git branch -f <branch> <upstream> when nothing has the branch checked out, or git -C <worktree> reset --hard <upstream> when a worktree does, because git itself refuses to move a branch checked out anywhere and a prescription blind to that would refuse the moment it was typed. --json carries the same facts additively, a recovery object (remote_tip, local_tip, upstream, worktree when one holds the branch, and the three steps verbatim) on the refused node, changing no existing field’s shape. The refusal stays honest rather than inventive everywhere else: a node with nothing merged on either tip, or with no upstream to compare against at all, carries no recovery object and reads exactly as the bare refusal always did.
Nothing above a task crosses by ceremony. There is no accept verb: feature and epic done are rollup observations sync records, each with done_basis: rollup, and the human’s deliberate close at the top of the tree is tiller archive E-j8m4q2, moving a done epic to archived. approve is the start gate, and Tiller computes its target from the current state: a draft epic approves into ready, and a specced feature approves into ready along with its initial tasks. There is no stored active epic state and no start epic; an approved epic is shown as active while it is stored ready and has work in flight, a derived display status computed from its children rather than stored. return is the narrow reverse for a mistaken human gate before work starts: feature ready goes back to specced, and task ready goes back to proposed, with a required reason and an event. It is refused after the feature or task starts, enters review, or reaches done; those cases are real plan changes and route through resume, cancel, fresh tasks, or an amendment. A draft or ready epic the human rejects is cancelled, not returned.
resume is the path back from review when the work itself needs more, not when a gate was mistaken. When a PR review requests changes, tiller resume <task|bug|chore|tweak> <node> --reason <text> moves the work from in_review to in_progress. The recorded pr stays because the same review surface receives the follow-up commits. Prior gate results flip to stale unless their recorded code identity still equals the current one, so a resume with unchanged code stales nothing and the next check reruns exactly what the follow-up commits changed, and the event records the resume reason. An attestation gate always stales, and a gate or criterion recorded without a code (an older record, or a manual or rubric verdict) stales too; inside a kept evals gate, one expired criterion takes the gate with it. Resume reads the code identity as check does; when it cannot (an unreadable file, say), it proceeds with no identity and stales every gate rather than refusing. This is distinct from return, which only undoes a human gate before any work has started: return is pre-work, resume is mid-review.
Review grain
Section titled “Review grain”review_grain records where human review happens: task by default, feature, or epic. It lives at the project default (execution.review_grain in tiller.yml), on an epic, and on a feature. Absence inherits downward and the most specific explicit value wins. Each home accepts all three values. A reviewed move may materialize an inherited value as an explicit feature override to preserve the reviewed execution grain at its new parent. Tree load refuses illegal stored values rather than silently defaulting. Bugs, chores, and tweaks ignore review_grain: each shelf node remains one unit with its own PR. A task’s first start records the grain it resolved as started_grain, and the task keeps that grain through later coarse settings. A task started at task grain still submits with its own PR, sync crosses it on its own merge, and the aggregate never absorbs it. A dependent’s start refusal says Blocked: task <ref> requires <ref>, which started at task grain; wait for its PR to merge and sync before starting., the task_review_open refusal at submit feature and submit epic adds that the task started at task grain and must merge first, and the readout lists each such task under a coarse feature as <ref> rides task grain; its PR must merge before submit feature <ref>. (JSON grain_notes), saying its PR or review ref while no PR is recorded. A remoteless task-grain task holds only a review_ref; task_review_open then names its branch (review_branch in JSON, with no pr) and is never sent to assemble the task into the aggregate. This is guidance only; no new refusal fires. A task with no started_grain resolves through its parents.
tiller configure records the setting outside a lifecycle gate:
tiller configure project --review-grain featuretiller configure epic E-j8m4q2 --review-grain epictiller configure feature form-engine/certification-form-loading --review-grain taskIts target is required. A bare tiller configure --review-grain <g> refuses with exit 2 and names all three targets. Every successful change records one of configure.project, configure.epic, or configure.feature; a no-op writes no event. A changed grain also prints “Commit this change to integration before starting tasks: work started in another checkout or worktree resolves the grain it can see.” after the resolved-grain line, because work started in another checkout or worktree resolves only the grain it can see. The manifest remains human-owned, but configure project is the recommended writer because it validates, records, and reports the resolved setting. Its surgical edit requires execution to be absent or a block mapping. A scalar or flow-mapping execution refuses with unsupported_manifest_shape before tree load, state, or event writes, and names block-form YAML as the remedy.
Safe configure window. A proposed change is checked only against descendants whose resolved grain would change. An affected feature PR or review ref, or an affected task in in_progress, check, or in_review, refuses with review_grain_work_in_flight. A recorded aggregate branch locks every review grain change on that feature: once an affected feature has recorded its aggregate branch, the change refuses with review_grain_branch_locked. Existing task PRs remain authoritative after a later coarse setting and must merge and sync before the coarser node submits. Never reinterpret a historical task branch as an aggregate.
--review-grain on approve. tiller approve epic <key> --review-grain <g> and tiller approve feature <key> --review-grain <g> record the grain in the same act as the plan gate. The flag is refused on a task and when combined with --refresh-basis or --reviewed-assumption. Re-approving an already-ready epic or feature with the flag uses the same safe configure window and records only an actual change.
Shared epic review and overrides. Different task owners may share an epic aggregate. Any registered developer may perform authorized assembly, checks, and submission; there is no coordinator claim. Coordinate one updater at a time through Git and the team. A feature overridden to task or feature grain finishes its own review and reaches done before epic submission.
submit feature, submit epic. Record the reviewed node’s PR or remote-less review ref without moving lifecycle; human merge and sync finish the crossing. Resolved grain, open review window, retired ancestors, active bases, questions, reviewed assumptions, required gates, acceptance, task readiness, review identity, and ancestry of both recorded task heads and current task tips remain guarded. Aggregate submission requires no task ownership; incomplete replacements still prevent it.
Task submit relies on the checked lifecycle state rather than coarse submit’s explicit required-feature-gate revalidation. Readout improvements do not widen either mutation contract. A branch reference in a riding task’s confirmation means a coarse review, not proof that the repository has no remote. Formal submission not recorded also does not mean no draft PR exists: pure reads do not inspect the host.
Ranked next and the queue. Under a coarse grain, a task reaching in_review continues within the approved review surface instead of becoming a merge wait. One shared classifier separates machine checks, rubric preparation, human acceptance, formal submission, and recorded review. Mixed outstanding work remains visible. Feature grain names the reviewed feature; epic grain names both the reviewed epic and each feature whose acceptance needs work, never an epic eval. Completed independent-grain overrides and historical task PRs retain their own review identity. Once the coarse review is formally recorded, the ranked list names a human merge wait; publication remains a separate observation.
A task-grain feature with every live task done needs shared feature acceptance and ordinary sync on integration. Coarse assembly/checks/rubrics/acceptance/submission and task-grain closeout appear once per applicable feature/epic, not under every contributor or the last finisher. WorkflowAdviceOf supplies classification and human-versus-agent actor; a suggestion is not authorization. Terminal violations remain visible without suggesting start/resume or starving live work.
Node status and console attention carry node-specific workflow advice alongside the unchanged lifecycle-table legal_next moves. Advice for a node never substitutes the project-wide selected node. Guide, full and correction Next, the Up next block, /api/next, and Home’s Up next use the same ranked list and explicit actor; no renderer infers human versus agent from a prose prefix.
Owned shelf work ranks as Continue after owned tree tasks. Owned check shelf work comes first, then owned ready or in_progress shelf work, retaining bug-before-chore priority and shelf order within each group. Live carried nodes are excluded. An unclaimed shelf node is never a row. A carrier_cancelled node is no longer carried and ranks from its stored lifecycle state when owned. Every readout and bare guide carries queue: N bugs, M chores ready; that standing census retains its existing population and does not become an execution-status count.
The project summary’s whose_move tally includes shelf nodes. A proposed bug, chore, or tweak awaits triage and counts as human. A live carried node counts for neither side. Every other shelf state, including carrier_cancelled, is folded through the same NextOwner lifecycle projection as tree work.
Large changes: amendments
Section titled “Large changes: amendments”Most changes are small and need no ceremony: add a feature, approve it, work it. A large change is different, because it reshapes existing structure rather than only adding to it, and the schema gives that reshape its own artifact, the amendment (see Evolving a project in artifact-schema.md). The CLI surface for it mirrors decomposition: the agent stages a proposal, the human reviews it in the tree, and one approve applies it.
# open the amendment; it mints the A- id and records base_revision from HEAD:tiller add amendment form-engine-rework \ --title "Restructure form engine from embedded JSON to discrete tables" \ --triggered-by design/solution-design/04-form-engine.md
# additive parts stage through the amendment too; they create nothing until approve.# stage them FIRST: a later --by, --superseded-by, or --blocks may name a staged key, and# that resolves only against operations already on this amendment.# add-epic is a root: no parent to resolve. Stage it first and a later# add-feature in this SAME amendment may name it as parent (epic before# feature is the order that makes it legal); add-task's feature must still# be live in the tree, never a same-batch staged one, since a new feature's# task explosion is specify's own act, cut once the feature is real.tiller amend A-d6w3f9 --add-epic print-layouts --title "Printable form layouts"tiller amend A-d6w3f9 --add-feature form-engine/table-backed-forms \ --change-type replace --title "Table backed forms" \ --precondition key_absent=form-engine/table-backed-formstiller amend A-d6w3f9 --add-task form-engine/rendering-modes/table-aware-rendering \ --title "Render from the table model"# a judgment call this reshape declines to make: a question, not a decision invented# here. --blocks may name a key staged earlier in this same call, resolved to the id# materialize mints for it once approve lands:tiller amend A-d6w3f9 --add-question "Keep the legacy embedded-JSON fallback for offline mode, or drop it once the table model ships?" \ --title "Legacy offline fallback" \ --blocks form-engine/table-backed-forms
# every destructive or structural operation carries the state or revision it was reasoned against.# --by here names a feature that already exists; --superseded-by below names the key staged above:tiller amend A-d6w3f9 --supersede F-79dh3q --by F-x5q1z8 --precondition target_status=done --precondition target_revision=git:7bc1a20tiller amend A-d6w3f9 --cancel T-p7kknm --reason "subsumed by the table model" \ --superseded-by form-engine/rendering-modes/table-aware-rendering --precondition target_status=checktiller amend A-d6w3f9 --revise F-n0c3v7 --precondition spec_revision=git:3e9293c --precondition eval_revision=git:1172ab9# correct how a done feature's contract is checked, after editing only its bindings:tiller amend A-d6w3f9 --correct-eval F-79dh3q --reason "loader tests moved into two files" --retire C-4# move unchanged, unstarted work rather than manufacturing a replacement:tiller amend A-d6w3f9 --move-feature form-engine/print-preview \ --to print-layouts/preview --reason "Keep print oversight in the print delivery increment"
# the design docs this reshape obliges, recorded on the amendment:tiller amend A-d6w3f9 --amends-design design/solution-design/01-overview.md
# the review surface, with each precondition's live validity:tiller show amendment A-d6w3f9
# the human reviews the staged batch and the diff, then applies it in one decision:tiller approve amendment A-d6w3f9tiller add amendment <key> opens the campaign, mints its A- id, and records base_revision from the current HEAD. It takes --title, --triggered-by <path> (repeatable), and an optional --implements <ADR-id> for a project that has one. Unlike the add verbs one grain down it is not idempotent: a key another proposed amendment already holds is refused rather than re-opened, because a second open proposal under one key is not the same proposal as the first.
tiller amend <A-id|key> stages exactly one operation per invocation: --add-epic <epic-key> [--title <t>]; --add-feature <epic-key>/<feature-key> --change-type <ct>; --add-task <epic-key>/<feature-key>/<task-key> --title <t>; --add-question <statement> --title <short-title> [--blocks <addr>]...; --supersede <addr> --by <addr>; --cancel <addr> --reason <text> [--superseded-by <addr>]; --revise <addr>; --correct-eval <feature-id-or-key> --reason <text> [--retire <criterion-id>]...; or --move-feature <feature-id-or-key> --to <epic-key>/<feature-key> --reason <text>. Repeatable --precondition name=value attaches to that operation. Repeatable --amends-design <path> may accompany an operation or stand alone. --source <citation> is free text by convention path#Lstart-Lend, legal on every operation and required on intake-minted amendments. A value written as a document citation (a .md path with a # fragment) that provenance cannot resolve still stages, with a warning on amend and source_unresolved on the operation in amend and show amendment. --to is legal only with --move-feature, including when explicitly empty; a missing destination or blank reason refuses as misuse. --retire is legal only with --correct-eval and is repeatable; elsewhere it refuses as retire_misplaced.
--by, --superseded-by, and question --blocks resolve live nodes or keys of add operations already staged on this amendment; only --blocks may name an epic. revise, correct-eval, supersede, and cancel never target epics, and correct-eval names a feature only. Reopening is not an operation: new children beneath a done epic or feature reopen parent rollups through ordinary sync, not immediately on addition, preserving completed descendants and clearing/staling current parent review and gate state as appropriate. Cancelled or archived ancestors are not executable continuation targets.
add-task requires an existing materialized feature, never one staged in the same batch. Materialize a new feature first, then create its tasks through ordinary just-in-time specification; no second amendment is required. add-feature may name an existing epic or an add-epic staged earlier in the same amendment. Reverse staging order refuses with exit 2, not_found, naming both the live and staged misses. Staging executes nothing; the human’s amendment approval applies the proposal, including staged cancellation. No link, unlink, or order operation exists. Post-materialization hard links obey the dependent’s ordinary eligibility checks; explicit soft ordering remains a separate tiller order action, not an effect of narrative intent.
Flag and address refusals use misuse (exit 2): an unknown amendment or one already approved or withdrawn, more than one operation flag in a call, a --precondition with no operation to attach to, a malformed name=value token or a precondition name outside the five, an unknown target, an epic named as a target or a replacement (--blocks excepted, which may name one), a --by on anything but a supersede, a --superseded-by on anything but a cancel, or a --blocks on anything but an add-question, a node named as its own replacement, a blank --source on an operation staged onto an intake-minted amendment (source_required; a hand-opened amendment leaves it optional), and required flag pairings (--add-task and --add-question need --title, --cancel needs --reason, --correct-eval needs --reason (reason_required), and --move-feature needs --to and --reason). --correct-eval also refuses a target that is not a feature (correct_eval_not_feature) and a feature with no feature.eval.md in the working tree (correct_eval_no_contract). Staging pins eval_revision to the working-tree contract when no --precondition eval_revision=... is given, so approval applies the bytes the human reviewed. Move staging additionally validates the live subtree and inherited constraints before recording its snapshot.
For supersede and revise, lifecycle state is deliberately not asserted during staging: staging a supersede before its target is done, or staging a revise before learning its target is terminal, remains legal. Approval enforces the operation rules intrinsically: supersede requires done, revise requires non-terminal work, and non-done replacement uses --cancel with --superseded-by. For a supersede, --precondition target_status=done is an optional stale guard that also proves the reviewed state has not moved; it is not the source of the done rule. tiller show amendment explains that distinction when a supersede stages no target_status.
correct-eval corrects how a done feature’s acceptance contract is checked, without superseding the feature. Edit only the bindings (and any baseline artifact path) in feature.eval.md, stage the operation with a reason, and name each criterion deliberately removed with --retire. Approval re-pins the completion record’s eval leg (verified_against.eval) to the corrected file and emits a correct-eval.feature event recording the old and new eval blobs, the reason, and the retired ids, so the next tiller drift no longer reports stale_spec on that leg. The spec leg and surface leg are unaffected. Approval judges it in this order, and every refusal writes nothing. A stale eval_revision is the exit 1 blocked answer, like any precondition. Then it refuses, at exit 2:
- A target that is not a feature (
correct_eval_not_feature, reachable only by a hand edit) or is not done (correct_eval_target_not_done). - A second correction of the same feature in one amendment (
correct_eval_duplicate_target). - An operation carrying no
eval_revisionprecondition (correct_eval_unpinned, reachable only by a hand edit). Withdraw and restage, since staging pins the reviewed bytes. - A completion record that pins no eval contract (
correct_eval_unverified). - A verified blob Git cannot read as the contract, because it is not in the object store or is not a readable blob (
correct_eval_base_unreadable). If an earlier correction or done crossing is uncommitted, commit it first. - A
feature.eval.mdmissing from the working tree (correct_eval_no_contract).
A working-tree contract that moved off the staged pin while approval was judging it is the exit 1 stale answer again. After that it refuses, at exit 2:
- A working-tree contract identical to the verified one (
correct_eval_no_change). - A verified contract that does not parse (
correct_eval_base_unreadable). - A corrected contract that does not parse or is not a valid contract (
eval_contract_invalid). - A change beyond bindings and
baseline.artifact(correct_eval_changes_contract). That covers a changed node, statement, polarity, required flag,baseline.surface, orbaseline.captured_at, an added criterion, a removed criterion that--retiredoes not name, and a retired id the contract still carries or never carried.
The approve confirmation notes the new pin under the applied operation and reminds the human to commit feature.eval.md, since a later correction reads its base back from Git. A criterion’s test binding may name several files as a YAML list under ref; see the artifact schema.
move-feature relocates a draft, specced, or ready feature with proposed/ready tasks only when neither the feature nor its descendants has execution or review history. Source and destination epics must be draft or ready; completed siblings do not by themselves block it. The destination may be a live epic or an add-epic staged earlier in this amendment. The operation preserves feature/task IDs, claims, lifecycle states, dependency endpoints, contract content, and historical acceptance provenance. It does not approve a draft destination or a specced feature. Changed inherited prerequisites or question holds refuse; effective review grain is preserved with an explicit override when needed.
Staging captures typed move preconditions under each operation’s move JSON field; show and successful approval expose that same reviewed snapshot. Human proposal output names the source, destination, reason, review-grain effect, and order effect. A cross-parent move removes the feature from the source order and appends it to the destination order in staged move order; a same-parent local-key change keeps its sibling position. Approval rechecks the snapshot and applies the entire batch atomically. A changed subtree, claim, parent policy, conflicting operation, or destination collision requires a fresh proposal rather than silently applying a different move. Authored Markdown and historical paths remain unchanged; review any surfaced path citations or acceptance bindings separately. Ordinary sync, not the move, recomputes parent rollups.
There is also no verb that removes a staged operation. Staging appends, and nothing edits or drops an entry once it is on the amendment. That is deliberate rather than pending: the batch a human reviews is the batch that was staged, so the way back from a wrong operation is tiller withdraw amendment <addr> --reason <text> and a fresh amendment staged against the tree as it now stands.
tiller show amendment <addr> prints the whole batch as a unit, the review surface alongside the git diff: the amendment’s title, status, base_revision, and its provenance lists, triggered_by and amends_design, then every staged operation with its targets rendered in the reference convention and each precondition’s live validity against the current working tree. Validity is one of three words. valid and stale are the verdicts approve itself would reach, stale carrying the actual value, or the note explaining why nothing could be compared, beside the expected one. unknown is neither verdict, and it is what an add operation’s precondition reads, because the node that precondition is about does not exist until approve materializes it. That last rule has one rough edge worth knowing rather than papering over: a key_absent whose key some live node already holds reads unknown on show, with the holder named in the note, while approve counts the same fact as stale and refuses. The note is what a reader acts on there, and the words themselves are the surfaces disagreeing about one fact. A batch that no longer resolves at all does not fail the read, since a stuck amendment is exactly what a human runs show to diagnose: the structure still renders and only the live columns go blank, with the one reason named once at the end. An intake-minted amendment carries one more live column beside the operations: its own source’s validity, valid or stale against the landed artifact’s current fingerprint, with no unknown tier, since the artifact names a real path from the moment intake lands it rather than a key an add op has not yet materialized.
tiller intake <file> --contributor <o> is the other front door onto this same machinery, the one for a document written outside the tree that critiques or redirects the work rather than a change the developer already has in mind: it copies <file> into .tiller/feedback/, fingerprints the landed copy, and opens a proposed amendment carrying that fingerprint as source, with nothing staged. The explosion into operations is model judgment a skill guides (tiller guide intake), staged afterward through the ordinary tiller amend surface above, which is why an intake-minted amendment requires --source on every operation staged onto it. Re-intake of a changed document is a new intake, never an edit to the old one: a basename .tiller/feedback/ already holds is refused rather than overwritten (artifact_exists), so the old artifact’s fingerprint keeps the old amendment’s citations honest. intake takes --key and --title (both default from the file’s basename) and the standard --contributor; its refusals are misuse (exit 2): a missing, unreadable, or non-regular source file (invalid_source), a basename collision (artifact_exists), or any refusal add amendment itself would give the same key (an invalid or already-held key, an unresolvable owner).
Intake currently opens one amendment per document. --split, oversized-review batching, and op-count-aware presentation remain deferred to field evidence; mapping and staging still use the ordinary skill and amendment surfaces.
tiller approve amendment <addr> is the single human gate, and it takes no other flag of approve‘s (--refresh-basis, --feature, and --reviewed-assumption are refused here as misuse). Resolving operation shape and addresses comes first, and carries its own misuse refusals (exit 2): two add operations in the batch staging the same key refuse as duplicate_staged_key, and a supersede’s --by or a cancel’s --superseded-by naming a same-batch staged add-epic key refuses as staged_epic_replacement, since a supersede or a cancel only ever targets a feature or a task and an epic reshapes through its features. Only then does it validate every staged precondition against the current working tree, writing nothing, before any lineage or intrinsic target-state check. A lineage operation then validates against an inventory of the current on-disk feature and task state files; the inventory and every graph file read are guarded until the same mutation lock that applies the batch. A supersede of a target that is not done refuses at exit 2 with supersede_target_not_done; a revise of done or cancelled work refuses with revise_target_terminal. Supersession must agree in both directions, stay at one grain, give each target no more than one replacement, and keep the complete graph acyclic. Violations refuse at exit 2 with supersession_noun_mismatch, supersession_target_reused, supersession_conflict, or supersession_cycle. Every one of these refusals writes nothing and its JSON error carries no applied payload. Once validation passes, approval applies the whole batch under one lock: the adds materialized, a staged feature in draft and a staged task in proposed, each entering the ordinary approval path from there; supersessions recorded through both directions of the lineage; cancels executed with their reasons and supersession threads; each non-terminal revision’s accepted basis re-pinned in the same act, so the batch never lands leaving an active_basis refusal standing on work the human just approved; and each correct-eval judged after the preconditions and the target-state rules and then applied by re-pinning the feature’s verified_against.eval and nothing else. A stale eval_revision stays the exit 1 answer. The correction’s own misuse refusals come in this order: correct_eval_not_feature, correct_eval_target_not_done, correct_eval_duplicate_target, correct_eval_unpinned, correct_eval_unverified, correct_eval_base_unreadable, correct_eval_no_contract, correct_eval_no_change, eval_contract_invalid, and correct_eval_changes_contract. The correct-eval paragraph above gives each one’s meaning. An add whose key another actor has landed live between stage and approve refuses at exit 2 with key_exists instead of folding into the idempotent no-op tiller add gives, because a batch that reported an add it never performed would be lying about what it did. A stale precondition is the blocked answer (exit 1) with no writes, naming every stale operation rather than only the first. Four other refusals are misuse (exit 2) instead of that blocked answer: three come from the tree rather than from the batch, and the fourth is a defect in the batch’s own operations, reachable only by a hand edit. A revise re-pins by hashing the files its target’s basis names, so a target missing one of them, a feature that never got its contract or a spec deleted since staging, refuses as revise_target_incomplete naming the file and both ways out. An intake-minted amendment’s own provenance, and each of its operations’ own citations, are checked first, ahead of every staged precondition: if the feedback artifact source names has changed since intake landed it, approval refuses as source_changed, naming both the recorded and the live fingerprint and pointing at re-reading and re-staging, or withdrawing; if the artifact has vanished from the working tree entirely, it refuses as source_vanished instead, a distinct message rather than a changed one reporting a blank hash; and an operation on that same amendment carrying no --source of its own, a shape only a hand edit can produce since staging itself already refuses a blank one, refuses as op_source_required, naming the operation by index. The confirmation names each applied operation by reference and what it replaced, and the gate prints its own readout block after it, tallying what was applied by operation kind. The schema owns what the apply writes and what re-approving a half-applied batch does (artifact-schema.md, Amendments).
These command guards are not a complete persisted-amendment validation contract. The deferred amendment validation boundary retains strict decoding, identity coherence, and the operation-shape work; the roadmap lists it as unbuilt scope.
Deferred flag-presence validation. Reject every explicitly supplied operation field that is inappropriate for the selected operation, even when empty or equal to its default. Distinguish supplied flags from untouched defaults: the implicit --change-type=create must not reject a valid non-add-feature invocation. Selectors, preconditions, owner, and design-amendment controls remain orthogonal to operation metadata. In particular, --revise with an explicit title or reason must refuse rather than silently discard it. Preserve legal combinations and untouched defaults. Shape and invocation failures must leave repository bytes unchanged and use exit 2 with a stable machine-readable reason, distinct from approval’s stale-precondition exit 1. An actual ignored-flag CLI smoke and affected regressions are required before closing this gap.
Missing-file basis-refresh diagnostics and the add-op key_absent difference between show’s unknown and approval’s stale result remain separate limitations; they do not change the approval preconditions above.
The root soft-order update is part of that same approval transaction. For each add-epic operation that this approval actually materializes, approval appends the fresh epic id to .tiller/order.yml in staged order, after every entry already recorded. It preserves the existing list byte-for-byte in membership and relative order: no sorting, no backfilling an older unlisted epic, no removal of a dangling id, and no expansion of a legal partial list. A batch with no add-epic operation does not read, guard, create, or rewrite the root order. If the append cannot commit, none of the new epics or other approval effects commit either.
This is the same dance as decomposition, one level up. There the agent lays a draft tree and the human approves it; here the agent stages a reshape and the human applies it. In both the reasoning is the agent’s, the structure and the gate are Tiller’s, and the approval is the human’s.
Inspecting and maintaining
Section titled “Inspecting and maintaining”Reading nodes: show, ls, status
Section titled “Reading nodes: show, ls, status”The read commands are tiller show <noun> <node> for one node in full, tiller show plan <file> for a semi-ephemeral execution plan, tiller ls <noun> [filters] for lists (tiller ls tasks --owner sam --status in_progress, tiller ls plans --owner sam), and tiller status <noun> <node> for the lifecycle-aware oracle or, with no node, the bare tiller status project summary. The noun grammar is uniform: ls accepts both the plural collection form and the singular (tiller ls feature and tiller ls features are the same), matching the singular every other noun-addressed command takes. It lists epics, features, tasks, questions, and amendments; epics, features, and tasks come back in dependency order with key order breaking ties, questions by id, and amendments by key, so tiller ls amendments --status proposed is the plain read for what reshape is waiting on a human. tiller status surfaces a node’s recorded gates: block, the lean oracle answer; tiller show is the full audit read and surfaces the whole integrity block where a node carries one: the gates: block including the evals gate’s per-criterion criteria map, the human attestations, the merge-time policy_violations, and the accepted/verified basis (accepted_against, verified_against). A human auditing a node’s integrity through the CLI reads it all off show, in both --json and human output. ls filters narrow by the fields a caller browses on: --status, --owner, plus --feature to list one parent feature’s tasks (tiller ls task --feature <feature>) and --epic to list one epic’s features (tiller ls feature --epic <epic>); the parent is named by key or id. ls stays the lean scan; a caller follows up with show for the full block. The event stream carries the mutation history for recaps and state timelines; git carries the reviewable history of committed file changes.
The bare status summary’s Git inspection disables optional index writes. Reporting uncommitted .tiller/ paths must not refresh cached file metadata in the caller’s Git index, even when file timestamps have changed without content changes.
Rendering the tree and dependency order: tree, check readiness
Section titled “Rendering the tree and dependency order: tree, check readiness”tiller tree shows labels and titles, with the title column set by the widest label and titles ellipsized to the terminal width at a TTY or 120 columns when piped. Address labels never truncate. --files adds paths beneath each node; vertical continuation bars preserve the spine when ancestors have later siblings.
Sibling rendering and composed batch argument lists share dependency ordering. Cross-epic edges lift to epic rank whether recorded feature-to-feature, feature-to-epic, or epic-to-feature. Strongly connected groups render at the layer their external dependencies allow, before their dependents. Within an epic cycle, the base feature flow supplies fallback rank, epics without features last; feature and task cycle peers fall back to canonical key. The precedence is recorded dependencies, soft order, epic-cycle flow rank, then canonical key. Questions retain id order. Cycles remain loadable; their readiness classification is separate from this deterministic grouped rendering.
Historical tree and relationship order is not a second next-task recommendation. Eligible unclaimed tasks follow the shared execution preference, where completed prerequisite depth does not outrank recorded intent.
Readiness cycle diagnostics exclude cancelled epics, features, tasks, and descendants of cancelled containers before deriving cross-epic edges. Cancelled predecessors inside a retained epic cannot make its replacement plan unready. The graph is recomputed, not merely filtered by reported cycle members, so remaining non-cancelled cycles still block. Done and archived work keep their existing treatment. Recorded dependencies, historical ordering, and relationship queries remain unchanged; cancellation never requires deleting historical edges.
Readiness reports genuine prerequisite loops as dependency_cycle gaps, setting ready: false and exit 1. Classification includes feature prerequisites, inherited epic prerequisites, sibling-task prerequisites, and the child completion obligations behind an epic target. Explicit epic cycles and mixed epic/feature completion deadlocks block, including a parent prerequisite on its own child. It is not enough to inspect only feature-to-feature edges.
Classification also resolves the effective review grain. An unfinished epic-grain feature cannot complete before its epic aggregate merges. That aggregate can be submitted only after every riding feature’s tasks are submitted or done, and every independent task- or feature-grain override is done. Submission and completion are distinct obligations: ordinary sibling task assembly does not require the shared merge first. The early/domain/later interleaving is therefore a genuine deadlock when the early and later features must share the first epic’s aggregate. An independent early-feature override can restore legal interleaving; making only the later feature independent cannot, because epic submission still waits for it. Completed features acquire no new aggregate obligation. Diagnostics name the implicated epic review alongside recorded prerequisite evidence, never internal obligation vertices.
A loop that exists only after projecting cross-epic feature edges into container order is a projected_epic_order_cycle advisory instead. JSON includes separate gaps and advisories arrays; advisories neither change ready nor turn success into a failure exit code. Human reports label them nonblocking, name the implicated features and recorded edges, and explain that grouped rendering cannot show every interleaved step. An early feature in one epic, a domain feature in another, then a later feature in the first epic can execute legally, even when the early feature is already done. Requiring the whole first epic before that domain feature instead creates a completion deadlock. Preserve valid prerequisites rather than breaking them just to sort the map. Reviewed feature moves use this same classification.
The layered topological sort emits every currently dependency-free sibling or condensed cycle group in each round. It never sinks a cycle and all its dependents into one alphabetical tail.
Orient and decompose readouts show a Dependency order check using the same blocking versus advisory classification and recorded-edge evidence. A dependency overriding soft order is an independent advisory clause, never a readiness gap. tiller next --json and the console’s /api/next carry the same dependency diagnostics, which Home shows as their own section, without changing the ranked moves.
Observing merges and reading holds: sync, status, ls
Section titled “Observing merges and reading holds: sync, status, ls”tiller sync observes merge crossings and reconciles source state against local Git and recorded reviews, writing one event file per observed crossing or logical transition. It preserves reopenings, skipped rollups, awaiting reviews, integration selection, attestations, and checks-due reporting even when no crossing occurs. It produces no Markdown status file. When the manifest turns usage capture on, sync then runs the same agent usage capture as tiller usage, best effort and within a time budget; the usage files it writes join sync’s other state changes and are committed the usual way.
The structured console streams expose operative question holds on owned, non-terminal work. They use the same walk-up guard as the mutations: an open question naming a node or one of its ancestors is inherited, an answered or cancelled question, or a question assumed at that guarded node, is not operative there, and one question naming several levels is reported once at the nearest matched target. Question holds stay a separate condition from manual blocked: true and retired-ancestor holds. No surface attributes held work to the question owner.
Every other question-hold refusal (start, check, submit guards) on a feature reads Blocked: ... answer them, cancel one that no longer needs a decision with tiller cancel question <Q-id> --reason "...", or pass --assume-question., since only a feature can record an assumption. The same refusal on a task, bug, chore, or tweak reads Blocked: ... answer them, or cancel one that no longer needs a decision with tiller cancel question <Q-id> --reason "...". The plan gate (approve) never offers an assumption and reads Blocked: ... answer them with tiller answer question <Q-id> --answer "...", or cancel one that no longer needs a decision with tiller cancel question <Q-id> --reason "...", fold the answers into the plan, and retry. The coarse submit refusal reads answer the questions, or cancel one that no longer needs a decision with tiller cancel question <Q-id> --reason "...", and retry. The queue, tiller next, and Home keep the single answer_question move; no ranked surface suggests a cancel, which is a judgment for the human.
Ancestor reads expose contributors: [{id,name}], always an array (empty []), sorted/deduplicated by started descendant task owner ID with roster-name/ID fallback. Scalar owner remains execution/question attribution only. ls --owner <id> never matches ancestors; ls --contributor <id> and corresponding API contributor select ancestor membership. Relationship query --where contributor=<id> filters reached ancestors after traversal, leaving ownerless nodes available as bridges. Contribution includes started done/cancelled tasks, not reservations/review actors/attached shelves. Personal streams show nonterminal assignments, not entire feature workloads duplicated under contributors; Home’s Up next ranks eligible ready task candidates under ready or active features among the reader’s other moves.
CLI read views remain local-working-tree views. Tiller may read Git metadata already available locally, such as the current branch, HEAD, upstream tracking ref, and ahead/behind counts; tracking refs represent the last fetch, not live remote state. The read-only console additionally observes bounded task execution state from validated local worktrees or committed local/configured tracking tips, without changing CLI eligibility. The console task execution source contract owns selection, confinement, history, evidence, and freshness. Tiller does not fetch, pull, push, or merge to make another engineer’s branch visible.
Rejected by design: CI status. There is no tiller ci-status command and no Tiller-backed CI or branch protection, and none is planned. Managed-project CI and merge eligibility must remain independent of Tiller availability and state validation, including optional advisory jobs. Local Tiller may invoke independently runnable project checks, never the reverse. The distribution contract defines project independence and the safe planning-only CI alternative.
Capturing agent usage: usage
Section titled “Capturing agent usage: usage”tiller usage reads this machine’s coding-agent session logs for this project and creates or grows one file per top-level session under .tiller/usage/, in the format and under the rules the artifact schema owns. It takes no arguments and is safe to run at any time and as often as wanted. It records no event and no person, so it needs no joined identity and reports no acting contributor.
It refuses with exit 1, writing nothing and saving no cursor, in three cases. usage_capture_off: .tiller/tiller.yml does not set usage.capture: true, and the refusal names that setting, so nobody’s logs are read without the team having chosen it. not_integration_branch: git.integration_branch is set and the checkout is on another branch; the refusal names both, and JSON carries them as current and integration. mutation_stale: another Tiller write landed while capture read the logs; run it again. A harness folder or log it cannot read is not a failure: it is skipped, counted, and tried again next run. Any other failure, such as a malformed usage file already in the repo, is an environment failure.
Where it looks. Four harness folders under the user’s home, read-only: Claude Code’s ~/.claude/projects (or $CLAUDE_CONFIG_DIR/projects), Codex’s ~/.codex/sessions (or $CODEX_HOME/sessions), OhMyPi’s ~/.omp/agent/sessions, and Pi’s ~/.pi/agent/sessions. It never writes there, never reads harness configuration, and reads only the sessions whose working directory is the checkout root, a path git worktree list reports, or a path beneath one. Each reader owns its harness’s encoding. Claude Code, OhMyPi, and Pi keep a folder per working directory: Claude Code replaces every / and . in the full path with -, OhMyPi does the same to the home-relative path, and Pi wraps the encoded full path in --. A folder matches when it is the encoding of the root or of a live worktree, or when it starts with the root’s encoding followed by --, which is how a path under one of the root’s dot directories encodes. That finds a task worktree under .worktrees/ or .claude/worktrees/ even after it is removed, while a sibling project such as /p/app-two never matches /p/app by name. A folder that only starts with a project path’s encoding followed by - may hold sessions started in a subdirectory (/p/app/ui encodes as -p-app-ui) or a sibling’s (/p/app-ui encodes the same way), so its logs decide: the working directory recorded on the first line that carries one (Claude Code’s message lines and the Pi session header both do) must be a project path or lie beneath one by whole path segments. That answer is remembered in the cursor, so each such folder is read once. A removed worktree outside the root is found only if capture ran while it existed, one more reason capture rides sync. Codex records the working directory on each rollout’s first line, which is read once per file, ever, and remembered in the cursor; a rollout matches when that directory is a project path or lies beneath one by whole path segments. Subagent logs fold into their parent session. Lines stream through a bounded buffer with a cheap substring test before any JSON decode. A line over 8 MiB, such as a large tool result, is skipped without being held whole, and a malformed line is skipped without failing the session.
What it prints. One line per harness. When the run wrote files: Claude Code: 12 sessions found, 4 captured, 1.2B tokens captured, 71% tied to a work item. When it wrote none: Claude Code: 12 sessions found, nothing new to capture. When the harness folder is absent: Codex: no session logs found. The figures are this run’s: the sessions found for this project, the files written, and the tokens in those files with the share credited to a work item. All-time totals are the Report page’s job. When it skipped folders or logs it could not read, the line ends with the count, as in , 1 log could not be read. --json returns harnesses, each with harness, found, sessions, written, tokens, tied_share, a fraction that is null when the run captured no tokens (unknown, not zero), and unreadable, the count of folders and logs skipped, and partial.
Cost. Discovery and parsing take no lock; the mutation lock is held only for the final write. A log whose size and modification time match the cursor is not opened, and a grown log is reparsed whole, which keeps the replay simple; session logs are tens of megabytes at most. The budgets, on a client platform build-sized logs, are under 10 seconds for a first full capture and under 100 ms for a capture with nothing new.
Inside sync. After its own work succeeds, and only when capture is on, sync runs the same capture with a 2-second budget checked at session boundaries. The first changed session always runs, so every capture makes progress; when the budget runs out, sync keeps what it read and a later run continues from there. With nothing new to capture, sync reads neither its tree again nor the event stream, and adds only the cursor check. A capture problem never fails sync, changes its exit code, or changes what it recorded. It becomes one line in the human output and usage_warning in JSON: Agent usage is captured on <integration>; skipped here. off the integration branch, Agent usage capture ran out of time; run tiller usage to finish. when the budget stopped it, Agent usage: 1 log could not be read; run tiller usage for details. when capture skipped logs or folders it could not read, and Agent usage not captured: <reason> for any failure, a mutation_stale refusal and a panic inside capture included. No skill or orientation text mentions capture, and no agent is asked to run it.
Turning it on mid-project. The first capture on each machine backfills every session still on disk that started on or after the ledger’s first event, credited against the current tree. A project with no event yet captures nothing until its first one. Each engineer’s history arrives only when that engineer captures (a current binary, a checkout on the integration branch, then a sync or tiller usage), and the console cannot show who has not, since that would put a person on the page. So the team turns capture on together and each engineer runs tiller usage once. Claude Code keeps transcripts for 30 days by default, so every day capture stays off loses history for good; the Report’s partial coverage rule keeps the loss from reading as low usage.
Detecting drift: drift
Section titled “Detecting drift: drift”tiller drift mechanically detects the enabled markers: stale_spec, active_basis,
answered_question, missing_surface, orphan_eval, stale_design, and shifted_design.
Their meanings, sweep sets, and exemptions belong to the artifact schema.
The command runs exactly the manifest’s drift.checks. Fresh scaffolds enable the first six;
shifted_design is opt-in. Existing manifests are never rewritten to enable either baseline
marker. A disabled check remains named in the footer.
candidates, grouped by marker, identify what moved and its recorded history anchor. Work-node
rows retain separate noun, id, and key fields. Baseline rows use noun design, an empty id, and
the canonical design-relative path as key. since is the confirmation commit; optional until
is the supported upper history bound. Tracked baseline candidates include every matching
feature’s id, key, spec path, and crossing commit in features. A range is history to inspect,
not a claim that the confirmed working-tree bytes were committed at its lower bound. Opt-in
working-tree comparisons can include dirty changes or external code that a project Git diff
cannot reconstruct.
uncovered names each comparison the sweep could not complete, with a reason. An unconfirmed
baseline doc is unconfirmed, not silently clean; unavailable history and unsafe or unreadable
paths remain explicit failures. The footer counts work nodes in compared and design docs
separately in compared_design, deduplicated across enabled markers. Baseline marker coverage
uses docs, not invented work nodes.
Exit 0 asserts that every in-scope comparison completed and nothing moved. Exit 1 means
candidates, uncovered entries, or both; 2 and 3 keep their usual meanings. Detection writes
no state and adds no lifecycle refusal. The existing enforcement remains on its own verbs:
active_basis at start, check, and submit, and answered_question at submit.
mend-drift drafts the reconciliation; the human reviews it and confirms named baseline docs
with tiller approve design <doc>.... A copied command uses -- before a quoted doc path so
a valid filename beginning with - cannot become a flag.
Migrating and upgrading: migrate, upgrade
Section titled “Migrating and upgrading: migrate, upgrade”Three version facts stay distinct. The binary has a release version. The manifest stamps the schema of the files under .tiller/ (schema_version, currently 12). The generated .tiller/orientation.md stamps an orientation generation, the version of the instructions agents read. Refreshing generated instructions is not a data migration, and replacing the binary is neither.
tiller migrate [--dry-run] [--ownership-review <file>] [--contributor <id>] is explicit local maintenance with the running binary. It never changes the installed binary or the checkout’s selected identity, and it touches only the current resolved checkout.
What migrate checks. Every check runs before any write, and a refusal changes no files.
| Condition | Answer |
|---|---|
schema_version equals the binary’s |
No source migration; only the generated refresh below runs. |
schema_version is newer than the binary’s |
Refuses schema_version_unsupported, exit 3, naming tiller upgrade as the remedy. |
schema_version is below the migratable floor (schema 2), or a step has no registered migration |
Refuses schema_version_unsupported, exit 3. Below the floor, the remedy is to remove .tiller/ and rerun tiller init. |
An epic uses a key the target schema reserves for a shelf folder (bugs, chores, tweaks) |
Refuses migration_reserved_epic_key, exit 3, naming a draft epic’s key rename with a binary that still reads the old schema, or, past draft, staying on that binary. |
| The repository has unresolved Git conflicts anywhere | Refuses migration_git_conflicts, exit 1. Resolve them first. |
| A migration input has staged or unstaged changes, hidden index state (assume-unchanged or skip-worktree), or no committed regular-file baseline | Refuses migration_inputs_dirty, exit 1. Clear the index flags, then review and commit the input; unrelated dirty files may remain. |
| Crossing from schema 2, 3, or 4 into schema 5 with unfinished shared-branch work | Refuses migration_work_in_flight, exit 3, naming the blocking nodes. Finish and sync that work with a compatible older binary first; cancellation is a separate human decision. |
| Crossing from below schema 6 with started unfinished work | Needs a human-reviewed ownership mapping (below); a missing, stale, incomplete, duplicate, conflicting, or unknown-owner mapping refuses before writes. |
A real source migration resolves an acting contributor through the normal precedence (--contributor, TILLER_CONTRIBUTOR, then .tiller/identity.yml) against the registered roster. A current-schema refresh needs no identity, so unjoined checkouts can refresh.
What migrate rewrites. It reads the schema leniently, applies the registered migrations one schema step at a time, and commits the manifest stamp and every node rewrite in one guarded source transaction with one migrate.schema event, recording the source and target schema and the invoking command. Statuses, IDs, claims, branches, PRs and review refs, accepted bases, evidence, and historical event bytes are preserved. It renames a legacy owner: key in this checkout’s .tiller/identity.yml to contributor: in place, keeping its value; it never selects a different identity or creates a missing one. Because identity is gitignored, each other checkout renames its own key with tiller upgrade --project-only or a bare tiller join after pulling the migrated manifest; other commands refuse the legacy key as identity_unreadable and name that remedy.
It then refreshes generated artifacts in one derived transaction: orientation, skill shims, harness managed regions, and required ignore entries, on current-schema and unjoined checkouts too. An on-disk orientation from a newer generation refuses orientation_newer, exit 3, rather than being rewritten to older text. Repeating maintenance on a current checkout is a no-op for events, the source generation token, and artifact bytes. A refresh failure exits nonzero without undoing a committed migration.
Reviewed ownership. tiller migrate --dry-run --json writes no source, cache, event, review file, or generated artifact. Its data.ownership_review is {source_schema, target_schema, snapshot, sources:[{path,sha256}], active:[{id,noun,ref,observed_owner,confirmed_owner}]}. Save the whole envelope or the extracted review outside source state. A human fills every initially empty confirmed_owner; only that field is editable. The source schema and snapshot, manifest, roster, inventory, owners, and relevant state remain bound to the inspection, so stale or edited input refuses. Apply with tiller migrate --ownership-review <file> or tiller upgrade --project-only --ownership-review <file>; the upgrade flag is project-only, never a combined binary upgrade option. Reviewed owner corrections are recorded as explicit migration provenance, and ready reservations remain mutable. Ordinary upgrade explains a required review and routes to offline maintenance; self-managed candidate preflight refuses a known missing review before executable replacement. The team procedure for adopting these older boundaries is in pre-release ownership adoption.
tiller upgrade [--version vX.Y.Z] [--contributor <id>] updates through the installed binary’s owner and then migrates and refreshes the current resolved checkout, including from a subdirectory. Outside a project only the binary is applicable; inaccessible or malformed project markers refuse rather than silently becoming outside-project. --project-only is network-free local maintenance with the running binary and never invokes brew. --binary-only explicitly excludes the checkout. The two flags are mutually exclusive; project-only with --version is misuse, and project-only outside a project refuses before writes. Other checkouts are never discovered or changed.
An unpinned already-current or ahead-of-published binary skips archive download and replacement but continues project maintenance. Exact self-managed pins preserve selection and reinstallation semantics; an older pinned binary must support inspection and the checkout’s schema and orientation before combined replacement. Binary-only pins need no project protocol. Self-managed releases are checksum-verified, inspected before swap, and applied by the selected installed executable’s exact resolved path, never an unrelated PATH entry or the still-running old process.
Hidden upgrade --project-only --inspect --json is maintenance inspection protocol 1, not a general capabilities API. It reports executing version and identity, supported migration path, schema and orientation observations, planned source and artifact paths, all deterministic drain blockers, and separate execution warnings. Blockers carry node reference, path, status, branch, PR, and retained review identity where present. Inspection performs no project, identity, harness, Git-index, event, or persistent-cache writes. Apply replans and guards source generation, input and destination snapshots, and executable identity; inspection does not reserve those facts, and neither inspect nor apply drains work.
Before replacement, retain and verify the outgoing executable outside installation-owner directories at ~/.local/share/tiller/previous/<version>-<sha256>/tiller. Refuse conflicting or unverified retention destinations or changed installation identity. Report the actual retained path, directory, version, and digest separately even if a later phase refuses; a retained copy does not prove schema compatibility. Distinct builds are kept until explicitly removed. The distribution contract defines the temporary PATH drain session that selects a retained build.
Verified Homebrew ownership delegates only to the installed Tiller cask or legacy formula in its owning prefix, suppressing implicit dependent checks and cleanup in that child. Exact pins refuse before brew. Project-only bypasses brew; unverifiable brew-looking paths and brew failures never fall back to direct replacement. Current-binary preflight can stop known blockers, but target compatibility may become known only after brew changed the machine. Metadata refresh and genuine dependencies remain Homebrew’s boundary; private asset authentication is not bypassed.
Upgrade retains public outcome, from, to, tag, path, refresh, and refresh_note where applicable. These describe actual binary and refresh facts, not overall completion or invented versions; an ahead-of-published binary’s executing version is distinct from the lower latest release. Structured results also report complete, installation owner, retained executable, project scope/root, schema and orientation before/after, source/generated paths, shim additions/removals, drain blockers, execution warnings, and phase failures. Outside-project project outcome is not_applicable; intentional scope exclusions are not failures.
The shared maintenance payload has the following fields:
| Field | Contract |
|---|---|
protocol |
1, checked before a parent trusts a selected target’s result |
complete, inspect |
Completion for requested scope; inspect identifies the read-only internal mode |
installation_owner, executing, retained |
Actual installation owner, executing {path, version, sha256}, and optional retained {path, directory, version, sha256} |
binary |
Phase {status, files?, failure?} for the installation boundary |
project.root, project.status |
Resolved checkout identity and requested applicability/outcome |
project.schema_before, project.schema_after |
Observed schema stamps when known |
project.orientation_before, project.orientation_after |
Observed orientation generations when known |
project.migration, project.artifacts |
Separate phase {status, files?, failure?} results; actual files belong only to completed phases |
project.planned_source_files, project.planned_artifact_files |
Planned paths, never evidence that those writes completed |
project.planned_shims_added, project.planned_shims_removed |
Planned discovery changes; unlike the actual shim fields, these may survive a refused or failed apply |
project.blockers, project.warnings, project.notes |
Structured migration drain blockers, separate non-blocking execution/ownership warnings, and migration notes |
project.shims_added, project.shims_removed |
Committed catalog shim changes only |
project.actor |
Resolved real-migration {id, source} only |
failure |
Overall typed {code, reason, message} failure, also carried by the failed phase where known |
Optional facts are omitted rather than invented. In particular, recovery_required and unknown phases must not be represented by an empty changed-file list that claims durable unchanged state.
Exit 0/ok: true requires every requested phase completed or genuinely inapplicable. Misuse stays 2, environment/installation/refresh failure 3, guarded business holds such as dirty migration input 1. Successful binary replacement survives later migration failure; a committed schema/event survives refresh failure. Confirmed rollback can establish unchanged destinations, but journal-clear or rollback failure reports recovery_required with mutation_recovery_needed, preserving the journal. Unestablished durability after child loss is unknown, never fabricated unchanged state. Recover existing transactions before retry; do not delete .tiller/.cache or roll a migrated checkout back.
Human output names the exact failed phase and remedy, and success names files to review/commit, not a join/migrate ceremony. Other checkouts use project-only when revisited; no repo registry or worktree traversal is implied. Restart existing serve processes and reload harness sessions as needed. upgrade --project, a launcher/engine cache, and an automatic upgrade nudge remain unbuilt.
Renaming and ordering: rename, order
Section titled “Renaming and ordering: rename, order”tiller rename <noun> <node> (--title <display-title> | --key <canonical-key>) requires exactly one axis. --title changes only display metadata at any state. --key moves only proposal-safe nodes, preserves id, and never renames a git branch or rewrites PR history. The exact state and reparenting rules, payloads, and refusal reasons are in Proposal correction commands.
tiller order <epic|feature|task> <node> <node> ... records the soft order: the intended sequence among siblings, wherever the recorded dependency graph leaves the choice open. It resolves every reference all-or-nothing and replaces the named parent’s recorded list wholesale, since the list states current intent rather than accumulating the way depends_on does; a partial list is legal and remains partial, and the siblings it names lead their unlisted peers wherever the tiers above still leave the choice open. --clear erases one parent’s list: tiller order epic --clear erases the root epic list, and --epic <e> or --feature <f> names the parent whose feature or task list to erase at the other two grains, the same parent-flag convention ls already uses. tiller order remains the only explicit replace or clear writer at every grain and the only writer at feature and task grain. Amendment approval’s automatic append of epic ids it freshly materializes is the sole root-grain exception; it never replaces the list or interprets an explicit intended placement. The verb refuses an unknown or ambiguous reference (all-or-nothing, with candidates named), a node listed twice, nodes that do not share one parent, an empty list with no --clear, and a --clear at feature or task grain naming no parent. The ledger noun and the amendment sit outside any ordered set: a question keeps id order. Any live sibling may be listed regardless of state, since ordering done work is harmless and renders wherever the dependency layers already put it. Who may run: both; an agent records an order on the developer’s explicit instruction naming the sequence, per the delegation rules (The actor model).
Guidance and next moves: guide, next, readout
Section titled “Guidance and next moves: guide, next, readout”tiller guide [<skill>] prints the text of a Tiller skill, or with no argument prints the ranked next list and names the skill for its highest-ranked agent move. It is the harness-agnostic backstop: because the skills are plain files, an agent in a harness with no skill-discovery system reaches the same guidance through the CLI that a richer harness loads on its own. The skill set specifies the skills themselves; this is only the command that surfaces them.
The current source catalog contains eleven named guides: orient, derive-baseline, decompose, specify, execute, capture, mend-drift, amend, intake, design-brief, and code-review. tiller guide design-brief prints the complete embedded skill; tiller guide design-brief --json carries the same text through the existing guide envelope. It accepts no target-node argument. The developer names the epic, feature, or task in the explicit UI/UX brief or prototype-handoff request to the agent, which resolves keys, IDs, or an unambiguous conversational target using existing reads.
This named guide bypasses no lifecycle gate because generation makes no lifecycle move. Bare guide prints the ranked list and never selects the export automatically. The agent returns one self-contained seven-part Markdown snapshot, writing only an explicitly requested deliverable, with no new state, schema, lifecycle readout, or command. Export never performs maintenance.
tiller next [--all] [--contributor <id>] prints the ranked next list: the legal moves for this checkout, best first, at most five. --all prints every remaining row in ranked order: every node’s best move, not every move on every node. The reader is --contributor, then TILLER_CONTRIBUTOR, then .tiller/identity.yml; the flag only chooses whom to rank for, so this read prints no acting-as line and carries no actor field. A heading names the reader, Up next for <id>:, or with no reader reads Up next (not personalized; run `tiller join`):. Each row reads <n>. You|Agent: <action> <node>. <Reason>. and ends on how to make the move: the exact command in backticks, Continue it on <branch>. for the reader’s own task in flight on another branch, or tiller guide <skill> for an agent move with no command. When more moves are legal than shown, a closing line counts them and names tiller next --all. A hint, when the list carries one, follows on its own line. With nothing legal it prints Caught up: nothing is legal to do right now. and the hint, with no heading. --json returns reader (empty when unpersonalized), candidates, caught_up, unplanned and hint when they apply, and dependency_diagnostics when the tree has dependency gaps or advisories. Each candidate carries actor (human or agent), kind, action, node (noun, id, key, ref, title), command or skill, reason, tier, and branch or workflow where they apply. Without --all, candidates is the shortlist. The command is read-only and needs no identity.
Bare tiller guide prints the same list, then Suggested skill: <skill>, the reason that skill applies, the standing queue line, and Run `tiller guide <skill>` to load it. The suggestion follows the highest-ranked agent move, not the top row, since the top row is often the human’s: derive-baseline, decompose, specify, or execute for continuing, claiming, and closeout work. When only human moves are legal, for example registered baseline docs awaiting confirmation or approvals awaiting the developer, it suggests orient, and a caught-up list suggests orient with the list’s hint. --json carries skill, reason, queue, and next, the shortlisted payload tiller next --json returns. tiller check readiness closes on the same suggestion. Named guide loads the named registry skill and reads no state.
tiller readout <seam> prints the one consistent block for a seam boundary, specified above under “The agent readout”.
Searching: search
Section titled “Searching: search”tiller search <query> [--kind <kind>] finds nodes and documents across .tiller/ and is the
command behind Explore search. A query that equals a node’s id, its id suffix, its full
<key-path> (<id>) reference, its whole key, or its key’s last segment is a lookup that names that
node. Otherwise the query is a case-insensitive substring matched against titles, keys, question
statements and recorded answers, and the project-authored regular-text Markdown corpus. There is no
query language and no semantic search.
Results are grouped, one per node and one per Markdown file no node owns. A file belongs to the node
whose folder holds it, and an amendment’s <A-id>-<key>.md belongs to that amendment. Nodes rank by
how they matched, strongest first: id, key, title (an equal title, then a title starting with
the query, then one containing it), key_fragment, then text for a node matched only in its prose
or ledger fields. Within a rank, the node with more matching lines leads, then kind order (epic,
feature, task, bug, chore, tweak, question, amendment) and reference. Documents follow the nodes:
a file whose path contains the query first, then by matching lines, then path. Each result carries
its total matching lines and ledger fields as count and the first three as hits; a node’s title
is its label and is never repeated as a hit. --kind keeps one kind (any node noun, or
document); an unknown kind is misuse (unknown_kind, exit 2).
The payload is {query, kind?, count, truncated, kinds, nodes, documents}. count is the number of
results within the kind filter and truncated says the page holds fewer; kinds tallies every
matched result by kind regardless of the filter. A page holds at most 200 results, shared between
nodes and documents so neither crowds the other out. Corpus
validation covers every regular, UTF-8, NUL-free file beneath the canonical .tiller/ root with a
lowercase .md extension, including generated orientation.md. Orientation
is excluded from results, but is still validated. A symlink, non-regular file, invalid UTF-8, NUL
byte, or canonical path escape makes the whole read fail as corpus_unreadable; search never returns
a partial set containing a result that cannot be opened.
The exact root-relative .tiller/board.md coordinate is excluded from the corpus. It is skipped
before opening or validating its type or content; a directory there is skipped with its subtree.
Stale contents, unreadable files, malformed bytes, symlinks, and FIFOs at that coordinate cannot
break or contribute to a read. Explicit prose requests return ordinary not found. Authored files
such as .tiller/design/board.md remain searchable, citable, and subject to normal safety checks.
Ranked next
Section titled “Ranked next”One engine answers “what are my likely next moves” for every CLI surface: tiller next, bare tiller guide, the full and correction readouts’ Next:, and the Up next: block after submit and a passing check. The console’s /api/next serves the same list, cut to the shortlist, for Home’s Up next. The engine enumerates every legal move for the reader’s checkout, ranks the moves by a fixed tiered rule that reads the reader’s own recent events, and returns them best first. Tiller never calls a language model to rank. An agent may say in its own words that it would take a different row first, but never rewrites the list. A ranked move is a suggestion, not authorization, and an explicit human instruction naming nodes overrides the order for exactly those nodes.
Candidates. The candidates are every legal move, human decisions included, not only the first match. Agent moves are the reader’s own unfinished tasks and owned shelf work (checked work first, then in-progress and claimed ready work, with tree work ahead of shelves and bugs before chores before tweaks), the reader’s own task observed in flight on a branch before its claim reached this checkout, every eligible unclaimed ready task under an approved ready or active feature (claim and start, with tiller claim task <key>), shared closeout once per reviewed unit, every frontier draft feature to specify, and decompose on a tree with no epics and a to-be design doc. Human moves carry the command tiller queue already carries: amendment, feature plan, task, and epic approvals, closeout waiting on a human, answers, attests, aggregate merges, and shelf triage. Beside them sit merge waits on in-review tasks and shelf work (merge, then tiller sync), confirmation of each pending baseline doc (tiller approve design <doc>), and the eligibility blocker on a live feature holding the reader’s own work. Epic approval offers every draft epic the Queue lists, including one with no live features yet.
Excluded throughout: work in flight on another contributor’s branch, a task held by an operative open question (the holding question is the candidate instead), work behind an unmet recorded dependency, an unclaimed shelf node, and a draft feature still waiting on unfinished work. A feature an open question holds is left to that question’s row and is not also a blocker row. A question holding a draft feature does not remove that draft’s specify row: spec feature is not question-guarded, so specifying a held draft is legal, and the holding question and the specify move both appear. This is deliberate; the question still guards the feature’s later start, check, and submit. On brownfield and legacy-migration with no confirmed baseline doc, the baseline moves are the only candidates, even when work exists: derive-baseline when no doc is registered, otherwise one confirmation row per pending doc. Those confirmations are human moves, so bare guide then suggests orient, and orient explains the pending docs rather than asking for another derivation. One confirmed doc releases the startup gate; any docs still pending remain confirmation rows beside ordinary work. Automatic uncovered-root routing is not built; baseline drift is the explicit tiller drift command.
Where the reader is working. Ranking reads the reader’s own events from the date-partitioned .tiller/events/ directories inside a fourteen-day window, newest first, without opening older partitions. Only deliberate acts count: claim, start, submit, check, spec, approve, answer, attest, add, link, reassign, and release events whose contributor is the reader, plus amendment_opened and amendment_staged. sync, migrate, contributor, join, and upgrade events never count, and an event naming more than three targets is not counted. A task event counts for its feature and epic, a feature event for itself and its epic, and a question event for the first node it blocks that still resolves. An add says where work was laid down, not where the reader is working, so it touches the added node’s parent instead: an added task touches its feature (the event’s recorded feature when the task has since been removed), an added feature touches its epic, an added question the node it blocks, and an added epic touches nothing. An add never sets the current feature or epic, so a decomposition burst leaves the reader with no current feature. The current feature is the feature of the reader’s most recent counted event, other than an add, that names one, even when a newer add touched the same feature. An act on an epic alone touches only the epic, and amendment steps touch nothing, since an amendment id maps to no feature, so neither moves the current feature. Recently touched is the features and epics of counted events inside the window, adds included, most recent first. A failed event read degrades to no events, so the list loses its personal tiers rather than failing.
Tiers. Each candidate falls in the first tier that admits it.
- Unblock nearby. Human moves that hold work in the current feature: a question holding the feature or one of its tasks, an approval, merge, or blocker on the feature, its tasks, or its epic. Any answerable question overdue or due within two days joins wherever it sits, and for every reader. Tier 1 sorts by due date first, overdue first, then questions holding the current feature. Later deadlines get no boost.
- Continue. The reader’s own unfinished work: an in-progress task, a task in
checkstill owing its submission, a claimed task not yet started, owned shelf work, and the reader’s own task in flight on a branch. - Nearby. Other moves in the current feature, then in its epic.
- Recently touched. Moves in other features and epics the reader acted on within the window, ordered by whole elapsed days since the touch, the grain the reason speaks in (
today,yesterday,<n> days ago). Touches on the same day fall through to the kind and plan order below. - Everything else, in plan order.
Inside a tier, rows sort by kind in a fixed order (baseline, continuing work, amendment review, closeout, claims, blockers, plan approval, epic approval, specify, task and shelf merges, aggregate merges, answers, attests, triage, decompose), then by each kind’s own order: execution preference for tasks (epic, then feature within its epic, then task within its feature, recorded sibling order first), Queue order for human decisions, and flow order for the frontier.
Rows. A node appears once and keeps its highest-ranked move; a move with no node is always its own row. This is deliberate, and it hides moves: the optional review attests on a task awaiting its merge are not rows, even under --all, because the task’s merge row stands for the node. They stay in Needs attention and tiller queue --kind attest_gate. Surfaces show the top five, fewer when fewer exist, and --all shows the rest. The reader’s top Continue node is always kept: when tier 1 alone fills five places, the last place goes to that node’s surviving row. Each reason says why the row sits in its tier: due tomorrow; <title>, overdue (was due <date>); <title>, holds <ref> in your current feature, in your current feature for a move on the current feature’s own node, your task is in progress, your task is claimed and ready to start, your task is in check; finish any outstanding checks, then publish and submit it (a bug, chore, or tweak names its own noun), your task, in flight on its branch for the reader’s task seen on another branch (the line itself names the branch), next in your current feature, in your current epic, in <ref>, which you worked on <n> days ago, or you worked on it today. A human decision placed by locality in tiers 1, 3, and 4 keeps its own reason, what it waits on, after a semicolon: in your current epic; its decomposition awaits your approval, in your current feature; PR #4 awaits your merge, or a question’s title after the hold it names. An agent move keeps its tier reason alone. A deadline row reads its due date, then the question’s title; today is the reader’s local calendar date. Tier 5 keeps the move’s own reason. A closeout move’s own reason is one line naming its phase, shared closeout: <phase> or shared closeout of a recovered review: <phase>, and an aggregate merge’s own reason says what it waits on: PR #4 awaits your merge, the review branch, or in review, waiting on your merge when neither is recorded. With no reader no merge is “your” merge: PR #4 awaits merge. The queue entry and Home’s Needs you card keep the full workflow advice. A reason ending on a question mark takes no extra period. A reason never repeats its own node ref or command. A checked task is never described as ready or as waiting for new publication approval, and no row prints a remote-backed bare submit lacking its actual PR number.
No reader. With no identity, the personal tiers and the current-feature holds are empty, the list falls back to plan order, and a question overdue or due within two days still ranks first. tiller next and bare guide head the list as not personalized; the readout’s Next: has no heading.
Caught up and unplanned. With no candidates, every surface says Caught up: nothing is legal to do right now. and adds a hint where one applies. When unfinished unclaimed tasks exist that no one can take, the hint is the unclaimed-work diagnostic, There are N unclaimed tasks; the first in dependency order, <task>, <why> (There is 1 unclaimed task in the singular). It names held, proposed, or otherwise ineligible work, including the operative question and Q-id for a question hold, and never offers a claim or start command. A task observed in flight is not held work, so the diagnostic leaves it out. A tree with no epics, no to-be design doc, and no pending baseline is unplanned: the list sets unplanned, and its hint names how work enters (for brownfield and legacy-migration, intake, capture, or a to-be design; for greenfield, design inputs under .tiller/design/), while every candidate is an open question or a design doc confirmation; beside live work, such as a claimed bug, there is no hint. A brownfield tree whose only move is its pending baseline is not unplanned. Owned shelf work, a proposed amendment, or a shelf merge wait on a tree with no epics are ordinary rows; decompose is a candidate only when a to-be design doc exists.
Observed execution. Ranking honors execution observed on local branches and worktrees, the same observations the console reads, so tiller next, the readouts, bare guide, and the console agree. Observations never change eligibility, which still comes from the integration tree. A task whose branch is the checkout being read is ordinary work. The reader’s own task in flight elsewhere is a Continue row that names its branch in place of any claim or start. Another contributor’s task in flight, or one whose branch records no owner, is no row. The in-review merge wait is not muted, since it names a human wait rather than work to continue. Observation is a read and degrades: when it fails, ranking proceeds on the integration tree alone. It sees only local refs and worktrees, so a teammate’s branch appears once it is fetched.
Shared closeout and ancestor gate arrivals are team asks even for identified readers; event actors and next_owner keep their existing meanings. A claim row names an actual eligible task, not a feature-wide reservation. Ranking is guidance, not proof that a later submit will pass its gates, active-basis, claim, question, or assumption guards.
Relationship queries
Section titled “Relationship queries”tiller query traverses a disposable relationship index over the current checkout. It is a read, not a query language, ranked prose search, eligibility check, or proof of implementation coverage.
# Recorded open-question targets under an epic, not effective work holds.tiller query --from E-abc123 --follow contains,blocked_by --depth 3 \ --where kind=question --where status=open --json# Done work claiming this file or a recorded enclosing scope.tiller query --from path:internal/store/write.go --follow covered_by \ --where status=done --json# Documents with literal references to the feature.tiller query --from F-abc123 --follow mentioned_by --jsonExample IDs are illustrative. The full grammar is:
tiller query --from <address> [--from <address>]... --follow <direction[,direction]...> \ [--depth <nonnegative-integer>] [--where <kind|status|owner|contributor>=<value>]... [--json]--fromis required and repeatable. Immutable work, question, and amendment IDs, unambiguous current canonical keys, and explicitdoc:<repo-relative-path>orpath:<repo-relative-path>addresses are accepted. Starts resolve and deduplicate before traversal. An unknown or ambiguous work address refuses. An indexed unresolved endpoint remains inspectable at its exact address; it never borrows a current node’s title, owner, status, or destination.--followis required exactly once. Forward and inverse names below form a sorted, deduplicated union at every hop, not an ordered sequence of stages. Unknown directions refuse; command help lists the catalog.--depthmay be supplied once and defaults to1;0inspects starts only. Breadth-first traversal visits each vertex once at its minimum distance, handles cycles, honors cancellation, and stops when no unseen vertices remain. There is no silent cap.- Repeatable
--whereclauses use exact, case-sensitive equality onkind,status, or scalarowner, plus ancestorcontributormembership, ANDed together. Missing fields do not match. Unsupported fields and malformed clauses refuse. Filters select results after traversal, never prune bridge vertices. Depth-zero starts can match too. - Results are deterministic: vertices sort by minimum depth then canonical identity; edges and their source locations are sorted. Dependency render order is not traversal distance.
The JSON data contains starts (canonical IDs), sorted follow directions, depth, where clauses as {field, value}, vertices (vertex fields plus minimum depth and boolean seed), matched IDs, canonical edges with provenance, diagnostics, and complete. Filtered-out bridge vertices and edges remain so callers can explain a match. Inverse traversal retains the edge’s canonical recorded direction rather than inventing another assertion. Human output names kind, reference, distance, relevant sources, and any incomplete facets. A valid empty answer is success.
Relationship catalog
Section titled “Relationship catalog”Each edge retains source file and field or operation, with line and criterion where available. Repeated assertions deduplicate the logical edge but retain their individual sources. Forward and reverse adjacency share those records.
| Forward | Inverse | Recorded meaning |
|---|---|---|
contains |
contained_by |
Immediate epic/feature/task containment |
depends_on |
depended_on_by |
Dependency, not implied epic ordering |
supersedes |
superseded_by |
Feature, task, bug, or chore replacement lineage; reciprocal fields reconcile into one edge without repairing contradictions |
attached_to |
attached_work |
Bug/chore’s found-in work |
resolved_by |
resolves |
Shelf item’s carrier |
duplicate_of |
duplicates |
Duplicate shelf item and survivor |
blocks |
blocked_by |
Question’s recorded targets, regardless of status |
assumes |
assumed_by |
Feature’s question assumptions |
has_document |
document_of |
Node-owned prose or eval document |
covers |
covered_by |
Work surface, baseline description, or path-shaped acceptance binding, distinguished by source |
mentions |
mentioned_by |
Literal Markdown work-ID citation |
amends |
amended_by |
Identity-safe amendment operation endpoints and application evidence |
triggered_by |
triggers |
Design documents prompting an amendment |
amends_design |
amended_by_design |
Design documents an amendment calls for updating |
implements |
implemented_by |
Recorded ADR reference, not an ADR lifecycle node |
sourced_from |
source_for |
Structured intake and supported local source citations |
derived_from |
derivation_of |
Baseline source path and pinned blob revision |
preserves |
preserved_by |
Feature-to-path criterion-scoped baseline surface/artifact reference, role and revision retained |
at_path |
document_at |
Document’s file coordinate, not coverage |
Documents retain their own doc: identity even when owned by work. Local path: and doc: coordinates are literal filenames, slash-separated and segment-normalized: filesystem-valid punctuation such as #, ?, or : inside a segment is part of the name, so notes#1.md and notes?1.md are two distinct documents, and ordinary coordinates are unchanged. Citation locators (#L<n>) and URL encoding are separate syntax applied at their own boundaries, once; an HTTP caller percent-encodes a coordinate in the query string or /file/ segment exactly once. Empty, absolute (including a drive-letter root), URL-shaped (scheme://), control-character, and root-escaping coordinates have no identity: a document with one is reported as an invalid_path diagnostic and never becomes an edge endpoint. Representing a coordinate grants no read: the hidden-path rule still decides what the console serves. external_path: coordinates stay separate and never alias local paths by suffix. Path identity does not assert present existence. A well-formed local path can start a query even when not already an endpoint: exact claims and enclosing scopes match on segment boundaries, so internal/store covers internal/store/write.go, not internal/storage.go. An enclosing match is derived and preserves the original scope. Reads do not enumerate the repository, expand directories or glob-like values, invent task scope from feature scope, or infer dependencies from shared files.
Coverage is a recorded declaration, never verified implementation. Eval contracts are parsed, not executed. Only test binding.ref values containing a forward slash are path-shaped, and a ref list is read item by item (one covers edge per path-shaped item, with criteria[n].binding.ref[i] as its field); bare test IDs and command/manual/eval refs remain classified metadata. Raw refs, opaque selectors, and rubric text survive; shell run strings are not parsed, and a run override does not erase the declared ref. Malformed, unavailable, or physically misowned contracts produce diagnostics rather than borrowing another feature’s criteria.
Mentions scan the supported lowercase .md corpus under .tiller/, including orientation.md, which is labeled generated. A root .tiller/board.md contributes no automatically discovered mentions or available file destination. Explicit authored citations to it remain unresolved, with their provenance and normal diagnostics intact. Exact uppercase E-, F-, and T- IDs use the renderer’s boundaries, including a standalone backticked ID but excluding commands, code blocks, existing links, URLs, and path fragments. The edge starts at the document and retains each source line; no synthetic work-to-work citation doubles it. Unknown IDs are unresolved mentions. Keyword search keeps its generated-result exclusion.
Recorded blocks is not an effective hold: status/readout remain authoritative for inherited blockers, assumptions, and legal moves. Soft order stays ordered container metadata, not a dependency. Unambiguous legacy dependency keys normalize consistently for forward/reverse reads, sibling ordering, and feature flow; they can change a legacy checkout’s rendered order without rewriting state or changing lifecycle gates.
Amendment relationships retain operation number, endpoint role, proposed/applied state, evidence, and direct versus ancestor-summary scope. A proposal does not create applied supersession. Historical ambiguity or a removed/reused key never resolves to today’s holder as a fallback. Provenance resolves only supported coordinates, not paths guessed from arbitrary sentences. Opaque ADR names, citations, and unsupported coordinates remain source-scoped unresolved references. Distinct revision pins retain distinct evidence; opening a current file does not display or verify its pinned historical bytes.
Local source citations accept both path#L3-58 and path#L3-L58, with positive, ordered line bounds. One citation may list several ranges of the same document after commas (path#L3-L58, #L321 or path#L3-L58,L321); it links to that document once, anchored at its first range. Citations across several documents are not one citation. Extraction preserves the original source string and does not guess paths from arbitrary prose. Unsupported syntax remains distinct from a supported citation whose target is missing; neither grants a destination that confined reads have not established.
Revision evidence omits an observation time when none was recorded. Recorded times retain their precision; reading a pin never invents a timestamp.
Owner IDs, arbitrary URLs, shell commands, Git pins, historical surface inventories, and node-local gate/criterion/attestation/evidence references are metadata, not automatically vertices. Human design frontmatter is prose, not an authoritative relationship catalog. These are read projections with no new persisted IDs, schema migration, or on-disk index.
Completeness, failures, and read lifetime
Section titled “Completeness, failures, and read lifetime”complete describes extraction across the request’s index, not whether every recorded reference is valid. A successfully indexed dangling assertion is a diagnostic but does not by itself make the read incomplete. An unwritten acceptance contract or an owned draft with no criteria is normal pre-specification state and emits no contract diagnostic. Malformed, unreadable, misowned, or otherwise invalid contracts, unavailable history, and ambiguous historical identity retain complete: false with the affected facets named. Unsafe or unreadable corpus files and an unstable Tiller mutation generation fail a query, never a partial success. This distinction is visible in both human and JSON output.
Diagnostics may include sorted, distinct subjects naming their related vertex identities. Ordinary reference warnings follow the reached vertices, including bridges excluded by terminal filters. Index-wide warnings and every explanation for incomplete extraction remain visible even when their subjects are outside the traversal: missing input can hide an incoming relationship. The map retains all diagnostics; node backlinks scope ordinary warnings to the node and its owned documents. The console read policy permits incomplete supplemental enrichment on node and map pages, without relaxing query refusal.
Each diagnostic may expose incomplete: true when it explains extraction loss; omitted or false means that diagnostic alone does not establish incomplete extraction. Consumers use this field, not their own list of diagnostic codes, to retain scan-wide warnings while filtering subjects. complete: true means extraction completed, not that every reference resolved or the project is healthy.
GET /api/query (also HEAD) takes repeated from, comma-separated follow, depth, and repeated where parameters with the same parser and traversal. Its success payload is exactly the CLI envelope’s data, like /api/search. Syntax errors are HTTP 400 / CLI exit 2; missing starts are HTTP 404 / CLI resolver-error exit 2; environment/read failures are HTTP 500 / CLI exit 3. The CLI retains the ordinary {ok, command, data|error} envelope.
Stable argument reasons include query_from_required, query_from_invalid, query_follow_required, query_follow_invalid, query_depth_invalid, query_where_invalid, and query_syntax. Malformed doc:/path: addresses use query_from_invalid; well-formed but unindexed document/work addresses use not_found, and ambiguous keys use ambiguous_key. A valid unindexed local path instead receives a virtual start for recorded scope matching. query_read_failed covers otherwise untyped supplemental-read and cancellation failures; existing store failure reasons remain intact.
Every CLI invocation or HTTP request builds and discards its own index after a fresh tree load, shares supplemental inputs within that read, and performs the final generation-stability check. There is no long-lived tree or relationship index, watcher, filesystem index, identity resolution/write, event append, binding execution, or Git-index refresh. The console’s process caches (the console read cost) reuse unchanged file bytes and parse results, but never skip a directory listing or a per-file identity check. Unjoined checkouts can query. Later requests see prose edits and branch changes without cache reset; the generation check detects Tiller mutations, not an atomic snapshot against arbitrary editors or Git changes. Structural-only ordering does not depend on readable prose, eval contracts, or event history.
Markdown mention parsing and unsupplied acceptance-contract parsing reuse process-local results keyed by SHA-256 of freshly read content. Every request still walks and reads the confined corpus, including current replacement, symlink, permissions, deletion, and displayability checks. File metadata is not a content identity. The mention cache retains only work IDs and source lines, not document bytes, Markdown trees, paths, ownership, or resolved relationships. The contract cache retains parsed models or parse failures and returns request-owned copies; supplied observations keep their existing precedence and never enter the cache. Current paths, tree identities, validation, diagnostics, history, and ordering are projected anew.
Each parsing cache has a 2,048-entry limit and an 8 MiB conservatively accounted retained-data budget, with least-recently-used eviction. Oversized documents and parsed values bypass retention. Results live only in the process until eviction or exit, with no persisted index or reset requirement. Cache access is synchronized; parsing and contract copying happen outside the lock, so concurrent cold misses may duplicate work without sharing mutable models. These limits cover retained cache data, not active request allocations. Separate CLI processes do not share results and still parse their inputs.
Querying never refreshes instructions, installs a binary, replans accepted work, or updates another project.
tiller serve
Section titled “tiller serve”tiller serve [--host [<ip>]] [--port <n>] [--open=false] launches the read-only web console over the current working tree with bounded task execution reads from validated local worktrees and locally available branch tips. Omitting --host uses 127.0.0.1; passing bare --host uses 0.0.0.0, exposing every IPv4 interface. Both use port 4400 unless --port overrides it; --port 0 requests an available ephemeral port. Explicit hosts work as --host <ip> or --host=<ip>. The /api/board streams retain their name and payload, with task activity supplied by the console’s execution-source policy, not a Markdown status file. Navigation and manual refresh reread those sources; remote-tracking refs are only the last externally fetched observation. CLI eligibility remains checkout-local. Console writes and automatic watcher refresh remain unbuilt. The designed write buttons would map one-to-one to CLI commands. Current behavior and deferred UI scope are specified in ui-surface.md.
Once the port is bound, serve opens the console’s address in the developer’s default browser and prints the address either way. Opening is a convenience, never a gate: when no browser can be started, serve prints one line saying so and keeps serving. Three settings turn it off, most specific first: --open=false on the command line; console: { open: false } in the git-ignored per-checkout .tiller/local.yml; and BROWSER=none in the environment, the conventional signal that this shell never wants a browser. An explicit --open beats a file that says no, and the file beats the environment. A local.yml that is present but cannot be parsed refuses before the port is bound, local_settings_unreadable, the same posture a corrupt identity file gets.
--host accepts bare IPv4 or IPv6 literals: loopback, wildcard, and global-unicast addresses, including private IPv4 and IPv6 unique-local addresses. Specific addresses must be assigned locally for the bind to succeed. An explicitly empty value, hostnames (including localhost), brackets, embedded ports, zones, IPv4-mapped IPv6, multicast, limited broadcast, and link-local addresses are invalid. Scoped and link-local serving are unsupported. Serve does not resolve DNS. IPv4 uses tcp4; IPv6 uses tcp6. 0.0.0.0 listens on all IPv4 interfaces; :: listens on all IPv6 interfaces, not both families.
The accepted request Host set follows the bind:
| Bind | Accepted names and IPs |
|---|---|
Default or explicit 127.0.0.1 |
localhost, 127.0.0.1, ::1 |
| Other explicit loopback | The default set plus the bound IP |
0.0.0.0 |
localhost, 127.0.0.1, and usable IPv4 addresses on up interfaces |
:: |
localhost, ::1, and usable IPv6 addresses on up interfaces |
| Specific non-loopback IP | Only the bound IP |
Wildcard addresses are enumerated once at startup, filtered to the listener family and supported address classes, deduplicated, and sorted numerically. Unspecified addresses are excluded; the family’s standard loopback address is retained. New interface addresses require a restart; removed addresses can remain admitted until restart. This is an address allow-set, not a client allow-set. Machine hostnames and DNS aliases are not accepted. Request authorities require bracketed IPv6 and, when supplied, a decimal port from 1 through 65535. Canonical IP equivalents and case-insensitive localhost are accepted; mapped addresses, zones, shorthand, userinfo, whitespace, trailing dots, and malformed authorities are rejected.
On every bind, GET /api/recap advances the last-visit mark only when the request carries the console’s X-Tiller-Visit: 1 header, which a cross-site page cannot send without a CORS preflight the server never answers. An unmarked request receives the same digest against the current horizon and leaves .tiller/.cache/console.json untouched. The recap contract owns the visit semantics.
Default and explicit 127.0.0.1 retain the ordinary serving URL and browser destination. Other loopback and specific network binds print and open the bound IP URL. Wildcard mode prints the actual listen address separately from deduplicated admitted IP URLs, never a wildcard URL or a remote DNS destination. All URLs use the actual bound port, including ephemeral ports, and bracket IPv6. The wildcard opener uses http://127.0.0.1:<port> for IPv4 or http://[::1]:<port> for IPv6; no opposite-family URLs are advertised.
tiller serve --host --open=falsetiller serve --host 192.168.1.50 --port 4400 --open=falsetiller serve --host :: --port 0 --open=falseUse a locally assigned address in place of 192.168.1.50. Non-loopback modes warn before browser opening that the planning ledger, the .tiller/ Markdown corpus, Markdown documents the ledger cites, and gate evidence files the ledger records are readable by anyone who can reach the port, without authentication or TLS. Other checkout files, such as uncited Markdown, dot directories outside .tiller/ or below it such as .tiller/.cache/, and gitignored secrets, are not served. Use only a trusted network or private VPN, never direct public-internet exposure. Wildcard binds expose every interface of the selected family, including other connected networks; choose a specific interface IP when only one network is trusted. Forwarded headers and reverse proxies do not establish trust.
Invalid hosts refuse before binding or opening a browser with argument reason host_invalid, exit 2, and supported literal examples. Repository and local-settings preflight rules remain unchanged. Wildcard interface or address enumeration failure refuses with environment reason host_interfaces_unavailable, exit 3; specific and loopback binds do not require enumeration. Bind failure remains port_unavailable, exit 3, and reports the full attempted address and underlying error. Choose a locally assigned --host, another --port, or stop the process holding the port. Startup failures after binding close the listener; context cancellation shuts the server down.
Its shared source-citation reader accepts GET or HEAD at /api/prose?path=<repo-relative-lowercase-.md>&line=<positive-1-based-line> and returns {path, content, line, line_text}. This direct reader serves both unowned search results and node provenance. Its scope is the .tiller/ Markdown corpus plus any other Markdown document a ledger record cites as a local source: a node’s source, or an amendment’s triggered_by, amends_design, implements, source.path, or operation source. Those are exactly the documents the relationship index can give a file destination. Any other path, such as an uncited README.md or a .claude/ note, is not found. A hidden path is never served even when cited: the .tiller/ root is the only admitted dot-prefixed segment, so a cited .notes/plan.md, docs/.drafts/plan.md, or anything beneath the gitignored .tiller/.cache/ is not found too. The relationship index applies the same hidden-path rule, so it never grants such a citation a file destination; it reports unresolved_document instead and the citation stays copyable. Both answers are decided before disk is read, the same whether or not a file exists there. The scope is the same on loopback and network binds. For provenance, a local design/... citation resolves on disk beneath .tiller/design/..., .tiller/... resolves beneath .tiller/, and every other cited local path, such as docs/..., resolves beneath the repository root. The response and /file/... address retain the human-authored path spelling. A citation range is represented by requesting its first line; the stored #L<n> or #L<n>-<m> locator remains unchanged.
Every direct read rejects an empty path, a path or file containing NUL, an absolute path, traversal or any other non-canonical path segment, an extension other than lowercase .md, a symlink at the leaf or any parent segment (whether or not its target stays inside the repository), a non-regular file, and invalid UTF-8. The file is opened segment by segment through the same symlink-refusing reader the store uses, so a served address only ever yields the regular file it names. These rules apply before or while reading so a FIFO or other special file cannot block the server. A missing safe file or requested line returns not found; malformed path or line input, and a path through a symlink, is caller misuse (400), as for evidence; a file larger than 8 MiB is too large (413); any other unsafe or undisplayable target, such as a non-regular file or invalid UTF-8, is a server read failure.
The evidence reader accepts GET or HEAD at /api/evidence?ref=<repo-relative-path> and returns the file’s bytes as text/plain with X-Content-Type-Options: nosniff, so captured output never renders as HTML. It serves only a ref that some node in the loaded tree records verbatim: gate or criterion evidence, a task’s or shelf item’s own evidence list, or an attestation’s or policy violation’s evidence_ref. These are the refs the console links. An empty, absolute, traversing, or dot-segment ref is caller misuse (400) even when recorded, and so is a recorded ref with a symlink at the leaf or any parent segment, whether or not the link stays inside the checkout: the reader serves only the regular file a ref names, never an alias’s target. An unrecorded ref is not found (404), decided before disk is read, and so is a recorded ref with no file. A non-regular file is a server read failure, and a file larger than 8 MiB is too large (413).
The server bounds slow clients on every bind: 10 seconds to read request headers, 30 seconds to read a whole request, 2 minutes to write a response, and 2 minutes for an idle keep-alive connection. No endpoint streams.
Explore search remains narrower than direct provenance reads. tiller search indexes only the canonically contained .tiller/ Markdown corpus specified above. A valid node citation outside that corpus can be opened directly through /file/... and /api/prose, but it does not become searchable.
Command reference
Section titled “Command reference”| Command | Drives | Actor | Touches |
|---|---|---|---|
tiller init |
creates .tiller/ |
human | writes manifest (empty roster) + skeleton, generates orientation.md |
tiller add contributor |
registers a person on the roster | human | appends or updates the contributor in the manifest’s contributors roster |
tiller remove contributor |
retires an eligible roster entry | human | refuses self-removal, unfinished assignments, or open questions; preserves historical attribution and contribution |
tiller join |
selects identity and wires this machine to the project | human | writes the gitignored .tiller/identity.yml, installs skill shims, writes the pointer block |
tiller doctor |
unbuilt design: verify setup | either | would check launcher, harness wiring, contributor identity, and gate state |
tiller check readiness |
(gate) | either | reads inputs + manifest |
tiller link design / tiller unlink design |
(baseline registration or provenance) | agent or human | updates ledger and events, never prose |
tiller approve design <doc>... |
(baseline confirmation) | human, explicitly delegable | atomically confirms named docs against working-tree code and one project HEAD |
tiller add epic|feature |
to draft |
agent or human | creates node files |
tiller add task |
to proposed |
agent | creates the task folder |
tiller add bug|chore |
to proposed |
agent or human | creates the shelf folder with its contract stub, and the shelf itself on first use |
tiller add question |
creates | either | creates the ledger entry; add adr and add design remain unbuilt design |
tiller link |
(records depends_on / surface / attached_to) |
agent or human | unions resolved targets into depends_on before affected work starts; unions paths into surface.paths through its separate surface arm; sets a bug’s, chore’s, or tweak’s attached_to through --attach, at any state |
tiller order |
(records the soft order) | agent or human | replaces the named parent’s recorded order list: root .tiller/order.yml (epics), the owning epic’s epic.yml (features), the owning feature’s feature.yml (tasks), and each shelf’s own order.yml at shelf grain (order bug, order chore, order tweak), which has no parent to name |
tiller spec feature |
draft to specced |
agent | validates the stage-2 output, sets state; a re-run on specced re-validates |
tiller approve |
start gate to ready (epic; feature plan with its initial tasks; task; bug, chore, or tweak, which is triage; amendment to approved); one or more nodes, or a feature’s proposed tasks via --feature, refusing a specced feature or proposed task while an open question holds its plan (open_question_blocker); or refreshes an accepted basis; or reviews an answered assumption in; or records the review grain in the same act (--review-grain, epic or feature only) |
human only | sets state, accepted revisions plus the HEAD commit anchor, the assumption’s answer revision, or review_grain; one event per node |
tiller configure |
records review_grain at the project, epic, or feature home; no lifecycle transition |
human only | writes execution.review_grain in the manifest, or review_grain on the node’s .yml; one event |
tiller return |
ready to specced (feature), ready to proposed (task, bug, chore) |
human only | reverses mistaken pre-work gates with a reason |
tiller start task|bug|chore |
ready to in_progress |
unit owner | permanently sets started ownership and records its branch; task start atomically activates its ready feature and initializes applicable aggregate identity, or records an existing ref at the derived aggregate name with --adopt-aggregate (task only; agents may pass it) |
tiller claim / release / reassign |
pre-start reservation on task, bug, or chore | owner for claim/release; human-directed reassign | changes only the named nonterminal unstarted unit |
tiller transfer task|bug|chore |
cancel original; create ready successor | human, explicitly delegable for named work and recipient | preserves approved scope/history and operative relationships; fresh identity/basis, no inherited review/evidence |
tiller check |
records gates; task, bug, and chore to check on green |
agent or human | validates active basis, runs gates (reusing a pass on unchanged code unless --rerun; --gate limits the run), records results; a shelf node reads the manifest’s on list at task grain and never runs the evals gate |
tiller eval |
(feature/task contract gate) | agent or human | runs mechanical criteria (reusing a pass on unchanged code unless --rerun), emits rubric packets, records rubric evidence; no bug, chore, or tweak form |
tiller attest |
(optional gate on feature, task, bug, or chore; criterion on feature or task) | human | records human attestation in the target node’s existing attestations list |
tiller submit |
task, bug, and chore to in_review; feature/epic record-only review |
agent or human; unit owner for task/shelf | preserves basis, question, assumption, ancestry, readiness, and review guards; aggregate actors need no task claim |
tiller resume |
task, bug, and chore in_review to in_progress |
agent or human | keeps the PR, stales gate results whose code identity changed, records the reason |
tiller sync |
task, bug, and chore to done (observed, or carried for a bug, chore, or tweak with a done carrier); feature and epic to done (rollup) |
either; --accept-fast-forward human only |
observes git, records rollups |
tiller block / unblock |
unbuilt design: blocked flag |
either | would set flag + reason |
tiller cancel |
to cancelled |
human | sets state; records the required reason, plus the required --resolution and, on a duplicate, --duplicate-of, on a bug, chore, or tweak |
tiller resolve bug|chore |
records or clears the carrier | human, or agent on explicit instruction | sets resolved_by; no status change |
tiller archive |
done to archived |
human | sets state |
tiller add amendment |
opens a reshape | agent or human | creates the amendment’s .md and .yml, records base_revision from HEAD |
tiller intake <file> |
opens a reshape, sourced from an external document | agent or human | copies <file> into .tiller/feedback/, fingerprints the landed copy, opens the amendment carrying it as source |
tiller amend |
stages one reshape op, or an amends_design addition |
agent or human | records proposals on the amendment; touches no other node |
tiller approve amendment |
applies the reshape | human only | validates every precondition, then executes the whole batch under one lock, re-pins each revised basis, and re-pins the eval leg of each corrected done feature |
tiller withdraw amendment |
drops a proposed reshape |
human | sets state; records the required reason on the event |
tiller answer question |
open to answered |
human | records answer + answer revision, clears computed blockers, including a plan gate’s hold, surfaces dependent drift |
tiller cancel question |
open to cancelled |
human | records cancel_reason, clears computed blockers as an answer does, writes no answer or revision and raises no drift; prints the cancel-question readout |
tiller link question / tiller unlink question |
(adds / removes blocks targets) |
agent may link, human unlinks | widens or narrows an open question’s blocks; records link.question or unlink.question |
tiller unlink |
(removes depends_on / surface / attached_to) |
agent or human | subtracts resolved dependency ids before affected work starts; removes literal surface.paths entries through --surface until the epic or feature is done; clears a bug’s, chore’s, or tweak’s attached_to through --detach, at any state; an absent edge, path, or attachment is a no-op |
tiller rename |
(display or proposal address correction) | agent or human | changes YAML title at any state, or moves a proposal-safe folder while preserving id |
tiller remove |
(proposal deletion) | agent or human | journal-deletes an unapproved proposal and cleans references, preserving one removal event |
tiller show / ls / status |
(read) | either | reads state |
tiller queue |
(read) | either | filters and renders current Queue entries without recomputing eligibility |
tiller drift |
(detect) | either | sweeps the enabled checks; candidates and uncovered listed separately; exit 0 asserts coverage |
tiller next |
(read) | either | ranks the legal moves for this checkout from the tree, local branch observations, and the reader’s last fourteen days of events |
tiller guide |
(surfaces skill text; bare, the ranked list and a skill suggestion) | either | reads the skills; bare guide reads what tiller next reads |
tiller readout |
(surfaces the seam block) | either | reads the tree, git, readiness, and the reader’s recent events; renders the block ending on the ranked list |
tiller search |
(read) | either | lookup by id, key, or reference, then keyword search over node fields and safe regular-text Markdown under .tiller/, skipping dot-prefixed entries such as .tiller/.cache/; one ranked result per node or document |
tiller query |
(read) | either | traverses recorded relationships with provenance and explicit extraction completeness; never writes an index |
tiller commit state |
unbuilt design: commit | human | would commit reviewed .tiller/ changes |
tiller serve |
(serve) | human | reads working tree |
tiller usage |
(captures agent usage) | either | reads this machine’s harness logs for this project’s sessions; creates or grows .tiller/usage/ files and the gitignored cursor; records no event and needs no identity |
tiller upgrade |
binary and current-checkout maintenance | human | installation-owner update, supported migration and generated refresh; explicit project-only and binary-only scopes |
tiller migrate |
explicit local migration and refresh | human | source schema/event when needed, then generated refresh without joining; failed refresh is incomplete |
Who may run what
Section titled “Who may run what”The schema’s consolidated lifecycle, re-expressed as commands. The invariant holds across the tree: the agent can take work right up to the edge of done, only the human’s merges move anything to done, and every crossing above a task is arithmetic over those merges, recorded by sync.
block entries below describe the unbuilt command design, not currently available verbs.
| Node | Agent may run | Human only | Observed (neither) |
|---|---|---|---|
| Epic | add (epic + its features), block |
approve to ready, cancel, archive |
done at rollup, via sync; recomputes |
| Feature | add, spec, check, authorized aggregate submit, block |
approve to ready (initial tasks included), return before start, attest, sync --accept-fast-forward, cancel |
active at first successful task start; done at rollup via sync |
| Task | add (to proposed), pre-start claim/release, owner start/submit/resume, check |
approve to ready, return before start, reassign, explicit transfer, attest, cancel |
done at merge via sync at resolved review grain |
| Bug, chore | add (to proposed), pre-start claim/release, owner start/submit/resume, check |
approve to ready, pre-start return/reassign, explicit transfer, resolve, attest --gate, cancel with resolution, sync --accept-fast-forward |
done at own or carrier merge via sync |
| Amendment | add amendment, intake (opens one sourced from an external document), amend (stages proposals, including the destructive ones) |
approve (validates and applies the batch), withdraw |
Attestation-backed review gates on features, tasks, bugs, chores, and tweaks, plus manual criteria and required eval criteria on features and tasks, are human attestations. An agent may record rubric input/output as evidence, but may not attest it. An amendment is how structural change to existing nodes happens: the agent stages the operations, and the human’s approve amendment is the single act that executes them, including the cancels and supersessions the agent may never perform directly. approve amendment and withdraw amendment sit in the ordinary human-only class rather than the strict one, so the delegation rule in The actor model applies to them unchanged: the human’s decision, not the human’s keyboard.
tiller add contributor carries the identical delegation contract, outside the node table above because it touches the roster rather than a node: it is human-only, and an agent runs it only on an explicit human instruction naming both the id and the display name, states what it is about to register, and echoes back what it recorded. Putting a person on the roster is never a judgment call an agent makes on its own, the gap the split from join closes.
tiller remove contributor retires a registered person, explicitly human-directed. It refuses self-removal and any unfinished task/bug/chore/tweak assignment or open question held by that owner. Pre-start reservations use release/reassign; started work needs explicitly authorized transfer or completion, never relabeling. Historical contributions do not prevent retirement and remain displayed with ID fallback. Event/action attribution and question asker semantics are unchanged.
tiller resolve joins the approve class, and for the same reason: recording that another node’s merge fixes a bug is a judgment about the work, not a keystroke. A human runs it, or an agent does on an explicit instruction naming both the bug and the carrier, stating what it will record and reporting what it recorded. An agent that notices mid-task that its work fixes a bug says so in its summary and does not resolve it unbidden. cancel on a bug, chore, or tweak stays where cancel already is, human-only, which is also why an agent cannot propose one: amend has no cancel operation reaching a shelf node, so the agent says so in prose and the human runs the command.
tiller configure and approve --review-grain carry the same delegation contract as the start gates: setting a grain is the human’s decision, not the human’s keyboard. An agent runs either only on an explicit human instruction naming the target and the grain, states what it is about to record before running, and reports what it recorded after. Neither sits in the strict human-only class beside attest, sync --accept-fast-forward, and the merge; both are ordinary human-only guidance the agent may execute on instruction, same as approve itself.
Decisions settled so far
Section titled “Decisions settled so far”Each line is a settled decision; the owning section above carries its full contract, and the design history explains the reasoning.
- The grammar is verb-noun with a required, validated noun, so a noun that mismatches the id is caught as a checksum failure.
- Exit codes separate a business answer (
1) from misuse (2) and environment (3), and every--jsonoutput uses one envelope. - Tiller records branches, PRs, and review refs and observes local Git, but never creates a branch, pushes, opens a PR, or merges.
- Creating a project (
init), registering a person (add contributor), and selecting a checkout’s identity (join) are three separate acts; only registration adds to the roster. - An identity resolved from a fallback names itself in the command’s output.
- Human-only gates are guidance, not enforcement: an agent may record a named start approval on explicit instruction, never an attestation, a fast-forward assertion, or the merge.
- A remote-less project records branch-and-commit review refs instead of invented PR numbers.
- Readouts are text the CLI renders, not agent-composed summaries, so every harness reports the same account.
- A task reaches
doneonly through an observed human merge; feature and epicdoneare recomputed rollups that reopen when new work arrives, and there is noacceptverb. - Drift detection reports candidates and refuses work only through the
active_basisandanswered_questionguards. - Structural change to accepted work is staged forward work in an amendment applied by one approval; display titles and unapproved proposals are corrected directly.
- Soft order is recorded intent that steers ranking and rendering, never a dependency.
- An open question that needs no decision is cancelled with a reason, releasing its holds without recording an answer.
- The ranked next list comes from a fixed tiered rule over recorded state and the reader’s own events; no model call ranks it.
migrateis the tree-side remedy andupgradethe binary-side one; refreshing instructions is not a data migration.- Managed-project CI never depends on Tiller, so there is no Tiller CI status command.
- Agent usage is captured by
tiller usageand best effort insidesync, never by asking the agent. It is off untiltiller.ymlturns it on, and a capture failure never fails sync or changes what sync records. - Sync’s capture keeps sync fast: it works from a cursor, holds the mutation lock only to write, stops at a 2-second budget, and with nothing new reads neither the tree again nor the event stream.
What’s next
Section titled “What’s next”This is the command layer. The skill set defines agent reasoning and the console contract defines the local web surface. The roadmap lists the designed commands that are not built yet.