CLI¶
python3 factory.py [MODE] PROJECT. The command line is parsed, not
indexed, so --help is safe. Modes are mutually exclusive; the project is
always the repository path, except in the host forms. python3 factory.py project VERB is a separate
command family, below.
| Invocation | Does | Touches |
|---|---|---|
factory.py PROJECT |
runs the loop: claim, work, verify, review, merge, repeat; exits on an empty board or a failed run; re-execs after a self-merge. Under [loop] workers > 1 it is the scheduler of a pool of --worker children instead (see The loop) |
Linear, store, worktrees, main, FINDINGS.md |
--report PROJECT |
prints the estimate-vs-actual table and the supervisor liveness line, then a failures KIND: N line per failure kind among the failed runs (unclassified for one with no kind), a blast radius: high H · medium M · low L line counting each run's newest blast_radius event (left out when no run has one), a complexity classifier: section with one row per ended run carrying a complexity event (its verdict and confidence, the profile and its source, pass, fail or - on its ticket's first attempt, outcome, review rounds, working minutes and any shadow's verify and round-1 verdict) and a complexity VERDICT: N runs · first-attempt pass P/F · median rounds R · median min M · shadowed S (verified V, round-1 approvals A) line per verdict in the order small, medium, large, none (the shadow part left out with no shadowed run, the section left out with no complexity events), an adversary FAMILY: P passes (full F · light L) · S subagents (MODEL N · ...) · reproduced R · traced T · concerns C · blocked B · M min line per adversary family with a pass, in the order claude, codex, configured, fallback, counting the subagents each pass reports starting by model (highest count first, the list left out at 0 subagents), its blocking findings by evidence level, its concerns, the passes with a blocking finding and the passes' minutes, then a consolidation: N rounds · M merges · H held concerns · U unavailable line over the consolidation events, U counting unavailable and malformed second passes (each kind left out when the store has no such events), a toil 24h: and a toil 7d: line counting the human interventions and merges in each window, then an interventions 24h by actor: maintainer N, operator M, supervisor K, unknown U and a 7d line counting every intervention in the window, migrate rows left out, by who acted (unknown for a row written before the store recorded an actor), a gap layers: line counting the gaps whose latest recorded layer is each of the six, in ladder order, and a gaps found: witness N, operator M line counting them by who found them; when the store holds a story, a Stories: section follows with each story's time from plan to approval and from approval to close (whole hours below two days, days and hours above, - for a step not taken), its children merged and abandoned, the interventions on its children's runs per child merge (one decimal, not applicable with none merged) and each witness's first green commit |
store, read-only |
--sweep PROJECT |
prints what an acting sweep would do; lists stray review containers; a failures KIND: N line per failure kind among the failed runs |
store (sightings only) |
--sweep --act PROJECT |
fails tripped runs, releases their leases, removes stray containers; retires finished tickets' worktrees and local branches; prints the same failures KIND: N lines |
store, Docker, git |
--board-diff PROJECT |
prints one line per board-owned field (title, body, priority, labels, column) where the store's row differs from the board's ready listing, one per listed issue with no store row, one per ready row with no live run the listing no longer names (in [board] mode = "store" only while its column is ready, since a store-mode ticket leaves the queue by its column), then a summary; exits 0 with none, 1 otherwise; a project with no [board] table exits naming the key |
Linear (read), store, read-only |
--status [--json] PROJECT |
prints what the factory is doing now: projects, live and parked runs, one stuck KEY: waits on DEP, which needs a spec: REASON (N queued behind) line for each ticket waiting on a dependency only a person can move (stuck in the JSON), two lines for each open story (state, generation, children merged/running/frontier/waiting, each witness's latest verdict at the last ledgered commit, errors, open decisions, open proposals, age, then one line per open proposal: proposal pN from KEY-m: TEXT), ready tickets, schema, lock holders; a live run's JSON row counts the holo steer notes no turn has carried yet as steer_pending; --json prints it as one JSON object |
store, read-only |
--import-store PATH --dry-run PROJECT |
opens the store at PATH and the project's own read-only and prints, per table, the rows an import would move, their id range, the offset a remap would add and a sha256 of the rows; refuses stores at different schema versions; --dry-run is required |
store, read-only |
--board-import [--dry-run] PROJECT |
copies every open issue of the project's Linear board, Backlog included, into its store by board id in one transaction, before the project moves to the native board: a row the store holds keeps its id, runs, ledger and dependsOn, a new one takes the issue's column and open blockers; prints [holo2] KO-n: new, changed (its revision moved) or unchanged per issue, then [holo2] board import: N new, M changed, K unchanged; P pushes and Q notes pending for Linear; a failure part-way rolls back and running it again is the restart; --dry-run prints the same and writes nothing; [board] kind = "native" is refused with exit 1 before Linear is asked |
Linear (read), store |
--supervise PROJECT |
the acting sweep every sweep_interval_sec, under the project's supervisor lock; runs the code it started with, and exits for its service manager to restart when a newer build has stamped the store; refused for a project host.toml lists, which the host sweep watches |
store |
--serve PORT PROJECT |
the JSON daemon on loopback (--serve 7710 binds 127.0.0.1:7710), which also serves the console at / from the built bundle; it reads by default and writes only through two opt-ins, [serve] actions (POST /actions/...) and [serve] config_edit (PUT /config) (The daemon's actions); --serve HOST:PORT binds the named address instead, and a non-loopback bind demands [serve] token_file, whose contents every JSON request but /peers must present as a bearer token (/, the console's files and /peers stay open; a loopback bind, 127.0.0.1:PORT included, ignores the key for reads, but either write opt-in demands [serve] token_file on every bind, loopback included, and the routes it opens answer only to the bearer) |
store, read-only by default; with [serve] actions the store and the systemd units, with [serve] config_edit the project's config.toml |
--requeue KO-n --note TEXT [--force] PROJECT |
walks a failed ticket back to ready with an interventions row. A ticket already relaunched twice against one failure is refused: two requeue rows, from any source, on its runs that ended failed or rejected, recorded since its newest human intervention other than a requeue (an abort, a redirect, an operator note). A drift requeue, whose run ends abandoned, is not one. The refusal exits 1 naming the two newest relaunched runs and writes nothing, because a third failure needs a diagnosis rather than another run; --force with the diagnosis as the note requeues it anyway and records the note as forced past N relaunches: NOTE, and each further relaunch needs --force again until a human acts on the ticket otherwise |
store |
--approve KO-n [--note TEXT] [--force] PROJECT |
releases a ticket parked by [merge] approve = "human": an interventions row with action approve, the parked run ended with its resume point at the merge gate, the ticket walked to ready; the loop's next claim reuses the preserved worktree and branch, re-runs the pre-merge verify and merges with no implementer or reviewer -- under [merge] mode = "pr", babysits the pull request once more and merges it through the API when green and quiet. Under [merge] approve = "human", a run parked on a pull request is first read from GitHub with the readiness check the daemon's POST /actions/merge uses: not ready, including GitHub unreadable (github_unreadable), it exits 1 naming the ticket and the reason (KO-n: review not approved) and writes nothing; --force, which requires a non-blank --note, releases it anyway and records the intervention's note as forced past readiness: REASON; NOTE. A run parked with no pull request, or under approve = "auto", is released without reading GitHub and its note recorded as given, --force or not; refuses any other state, naming it |
store |
--babysit KO-n [--note TEXT [--author NAME]] PROJECT |
sends a ticket parked on its pull request ([merge] mode = "pr") back to the babysitter; a custom note, or any note with --author, is a maintainer instruction, like holo send-back's, whose author is --author or the caller's login (--author maintainer records the interventions row's actor as maintainer, as it does on holo send-back and holo steer; every other command-line intervention records operator); otherwise the interventions row store.babysit() writes, the parked run ended with its resume point at the merge gate, the ticket walked to ready; the loop's next claim resumes the candidate on the PR and reads its threads and checks again, parking again under approve = "human" rather than merging; refuses any other state, naming it |
store |
--repoint KO-n SHA --note TEXT PROJECT |
moves a parked candidate to a rebuilt branch tip: an interventions row with action repoint carrying the note, a runEvents row naming the old and new shas, then runs.candidateSha set to SHA (a full 40-hex commit id); the run stays parked and the branch is not touched; the merge gate --approve resumes into holds the branch to the new sha; refuses a ticket not parked awaiting merge approval, one already approved (its release is in flight: requeue instead) or a malformed sha, naming it |
store |
--pause KO-n --note TEXT PROJECT |
asks the ticket's run to stop at its next safe point: a pause intervention carrying the note and the run marked, in one transaction; the run later commits its work as WIP, keeps its worktree and branch, ends paused and parks the ticket blocked_on_operator (see Operating); refuses a ticket with no run or a run already ended, naming its outcome; repeating a pending request keeps the first note |
store |
--resume KO-n --note TEXT PROJECT |
releases a paused run to claim: the resume intervention carrying the note, the run released with its resume phase, the ticket walked to ready and its question cleared, then the pull request's pause notice removed; the next claim reuses the worktree and continues from the recorded boundary; refuses a ticket whose latest run did not end paused |
store |
--abort KO-n --note TEXT PROJECT |
ends a run now: an abort intervention and the run marked in one transaction; the run's worker, or the command itself when no worker is live, kills the turn, commits the tree as WIP, pushes an open pull request's branch, ends the run abandoned and parks the ticket blocked_on_operator (see Operating); refuses an ended run or one parked blocked_on_operator |
store, worktree, Linear |
--abort KO-n --close-pr --note TEXT PROJECT |
the same abort recorded as abort_close; once the run has ended it comments the note on the pull request and closes it, keeping the branch |
store, worktree, Linear, GitHub |
--hold --note TEXT PROJECT |
holds the project's admission: a hold intervention carrying the note; no new ticket is claimed while existing runs finish; creates the project's store row from [board] when it has none; refuses a project already held, or one with neither a row nor [board] |
store |
--release-hold --note TEXT PROJECT |
enables admission again: a release_hold intervention carrying the note; refuses a project already enabled, or one with neither a row nor [board] |
store |
--close KO-n --landed URL [--note TEXT] PROJECT |
closes a ticket whose change landed outside the factory: a close_out intervention on its last run naming URL and the note, the question cleared and the ticket walked to merged with no merge sha, in one transaction; then the lease label removed, the board issue moved and the ledger posted as a comment; refuses a ticket already merged, one with a live run, or one whose last run did not end rejected, failed, abandoned or killed |
store, Linear |
--gap-layer KEY-n LAYER --note TEXT [--carried-by KEY-n] [--found-by witness\|operator] PROJECT |
records the correction layer the lesson of the gap ticket KEY-n answers landed in: appends one gapLayers row with LAYER (impossible, static, witness, guidance, review or none), the note, the operator's user as author, --carried-by, the ticket carrying the lesson when it is not the gap's own, and --found-by, who found the gap (foundBy, operator when omitted); --report's gap layers: line counts each ticket's latest row and its gaps found: witness N, operator M line counts those rows by finder. No interventions row is written: the verb changes no run, ticket or project state, and an intervention would count as human toil in --report and /status. An unknown layer or finder, or --found-by without --gap-layer, is a usage error (exit 2); a ticket the store does not hold exits 1 naming it |
store |
--file-ticket TICKET.md [--state Todo\|Backlog] [--priority urgent\|high\|medium\|low] [--note TEXT [--author NAME]] PROJECT |
validates, then refuses a body the claim would park as stale: each path named that main lacks and each symbol not in its paired file prints [holo2] PATH: REASON and exits 1 with nothing filed, or prints [holo2] PATH: warning: REASON and files when the body names a Depends on; creates the issue in the Linear project the project's [board] names, reads it back, validates again; then posts AUTHOR: NOTE as its first board note through the board's comment(), AUTHOR the caller's login when --author is left off; a note the board refuses exits 2 naming the filed ticket |
Linear |
--worker PROJECT |
internal, spawned by the scheduler under [loop] workers > 1: claims one ticket, works it to merge or park, exits with the run's status (0 merged, 1 failed, 2 parked, 3 nothing to claim, 4 stopped for a human); skips the startup probes, the sweep and the supervisor spawn, which the scheduler ran for the pool |
Linear, store, worktrees, main, FINDINGS.md |
--witness-step PROJECT |
internal, spawned by the scheduler under [loop] workers > 1 when a story's witness pass is pending: takes the project's witness.lock (an flock beside merge.lock), runs the loop's witness step once and exits 0; when another pass holds the lock it prints another witness pass holds the witness lock; skipping and records nothing. Skips the startup probes, the sweep and the supervisor spawn, and needs no agent route |
store, a detached witness worktree |
--shadow BRIEF PROJECT |
internal, spawned detached by a fresh claim's implement stage under [agents.implementer_shadow]: takes the project's shadow.lock (an flock beside merge.lock), runs the shadow implementer from the brief and records its shadow_result on the brief's run; when another run's shadow holds the lock it records shadow_skipped naming that run and cuts nothing. Deletes the brief and exits 0 either way |
store, a shadow/ branch and .shadow worktree |
--file-ticket TICKET.md --update KO-n PROJECT |
same, the stale-body refusal included, replacing an existing issue's title, body and estimate; a blocker the file's Depends on: names and the board lacks is recorded and printed as +KO-a, one the board holds and the file no longer names is printed as board also holds KO-c -- relations are added, never removed; with --note TEXT [--author NAME], a successful update posts AUTHOR: NOTE on the issue as a filing does |
Linear |
--file-ticket TICKET.md --update KEY-n --revision N [--priority urgent\|high\|medium\|low] [--labels a,b] PROJECT |
on a native board: after the same validation and stale-body refusal, replaces the ticket's body, and its priority or labels when given, at revision N; a ticket at another revision exits 1 printing KEY-n is at revision M, not N; nothing changed, and every refusal of the store's is one line with exit 1; on a Linear board all three are refused beside --update; with --note TEXT [--author NAME], a successful update records AUTHOR: NOTE as the ticket's comment note, and a refused one records none |
store |
--file-story SLUG [--priority urgent\|high\|medium\|low] PROJECT |
on a native board, or a Linear board in store mode: validates the story directory stories/SLUG in the project's state directory with the story validator, then files it in one transaction: the parent with the story body, its contract withheld, at needs_spec in Backlog; each child in Backlog in dependency order with the priority, its Depends on: sibling slugs rewritten to their identifiers; and the stories, storyWitnesses and storyChildren rows, the story planned. Prints filed KEY-n: TITLE (ROLE, Backlog[, PRIORITY]) per ticket, then writes Story: KEY-n atop story.md and Ticket: KEY-m atop each child file. A story the validator refuses, a child depending on an unmerged ticket, or a story.md already headed Story: KEY-n (change a filed story with --update) exits 1 printing the problem, and nothing is written. On a Linear board every issue is created first, in Backlog: the parent unlabelled, each child a sub-issue of it with [board] label and a blocks relation per dependency; the store rows follow. A board failure part way exits 1 printing the identifiers already created, to cancel on Linear, and writes nothing to the store; a Linear board in mirror mode exits 1 |
store, story directory |
--file-story SLUG --update KEY-n --revision N [--priority urgent\|high\|medium\|low] PROJECT |
on a native board, or a Linear board in store mode: validates the story directory stories/SLUG, headed Story: KEY-n, and applies it to that filed story in one transaction with the parent read at revision N: the parent's body is replaced, its contract still withheld; each child file headed Ticket: KEY-m is edited at its current revision when its body changed; each child file without a header is filed in Backlog with the priority and gets its Ticket: header; and the story's witnesses, roles and standing orders are rewritten. A change to the parent body, a witness, the witness paths, a role, a dependency or the standing orders is a plan change: it records the parent's next revision, one per update, and returns an approved or parked story to planned, so the plan is approved again; a change to a child's other sections alone leaves the story as it is. Prints updated KEY-m (revision R) or filed KEY-m: TITLE (ROLE, Backlog) per changed child, then story KEY-n is STATE at revision N, the parent's committed revision. A parent not at revision N, a stored child whose file is gone (cancel a child with --cancel), a child file naming another story's ticket, or an invalid directory exits 1 printing the problem, and nothing is written. On a Linear board the board is written first: the parent's body is replaced through the board's update when story.md changed, each changed child is edited there with a blocks relation added per new dependency, and each new child is created in Backlog as a sub-issue of the parent with [board] label and its blocks relations; then one store transaction mirrors the touched issues and rewrites the story rows. A child's change is judged against its store row, so a rerun after a fault converges. A relation the directory dropped is printed as BLOCKER still blocks KEY-m on the board, to remove on Linear; a stored child whose file is gone exits 1 saying to cancel it on Linear and keep its file. A board failure part way exits 1 printing the issues already updated and those created, to cancel on Linear, and writes nothing to the store; a Linear board in mirror mode exits 1 |
store, story directory, board |
--approve-story KEY-n --revision N --note TEXT [--baseline-green W] [--baseline-red-kind exception W] PROJECT |
on a native board, or a Linear board in store mode: approves the planned story KEY-n with its parent read at revision N. A parent at another revision, a story not planned, another story of the project approved or parked, or an override naming a witness the story lacks exits 1 printing the problem before anything runs or is written. Then the baseline: every witness runs at main's tip with the story's witness files copied in, each verdict appended to the ledger as baseline, and baseline at SHA: W1 red (assert), ... is printed. A witness green at the tip exits 1 unless --baseline-green W names it, a witness red by exception exits 1 unless --baseline-red-kind exception W names it, and an error or absent verdict always exits 1; either override may be given more than once, and a kind other than exception is a usage error (exit 2). A refused approval keeps its baseline rows and leaves the story planned and its children where they are. On a pass one transaction freezes the plan with an approve_story intervention carrying the note, with (baseline overrides: W green, W red by exception) when one applied, and releases every open child: on a native board moved to Ready at its current revision, on Linear given a queued Todo push the host sweep sends, its column following the board. Prints approved story KEY-n at revision N and released KEY-m to ... per child; a Linear board in mirror mode exits 1 |
store, git |
--witness-pass KEY-n PROJECT |
runs a witness pass by hand on the approved or parked story KEY-n: every witness runs at main's tip as it stands on main, its file not copied in, so a witness whose file has not landed is absent; each verdict is appended to the ledger with verifier operator, even at a tip the ledger already holds, and witness pass at SHA: W1 green, W2 absent, ... is printed. A witness red at the tip whose ledger holds a green at an earlier commit is rerun once at the same commit and both rows are kept, the later one its verdict there. A witness whose verdict differs from the one at the previous commit in the ledger gets one note on the parent naming both, posted once per witness and commit. A hand-run pass runs every witness, even at a commit where a loop pass would find the story's witness set not due and carry its last verdicts forward. A ticket that is not a story's parent, a story not approved or parked, or a held project exits 1 printing why, and nothing runs |
store, git |
--decide KEY-n ID [OPTION] --note TEXT PROJECT |
ID is N, a decision of the parked story, or pN, a proposed child of the story. For N: answers decision ID of the parked story KEY-n with OPTION, the option's number counting from 1 as the decision lists them, or default; left out, the decision's default. One transaction records the answer and a decide intervention carrying the note, and applies the option: "abandon the story" abandons the story and its parent and moves its unclaimed children to Backlog; "accept the changed witness file" makes the witness file at main's tip the witness's approved source and hash, so the next pass can close on it; "re-approve the plan as it stands" makes the child's current dependencies its approved edges; "amend the witness (re-plan)" and "drop W (re-plan)" return the story to planned for --file-story --update and a new --approve-story. "rerun" then runs a witness pass as verifier operator, which settles the story. "file a follow-up child", "file a fix child" and "restore the approved edges" ask a person to act first and are recorded only. With no decision left open, a parked story returns to approved. Prints decision ID of story KEY-n: ANSWER and story KEY-n is STATE. A ticket that is not a story's parent, a decision already answered, an ID the story does not hold, or an option number out of range exits 1 printing the problem, and nothing is written. For pN, of a planned, approved or parked story: option 1 (the default), "accept the proposed child", files it in Backlog through the board with its Depends on: the child that raised it and a Role: scaffolding line (on Linear a sub-issue of the story's parent, blocked by that child), then one transaction makes it a scaffolding child of the story, adds it and its edge to the approved plan, marks the proposal accepted and records the decide intervention; the story's state and generation stay. Option 2, "reject the proposed child", marks it rejected with its intervention and files nothing. Prints proposal pN of story KEY-n: ANSWER, child KEY-m filed in Backlog on an accept, and story KEY-n is STATE. A proposal already answered, one the story does not hold, an option out of range or a closed story exits 1 and writes nothing; a board refusal exits 1 writing nothing; a store refusal after the board filed exits 1 naming the key to cancel |
store, git, board |
--move KEY-n ready\|backlog --revision N [--note TEXT] PROJECT |
on a native board: moves the ticket to Ready or Backlog at revision N, recording the note, and prints moved KEY-n to COLUMN (revision M); a live run continues and its ticket is not claimed again; a ticket at another revision exits 1 printing KEY-n is at revision M, not N; nothing changed, and a refused move (a draft to Ready, a canceled or closed ticket) exits 1 printing the problem; a move to Ready of a ticket parked needs_spec for stale landmarks or by the critic, in Ready or Backlog, first re-checks it against main fetched from origin, without asking the critic again: with reasons remaining it exits 1 listing them and changes nothing, and with none it re-derives the ticket to ready (or blocked_on_deps) and records a project requeue intervention, printing re-checked KEY-n against main: STATUS (revision M) when the ticket was already in Ready; any other move to the column a ticket is in exits 1 printing KEY-n is already in COLUMN at status STATUS; nothing changed; on a Linear board a usage error (exit 2) that asks Linear nothing |
store |
--cancel KEY-n --revision N --note TEXT PROJECT |
on a native board: cancels the ticket at revision N, recording the note, and prints canceled KEY-n (revision M); a live run gets an abort intervention carrying the note and ends abandoned at its next safe point, and the line adds ; run R ends abandoned at its next safe point; a last run parked awaiting merge approval gets a close_out intervention and ends abandoned at once, its pull request left open, and the line adds ; run R ended abandoned and , URL left open; a last run parked blocked_on_operator after its merge landed cannot end abandoned, so the cancel exits 1 naming it and nothing changes; a stale revision or a refusal exits 1 as --move's does; in the same transaction each open dependent of the ticket, by id, has the canceled ticket replaced in its Depends on: by the canceled ticket's own dependencies (or none), records a rewire note, moves to ready when its new dependencies are all merged or none, and a story child's approved edge follows, and the line adds ; rewired KEY-m to DEPS per dependent, after the run clauses; on a Linear board a usage error (exit 2) |
store |
Project commands¶
python3 factory.py project VERB [--store PATH] registers projects in the
host registry, HOLOPHYTE_HOME/host.toml, and changes their admission in
one store. --store PATH names the database; without it project add uses
the added repository's store and the other verbs the store of the
repository the command runs in. NAME means two things, by verb:
project remove NAMEmatches a registry entry: its[serve] name, or the path it is registered under. It reads each entry's config on its own, so an entry whose config no longer loads, or two entries sharing a name, can still be removed by path.project enable,holdanddisable NAMEmatch a store row: the basename of the row's repository path. A name that matches no row, or more than one, is refused.
See Operating.
| Invocation | Does | Touches |
|---|---|---|
project add PATH [--store PATH] |
validates PATH as a repository root whose config.toml passes and has a [board] table naming a team, then registers it enabled, recorded as register_project, without starting a run, and adds its resolved path to host.toml; a row the loop already wrote for the same team at the same path is adopted, and a row is recorded as register_project once however often it is adopted; refuses a name already in host.toml, or a path already there whose store holds its row, naming the entry, and the same team at another path, naming the row. On a registered path whose store has no row for it (the store deleted or recreated after registration) it is the repair the daemon and the sweep name: it writes the row as above, says so, and leaves host.toml unchanged. host.toml is rewritten whole through host.toml.tmp, created exclusively, and a rename, so two adds at once keep both. With --store naming a database other than the repository's own store, it registers in that store only and leaves host.toml untouched, since the registry holds paths and the host reads each project's own store |
store, host.toml |
project remove NAME\|PATH |
drops the entry whose [serve] name or registered path is given from host.toml; touches no store; refuses one the registry does not hold, listing the ones it does, and a name two entries share, naming both paths so one can be removed by path |
host.toml |
project list [--store PATH] |
without --store, prints each project in host.toml: name, path, and the admission and note its own store holds (- without a store or row); a project whose config or store cannot be read is listed with error= and the rest still are, and the exit is then 1; with --store, each row of that store: name, path, admission, note and newest run |
host.toml, store, read-only |
project enable NAME [--note TEXT] [--store PATH] |
enables admission again, recorded as release_hold; the note defaults to enabled by operator; refuses a project already enabled |
store |
project hold NAME --note TEXT [--store PATH] |
stops new admission while existing runs finish, recorded as hold; refuses a project already held |
store |
project disable NAME --note TEXT [--store PATH] |
stops admission, recorded as disable; a disabled project's supervisor exits at startup and /status reports the state and note with no runs; refuses a project already disabled |
store |
Host forms¶
A mode given no project means the host: every project listed in the host
registry, HOLOPHYTE_HOME/host.toml, which project add and project
remove write. --status, --serve and --supervise are the host forms;
any other mode without a project is a usage error.
| Invocation | Does | Touches |
|---|---|---|
--status [--json] |
the host form: the checkout's build and the last sweep's, the home's supervisor.lock, sweep.json (sweep: none without one), then every project in HOLOPHYTE_HOME/host.toml as the project form prints it, each line prefixed [NAME]; a project whose config, store or file cannot be read is its own error line and the exit is 1; no host.toml is exit 1 naming project add |
host.toml, each store, read-only |
--serve [HOST:PORT] |
the host daemon: every registered project's routes under /projects/NAME/... (NAME its [serve] name), and the host's /status and /attention at the root; serves on the socket the service manager hands over (LISTEN_FDS), else the address given, else host.toml's [serve] bind; host.toml's [serve] machine_token_file is the bearer beyond loopback and for every write, [serve] actions opens the project actions and POST /actions/run-sweep. On a factory HEAD move it exits 0 when handed its socket, and re-executes otherwise |
host.toml, each store; writes only through the opt-ins, and run-sweep appends to HOLOPHYTE_HOME/host-actions.jsonl |
--supervise [--once] |
the host sweep: under HOLOPHYTE_HOME/supervisor.lock (a second run beside a live one exits 1 naming its pid), sweeps every registered store and bumps its one host-sweep beat (pid 0), then acts on the trips and reconciles pull requests, board closes and owed loops round-robin under a deadline of half [supervisor] sweep_sec; a project whose own supervisor.lock names a live pid is skipped, a dead one is removed; a disabled project, or one with no store or no row for its path, is skipped; writes HOLOPHYTE_HOME/sweep.json after every project; --once is one run, exit 1 when any project errored, and without it a run every sweep_sec until SIGINT/SIGTERM |
host.toml, each store, sweep.json, Linear, GitHub, the loop units |
holo¶
holo COMMAND [ARGS] [-p NAME|PATH] [--verbose] is the short form of the
modes above. Each command but send-back, steer, start and stop stands for one
factory.py invocation and runs through the same parser, so its refusals, messages and
exit codes are the factory's own. A command's project is the first of these
that answers:
-p NAME|PATHon the command line;HOLO_PROJECTin the environment, read as-pis;- the current repository: the top level of the git work tree the shell is
in (
git rev-parse --show-toplevel, so a subdirectory counts), whenhost.tomlregisters that path; any other directory does not answer; default_projectinHOLOPHYTE_HOME/client.toml, read as-pis.
A value holding a path separator, or naming an existing directory, is a
repository path; any other value is a [serve] name in host.toml, and a
name it does not register exits 2 naming the registered names rather than
asking the next source. With no source answering, status, serve and
supervise are the host forms, as is the attention read
below, and any other command exits 2
naming all four sources. --verbose prints the project and the source that
named it to stderr. NOTE is free text, the last argument;
-n/--note NOTE is the same. holo project VERB is factory.py project
VERB with its arguments unchanged. --worker, --witness-step and
--shadow have no command: they are internal, spawned by the loop's pool
and by a fresh claim's implement stage.
| Command | Aliases | Factory invocation |
|---|---|---|
holo status [--json] [--watch [SECONDS]] |
--status [--json] [PROJECT] |
|
holo report [--since WINDOW] [--notes] [--json] |
none: the window's counts, read from the store as below; factory.py --report PROJECT is unchanged |
|
holo sweep [--act] |
--sweep [--act] PROJECT |
|
holo board diff |
--board-diff PROJECT |
|
holo board import [--dry-run] |
--board-import [--dry-run] PROJECT |
|
holo store import PATH --dry-run |
--import-store PATH --dry-run PROJECT |
|
holo draft [KEY] |
none: prints ticketTemplate.md, its header's last line # this project's implementer profiles: NAME, ... when the project configures [agents.implementer_profiles]; with KEY, the ticket's stored body with its **UI change:** and **Blast radius:** lines, ## Estimate & dependencies, ## Mock-up and ## Evidence moved into a +++ header, ready for holo file FILE --update KEY. An evidence state without a "NN-slug.png: text" file is given NN- and its first three words as its file. A line no header field holds is printed on stderr and the command exits 1, as does an unknown KEY; a body already in the header form prints unchanged. It reads the project on this machine, never through a remote host |
|
holo file FILE [--backlog] [--priority P] [NOTE [--author NAME]] |
holo ticket file |
--file-ticket FILE [--state Backlog] [--priority P] [--note NOTE [--author NAME]] PROJECT |
holo file FILE --update KEY [--revision N] [--labels a,b] [NOTE [--author NAME]] |
holo ticket file |
--file-ticket FILE --update KEY [--revision N] [--labels a,b] [--note NOTE [--author NAME]] PROJECT, the note posted on KEY after the update |
holo move KEY ready\|backlog --revision N [NOTE] |
holo ticket move |
--move KEY ready\|backlog --revision N [--note NOTE] PROJECT |
holo cancel KEY --revision N NOTE |
holo ticket cancel |
--cancel KEY --revision N --note NOTE PROJECT |
holo requeue KEY NOTE [--force] |
holo ticket requeue |
--requeue KEY --note NOTE [--force] PROJECT |
holo approve KEY [NOTE] [--force] |
holo ticket approve |
--approve KEY [--note NOTE] [--force] PROJECT |
holo babysit KEY [NOTE [--author NAME]] |
holo ticket babysit |
--babysit KEY [--note NOTE [--author NAME]] PROJECT |
holo send-back RUN NOTE [--author NAME] |
none: the console's send-back of run RUN, its note by --author, or else the caller's login (over http, the daemon's maintainer) |
|
holo steer KEY NOTE [--hint] [--withdraw] [--author NAME] [--now] |
none: a steerNotes row by --author, or else the caller's login. Amendment notes accept case-insensitive edit lines: REPLACE C4: new text, DROP C4 (or DROP C4: reason), and ADD: text. A replacement keeps its id and preserves the old text in history; a change of intent should use a drop plus an add. Unknown or dropped criterion ids, edit lines in a --hint, and new criterion text with an unwitnessed operator or post-merge check are refused without writing. Reviews use the amended criteria; withdrawal restores the prior criteria, and added ids are never reused. On a ticket with no live run, a steer interventions row (on its newest run, or the project's when it has none) and an amendment every later run's ticket text carries, so its implementer and reviewers see it; with --hint, advice the next implement turn's opening carries once and no reviewer sees. On a run parked on its pull request, holo send-back's release, its note a hint the reviewer's ticket leaves out under --hint. On a ticket whose run a send-back or approval released to resume on its pull request, the same operator_note row and event without a second release, which that resumed babysit pass reads. On a live run in claimed, working, verifying, reviewing or addressing, a steer interventions row on that run and the note, which its next implementer turn carries (the implement turn, a fix turn, or a steer turn the loop starts right after the implement turn, at the top of a review round, in place of ending on an approval while a note is pending, or before the terminal adjudication, which judges again after a steer turn that follows a pass), consumed by that turn; an amendment joins the ticket text every later review round and the terminal adjudication read. An amendment steered onto a live run after its ticket moved to a later revision adopts that revision's criteria: every later review round and the terminal adjudication judge the newest revision recorded before the amendment, under the numbers the store holds for them, while the run keeps its claimed verify block. On a live run in merge_gate, verifying or reviewing babysitting its pull request, a steer interventions row on that run, then an operator_note event on it with holo send-back's payload and no release; the babysitter's next poll reads it as a maintainer thread for its next fix round, and an amendment joins the ticket text its later reviews read; the run enters merging only with no note pending, else it takes another pass, so a note steered as late as the pre-merge verify is fixed before the merge. A live run past those phases, one resuming a candidate at the merge gate, or one that has closed steering (a steer_closed event, recorded with no note pending once no implementer turn remains: before the merge gate, its lock wait included, or a not-reproduced park) is refused naming its phase, holo pause and holo abort; a merged or abandoned ticket, one parked with no pull request, or one whose next run resumes a candidate past the implement turn with no pull request to read a note (an approved local candidate, or a run paused there) is refused naming its state. With --now on a live run before its pull request, the verb first asks whether the implementer session can resume: if not, it prints --now: cannot resume the implementer session (REASON); the note lands at the next turn boundary (fallback implementer route, no recorded session or no implementer_resume template) and records the note as without --now; if so, the note is recorded with interrupt set, the run's heartbeat records a steer_interrupt event within one beat and kills the running implementer turn (a reviewer, verify or trim turn is never stopped; the note waits for the next boundary), the turn-end sweep commits its edits as WIP: implementer steered mid-edit (KEY); not verified and removes the ignored files it created, and the same session resumes with the note as its prompt under what was left of the stopped turn's time, or a fresh turn naming the WIP commit when no session can resume then (a steer_resumed event names why). On a run babysitting its pull request, --now stops its running fix turn the same way, the resumed turn carrying the note and consuming its operator_note event. --now on a ticket with no live run is refused naming its state and writes nothing. With --withdraw, a steer interventions row first, then every amendment of the ticket not yet withdrawn is marked withdrawn (withdrawnBy), so no later run's implementer or reviewer reads it, an amendment sent back as an operator_note included; refused, writing nothing, while the ticket has a live run or has no amendment left. A run that fails on its time cap or its review-round cap while its ticket holds an amendment parks the ticket blocked_on_operator at once, with a question naming the cap and the amendments and suggesting a follow-up ticket for the steered scope, holo steer KEY --withdraw and holo requeue KEY; failure triage then requeues nothing |
|
holo repoint KEY SHA NOTE |
holo ticket repoint |
--repoint KEY SHA --note NOTE PROJECT |
holo pause KEY NOTE |
holo ticket pause |
--pause KEY --note NOTE PROJECT |
holo resume KEY NOTE |
holo ticket resume |
--resume KEY --note NOTE PROJECT |
holo abort KEY NOTE [--close-pr] |
holo ticket abort |
--abort KEY --note NOTE [--close-pr] PROJECT |
holo hold NOTE |
--hold --note NOTE PROJECT |
|
holo release NOTE |
--release-hold --note NOTE PROJECT |
|
holo start [NOTE] |
holo loop start |
none: systemctl --user start holophyte-loop@NAME, recorded first as the console's launch-loop is |
holo start --foreground |
holo loop start |
PROJECT: the loop in this terminal |
holo stop NOTE [--now] |
holo loop stop |
--hold --note NOTE PROJECT; with --now, then --abort KEY --note NOTE PROJECT for each live run |
holo close KEY URL [NOTE] |
holo ticket close |
--close KEY --landed URL [--note NOTE] PROJECT |
holo gap KEY LAYER NOTE [--carried-by KEY] [--found-by F] |
holo ticket gap |
--gap-layer KEY LAYER --note NOTE [--carried-by KEY] [--found-by F] PROJECT |
holo story file SLUG [--update KEY --revision N] [--priority P] |
--file-story SLUG [--update KEY --revision N] [--priority P] PROJECT |
|
holo story approve KEY --revision N NOTE [--baseline-green W] [--baseline-red-kind KIND W] |
--approve-story KEY --revision N --note NOTE [--baseline-green W] [--baseline-red-kind KIND W] PROJECT |
|
holo story witness KEY |
--witness-pass KEY PROJECT |
|
holo story decide KEY ID [OPTION] NOTE |
--decide KEY ID [OPTION] --note NOTE PROJECT |
|
holo supervise [--once] |
--supervise [--once] [PROJECT] |
|
holo serve [ADDR] |
--serve [ADDR] [PROJECT] |
holo start starts the project's loop unit and returns: it records a
launch_loop intervention, then runs systemctl --user start
holophyte-loop@NAME, NAME the project's [serve] name in host.toml,
and prints the unit and the ready count. A project with no [serve] name
in host.toml has no unit and is refused naming the entry, as is a
disabled project. A held project needs
the note: start releases the hold with it before starting the unit, and
without one it exits 1 naming the hold. Closing the terminal leaves the
unit running. holo start --foreground is factory.py PROJECT, the loop
in this terminal.
holo stop NOTE holds the project's admission as holo hold NOTE does,
so the loop admits no new ticket and the host sweep starts no loop for it;
a project already held is refused as hold refuses it. Its live runs, every run
not ended and not parked, finish their work, and the loop exits at its next
idle check once none is left; stop names the runs it waits for. holo
stop --now NOTE also aborts each live run as holo abort does, with the
note: each stops at its next safe point and keeps its work. Neither stops
the unit, because stopping it kills a live run mid-turn and loses its work;
the loop exits on its own once the hold leaves it idle.
The write commands, each that takes a NOTE and holo file, print one
result. Without --json it is one line: ✓ and what the verb did, then
intervention N recorded when it wrote an interventions row, on stdout; or
✗ and the refusal on stderr. With --json it is one JSON object on
stdout, the keys the daemon's POST /actions/... answers use:
| Key | Value |
|---|---|
action |
the command's words, requeue or story approve |
ok |
true when the verb exited 0 |
detail |
the verb's message or its refusal, without the [holo2] prefix |
recorded |
the id of the interventions row the verb wrote, or null when it wrote none (a refusal, gap, move, file) |
ticket |
the KEY the command names, or file --update KEY's, when it names one |
run |
the run the interventions row is on, or send-back's RUN |
$ holo requeue HOLO-133 "rerun" --json -p holophyte
{"action": "requeue", "ok": true, "detail": "HOLO-133 requeued after run 941", "recorded": 1704, "ticket": "HOLO-133", "run": 941}
--json changes the output, never the exit code below: a usage error is
still exit 2, its result ok: false with the error line as detail.
holo status without --json renders the --status --json object, the
project form or the host form, as a page read top down; with --json it
prints that object unchanged. Its lines carry no [holo2] prefix:
| Section | Lines |
|---|---|
| header | the project or the home, then the date and clock time, Tue Oct 6, 10:42 PDT |
Needs you (N) |
!, the project, the ticket and the question of each parked ticket; a project whose admission is held or disabled, with its note and ready count; a story planned and waiting on approval, or parked with its open decisions counted; the reason of each stranded run and how long ago it ended, 14 min; a ticket waiting on a dependency only a person can move, with the dependency, its state and reason, and · N queued behind |
Running (N) |
>, the project, the ticket, the phase and the heartbeat's age, heartbeat 20 s ago, then steer N pending while the run has holo steer notes no turn has carried; a merge lock or a project supervisor lock held by a live holder |
Quiet |
✓ and the ready count of each enabled project with nothing live, parked, stranded or wrong and no story planned or parked |
Problems (N) |
✗ and a try: hint for a lock naming a dead pid, a run no longer live or no holder, an unreadable project, a project the last sweep errored on, a sweep that failed, or one that started over two minutes ago and never ended; a project here has no quiet line |
| footer | the sweep's state and age, Sweep ok 40 s ago or Sweep killed, started 94 h ago, the home lock's live pid, and the build as a short hash, build 92ef2b0 (host form only) |
A free lock prints no line. Ages are s under a minute, min under an
hour, then h. The symbols are coloured (! orange, > blue, ✓ green,
✗ red) only when the output is a terminal and NO_COLOR is unset or
empty; the write commands' ✓/✗ follow the same rule.
holo status --watch [SECONDS] draws that page again every SECONDS
(default 5, any number above zero) until Ctrl-C, which exits 0. On a
terminal each frame clears the screen first, so the page redraws in place;
piped, each frame follows a line carrying its time, --- 10:42:05 PDT ---.
--watch takes no --json.
holo follow [--since AGO] [--every SECONDS] [--json] streams one line per
thing that happens in the project: its runs' narrative events (a phase
change, a re-point) and its ledger entries (a round's
verdict, an adjudication, a merge, a failure, an intervention, a note),
each shaped as a GET /ledger entry, with the store's schema migrations
that GET /ledger lists among them. Each line is the local clock time, a symbol (✓ a
merge, ✗ a failure, ! an intervention, > anything else), the ticket and
a one-line summary, led by its kind for a ledger entry:
14:02:11 > HOLO-1 working -> verifying
14:06:40 > HOLO-1 round: r1 approve
14:07:02 ✓ HOLO-1 merge: merged 92ef2b0
It starts from now, or from --since AGO before it (90s, 30m, 1h,
2d), polls every --every SECONDS (default 2), prints what each poll
finds oldest first, and prints each event and entry once, by its store id
(a migration by its time and versions), however many a poll finds. It never prints the
agents' own output; that is holo run N --turns. Each poll also reads
GET /attention: a live run whose heartbeat is older than the project's
heartbeat_stale_min, or a supervisor whose beat is, prints one ✗ line
naming it, and no other until it recovers and goes stale again, so silence
means a quiet project, not a dead one. A line with --json is one JSON
object: stream (event, ledger or stall), at, run, ticket,
kind and summary, an event's or entry's id, and a ledger entry's other
fields. stderr says once
where it starts from; Ctrl-C exits 0.
holo report reads the project's store read-only and opens with counts
for a window, --since: Nh or Nd (24h, 7d, 30d), or all;
default 7d. Any other form exits 2 naming the accepted ones. The four
count lines come first. A run is in the window when it ended in it, so a
live run is in no Shipped, Failures or Runs count; Hands-on counts
interventions and send-backs by when they were recorded, including those
from live runs. Gaps counts the whole store. A project with no store
exits 1, its refusal on stderr with the [holo2] report: prefix. The
page's lines carry no [holo2] prefix:
| Section | Lines |
|---|---|
Shipped |
the merged, abandoned and failed runs, and the median minutes of work per merged run with the median estimate beside it, 3 merged · 1 abandoned · 2 failed · median 16 min per ticket (estimate 30); with none of the three, nothing shipped in the last 7 days, or in the store's history for all |
Failures |
the failed runs by failure kind, most first, verify 3 · infra 1, or none; the stored kind review_route is shown as review |
Gaps |
the gaps whose latest layer is none, as open, then each other layer holding a gap, over the whole store as factory.py --report's gap layers: line counts them |
Hands-on |
the human interventions by action, 3 interventions (requeue 2 · approve 1), and the send-backs, the human operator_note interventions, which the interventions count leaves out |
Runs (N) |
the window's ended runs in end order: ✓ merged, ✗ failed, · otherwise, the ticket, the outcome, the work against the estimate in minutes, the review rounds and how long ago it ended |
Notes (N) |
with --notes only: each send-back note consumed in the window, newest first, with the time it was consumed, the ticket, run and round, and its author |
Steers (N) |
with --notes only: each holo steer note recorded in the window or still pending, oldest first, with the time it was recorded, the ticket, its kind (amendment or hint) and author, and pending or consumed by run N |
| footer | the project's directory and the window, holophyte · last 7 days |
holo report --json prints the object the page renders from, the notes
included with or without --notes:
| Key | Value |
|---|---|
project |
the repository path |
window |
since as given, from_ms the window's start (null for all, 0 for a window reaching before the epoch) and now_ms |
shipped |
merged, abandoned, failed, median_min and median_estimate_min (null with no merged run measured) |
failures |
failure kind to count, review_route named review |
gaps |
open, layers (each layer to its count) and found_by (each finder to its count) |
hands_on |
interventions and by_action (action to count), both without operator_note, and send_backs, its count |
runs |
the window's runs in end order, each with run, ticket, actual_min, agent_min, verify_min, estimate_min, ratio, rounds, outcome, host, ended_ms, merge_sha and wall_min, as a GET /runs row has them |
notes |
the window's consumed notes, newest first: run, ticket, round, event_id, author, note and consumed_ms |
Five reads are no factory.py mode: each calls, in process, the view
function the serve daemon answers its route with, so the command
and the route agree. Each opens the store read-only and writes nothing.
--json prints the route's body as the daemon would send it; without it,
holo run N with no --files, --ledger or --turns prints the page
below, and each other read prints a compact listing, one line per run,
item, column entry, file, ledger entry or turn. The view's status is the
exit: 200 is 0, 400 is 2, and 404, 503 or any other is 1, with the body's
error on stderr. The project is found as above; attention with none is the host form, the host
daemon's root /attention built from host.toml with no daemon running.
| Command | Aliases | Route whose body --json prints |
|---|---|---|
holo runs [--limit N] [--json] |
GET /runs[?limit=N] |
|
holo run N [--json] |
holo run show N |
GET /runs/N |
holo run N --files\|--ledger\|--turns [--json] |
holo run show N |
GET /runs/N/files, /runs/N/ledger, /runs/N/turns |
holo attention [--json] |
GET /attention, or the host daemon's root GET /attention |
|
holo board [--json] |
GET /board |
|
holo ticket KEY [--json] |
GET /tickets/KEY |
holo run N without --json renders GET /runs/N's body and
GET /runs/N/files's as one page, in the client's zone and coloured as
holo status is:
| Section | Lines |
|---|---|
| header | the ticket and its title; then run N, its outcome, its phase or parked, awaiting merge approval, its elapsed minutes against its time box, 43 of 30 min, the heartbeat age of a live run not parked, and its pull request, PR #432 |
| timeline | one line per event and review round in time order: a symbol (✓ done, ✗ changes requested or a failure, ! parked on a person), the phase reached or the event's kind or review rN, the clock time, then the event's note or summary, or the round's verdict, reviewer and first finding as p2 store/board.py:140 |
Files |
each file the run touched with its added and removed counts, store/board.py +1 −1, or the files route's error |
Next |
the commands the run's phase and outcome call for: parked awaiting merge approval, holo approve KEY, and with a pull request holo send-back N "note" and holo babysit KEY; failed, holo requeue KEY "note"; then for every run holo run N --ledger |
holo mcp is a Model Context Protocol
server on stdio, for an MCP client such as Claude Code or Codex to launch
as a subprocess. Register it once, then the client lists its tools:
claude mcp add holo -- holo mcp
Each tool runs the holo command beside it as a subprocess with a
two-minute timeout, so it answers what that command answers: the project
is found the same way, and with host set in client.toml the command
reaches the host over ssh. Every tool takes an optional project, a
[serve] name or a repository path. The reads are marked readOnlyHint:
true.
| Tool | Arguments | Runs |
|---|---|---|
status |
holo status --json; the host form with no project found |
|
attention |
holo attention --json; the host form with no project found |
|
report |
since |
holo report --json [--since WINDOW] |
runs |
limit |
holo runs --json [--limit N] |
run |
run, required; view: detail (default), files, ledger or turns |
holo run N --json, with --files, --ledger or --turns for the view |
board |
holo board --json |
|
ticket |
key, required |
holo ticket KEY --json |
board_diff |
holo board diff |
|
sweep_preview |
holo sweep, which acts on nothing and writes only sightings |
The six write tools are marked readOnlyHint: false and
destructiveHint: false. Each requires a non-blank note and author; a
missing or blank one is a result with isError: true naming it, and no
command runs. The verb records actor operator on its interventions
row, and the author rides in what the verb records as AUTHOR via MCP,
as the console's actions record AUTHOR via the console. The verb's own checks and
refusals hold, and its result object below is the tool's
structuredContent; a refusal is isError: true.
With transport = "http" in client.toml a write tool is refused before
anything runs: the daemon's routes record their own author, AUTHOR via
the console, so the reads alone go over http, and the writes over ssh or
on the host.
| Tool | Arguments | Runs | Records |
|---|---|---|---|
file_ticket |
body, required |
holo file - --backlog --note NOTE --author "AUTHOR via MCP" --json, the body on stdin |
the ticket in Backlog, never Ready, and its first board note AUTHOR via MCP: NOTE; an invalid body files nothing and is an error carrying the template checker's first problem |
send_back |
run, required |
holo send-back RUN --note NOTE --author "AUTHOR via MCP" --json |
the maintainer instruction NOTE, its author AUTHOR via MCP |
babysit |
ticket, required |
holo babysit KEY --note NOTE --author "AUTHOR via MCP" --json |
the same, for the ticket's parked run |
steer |
ticket, required; hint and now, booleans |
holo steer KEY [--hint] [--now] --note NOTE --author "AUTHOR via MCP" --json |
the steer note NOTE, an amendment or with hint a hint, its author AUTHOR via MCP; the verb's line is the result's detail |
requeue |
ticket, required |
holo requeue KEY --note "AUTHOR via MCP: NOTE" --json |
a requeue interventions row with that text |
hold |
holo hold --note "AUTHOR via MCP: NOTE" --json |
a hold interventions row with that text |
A tool whose command prints a JSON object returns it as structuredContent
and as text; board_diff and sweep_preview return the command's text,
board_diff also when it exits 1 for a difference. A command that refuses
is a result with isError: true carrying its output and its message, its
JSON object, such as {"error": "no such run", "run": 999}, also as
structuredContent; so are arguments the tool's input schema refuses. An unknown tool is a
JSON-RPC error, -32602. stdout carries only protocol messages, and closing stdin exits 0.
holo mcp --http [HOST:PORT] serves the same tools over HTTP at POST
/mcp, on the writer host behind its machine token, the writes only under
host.toml's [serve] actions: POST /mcp.
holo mcp alone needs the MCP Python SDK (mcp in requirements.txt);
without it, it exits 1 naming the package and
python3 -m pip install --user -r requirements.txt, and every other
command runs as before.
holo completion bash|zsh|fish prints a completion script for that shell.
It completes the commands, their aliases and flags, the choices the factory's
parser knows (ready|backlog, the gap layers, the priorities) and, where a
command takes a KEY, the keys of the project's open tickets. Install it once:
holo completion bash > ~/.holo-completion.bash # and in ~/.bashrc: source ~/.holo-completion.bash
holo completion zsh > "${fpath[1]}/_holo" # a directory on $fpath, before compinit runs
holo completion fish > ~/.config/fish/completions/holo.fish
bash 3.2, the macOS one, does not reliably source <(...), so write the
script to a file and source the file. Each script asks the hidden
holo __complete WORDS... for its candidates, one per line, so a new command
completes without a new script. The keys come from holo board --json for the
project the words name, found as above, and are cached in
HOLOPHYTE_HOME/completion/, one file per project, for 60 seconds from the
file's mtime: a ticket filed within that minute completes once it has passed.
With no project, or a board read that fails, no key completes and nothing is
printed.
HOLOPHYTE_HOME/client.toml is the client config every holo command reads
first; a key it does not hold below, or a file TOML cannot read, exits 2
naming the file.
| Key | Purpose |
|---|---|
default_project |
the project, a [serve] name or a repository path, when -p, HOLO_PROJECT and the current repository give none |
host |
the ssh destination every command runs on, as ssh reads it (user@name, or a Host alias in your ssh config); unset, holo runs locally |
remote_command |
the command that runs holo there, a path and its arguments with no shell operators; default holo |
transport |
ssh or http: how holo reaches the host; default ssh when host is set, http when only url is |
url |
the host daemon's base URL, http:// or https://, for transport = "http"; it needs token_file, and one without it exits 2 naming both keys |
token_file |
a file holding the daemon's machine token, sent as Authorization: Bearer ...; the token is never taken on the command line or printed, and a file that is not one line of printable ASCII exits 1 naming it, nothing sent |
timezone |
the zone holo pages show clock times in, an IANA name such as "America/Los_Angeles"; default the local zone; a name zoneinfo does not know exits 2 naming the key and the value |
With host set, holo runs the same command on that host: holo requeue
HOLO-1 "note" runs HOLO_TRANSPORT=local holo requeue --note=note -p NAME
--json -- HOLO-1 through ssh -o BatchMode=yes HOST, every argument quoted for
the remote shell, so a missing key fails rather than prompting. stderr
says via ssh to HOST, and a --json result gains "transport": "ssh"; a
local one has no such key. A command with a JSON form runs with --json and
is printed here by the local renderer; report, sweep, board diff,
board import, store import, story witness and holo project VERB stream
the remote output as it arrives. holo follow runs there with --json and each
object is rendered here as its line arrives; holo status --watch asks for
the --status --json object over ssh once per frame and draws it here. The project comes from -p,
HOLO_PROJECT or default_project and goes over by name, never from the
current repository, whose path is this machine's; with none named here, the
host resolves one as it would for a local command. The remote side always
runs locally (HOLO_TRANSPORT=local), whatever host its own client.toml
names.
holo file TICKET.md sends the file over the session's stdin, which the
host reads as holo file -, a ticket body from stdin; a file this machine
cannot read exits 2 before ssh runs. serve and supervise start
long-lived processes and story file reads a directory, so over ssh each
exits 2 before ssh runs. The remote command's exit code is holo's; ssh's
own failure, exit 255, is exit 1 naming the host and ssh's message, and a
write command's --json result says so with ok: false.
With transport = "http", holo calls the host daemon's routes at url
instead (HTTP endpoints), under
/projects/NAME/..., NAME the project's [serve] name. The reads runs,
run N (--files, --ledger, --turns), attention, board, ticket
KEY and report (--since sent as since, --notes drawn here from a
body that always holds the notes) print what the local command prints,
board's editable false as there; attention with no project is the
root's. requeue, send-back, hold, release, pause, resume, abort
and start post to POST /projects/NAME/actions/...
(The daemon's actions): release to release-hold, start to
launch-loop, which takes no note and releases no hold, and pause and abort first read the
ticket's live run from GET /tickets/KEY. A write's result is the daemon's
reply, printed as a ✓/✗ line or, with --json, as given but with the
command's words as action (start, release), as locally; its
recorded is the id of the interventions row the daemon wrote, as
locally. stderr says via http to URL, and a --json result gains
"transport": "http". Every other command, status included, has no
route: it exits 1
saying so and, where ssh carries it, that transport = "ssh" runs it, and
nothing is sent over either road. A redirect is not followed, so the token
goes nowhere but url, and an answer that is not a JSON object exits 1. A 401 exits 1 naming token_file's file; a 404 on an action
names the host's [serve] actions; any other refusal exits 1 with the
daemon's message (2 for its 400), and a daemon that does not answer exits 1
naming the URL.
Startup checks¶
Every mode validates every config.toml table it can see and refuses an
unknown key. The loop, --worker, --supervise, --requeue, --approve,
--babysit, --close, --abort and --file-ticket need a [board]
table; --pause, --resume, --hold, --release-hold, --repoint and
--gap-layer do not; --move and --cancel need a native one. The loop additionally live-probes each configured agent
route and the reviewer image before claiming, and runs a read-only sweep
whose output it prints.
Exit codes¶
| Code | Meaning |
|---|---|
| 0 | done, or the board was empty |
| 1 | a startup refusal, a failed run under stop_on_failure, an invalid ticket file, a refused requeue, approval, babysitter, re-point, pause, resume, abort, close-out, hold or hold release, a refused project command, a stale revision or refused move or cancel; a holo read answered 404, 503 or any status but 200 and 400; holo report on a project with no store |
| 2 | --file-ticket: the issue exists but its stored body failed re-validation; argparse errors, project commands' included; holo: no project found, a name host.toml does not register, or a refused client.toml; a holo read answered 400; a holo report --since window it does not take |
Output prefix¶
Every line the factory prints begins [holo2]; the verify gate's report
lines begin [verify]. A worker of the pool prints [holo2 wN] instead,
N its slot number, so the lines of a pool sharing one log tell apart. The loop's tmux log is the operator's first source
after the store.
Environment¶
| Variable | Read by | Purpose |
|---|---|---|
HOLOPHYTE_HOME |
Project |
the state root, default ~/.holophyte; tests point it at a temp dir |
HOLO_PROJECT |
holo |
the project, a [serve] name or a repository path, when no -p is given |
HOLO_TRANSPORT |
holo |
local runs the command here whatever client.toml's host says; the one value |
NO_COLOR |
holo |
set and not empty, holo prints its symbols without colour on a terminal too |
LINEAR_API_KEY |
linear_provider |
the board's API key; env or .env beside the module |
HOLOPHYTE_TARGET, HOLOPHYTE_SERVE_ADDRESS, HOLOPHYTE_SERVE_PORT |
the project units (holophyte-serve@, holophyte-supervise@), and HOLOPHYTE_TARGET alone the loop unit |
one instance's project, bind address, port |
LISTEN_FDS, LISTEN_PID |
--serve |
set by the service manager's socket unit: with LISTEN_FDS=1 and LISTEN_PID this process's pid, the daemon serves on fd 3 instead of binding, and exits 0 on a factory HEAD move for the socket to start the new code |