FireTrident architecture #
FireTrident is a git platform for many coding agents working one repository at once, built on Cloudflare Workers and Artifacts. It coordinates agents the way colonies do: through signals left in a shared environment, local decisions, and parallel integration, instead of locks and one merge queue. The names the code borrows from natural systems (colony, field, antibody) are mapped to plain terms in glossary.md.
The model in one screen #
- Colony: one codebase. A trunk repository in Artifacts plus the coordination state around it.
- Field: decaying marks on regions of the code (claim, need, trail, failure, danger). Agents read the field instead of messaging each other. Reading it costs the same no matter how many agents there are. In
take(request flow step 2) an open intent's need, its priority growing with age, attracts; claims and danger on its own files repel, and so do claims on their co-change neighbors and, withFT_IMPORT_WEIGHT, on their direct imports and importers. Trail and failure marks are kept forsense, the map and replays, and steer no take. - Intent: what should change and why, declared before any code is written, with a predicted footprint (the paths it expects to touch). Its
priorityorders all work:takeranks open intents by it (and age,core/threshold.ts), its need marks carry it as their strength, and its changes wait in their region's lane by it. The platform ranks its own work first: a red-trunk repair is 10 (certify/repair.ts), a megafile split 8 (colony/megafile.ts), a spec's intents what the spec declares, or one more than the highest intent that uses them (the demo spec uses 1 to 5), a rule's intents 1, and a person's intent whatever the person asks. An agent's intent takes at most the priority of a parent it holds or declared, else 1 (colony/priority.ts), and keeps what it asked asrequestedPriority, so no agent can rank its work above a repair.assignedTo(RESTPOST /api/colonies/:id/intents, admin token) declares an intent already claimed by a named agent: the intent is createdclaimed, its claim marks are deposited and the agent is set working in the same step, so no other agent can take it, and the agent gets it back from its nexttake. Scripted checks and benches use it to place work exactly (the cascade, text merge, insertions, the resolver, goal conflicts, the megafile check); no MCP tool or Agent2Agent call sets it. - Rule (morphology): a declared architecture rule. Every violation becomes a need in the field and an intent.
- Workspace: an Artifacts fork leased to one agent with a short-lived, workspace-scoped token. Agents never hold a trunk write token.
- Proposal: an intent's change: base and head commits in the workspace, the files written (write set), the files read (read set), declared footprint, and evidence.
- Lane: a region's integration queue. Each cycle it picks a maximal set of non-conflicting, still-valid proposals, greedily in priority then age order (not necessarily the largest such set), and hands them to the trunk writer, which lands them as one commit.
- Inline changes (
core/changeset.ts,colony/inline.ts): a proposal may carrycontents, the text of every file it writes keyed by git blob id; its head ischangesetId(baseSha, writeSet), andinlineProblemchecks it (every text hashes to its blob, text only, at most 64 files and 512 KB). The colony keeps the texts inproposal_contentsuntil the proposal settles; the decide stage, contest judging and the trunk writer read them there, never a workspace. The API (contentsonPOST /proposals) and MCP (proposewithfiles) accept them whenFT_INLINEcovers the colony. Every take still leases a workspace repository, inline changes included, so a run of thousands of agents leaves about one workspace repository per agent until its colony is deleted. - Lineage: every landed commit carries
FT-*trailers and a JSON note underrefs/notes/firetridentwith the intent chain, read and write sets, decisions, and evidence. Each decision is a review receipt: the head it was made on, the sha256 of the exact view its decider was shown (marked clipped when it was partial), and for a person whether the name was verified by Cloudflare Access (FT_ACCESS_TEAM_DOMAIN,FT_ACCESS_AUD: theCf-Access-Jwt-Assertiontoken checked against the team's keys) or only stated; past the last 8 decisions the note says how many earlier ones it left out. Secret-looking text in a note's free-text fields (what the agent did not verify, check details, directives, decision questions and answers) is replaced with[masked]before the note is written (trunk/mask.ts). Each proposal's entry carriesprovenance(trunk/provenance.ts), computed by the trunk writer from the colony's record of the agent, never from the proposal:author(platform,external,synthetic),auth(platform; for an external agentmcpwhen it holds a resume key,tokenotherwise) andmodelwith its evidence:observedfor in-platform agents, named after the model their calls went to (an intent may ask for one that is not the agent's own) and with the calls and tokens the agent loop counted;declaredfor every other. The API refuses to register an in-platform agent (they start throughPOST /swarm) or to propose for one. A note names its landing, never its commit, so it is final before the commit is built, and the commit carries its digest as its last trailer:FT-Note: sha256:<hex>, over the masked note as written to git, withoutcertification(trunk/digest.ts).TrunkDO.verifyNotes(trunk/verify.ts;GET /api/colonies/:id/notes/verify, public;ft verify <colony>) compares the newest notes onrefs/notes/firetridentwith their commits' trailers and reports eachintact,alteredormissing;scripts/verify_note.mjsdoes the same with stock git.TrunkDO.verifyLanding(GET /api/colonies/:id/notes/verify?landing=<landing id or commit>) checks one landing's note, and every note in the Why tab shows the result as a badge (intact, altered, or landed before notes were sealed).GET /api/colonies/:id/landings/:ref/diff?path=returns a landing's change to one file, at most 64 KB, for colonies listed inFT_PUBLIC_GITorFT_PUBLIC_MCPand 404 for any other; the Why tab shows it behind "Show the change". WithFT_NOTE_KEY(a Worker secret holding an Ed25519 private key as a JWK), the trunk writer signs each commit it builds (trunk/signature.ts):FT-Note-Signature: ed25519 <key id> <signature>, just above FT-Note, overfiretrident-note-signature v1, the note's digest, the tree and the parents./.well-known/firetrident-keys.jsonpublishes the public key (its id is the first 16 hex digits of the SHA-256 of the key) and/api/aboutnames it; the note checks,ft verify, the Why badge andscripts/verify_note.mjs --keys <url>say signed, bad signature or unsigned. Without the secret, commits are built exactly as before. The landing page's signed card and the colony top bar read the same check and/.well-known/firetrident-keys.json.GET /api/colonies/:id/root(trunk/root.ts) is the colony's root commit, read once and kept, and the top bar andft statusshow it;verify_note.mjs --colony <root>andft verify --all --colony <root>fail any other history. From a stock clone,--allchecks every landing's note, signature and files against its write sets (antibody commits against the one file they write, reverts against the blobs they restore), one line per landing with who is answerable, and--tamperchanges one field of a copy of the newest note in memory and must see it caught;ft verifyprints the same lines as the script. WithFT_NOTE_KEY, every landing and system commit also carries a standard git signature (trunk/gitsig.ts): agpgsigheader holding an SSH signature (namespace "git", SHA-512) by the same Ed25519 key, sogit verify-commitwithgpg.format=sshchecks it against/.well-known/firetrident-allowed-signers.FT_NOTE_KEYS_RETIRED(JSON,[{x, since, until}]) lists rotated-out public keys:/.well-known/firetrident-keys.jsonand the allowed-signers file list them with their windows, and every checker accepts a signature by a key whose window covers the commit time, so rotation never breaks older signatures (rotation steps are intrunk/signature.ts). WithFT_CREDIT_TRAILERS, landing commits carryCo-authored-by: <agent> <agt_x@<colony>.noreply.firetrident.dev>for each agent whose change lands andApproved-by:for the person who approved, above the FT trailers, so any git host shows them.
Request flow (an in-platform agent) #
ColonyDO.registerAgentgives the agent an id.ColonyDO.take(agentId)builds candidates from open intents, reads the field from the relevantRegionDOs, and picks one with the response-threshold rule (core/threshold.ts). Three signals shape the choice:- Lateral inhibition: a candidate is skipped when other agents' live claims on its exact footprint files reach a threshold (half a fresh claim by default): their sum, or with
FT_INHIBIT_STRONGESTthe strongest single file's. Claims decay, so a silent agent's claims stop inhibiting after one half-life, about 3.5 minutes, for one shared file; summed over n shared files they hold for 5·ln(2n) minutes, by the strongest file one half-life however many overlap. There is no lock to release and nothing to deadlock. - Co-change repulsion (soft): TrunkDO mines trunk history into a Hebbian co-change map (
core/cochange.ts,trunk/cochange.ts); candidates whose files usually change together with files other agents have claimed score lower. - Admission:
RegionDO.admitchecks the region's admission cap and deposits the agent'sclaimmarks in one synchronous step, so concurrent takes cannot over-admit a region. Only then doesPoolDO.leasehand out a workspace.
The assignment also says what review will ask of the change (
review,agents/preview.ts): the questions of up to five live antibodies whose path matchers cover the footprint, and the rules that cover it. An agent sees a pattern people rejected before it writes it. Security antibodies are left out: an agent whose change one of them returns is told only the class, never the question that would show how to slip past it.It also says why the code is the way it is (
context): the latest landings, at most three, that changed the footprint's files, with their intents' titles and whys, from a per-path index the colony fills as landings arrive (no trunk call per take).And it says why the intent's latest change came back (
lastReturn): the proposal, when, the paths it wrote and the reason its agent was given, unless a change for the intent has landed since. Direction returns and contest losses are skipped, and a security return names only the class. In-platform agents see it as fenced data to address first; MCPtakeandcheckoutreturn it aslastReturn. A change that came back because trunk moved its files names the landings that moved them (movedBy: the landing, its intent's title and why, its agent, the model its note names and the trunk commit), on the proposal, in the inbox, in take'slastReturn, in the redo prompt (fenced as data) and on the web cards, and its reason ends: keep what that landing did unless your intent says otherwise, and escalate if both cannot hold.- Import repulsion (
FT_IMPORT_WEIGHT, off when absent or 0): claims on the direct imports and importers of a candidate's files repel it the way co-change neighbors do. TrunkDO keeps the import index (trunk/imports.ts,importLinks) current from landings; a take asks for it once per batch of files, cached, and fails open. In the simulator it cut rework from 31% to 16% at weight 1, for about 10% fewer landings a minute (bench results 2026-10-13-papers). Where nothing conflicts it is pure cost: on a preview, the Hono rule run (20 Gemma agents, 0 returns either way) took 199 s to zero at weight 1 against 137 s and 143 s without it, so it stays off unless a colony's work contends on hub files.
- Lateral inhibition: a candidate is skipped when other agents' live claims on its exact footprint files reach a threshold (half a fresh claim by default): their sum, or with
The agent (
AgentDO) clones trunk atlease.baseShainto a@cloudflare/computerWorkspace using the read-only trunk token, reads its footprint's files and the files its intent builds on (each recorded in its read set with its blob id; the project tree its write prompt lists, up to 400 paths, and the modules its import check reads are not recorded), edits with an LLM, commits, and force-pushes its commit tomainof its own workspace repo with the workspace write token. The fork's prior content never matters. (At very large scale agents read from trunk mirrors instead of trunk, to respect per-repo git request limits.) With virtual checkouts (FT_VIRTUAL_CHECKOUT:onor colony ids) an agent never clones: it reads the commit, the root tree, the subtrees along each wanted path and then the blobs through theARTIFACTSbinding atlease.baseSha, cached by id (agents/virtual.ts), and proposes an inline change: no commit and no push. A change over the inline limits, or with binary content, still goes the git way for that change.ColonyDO.proposerecords the proposal, computes the residual (paths changed outside the declared footprint), runs innate checks, and enqueues it in the lane of the proposal's primary region (RegionDO.enqueue).On each lane tick (
RegionDOalarm, interval fromcore/batch.ts#laneIntervalMs), the region asksTrunkDO.blobsForfor current trunk blobs, validates proposals withcore/occ.ts, selects a batch withcore/batch.ts#selectBatch, and callsTrunkDO.land(batch). Stale proposals are returned to their agents with each stale path, the blob it read and the blob now on trunk.TrunkDO.landqueues the batch. One cycle at a time takes every waiting batch (up to 200 proposals), re-validates each proposal against trunk and the batches ahead of it, reads each proposal's new blobs from its workspace repo (readBlobon the binding), or, for inline changes, their texts from the colony in one call per cycle (checking each blob id), builds one commit per batch on top of the previous one by replacing only the changed paths (FT-*trailers, a lineage note each), and pushes the tip once with the expected old value (compare-and-swap) (with asynchronous publishing,FT_ASYNC_PUBLISH, the cycle records its landings and moves the trunk writer's own main instead, and a publish loop beside the cycles pushes every commit recorded since its last push, in order: leases,certifiedand the lineage notes follow the published head, lanes validate against the recorded one, the recorded head stays within 2,000 landings or 60 s of the published one, a restart publishes first, and a take whose intent covers a file changed by an unpublished landing waits half a second instead of leasing a base without it); the notes follow in one notes commit onrefs/notes/firetrident. The lane then reports eachLandingtoColonyDO.onLanding, which updates intents, depositstrailmarks, reinforces the agent's threshold for the region, and broadcasts events.
Durable Objects and their RPC contracts #
All are SQLite-backed (new_sqlite_classes). Callers use RPC on stubs (env.COLONY.get(env.COLONY.idFromName(colonyId)).take(agentId)). Types come from @fire_trident/core.
ColonyDO (name: colonyId) #
Owns colony config, intents, rules, agents, proposals index, review queue, antibodies, decision counters (the funnel), metrics, and the live WebSocket hub (hibernation API) for the UI. It also prices the colony's model calls (colony/spend.ts, core/pricing.ts): in-platform agents count calls and tokens per model in their journals, which the colony reads into its own tables (16 at a time, at most every 15 s, until an agent has stopped and been quiet 5 minutes), and review, the contest judge, the conflict resolver and antibody minting count theirs per model as they call. GET /api/colonies/:id/metrics and GET /api/colonies/:id carry spend: USD in total, by layer (agents, review, judge, resolver, other) and per landed intent, tokens by model, and the tokens of any model with no listed price, which count in no dollar sum; prices are Workers AI list prices on 2026-10-12. The top bar, ft status and the landing page's rule card show it as model calls this platform made. Tokens counted before the per-model counters existed are priced at the model their layer is set to use (review at Clef, second opinions at the dearest second-opinion model). Per intent (colony/intentspend.ts, FT_INTENT_SPEND): agents count each call under their job's intent in their journals, review and the contest judge count by proposal, and GET /api/colonies/:id/intents/:id/spend prices them by layer and model; the resolver and antibody minting are not counted per intent.
init(p: { colonyId; name; trunk: RepoRef; trunkSha: Sha; source?; regionDepth? }): Promise<Colony>snapshot(): Promise<ColonySnapshot>addIntent(i: Omit<Intent, "id" | "status" | "createdAt">): Promise<Intent>(also depositsneedmarks)addRule(r: Omit<Rule, "id" | "createdAt">): Promise<{ rule: Rule; violations: number; intents: number }>registerAgent(a: { kind: AgentKind; name: string; model?: string }): Promise<AgentInfo>take(agentId: Id): Promise<{ intent: Intent; lease: WorkspaceLease; rule?: Rule; review?: ReviewPreview } | { wait: true; retryInMs: number; reason: string }>getProposal(id: Id): Promise<Proposal | null>(agents poll their outcome)getRule(id: Id): Promise<Rule | null>heartbeat(agentId: Id): Promise<void>(refreshes claim marks)propose(p: Omit<Proposal, "id" | "status" | "createdAt" | "evidence"> & { evidence?: Partial<Evidence> }): Promise<Proposal>updateProposal(id: Id, patch: Partial<Proposal>): Promise<void>(called by RegionDO/TrunkDO)onLanding(l: Landing): Promise<void>review(): Promise<Array<Proposal & { review?: ReviewView }>>(each escalated proposal with its masked review view),decide(proposalId: Id, d: "approve" | "reject" | "return", reason: string, by: string): Promise<void>emit(e: ColonyEvent): Promise<void>(broadcast to UI sockets; also used by other DOs)fetch(req): WebSocket upgrade for/api/colonies/:id/live
RegionDO (name: ${colonyId}:${region}) #
Owns the field for one region and that region's lane.
configure(p: { colonyId: Id; region: string }): Promise<void>deposit(marks: Array<Omit<Mark, "id">>): Promise<void>clear(filter: { agentId?: Id; intentId?: Id; kind?: MarkKind; paths?: Path[] }): Promise<number>reading(): Promise<FieldReading>enqueue(p: Proposal): Promise<void>recordCause(e: { id: string; causeId?: string; at: number }): Promise<void>(criticality governor)alarm(): lane tick
TrunkDO (name: colonyId) #
The only writer of trunk. Holds a @cloudflare/computer Workspace with trunk cloned, an index of trunk paths to blob ids, and the lineage tables.
init(p: { colonyId: Id; trunk: RepoRef }): Promise<{ trunkSha: Sha; files: number }>files(): Promise<Array<{ path: Path; size: number; blob: Sha }>>blobsFor(paths: Path[]): Promise<Record<Path, Sha | null>>readFile(path: Path): Promise<string | null>baseBlobs(baseSha: Sha, paths: Path[]): Promise<FileVersion[]>land(batch: Proposal[]): Promise<{ landing?: Landing; returned: Array<{ proposalId: Id; reason: string }> }>why(path: Path, line?: number): Promise<Array<{ landing: Landing; note: LineageNote }>>landings(limit?: number): Promise<Landing[]>revert(ref: string, p: { by: string; reason: string }): Promise<RevertResult>(see "Changing direction")markCertified(sha: Sha): Promise<boolean>(see "Trunk certification")
PoolDO (name: colonyId) #
Keeps a warm pool of workspace forks of trunk across the ft-ws-* namespaces. Each namespace holds a seed repo that mirrors trunk main, and workspaces fork from their namespace's seed (forks cannot cross namespaces, and forking a repo just pushed fails about one time in five: spike/RESULTS.md facts 2 and 3). Workspaces are refreshed to the current trunk on lease and recycled after landing.
init(p: { colonyId: Id; trunk: RepoRef; target: number }): Promise<void>lease(agentId: Id, trunkSha: Sha): Promise<WorkspaceLease | null>release(agentId: Id): Promise<void>status(): Promise<{ ready: number; leased: number; forking: number; target: number }>
AgentDO (name: agentId) #
An in-platform agent built on @cloudflare/computer (a git workspace) and Workers AI (or Claude when ANTHROPIC_API_KEY is set). It loops: take, work, propose, wait, repeat. Its model gets no tools: the loop reads the files and records the read set (request flow step 3). It journals each step so a killed agent resumes.
start(p: { colonyId: Id; agentId: Id; model: string }): Promise<void>stop(): Promise<void>,status(): Promise<AgentInfo>
FireTridentMcp #
An agents/mcp McpAgent exposing tools for external agents (any MCP client): join, sense, take, declare, checkout, read, propose, inbox, release, escalate, why, impact. Every tool call heartbeats the agent's claims, so a slow external agent keeps its work; an agent silent past the colony's limit (ten minutes unless a person sets one; in-platform agents keep ten) loses its intent to the colony, with a failure mark (see Closure ledger). take and inbox accept wait (the server waits for work or for the proposal's outcome). join registers the session as an external agent and returns a resumeKey once (the colony keeps only its SHA-256); another session acts as that agent only with agentId and that key. take returns git steps that need no shell state (no cd or export), and with checkout the questions review will ask of the change, the rules that cover it and the why of the latest landings on its files; escalate reports an intent that cannot be done as written (see Escalations); propose records the agent's notVerified statement for review; why (trunk/whywalk.ts) says whether each landing still holds on today's trunk (still as written, changed since by a later landing, or deleted), and the readers add the current certification of its covering tests (core/whyholds.ts); for a line it skips formatting-only landings (whitespace, quote style, trailing commas and semicolons, rewrapping: core/reformat.ts) and follows blocks of three or more lines moved from another file, bounded at 200 landings, 3 moves and 2 s and failing open, with the line's owner first; why trims each note to the proposals that wrote the path (otherProposals counts the rest). Session state tolerates parallel tool calls. A failed call returns isError with structuredContent.error and the same JSON as its text (mcp/errors.ts): kind (validation, auth, not_found, conflict, too_large, rate_limit, server, unavailable), message, retryable, retryAfterMs (a jittered 1 to 5 s for platform failures such as an overloaded object), status, and next where the next call is clear. declare remembers each call's fingerprint (title, why, sorted footprint, acceptance, priority, parent) in the session's object for ten minutes, and a repeat returns the same intent with replayed: true; take, declare, propose, release and escalate are annotated idempotent. impact lists a file's importers up to three levels (at most 200 files, each with the import it comes via), the live claims on them (agent, intent and claim age, read from at most 32 regions) and the whys of up to 10 recent landings on them; it labels agent-written text as data and fails open (the import index gets 3 s to sync, or the answer says it may be behind). It also answers on the public MCP for listed colonies, as GET /api/colonies/:id/impact?path= and as ft impact. With FT_MODEL_STEPUP (on or a list of colony ids), an intent whose changes came back twice since it last changed course is taken next with the stronger model class (default): the take says why, an in-platform agent uses it and its note's observed model shows the switch, and an outside agent gets it as advice; a third return still reaches the stuck strip. Beside MCP, with FT_A2A (on or a list of colony ids) an orchestrator hands a colony work over the Agent2Agent protocol (api/a2a.ts, colony/a2a.ts): a public agent card at GET /api/colonies/:id/.well-known/agent-card.json and JSON-RPC at POST /api/colonies/:id/a2a, where SendMessage declares an intent (the first line of the text is the title, the rest the why; data parts may carry the footprint and acceptance) and needs the admin token, GetTask and ListTasks follow intents as tasks without one (open is submitted, claimed or proposed is working, landed is completed with the commit and a link to the why, withdrawn by CancelTask is canceled, withdrawn otherwise or escalated and not approved is failed), and CancelTask withdraws (admin token). A twin answer returns the existing intent's task and says so. See docs/agents/a2a.md.
Artifacts layout #
ft-trunknamespace (bindingARTIFACTS):<colony>--trunkand, later, lane repos<colony>--lane-<region>.ft-ws-0..3namespaces (bindingsWS0..WS3): workspaces<colony>--ws-<n>, sharded byn % 4to spread the per-namespace control-plane limit.ft-sessionsnamespace (bindingSESSIONS): one repo per in-platform agent session.
Measured in the spike (spike/RESULTS.md): import and fork are synchronous (about 1.3 s per fork, parallel even from one source); about 20% of forks issued within ~0.1 s of a push to the source fail with INTERNAL_ERROR and must be retried; git inside a Durable Object via @cloudflare/computer clones in ~350 ms, commits in ~90 ms, and pushes in ~300 ms; a commit built by tree surgery takes ~40 ms; forks copy every ref including notes (defaultBranchOnly is ignored); binding read calls need main or a full commit id (HEAD and refs/... return null); the server rejects pushes whose expected old value is stale (compare-and-swap) but accepts forced non-fast-forward pushes; the namespace push trigger starts a Workflow for every repo in the namespace (~0.5 s to instance, ~2.3 s to run()).
Facts the design relies on (from the docs and the spike): each repo is one Durable Object; git requests are limited per repo and control-plane calls per namespace; the binding reads but cannot write files, so writes go through git; there is no server-side merge, hook, or branch protection; Artifacts accepts non-fast-forward pushes, so only TrunkDO ever holds a trunk write token and it pushes with an expected old value.
Live events #
ColonyEvent (see packages/core/src/types.ts) is the single event shape for the UI and for internal fan-out. The UI first loads ColonySnapshot, then applies events from the WebSocket.
Additional RPCs (as built) #
- ColonyDO:
getColony,listRegions,getIntent,getAgent,registerAgents(batch),abandon(agentId, reason),inbox(agentId, limit, opts?)(proposals and directives),updateProposals(batch),recordDecision,landContext(intentIds, agentIds, proposalIds?)(compact lineage context for lanes: intents with their direction, agents, recorded decisions). Changing direction:amendIntent,withdrawIntent,retractRule,direct,recall,relay,intentHistory(see "Changing direction"). - AgentDO:
direct({ directive, intent? })(a directive pushed by the colony; see "Changing direction"). - RegionDO:
reading(opts)(optionally filtered by paths),apply,dequeue,status. - TrunkDO:
initaccepts an optionalregionDepth;land(batch, ctx?)takes compact lineage context (lanes pass it, so the writer never pulls a snapshot);stats,flushNotes(re-pushes any note whose push failed after its landing),mirrorTo,refreshTokenIfNeeded. TrunkDO does not callonLanding; the lane does. - PoolDO:
stats,teardown. Forks cannot cross namespaces, so eachft-ws-Nnamespace holds a seed repo<colony>--seedthat workspaces fork from. - AgentDO:
start({ colonyId, agentId, model?, kind?, name?, delayMs? });status()returns AgentInfo plus journal fields. - RegionDO:
configure({ colonyId, region, regionDepth });admit(atomic admission plus claim deposit, see request flow step 2);landedFor(paths)answers which landing last changed a path, so a stale return with no local cause records the landing in another region as its parent (the cascade detector sees cross-region causes). - TrunkDO:
neighbors(paths)andcochangeStats()read the co-change map;landingsOf,systemCommit(antibodies and reverts; a file may name the blob it expects to replace, and a mismatch refuses the whole commit).verifyLanding(ref)andlandingNote(ref)(one landing's note check, and the writer's copy of its note for the diff route),protect(globs)(the protected globs every cycle reads) and the impact walk over the import index. - Assumptions. The decide stage records on each proposal the trunk files its new or rewritten relative imports resolve to (
assumptions, its own writes excluded).planCyclereturns a proposal whose assumption is gone from the virtual trunk (deleted on trunk, or by a batch ahead in the cycle) and one that would delete a assumption of a proposal accepted earlier in the cycle, so a deletion and a new import of the same file never both land in one cycle (a deletion that lands in a later cycle is caught by the deletion guard when it is on, otherwise only by certification). Only existence counts. - Import-name revalidation (
FT_REVALIDATE:on, or a comma-separated list of colony ids; off when absent). The decide stage also recordsnameAssumptions: for each trunk file a change's relative imports resolve to, the names it imports from it. A change that is stale only because files it read changed (none it writes, no file or directory clash) lands when every such file is covered by a name assumption and every name is still exported on the virtual trunk (the lane passes it on, andplanCycledecides from export lists read from blobs, cached by blob id). Its lineage note recordsrevalidated(path, blob read, blob landed on, the names that held), and arevalidatedevent is published. Namespace, side-effect and dynamic imports,export *, non-script and sensitive files are not covered, so a change relying on them goes back as before. Names are not enough on their own: each file the change read must also have only grown since the version it read (decide/statements.ts): every top-level statement of the read version is still present, token for token and in order, in the version the change lands on, and whatever is new is a declaration (the prologue of imports and directives stays in place). A changed or missing statement sends the change back naming the file and the first such line; a file whose statements cannot be cut reliably (top-level control flow, labels, decorators without a class, JSX, an ambiguous slash, more than 20,000 statements) sends it back too. The note'srevalidatedentry gainsgrew: true. What a new declaration does at load time is not checked: certification is the backstop. The cycle reads the blob each stale read saw as well, within the sameSURFACE_READSandSURFACE_BYTES. Checked on a preview (2026-10-13) with the cascade scenario on Hono, 30 readers importingHonofromsrc/hono.tswhile a writer added an export to it: with revalidation on, 0 of 30 went back and all landed on their first proposal; with it off, 30 of 30 went back, σ peaked at 29 and the region was inhibited 14 times. - Text merge (
FT_TEXT_MERGE:on, or a comma-separated list of colony ids; off when absent). A proposal stale only on files it writes (textMergeCandidate: each with a blob on its base, on trunk and in the proposal) passes the lane and the trunk writer's screen, andplanCyclemerges each such file line by line against the virtual trunk, trunk or a version landed earlier in the same cycle (core/textmerge.ts: git's default rules; line endings, final newline and BOM kept; at most 1,000 changed lines a side). When every file merges cleanly the proposal lands with the merged blobs, which later proposals in the cycle see; the landing and the lineage note recordmerged(path, base, trunk, theirs, result), and the trunk writer countstrunk.merged. A conflict sends it back as before, naming the conflicting hunks and their lines. Binary files, lockfiles and files over 512 KB are never merged, and the trunk writer loads at most 400 blobs and 8 MB of text a cycle for merges. Checked on a preview (2026-10-14,packages/bench/results/2026-10-14-round5/): two agents editing lines 2 and 11 of one file from the same trunk both landed, the second as a merge whose result is the text with both edits; with text merge off it went back stale; two edits to line 6 ended with one landing and the other back, naming the hunk. WithFT_MERGE_INSERTIONS(onor a list of colony ids), conflicting hunks that are insertions by both sides at one place land as their union, trunk's lines first, recorded asrule: "insertions"inmergedand counted intrunk.mergedInsertions; a line of the change is left out as already inserted only when it is a whole statement or entry standing alone in both insertions (core/insertions.ts), so shared braces and blank lines stay. - Conflict resolver (
FT_RESOLVER:onor a list of colony ids;FT_RESOLVER_MODEL, defaultFT_DEFAULT_MODEL). Where the flag covers the colony, a change sent back because text merge found overlapping hunks carries the conflicts (path, base, trunk and theirs blobs, hunk lines) through its lane to the colony (colony/resolver.ts), which holds the intent for the platform agentresolverand tells the change's agent. One model call through the decide limiter sees each conflicting file with conflict markers, fenced as data, and returns its whole merged text keeping both changes, orCANNOT: <reason>. The merge must keep every line outside the conflicting hunks as the clean merge has it, take neither side's version of a hunk whole, still parse, and touch no other file (colony/resolver-merge.ts); then it is proposed inline for the same intent, through review like any change, with provenanceplatformand the modelobserved, and its note recordsresolves(the returned change, the landings it merged with, the files). Any failure, a 60 s timeout or a second return lets the original return stand and reopens the intent. At most 20 resolutions an hour and 2 at once per colony; metricsresolver.attempted,resolver.landed,resolver.failed. Checked on a preview (2026-10-14,scripts/resolver.ts): two edits of one line from one trunk, the second sent back, then merged with both edits kept and landed; with the resolver off it went back as before. The merge must also keep every line either change added inside a conflicting hunk, in that change's order (a line may be rewritten only where it rewrites a base line the other side also changed); a merge that drops or reorders one is refused, naming the line and its place in the base. - Who is answerable (no flag;
core/answerable.ts). New notes carry each intent'ssourceandcreatedByand, for a rule's intent,ruleApprovedBy, and each landing recordsanswerable. Every landing card (the Why tab, the note card,ft why, both MCPwhytools,scripts/verify_note.mjs) names the nearest answerable person: a person who approved the change, else the approver of the rule whose deterministic proof landed it, else the person who asked for the intent, else "no person", with why; older notes readunknown.GET /api/colonies/:id/metricsservesanswerable(landings, none, unknown, share) andscripts/coordination.rbreports it. Every evidence row says who said it (checked by FireTrident, a model by name, a person, the agent's claim), platform facts first, andnotVerifiedBykeeps FireTrident's sentence apart from the agent's own words, which show in quotes. - Already on trunk (
FT_ALREADY_ON_TRUNK:onor a list of colony ids; off when absent;trunk/ontrunk.ts). A change that would change nothing on the trunk it lands on (every file it writes already holds that content and every deletion is already gone, after the changes ahead of it in the cycle and after a text merge) lands no commit and is not sent back: its intent closes as landed with causealready_on_trunk, naming where the content came from (the landing ahead in the cycle, else the landing that last changed those files, else the trunk head), and the agent is told there is nothing to redo. The lane passes a stale change whose writes trunk already holds on to the trunk writer for this check. It is counted apart from landings and returns (alreadyOnTrunkin/metrics, the coordination report and the run scripts);scripts/beat_two_agents.rb --sameshows it. Off, such a change lands an empty commit with a note under text merge, or comes back stale without it. A true retry of the same proposal is still answered with its earlier result. - Spend cap (no flag;
colony/spendcap.ts,api/spendcap.ts). A person sets a colony's dollar cap and a reserve kept for review (POST /api/colonies/:id/spend-cap {usd, reviewReserveUsd}with the admin token,GETto read it,ft spend-cap <colony> <usd> [--review-reserve <usd>] | off, shown inft statusand the top bar). The colony object keeps the ledger: every model call is reserved before it starts (prompt characters ÷ 3 plus its maximum output tokens at list price, twice that when a hedged copy may go out; in-platform agents ask the colony before each call and settle after) and settled at the tokens it reported, so concurrent calls cannot overshoot. A call that would fit once calls in flight settle waits (429); one that cannot fit is refused, and the pause trips with the reason "spend cap: $X of $Y spent on model calls this platform made" and the actorplatform:spend-cap. Everything but review stops at the cap minus the reserve; review may spend the reserve, and once it is gone review makes no model call and the change goes to a person (rule L8, never bounced by the alert budget). A refused call is never an approval. When a proven change has matching antibodies and the reflex gives no answer for any reason (the cap, an error, a timeout), it goes to a person (rule M2) instead of landing unscreened. Raising the cap past the spend or removing it lifts the pause; resuming while still over says so and re-trips at the next call. Every spend figure reads "model calls this platform made". - Recipe landings (no flag for built-in recipes;
core/recipe.ts,trunk/recipes.ts,colony/recipe.ts,api/recipes.ts). A recipe is a third kind of change beside a proposal of files and an intent: a deterministic transform with a pinned version, arguments and scope globs. Built in:rename-identifier(a JS/TS identifier renamed wherever it is an identifier token: declarations, imports, exports and references, never inside strings, template text, comments or property keys; a tokenizer, not a parser, so no scope analysis),replace-import(an import or export specifier) andtext-replace(an exact string or a whole-line anchored pattern). Each returns the new text or a typed reason it could not (unparsable with the line, binary, out of language, name taken, unread). A person declares one (POST /api/colonies/:id/recipes, admin token;ft recipe <colony> rename <old> <new> --scope <glob>); it becomes an intent no agent takes, and the trunk writer lands it between landing cycles in batches of at most 100 files or 4 MB, each run twice and compared, danger-scanned (a hit waits for a person, whose approval lands exactly that output) and landed as one commit whose note records the recipe, each output's blobs and the files left as they were. Commutation: inplanCycle, a change built before a recipe's last batch has the recipe applied to the files it wrote, and its bases and reads count as current when trunk changed them only through the recipe, so it lands withrecipes: [{recipe, landing, paths}]in its note instead of going back; a file the recipe cannot rewrite, or whose rewrite would add a danger pattern, keeps the change's own version under the ordinary rules, and a return names the recipe's landing; a merge-locked change goes to a person with the rewrite.GET /api/colonies/:id/recipes/:rid(admin token) reports progress and what the recipe would still change. Module recipes (FT_RECIPE_MODULES, off) run a recipe module sealed in a Dynamic Worker, twice. - Coordination tokens (no flag;
colony/coordtokens.ts). Each take counts the characters of what it hands an agent beyond its intent (why-context, lessons, last return, review preview) with the agents active and a read-everything size (all landed whys and active lessons), per power-of-two bucket of active agents; heads-ups count apart;GET /api/colonies/:id/metricsservescoordTokens(characters ÷ 4, an ESTIMATE), andscripts/coordination.rbreports it beside access by model and agent kind, return to reland, culprit precision, residue and review rounds, and lines alive by model. - Recipe scenes (
scripts/scene_rename.rb,scripts/scene_smuggle.rb, on a generated 300-file project). Checked on staging 2026-10-17: a person's rename across 212 files landed while 30 scripted agents held edits on 40 of them, and all 30 changes landed with the new name and none came back, where stock git conflicts on all 30 of the same edits; and onefetchline hidden in an 893-line change that names its recipe had 889 lines reproduced and the one line sent to a person by the danger scan, while the same rename without it landed on D5, both with no model call. With--recordthey bundlerecordings/rename.json.gzandsmuggle.json.gz, and a director scene naming arecordingopens it from any other run; a replay shows the review card of each change waiting for a person at that point, from the recorded review queue. - Review by reproduction (
FT_REPRODUCE:onor a list of colony ids;decide/reproduce.ts). A proposal (REST or MCPpropose) may name the recipe that made it (recipe: {kind, version, args, scope}). The decide stage replays it twice on the change's base files: files whose output matches are reproduced, everything else is the residual, shown against the replay, and only the residual goes to the reflex, antibodies and the review card ("Reproduced by recipe" above it). Danger patterns, protected paths, damage and innate checks still read the whole change, and a matching file with damage of its own moves to the residual. A fully reproduced change lands on rule D5, reproduced by recipe, with no model call. Unknown, unpinned, module,text-replace(it can change what code does) and nondeterministic recipes get ordinary review with the reason in the evidence. The evidence, the decision and the lineage note carryreproduction(files and lines on each side). - Rules that carry recipes (
colony/rulerecipe.ts). A rule may carry a recipe (ft rule … --recipe '<kind> <args>', the REST rule route): the colony lands it as the rule's own intent, answerable to the rule's approver, recounts violations after each batch, and declares agent intents only for the residue the recipe could not fix; the spend card shows the mechanical share at $0 in model calls. - Preview and the mosaic (no flag;
api/preview.ts).GET /api/colonies/:id/preview/:commit/*pathserves a trunk file at a landing commit (a full SHA, a prefix or a landing id) for colonies inFT_PUBLIC_GITorFT_PUBLIC_MCP, with asandboxcontent security policy (no scripts; an opaque origin),nosniff, a type from a short list (scripts as plain text), 413 over 512 KB, 503 with Retry-After when busy, and an immutable cache for pinned SHAs. A replay of a colony with a rootmosaic.jsondraws it as a picture at the replay's landing;scripts/mosaic.rblands 100 tile intents on that one file from 20 scripted agents by text merge, every tenth also adding a line toroll-call.mdby the insertion rule, and--recordbundles the replay. - Closure ledger (no flag;
colony/closure.ts,api/closure.ts). Every close or reopen of a held intent records its cause (landed,released,returned,escalated,withdrawn,gave_up,stopped,reaped_silent,already_on_trunk), the agent and its kind, and the gap from the agent's last sign of life (0 when the agent's own call closed it); the latest closure is on the intent, the Intents tab and the events log label it,ft intent showprints the history,GET /api/colonies/:id/metricsservesclosuresby agent kind and cause, andscripts/coordination.rbreports them. The reaper takes an intent back from an outside agent silent past the colony's limit (ten minutes unless a person sets one withft policy --silent-ttlorPOST /api/colonies/:id/silent-ttl, 60 s to 24 h; in-platform agents, which report every three minutes, keep at least ten) and leaves the failure mark an abandon leaves.scripts/walkaway.rbruns it as a beat on a throwaway colony and can record it for offline playback (/recordings/walkaway.json.gz). - How a colony decides (no flag;
decide/presets.ts,decide/ruleline.ts,GET /api/colonies/:id/decides, public). The colony's decide policy is named as one of four presets (local development, central development, which isDEFAULT_POLICY, pre-production, production), or custom with the values that differ, each with one sentence on what lands without a person; the card on the colony page and the Why tab adds the last 30 days of landed changes by what confirmed them (deterministic proof, a model, earned autonomy, a person, no review), fromdecide_outcomes, cached 60 s. Each gate check keepsgate: the rule, the preset then, and the values it compared with the bound each met (e.g.R3: confidence 0.93 ≥ 0.9, p(needs_human) 0.04 < 0.2, approved landings 23 ≥ 20), so every landing card,ft whyand new lineage notes name the confirming rule;ft statusprints the preset and the split. - Tests before the person (no flag;
certify/reviewtests.ts). When a change is sent to a person, the colony's tests run on it at its base in the certification sandbox (60 s, 2 at a time per colony, 32 waiting), reusing stored results for test files the change does not reach; each failure is compared with trunk (passes on trunk, fails on trunk too, new). The review card andft reviewlead with "Tests on this change: …",ft review <colony> --retest <id>andPOST /api/colonies/:id/proposals/:pid/tests(admin token) run them again, and a person's decision and the lineage note record "tests before approval: …". Where certification is off it says "not run" and why. - Pause switch (no flag;
colony/pause.ts,api/pause.ts,trunk/pause.ts). A person pauses every agent of a colony (POST /api/colonies/:id/pause {reason},ft pause <colony> --reason) or of the deployment (POST /api/pause,ft pause --deployment), and resumes the same way. While paused, agent writes (take, propose, an agent's declare, escalate, registering platform agents,POST /swarm, A2ASendMessageandCancelTask) are refused with one sentence naming who paused and why; they are checked at the MCP tools, A2A and/swarmand always inside the colony object. Lanes hold, and the trunk writer defers anything handed to it before the pause (a pause waits for the cycle in flight), so trunk does not move; in-platform agents start no model call and wait without counting a failure. Reads, join, heartbeats, release and every person action go on, so a person can still clear the queue. The pause is stored (it survives a restart), recorded aspausedandresumedevents, shown as a banner on the colony page and in the snapshot'spauses, and a deployment pause in/api/about. Other code trips it with its own reason and actor (pauseColony,pauseDeployment). - Returns quote what beat them (
FT_RETURN_HUNKS:onor a list of colony ids;colony/returnhunks.ts,trunk/quote.ts). A change that goes back because trunk moved its files quotes what beat it: each landing's unified diff of the moved files (movedBy[].hunks: path, trunk commit, diff, truncated) and each contested region of a text merge (contested: path, base line, base, trunk and own lines), asked of the trunk writer once per batch of returns within a second, capped (40 lines and 240 characters a line per file, 3 files a landing, 120 lines and 8,000 characters a return, contested regions first) and masked. The redo prompt carries them inside its fence, MCPtake,checkoutandinboxcarry them labeled as data, and the review card shows them. On a timeout or error the return goes out as before; off, nothing is asked. - Heads-up and watermark (
FT_HEADS_UP:onor a list of colony ids;colony/headsup.ts,mcp/headsup.ts,api/headsup.ts). Every MCP reply to an agent holding an intent carriesheadsUpwhen one applies:trunkMoved(landings since its lease base that touched files it read or will write, with landing and intent title, from the landing-paths index) andwaitingAhead(changes in flight whose write sets meet its read or write set). The colony re-reads in-flight changes at most every 2 s, a session reuses an answer for 3 s and waits at most 50 ms. When that check errors or misses the 50 ms the tool's answer still goes out, withheadsUp: {unavailable: "timeout" | "error", message}, so an agent never reads silence as nobody touching its files; a scan that reached its 200-change cap says how many changes it read (aheadScanned,waitingAheadScanned). Each such reply carriesdeliveredThrough, stored per take and sent with the proposal astoldOf; a return because trunk moved says whether and when the agent was told of the landing that beat it, counted in/metricsasheadsUp.warnedorheadsUp.unwarned(andscripts/coordination.rb).GET /api/colonies/:id/agents/:aid/headsup(MCP or admin token, or the agent's resume key inX-FT-Resume-Key) serves the Claude Code hook thatft hook installwrites into the checkout's uncommitted.claude/settings.local.json: a PreToolUse check before edits that adds one line of context when something waits, or says the heads-up was unavailable, and always exits 0, so it never blocks the agent. Take and checkout setgit config firetrident.agentso the hook knows the agent. - Hedged model calls (
FT_HEDGE:onor a list of colony ids;FT_HEDGE_MSoverrides the waits;colony/hedge.ts). Every model call the platform makes (Clef, the second opinion, the contest judge and jury, antibody rephrasing, in-platform agents, the resolver) can go out once more when it has no answer after its site's wait (Clef 3 s; the second opinion, judge and antibody 12 s; the resolver 20 s; agents 60 s, set from recorded latencies), or at once when the original fails fast; a throttled original sends no copy to the same model. The second opinion's copy goes to the next candidate model, the judge's and the resolver's only when a limiter slot is free. The first answer wins and the other is aborted; when both fail, the original's error stands. The winner's tokens count as before and a losing copy that still answers is counted when it arrives; the decision recordshedged(after how long, which copy won, its model), and the check text and the note's decision line say so. A call site can pass a validity check, so a malformed first answer counts as a failed attempt (causeinvalid) and the copy can answer. Off, the calls are exactly as before. Separately, the resolver retries once, hedged or not, when its reply will not parse or keeps conflict markers (never when the dropped-lines audit rejects a well-formed merge); the retry counts in the limiter, the spend cap and the spend meter, and the evidence recordsresolver: retry. - Merge locks (no flag,
trunk/mergelock.ts). A change a person approved, one with a danger pattern a person must see, or one that writes a protected path never lands merged with a newer trunk on its own. A clean merge goes back to a person with each file's base, trunk and merged blobs and two capped diffs (what trunk changed since the change's base, and the merged result against the version that was decided on), shown on the review card and inft review; an approval after that lands exactly that merge, and if trunk moved again it is held again. A locked change whose merge conflicts goes back to its agent without its conflicts, so the conflict resolver never takes it. When the locks cannot be read, the cycle merges nothing. The trunk writer also never changes a submodule (a change that writes or deletes a submodule's path, or writes inside one, goes back to its agent; it reads trunk's tree along the paths a change touches, because its file index lists no submodules), refuses a change that would retarget a symbolic link, and refuses a new path that differs from a trunk path, or from another path in the same change, only in letter case or Unicode form (trunk/structure.ts). - Megafile alarm (
FT_MEGAFILE:onor a list of colony ids). From landings and returns (colony/megafile.ts,core/hotness.ts), a script file (tests excluded) turns hot at 600+ lines with 25% growth and 4+ distinct writers within 30 minutes, or after 6+ returns that wrote it. A hot file gets one split intent (sourcesystem, priority 8, footprint the path,dir/*anddir/stem/**, acceptance: every name it exports stays importable from it) and a hold: take skips other intents whose footprint covers it, a change that writes it goes back at propose (with review off too), and changes already on their way go back when the hold begins. The hold lifts when the split lands, is withdrawn or escalated, after 60 minutes, or when a person releases it (POST /api/colonies/:id/megafile {path, action},ft megafile); the colony's status lists held files, and the split's evidence carries the file's lines before and after into its note. Checked on a preview: a file grown from 650 to 830 lines by five agents was held, its in-flight changes went back, the split landed (830 → 3 lines, all 69 exports kept) and the hold lifted. - Field guide (
FT_FIELD_GUIDE:onor a list of colony ids). An agent may leave alesson(at most 300 characters) with a proposal: MCP or RESTpropose, or an in-platformLESSON:line after the file. It is cleaned, masked like note text and kept as a candidate with the proposal's provenance (colony/guide.ts,core/guide.ts). It becomes active at the first green certification of a commit at or after its landing, or when a person approves it (POST /api/colonies/:id/guide/:lessonId,ft guide), and is dropped when its landing is reverted, a red certification names its change, its change comes back, or a person rejects it. At most 30 active lessons and 6,000 characters (the oldest retires); near-duplicates are refused.takeandcheckoutcarry the newest active lessons (up to 4,000 characters) asguide, and prompts fence them as data under a label that says who wrote each and the landing it came from. Checked on a preview (scripts/field_guide.ts): a lesson went active on its landing's green certification and reached the next agent's take with its provenance; a reverted landing's lesson was dropped. Each new lesson keeps the files its change read and wrote with their blobs (scope, written files first, at most 20); withFT_LESSON_SCOPEa take ranks lessons on the intent's footprint first, then newest, names the files later landings changed (movedSince, with the landing and its intent; prompts and MCP say "Written before these files changed"), and leaves a lesson out once every file of its scope has changed (colony/lessonscope.ts). Lessons without a scope show as before. - Deletion guard (
FT_DELETION_GUARD:on, or a comma-separated list of colony ids; off when absent). In a cycle that deletes trunk files, the trunk writer brings its import index (trunk/imports.ts) up to date and sends back a change that deletes a file while a trunk file's import of it would resolve to nothing after the cycle, naming up to ten importers ("deleting src/b.ts breaks the imports of src/a.ts: change them in the same change, or first"). Importers that the same change, or one accepted ahead of it in the cycle, deletes or rewrites so they no longer import the file do not count. Imports resolve as revert's guard resolves them (relative specifiers; path aliases and packages are not followed). Only cycles that delete files pay: locally, on Hono's 569 files, the first such cycle took about 150 ms more (building the index) and later ones about 1 ms. It fails open: when the index cannot be brought up to date within 3 s, the cycle runs without it. Removing an export is guarded by the export guard when it is on (next). - Export guard (
FT_EXPORT_GUARD:on, or a comma-separated list of colony ids; off when absent;trunk/exportguard.ts). In a cycle that changes trunk script files, the trunk writer reads the names each changed file exports before and after (cached by blob id); for a name it stops exporting, it asks the import index which trunk files import that file and reads their imports (the same 3 s budget, at most 400 blobs and 8 MB). A change goes back while a file still imports the removed name and neither the change nor one accepted ahead of it in the cycle rewrites that file: "removing export foo from src/b.ts breaks the import in src/a.ts: change src/a.ts in the same change, or first". A removal and a new import of the same name never land in one cycle: the second goes back naming the first. Whatever it cannot read for sure (an index that is not current,export *in either version, CommonJS, path aliases the index does not see) it skips and counts astrunk.exportsUncovered; it never sends back on a guess. - Derived indexes (
trunk/derived.ts): every index derived from the landing log shares one mechanism: a name, a version, eager or lazy, an idempotentapplyper landing and a cursor inft_derived. Eager indexes apply each landing in the landing hook; lazy ones catch up from their cursor when read, in bounded slices; a version bump rebuilds from the log; a landing that cannot be applied is skipped and logged. TrunkDO: the import index (eager), the co-change map and path history (lazy;pathHistory(path)answers who changed a path without reading notes). ColonyDO: the why-context index (eager). destroy()on RegionDO, TrunkDO, PoolDO and AgentDO wipes the object's storage and alarms;DELETE /api/colonies/:idcalls them.deprovisionTrunk()inartifacts/colony.tsremoves a colony's repos.- MCP:
/mcp(Streamable HTTP) and/sse, gated byFT_MCP_TOKEN, sent as anAuthorization: Bearerheader and compared in constant time; atokenquery parameter is refused, and without the secret the endpoint refuses connections./public/mcp(mcp/public.ts) is a stateless, read-only MCP for the coloniesFT_PUBLIC_MCPlists (status,why,intent,sense,impact, each takingcolony; no token, nojoin), and answers 404 when the variable lists none. Its tools reach only read methods and serve what the public read routes serve (remotes hidden, security return reasons withheld); a request is capped at 64 KB and 8 messages, a call at 15 s, and an isolate at 32 requests in flight. WithFT_AGENT_CALL_BUDGET(on: 4 tool calls a second, 20 at once after a pause; or<rate>,<burst>), each MCP session has a token bucket (mcp/budget.ts): a call that finds it empty answersrate_limit(429, retryable after the time until a token is back) without running, so it never reaches the colony. The bucket lives in the session object's memory, so a new session starts full. Checked on a preview with the bench scenarioflood(16 greedy sessions pollinginboxwithoutwaitagainst 4 honest agents): 63,444 of 71,418 greedy calls were refused before the colony (7,974 reached it, against 56,450 with the budget off), and the honest agents landed at the same rate with no refusals.
Decide: the reflex arc and the immune review (packages/worker/src/decide/) #
A proposal enters checking and the decide engine routes it:
- Innate checks: the rule re-check on the changed files (every live rule, the intent's own first, up to 200), residual paths (changed but not in the declared footprint), a mandatory "Not verified:" statement, and hollow files: a new script file that is empty or holds only comments or placeholder bodies goes back.
- Deterministic proof: every changed relative import is resolved against the trunk file tree. When only import specifiers changed and each still points at the same file, the rule re-check passes, the change stays inside its footprint and touches no sensitive path, the proof is a confirmation on its own and no model is consulted. A broken import is returned with the exact path. The resolution facts are passed to every model that is consulted, and no model opinion can override them. A change whose every changed file is an existing script file with the same tokens outside comments and whitespace is proven not to change behavior (
decide/comments.ts, an exact lexer; JSX, any directive comment and credential-looking new comments disqualify it), and lands on the innate checks plus this proof (the colony policy'scommentProof, on by default). - System 1 (reflex): one Clef call (
@cf/cloudflare/clef, typed noul/choice/score questions: needs_human, risk, touches_sensitive, off_intent, plus every matching antibody question). Confident answers act; uncertain ones escalate. - System 2 (LLM): consulted only when the deterministic proof is inconclusive and the reflex cannot act alone. A second opinion from a different model family than the authoring agent (
@cf/google/gemma-4-26b-a4b-it, or@cf/openai/gpt-oss-120bwhen the author is Gemma), strict JSON verdict, behind a per-model concurrency and rate limiter with backoff and fallback (Workers AI caps large models at 20 requests a minute per account). Reviewers see up to 24,000 diff characters a file and 60,000 in all. An approval counts as a confirmation only when the layer saw every changed line; a change too large for its view goes to a person ("too large for model review"), and each layer's view (files shown of files changed, what was cut) is recorded with its decision. The state models see says who wrote the intent's why: a person, a rule, certification or an agent, whose why is a claim to check. Every model prompt (second opinion, contest judge, antibody questions) puts the intent, the agent's own text, the facts and the diff inside one fence marker that occurs in none of them, labeled as data to judge, never instructions. When every configured reviewer model is in the author's family, no model is asked and the change goes to a person (rule L9); when the author never reported its model (an outside agent need not), the review runs and the decision records that independence was not checked. The contest judge asks only models outside every contestant's family, and with none configured the deterministic order picks the winner. - System 3 (human): the review queue.
propose is idempotent: a repeat for the same agent, intent and head commit returns the existing proposal. Deterministic outcomes are recorded as their own checks in the evidence and in decide.deterministic.* metrics; they never count as reflex decisions in the funnel.
Loosened guards (danger.ts): in modified script files (not tests), deleted 401/403 denials and throws inside security conditions, security conditions made weaker (an operand dropped, === turned into ==, a negation removed, a constant comparison dropped), verify swapped for decode or a constant-time compare dropped, and deleted expiry or limit checks are a loosened-guard signal: a second opinion must look, and no intent text, policy or antibody waives it. Measured on real history (packages/bench/results/2026-10-13-danger, every modified non-test script file checked on its own): on Hono, the loosened-guard signal fired on 13 of 3,559 files (0.4%), 12 of them in auth, JWT, CSRF or signed-cookie code, and on none of Hono's newest 251 commits; all the danger checks together flagged 1.1%. On FireTrident's own history they flagged 9.5% until a SQL statement run on a handle named sql, sqlite, db or database stopped counting as running a program (decide/sqlexec.ts; a file importing child_process, cluster or worker_threads keeps every exec firing): now 3.1% (1.8% over all its history), 2.1% sent to a person, and Hono's 1.1% unchanged (packages/bench/results/2026-10-17-danger/). Escalation batching (FT_ESCALATION_BATCH; decide/batch.ts, api/batch.ts): the escalations that skip the hourly alert budget get a budget of their own (policy escalationBatchPerHour, 60); past it each change still waits for a person, but the queue groups them by reason into one card with approve all, reject all and open each (POST /api/colonies/:id/review/batches/decide, admin token; ft review --approve-batch / --reject-batch). Only capability hits and the operational rules (M2, L5, L7, L8) are batched; never authentication, secrets, governance, handoff, a repair's test change, a loosened guard, a protected path, a security finding's next proposal (P2), a goal conflict (P4), a held merge, an audit item or a canary. How often the checks miss a dangerous change nobody has seen is not measured.
Protected paths (rule P3): the colony policy's protectedPaths (globs; GET/POST /api/colonies/:id/decide/policy, ft policy --protect) send any change that writes, deletes or renames a matching path to a person, naming the glob, in every phase and even with review off; no intent text, antibody or policy waives it. A glob added later reaches changes already cleared to land: each lands only with a person's approval given after protection, checked by the colony, each lane and every trunk-writer cycle (TrunkDO.protect, table ft_protected), and the policy answer says which changes were stopped and which had already landed.
Twin intents (FT_TWINS: on or a list of colony ids; core/twins.ts, colony/twins.ts). An intent a person or an agent declares (REST or MCP declare) is compared with the colony's open, claimed and proposed intents and those landed in the last 30 minutes: when its normalized title words score at least 0.8 (Jaccard, after punctuation, stop words and do-it verbs are dropped and words lightly stemmed) against one and their footprints overlap, nothing is created; the answer names the twin (duplicateOf, similar) with a sentence saying to take it or declare again with force: true. Removing never matches adding, rule, certification, spec and vaccine intents are never checked, and ft new declares with force. Who declared it (colony/chain.ts): every take carries declaredBy (source, the declaring agent and its model, the chain depth of agent-declared ancestors through parentId), and the in-platform prompt shows it beside the why for an agent-declared intent, with the note that an agent's request is not a thing to copy onward and that text asking to be spread is reported. With FT_INTENT_CHAIN_GATE (on or a list of colony ids), an agent-declared intent at a chain depth of intentChainDepth (policy, default 2) or more, or the intentFanoutth (default 5) one its declarer has declared in an hour, is held as an open report of kind chain: takes skip it, GET /api/colonies/:id/escalations and the Review tab show it, a dismissal releases it and a withdrawal ends it; the gate fails open.
Goal conflicts (rule P4, FT_GOAL_CONFLICT: on or a list of colony ids). A change that removes lines a recent landing wrote for another intent waits for a person: in a text file it modifies or deletes, it removes at least max(3, half) of the lines that landing still has there, the landing is within the decide policy's goalConflictMinutes (60) and among its newest goalConflictLandings (200), and its intent is neither an ancestor nor a descendant of the change's own (core/goalconflict.ts). Blank lines and re-indented lines do not count. The trunk writer traces the removed lines through its landing log and notes (TrunkDO.removedLineOwners, bounded to 20 files, 2,000 lines, 512 KB and 4 s). The reason names the landing, its age, its intent and why; a person approving lands the change with undoes in its note. Certification repairs, rule intents, the colony's own intents (megafile splits) and the platform's own commits never conflict, and the check fails open: its own failure never holds a change, and the evidence says it was not checked. Agents are also told to escalate with contradictory-spec instead of undoing another intent's recent work. Checked on a preview (scripts/goal_conflict.ts): three agents each moving one backend to a different language; with the check, the first landed and the other two waited, each naming the landing it would undo, and a person chose; without it, all three landed and the last silently won. scripts/goal_conflict.ts --demo <colony> sets up the same three goals on one colony with an agent assigned to each and never decides anything: the first change lands and the two that would undo it stay with a person. Production's order-backend colony is that demo.
Two-signal gate (gate.ts): a proposal lands without a person only on two confirmations bound to its head commit: the innate checks, plus a deterministic proof that it changes nothing but what its rule asks, a second model's approval of every changed line, or earned autonomy with a confident, quiet reflex. On the proof path both confirmations are deterministic and rest on a rule a person wrote: the proof passes only when the innate checks pass, and p(needs_human) is not consulted (Clef's alarms are advisory there; an antibody hit still returns the change). A second model's approval and earned autonomy also need a low p(needs_human). Earned autonomy applies only to intents written by a person or a rule, and only when the reflex saw the whole change. A second opinion without a needs_human probability (missing or not a number) reads as 1, so it never lands a change on its own. Neither earned autonomy nor a deterministic proof (D3, M1) lands a destructive change alone (destructive.ts: a file deleted or renamed, an export removed, 200 or more net lines removed, or more than half of a file of 40 or more lines): it goes to the second opinion with the reason named, and the fact shows on the review card. Earned autonomy needs the fact computed: a file it cannot read to tell (binary, CommonJS, export *) leaves it unknown, and the reflex alone does not land the change. One signal returns it to its agent. Escalation to a human needs a detector hit plus damage or a capability change. Regulation: an alert budget caps human pages per hour; a detector marked not useful ≥10% goes advisory and >25% goes off. Danger patterns (danger.ts): hits for new process, network, eval or crypto use are waived only when an intent written by a person or a rule asks for them; an agent's or a repair's text never waives them. Files another tool runs or obeys (handoff.ts, one list: MCP client configuration, editor and dev-container settings, .envrc, git hooks, .gitmodules, nested .git, virtual environments, other agents' instruction files, and .gitattributes lines that add or alter a filter, diff or merge driver) are a must-human class, handoff, that no policy, antibody, intent text or earned autonomy waives, deleted ones too. Governance, handoff, the sensitive paths and protected paths all match case-folded and NFC-normalized (pathfold.ts). In a repair intent from certification, any test-file change beyond imports is a must-human hit no policy or antibody can waive, and node:test's skip and todo forms, and assertions that disappear (commented-out ones do not count), are test-weakening. Security returns: a return for a security finding tells its agent only the class; the full reasons stay in the decision record and the review view, the proposal route withholds them from readers without the operator token, and the intent's next proposal goes to a person. Antibodies: a human rejection mints an antibody (path matcher + yes/no question + threshold) only when no existing antibody fired on the proposal (otherwise it counts a recurrence on the one that did), committed to trunk as .firetrident/antibodies/<id>.json by TrunkDO.systemCommit with a lineage note; every later matching proposal is screened, and the return reason names the antibody's question. Antibody files are authoritative: a landing that edits, renames or deletes one reloads or retires it. Vaccination meter: POST /api/colonies/:id/vaccinate {count} injects sealed bad proposals of several threat classes through the normal path; they can never land, and recall per class is reported with a Wilson 95% interval (GET /api/colonies/:id/vaccinate). The report separates vaccines returned (stopped by a model or a check) from vaccines escalated (sent to a person, whose decision the meter does not measure), and a vaccine whose second opinion was unavailable is an error, not a catch. Review view: for every escalated proposal the engine keeps a masked view in decide_review (each file's hunks up to 24,000 characters, 60,000 in all, the danger hits with line numbers, the residual paths; lines that may hold a secret are masked), the review queue serves it, and the web card and ft review lead with the lines to look at first.
Quoted acceptance (FT_QUOTED_ACCEPTANCE: on or a list of colony ids; decide/quotes.ts, decide/acceptance.ts). For an intent with an acceptance list, the second opinion answers each line as met, not met or unclear, naming a file and quoting at least 8 characters of a changed line for each met; the acceptance list is fenced as data in the prompt. The platform looks for each quote, whitespace-collapsed, in the diff it showed the model (a quote it cannot find, or one from a removed line, counts as unclear), and the approval counts as a confirmation only when every line is met with a found quote. A line stating what must not be there has no line to quote: a met counts on the model's word, recorded as basis: absence. A line not met returns the change naming it (so does a keep line: the acceptance of the newest landing by another intent on the files the change modifies); an unclear one goes to a person. The decision, the lineage note, ft why, the Why tab and the public MCP why keep each line with its verdict and quote. Rule intents on the proof path, certification repairs and vaccines keep today's rule.
Review view (FT_REVIEW_VIEW: on or a list of colony ids; needs FT_QUOTED_ACCEPTANCE; decide/reviewview.ts, decide/keepcheck.ts, decide/tsexports.ts). Before tests before landing and any model, FireTrident checks each acceptance or keep line that names a file and what it exports (a function signature, an interface, a type alias, a constant, or that the file exists) by reading the files after the change and the trunk files the line names. A line it proves is met, marked checked by FireTrident on the review card and in the lineage note (basis: checked); a line it disproves sends the change back with the exact mismatch (rule A1), a keep line only when it held before the change. The second opinion is asked only the lines left open, sees each changed file whole where it fits the review budgets (24,000 characters a file, 60,000 in all; what it sees of the change is never less than the plain diff showed), a quote found in a changed file after the change counts, and its review receipt digests that whole-file view.
Thresholds (DEFAULT_POLICY, decide/policy.ts). One policy per colony; POST /api/colonies/:id/decide/policy (admin token) changes it, and mergePolicy clamps each number to its range. The gate's rules above read them by these names.
| Setting | Default | What it does |
|---|---|---|
enabled |
on | Runs the reflex arc on every proposal; off, only protected paths still reach a person (P3) |
deterministic |
on | Lets a mechanical proof confirm a change with no model (D3, M1) |
commentProof |
on | Lets the comment-only proof (comments.ts) confirm a change to existing script files whose code is unchanged outside comments and whitespace |
confidence |
0.8 | A layer acts alone only when the answers that decide are at least this confident (|2p − 1| for yes/no answers): R1, R3 |
hit |
0.5 | A yes/no detector answer at or above this probability is a hit (needs_human, touches_sensitive, off_intent, antibodies) |
needsHuman |
0.5 | p(needs_human) must be below this for the reflex (R3) or the second opinion (L3) to land a change |
alertsPerHour |
30 | Escalations to a person per rolling hour; past it, one below budgetRiskFloor goes back to its agent, except the rules marked as never bounced (P2, P3, P4, M2, L5, L7, L8) |
budgetRiskFloor |
2 | Risk (0 none to 3 high) at or above which an escalation still reaches a person when the budget is spent |
autonomyAfter |
3 | Landings in a region approved by a deterministic proof, the second opinion or a person since its last revocation (approvedLandings) before R3 may land there alone |
probation |
0.1, 0.25, 5 | After 5 fires, a detector overruled (by a higher layer, a person's approval or feedback) at least 10% of the time is advisory (recorded, not gating), more than 25% off (not asked) |
maxAntibodies |
60 | Matching antibodies asked per proposal (one Clef call takes 64 questions; 4 are the standard ones) |
llmConcurrency |
8 | System 2 calls in flight at once, per colony (the contest judge's own limiter takes the same numbers) |
llmPerMinute |
120 | System 2 calls started per minute, per colony and model |
goalConflictMinutes, goalConflictLandings |
60, 200 | P4 (FT_GOAL_CONFLICT): a landing is recent for 60 minutes and while among the colony's newest 200 landings |
intentChainDepth |
2 | FT_INTENT_CHAIN_GATE: an agent-declared intent with 2 agent-declared intents in its ancestry waits for a person |
intentFanout |
5 | The same gate: agent-declared intents one agent may declare in an hour before the rest wait |
reflexModel |
@cf/cloudflare/clef |
System 1 |
secondOpinionModels |
@cf/google/gemma-4-26b-a4b-it, @cf/openai/gpt-oss-120b, @cf/zai-org/glm-4.7-flash |
System 2: the first from a different family than the author's |
juryModels |
@cf/google/gemma-4-26b-a4b-it, @cf/openai/gpt-oss-20b, @cf/zai-org/glm-4.7-flash |
FT_CONTEST_JURY: two or three outside the contestants' families sit in place of the judge |
dangerAllow |
none | Danger classes a person allows without a person (optionally under some paths); allowed hits stay danger signals, so a reviewer's approval is still needed |
protectedPaths |
none | Globs only a person may change (P3), at most 50 |
auditRate |
0.03 | The share of changes that landed without a person (proof, reproduction, a model or earned autonomy) sent to a person after landing, sampled per confirming layer, 0 to 0.2 |
canaryRate |
0 | The share of escalations accompanied by a sealed known-bad change in the human queue to measure what reviewers catch, 0 to 0.2; off by default |
Routes: POST|GET /api/colonies/:id/vaccinate, GET /api/colonies/:id/immune, POST /api/colonies/:id/decide/policy, POST /api/colonies/:id/detectors/:did (useful / not useful feedback). ColonyDO: vaccinate, vaccineReport, immune, setDecidePolicy, detectorFeedback. Every decision is recorded (recordDecision) and emitted, so the UI funnel shows reflex / LLM / human counts. GET /api/colonies/:id/immune is public: each autonomy row gives the approved landings since the region's last revocation, that revocation's reason and when the region last moved (lastAt); the Review tab shows them latest first ("Autonomy earned in N of M regions"), and ft immune <colony> prints them with the antibodies and detectors.
Changing direction (chemotaxis) #
A colony is steered the way cells follow a chemical gradient: when the gradient changes, cells re-orient. Here a human changes the gradient (amends an intent, withdraws it, retracts a rule, or sends an agent guidance), the field updates (needs evaporate, move or appear), and agents re-orient at their next step. Nothing is preempted by a lock or a queue. Agents also tell the colony what they are doing, so a human can see where to push. Pure helpers live in colony/direction.ts (colony side) and agents/direction.ts (agent side).
Amend (PATCH /api/colonies/:id/intents/:iid {title?, why?, footprint?, priority?, reason, by?}, ColonyDO.amendIntent, ft intent amend). The intent's revision goes up by one and the change is kept in intent_revisions (fields changed, before and after, reason, by, at). Need marks move with the footprint (strength = priority) and claims leave the paths it dropped; the holder's claim follows the paths it gained. A title, why or footprint change is substantive: changes in flight built on the older revision go back (intent revised: r1 → r2, why changed (...)), and the holder keeps the intent, claim and lease and redoes the work against the new revision. A priority-only amendment just moves the intent in the queue. GET /api/colonies/:id/intents/:iid/lifecycle (public; core/lifecycle.ts, ft intent show) returns each change's lifecycle strip: declared, claimed, proposed, confirmed (the layer and rule), landed, certified, taken back and merged by a person, each with its evidence and time, and where a change left the path (returned with the landing that beat it, escalated, withdrawn, reaped); the Why tab, the review card and the Intents tab draw it. The last two steps happen in the team's repository, so FireTrident never marks them done. Propose is idempotent per agent, intent and head, retries in flight included; over MCP, a retry after a lost reply returns the same proposal marked replayed and writes nothing. Only a person's amendment, a rule or the platform raises priority: an intent an agent declares (MCP declare, A2A SendMessage, POST /intents with source agent) takes at most the priority of a parent the agent holds or declared, or 1 otherwise (colony/priority.ts); what it asked is kept as requestedPriority, with priorityCap saying which cap applied, the lineage note records both, and the answer says what it got (priorityNote). Landed, withdrawn and sealed intents cannot be amended (409); GET /api/colonies/:id/intents/:iid (ft intent show) returns the intent with its revisions and directives.
Withdraw (POST .../intents/:iid/withdraw {reason, by?}, ft intent withdraw). The intent becomes withdrawn (kept, with the reason), its need marks evaporate and its claims clear, changes in flight go back (withdrawn: ...), and its holder goes idle and gives its lease back. Idempotent. Retract a rule (DELETE /api/colonies/:id/rules/:rid {reason, by?} or ?reason=, ft rule retract): the rule stays in history (retractedAt, retractedBy, retractReason) but is no longer checked (the decide arc and agents skip it), its open violations stop counting, and every intent it created that has not landed is withdrawn. Landed work is never touched.
Directives (POST /api/colonies/:id/agents/:aid/directive {message, intentId?, by?}, ColonyDO.direct, ft direct). Free-text guidance for one agent, about the intent it holds unless intentId names another. Revisions and withdrawals reach the holder the same way, as directives of kind revision and withdrawal. An in-platform agent gets them pushed (AgentDO.direct); external agents read them in their inbox (ColonyDO.inbox, which the MCP inbox tool calls): a directive entry lines up with a proposal entry (id, intentId, status: "directive", reason = what to do, createdAt). Reading a revision notice records that the agent saw that revision.
How an in-platform agent re-orients (loop.ts, applyDirection). Directives are kept in the agent's journal and acted on before its next step. A withdrawal ends the job (the colony already released the claim). A revision ends it too, and the next take hands back the same intent at the new revision. Guidance joins every later prompt for that intent (a "Directives from a human operator" block); work already produced is redone with it, after the agent recalls a proposal still in flight (ColonyDO.recall: returned as superseded: ..., intent, claim and lease kept). A directive that makes the step in flight moot (any withdrawal or revision of the job's intent, guidance while it produces or proposes) moves the agent's epoch, so the step halts at its next check and no more model calls go the old way.
When a change of direction takes effect. An amendment or withdrawal sends changes in flight back and dequeues them; ColonyDO.propose refuses (409) a change built on an older substantive revision (an external agent passing no intentRevision is held to the revision it last saw); the lane returns stale direction before TrunkDO.land. The commit point is the trunk writer: the colony records the new direction with TrunkDO.direction (table ft_direction), and each landing cycle reads it once before building. Recorded before that read, the change stops; after it, the change lands, and the call waits for that cycle (at most 15 s) and names the landing, which ft revert undoes. Responses give returned (changes stopped), landed (each with its landing and trunk commit) and confirmed (false when the trunk writer did not answer, so those changes may still land). A withdrawal that lost the race leaves the intent landed; an amendment that lost it keeps the intent live ("landed a change built on r1; r2 still to do"). Direction returns leave no failure mark, are not counted by the cascade detector, count as redirected (not returned), and never reopen the intent. scripts/direction_race.rb checks both outcomes on a deployment, with FT_TEST_PUSH_DELAY_MS holding each push open. Checked on a preview (2026-10-13) with the push held 6 to 10 s: 7 withdrawals that arrived after the cycle's check were each answered with their landing, and trunk agreed; 2 sent while the change waited in the writer's queue stopped it. The first queued try found that the colony recorded the withdrawal at the trunk writer only after telling the lanes, whose dequeue can wait behind a landing in flight, so the change's own cycle never saw it; the colony now starts TrunkDO.direction first.
Relay (POST /api/colonies/:id/agents/:aid/status {note}, ColonyDO.relay). An agent's one-line "what I am doing now" is stored on the agent (GET agents and ft agents show it in the NOW column), emitted live (at most one event per agent per second), and recorded with its next proposal. In-platform agents relay as they enter each step and when they act on a directive (at most every 3 s; synthetic agents at most once a minute).
Lineage. At propose time the colony records the direction a change was built under in its evidence (evidence.direction: intentRevision, the amendments up to it, the human guidance the agent had for the intent, its last status note), so it lands in the lineage note under refs/notes/firetrident. landContext(intentIds, agentIds, proposalIds) also hands the lane each proposal's recorded decide-arc decisions (layer, kind, p, action, at, the question, who answered, a one-line answer; the latest 8 per proposal), which TrunkDO writes into the note's decisions. A deterministic proof that decided alone appears in the note's evidence checks (deterministic: ..., gate). ft why and the Why tab show the revision history, directives, note and the decide arc.
Revert (POST /api/colonies/:id/landings/:ref/revert {reason, by?, redo?, proposal?}, ColonyDO.revertLanding, ft revert; ref is a landing id or a trunk commit of 7+ characters). A person undoes a landing as a new trunk commit that puts every path it wrote back to its blob from before (trunk/revert.ts plans it; TrunkDO.revert writes it through the serialized system-commit path). It is refused (409) when any of those paths has changed since, naming each path and the latest landing that moved it, or when it would delete a file that trunk files import, naming the importers; both checks run again inside the commit. The intents the landing completed come back as new intents (parent: the reverted intent, why: the reason first; redo: false declares none), and a rule's violations count again until they land. The revert commit carries FT-Revert-Of and a lineage note with revert {of, commit, by, reason}; certification runs on the result; a revert can itself be reverted, which withdraws the intents it declared again (ColonyDO keeps each revert in reverts). With proposal (ft revert <colony> <prp_...>), only that proposal of the landing is undone: its own paths (the landing's recorded paths, filtered by the proposal's write set in the note), with every guard applied to them, and only its intent comes back. The commit carries FT-Revert-Proposal, the note's revert names the proposal, and a trunk writer still on older code refuses the call (503) rather than undo the whole landing.
Landing notices. When a landing changes files that agents still working hold in their footprint (never its authors), each gets one directive of kind landing with the landing, the changed paths and their blobs before and after, and the landing intent's title and why: facts, never instructions. An in-platform agent that has not proposed yet rebases on trunk's tip and re-reads the changed files (at most twice); after proposing it does nothing, since the lane catches staleness. MCP agents read notices in their inbox. The directive event carries kind: "landing", the landing id and the paths.
The fence fails closed. When a cycle cannot read the direction record, or a row of it does not parse, the trunk writer holds the changes it could not check for the next cycle instead of letting them through: nothing lands or comes back, no failed attempt is counted, the lane rechecks after 1 s, and /metrics counts it (direction.unchecked, and trunk.directionUnchecked on the cycle). The protected-path check does the same (protect.unchecked): with its record unreadable no change lands, because a path protected a moment ago may be among those the lanes passed.
Events: amended, withdrawn, retracted, reverted, directive and status (plus intent with status withdrawn). The Events tab groups the first five under Direction; the Intents tab shows an rN badge on amended intents and groups intents by what asked for them, then by region, each rolled up by state and sorted most urgent first (packages/web/src/intentTree.ts).
Stuck work #
colony/stuck.ts checks, from indexed queries, for work that stopped moving: an intent claimed with no change for 10 minutes (with the agent's own note), an intent whose changes came back 3 or more times (with the last reasons), certification red for 15 minutes (with where its repair stands), a report or a change waiting 10 minutes for a person, and an agent alive but not moving for 10 minutes. The snapshot and the metrics report carry stuck: { at, items, total }; each item says what to do (review the change, answer the report, look at the agent, amend or withdraw the intent, look at the tests), on the needs-you strip and in ft status. The colony's tick, at most every 30 s, publishes one stuck event per new item (stuck_reported keeps the keys); an item that clears publishes nothing, and one that recurs is reported again.
Escalations #
An agent that finds an intent cannot be done as written (a wrong test, a contradictory spec, an impossible ask) reports it instead of working around it: the MCP tool escalate {intentId?, kind, report}, or an in-platform agent's ESCALATE <kind>: <report> reply. ColonyDO.escalate keeps the report on the intent (escalation: kind, report, agent, time), releases the agent's claim and lease, and keeps the intent out of take() until a person answers; at most one report waits per intent. A person resolves it with POST /api/colonies/:id/intents/:iid/escalation {decision: "amend" | "withdraw" | "dismiss", note, by?, amend?} (or ft escalation): amend applies the change through amendIntent and reopens the intent, withdraw withdraws it, dismiss reopens it unchanged. GET /api/colonies/:id/escalations and ft escalations list the open reports; the web Review tab shows them as cards. The repair intents of certification tell their writers to escalate a wrong test with kind broken-test. Checked on a preview (2026-10-13) with scripts/mcp_escalate.ts: an external agent escalated over MCP, no agent could take the intent while the report waited, and a person's dismissal reopened it.
Contest mode #
An intent declared with contest: n (2 to 4; API, spec intents[].contest) is raced by up to n agents at once: contestants' claims on its files neither repel nor inhibit each other, and later contestants skip admission. Each contestant's change goes through the decide arc as usual; a contest change that reaches ready is held until every live contestant has one ready or ten minutes after the first (a contestant that gives back, stops or dies does not block). Then decide/contest.ts picks one: a deterministic proof first, then fewer residual paths, then no danger signals, then a pairwise comparison by a second-opinion model outside the authors' families, asked in both orders so a disagreement counts as a tie, then the smaller diff, then the earlier change. The winner goes to its lane; each other change goes back with supersededBy and the reason; contestants still working get a withdrawal notice; the winner's lineage note keeps the others as alternatives (proposal, agent, model, why it lost), and a contest event is published. If the winner does not land, a new round starts. Judge calls count in the colony's decide spend. Checked on a preview (2026-10-13): three in-platform agents raced one intent (a small formatting module and its tests); the colony held their ready changes, the judge (gpt-oss-120b) preferred whichever change it was shown first in both orders, so the comparison counted as a tie broken by the smaller diff, the winner landed about two minutes after the race began, and its lineage note keeps the two others as alternatives. In an earlier try with a harder task (wiring a new route into the router), none of the 49 changes the three agents proposed passed review, so nothing was compared: a contest only helps when at least one contestant can do the work. With FT_CONTEST_JURY (on or a list of colony ids) the pair goes to a jury of two or three small models from different families (policy juryModels), each asked in both orders at once within the judge's deadline; only a member whose two answers agree votes, the majority wins, and a split or no vote is a tie broken as before; the verdict names every vote, and scripts/contest_bias.ts measures the judge or the jury over N contests.
Agent-scale source control #
Anthropic's account of Scion, its in-house version control system for 30,000 merges a day (2026-10-13), lists the principles of source control for agents. Where FireTrident stands on each:
| Scion | FireTrident |
|---|---|
| No merge queue; test after merge, before deploy | Lanes land optimistically; certification runs after landing; certified moves only to a passing commit; a red trunk becomes a repair intent, and culprit finding names the suspects |
| Commits, merges and rebases on the server | The trunk writer builds every trunk commit by replacing only the changed paths and moves trunk with compare-and-swap; agents never push to trunk |
| Changesets: changed files with content pointers, accepted without coordination; refs moved separately | Inline changes carry their contents; with asynchronous publishing the trunk writer records landings and pushes git beside its cycles |
| Every client query is an API; jobs need no local repository | why and blame per line, path history, landings, certifications, review, escalations and the MCP tools; agents read files through the server |
| Sparse and virtual checkouts | Virtual checkouts: agents read only the objects they need, cached by id |
| Derived data through one mechanism | Derived indexes: import index, co-change map, path history, why-context |
| Engineers tell agents "no"; people review a change's intent, not every line | Rules and antibodies keep each "no"; the review card leads with the intent and the lines to look at |
| Permissions per directory; approvals that tell comment-only changes from logical ones | Protected paths always reach a person; comment-only changes land on a deterministic proof |
| Component "superfans" watching telemetry | The stuck-work detector, and the cascade detector |
What differs: Scion leaves git and serves a repository with almost 100 GB of history, checked out on the order of 25 million times a day for CI; FireTrident stays git (any client can clone a colony and read its notes) and has run on repositories of up to 1,287 files. Scion lets concurrent changes conflict and fixes them afterwards; FireTrident also validates what each agent read before its change lands.
Scale #
Every agent talks to one ColonyDO, so the take path is built to stay cheap per call:
- Shared field reads. A take scores up to 200 open intents against the claims of the regions they touch. Each region's claim field is read once and shared by the takes running at the same time (
colony/views.ts), so scoring a candidate costs map lookups instead of glob tests against every claimed path. Results equal corerepulsion. - Take gate. At most 128 takes score at once; more queue behind them (
TAKES_IN_FLIGHT). Without the gate the object ran out of memory and reset at 5,000 agents. Waiting agents are paced (WaitPacer): each wait gets a retry time that keeps the whole waiting crowd near a fixed rate of takes a second, so idle agents do not retry in step. - Pushed outcomes. When a proposal settles, the colony pushes the outcome to the agent (
AgentDO.outcome), so in-platform agents stop pollinggetProposal. - Live claims only. Stopping or reaping an agent clears its claims in every region, and admission counts only live claims, so a stopped run no longer holds a region's admission for its claims' decay time.
- Lease book. Leasing a workspace is pure SQL (
pool/slots.ts). Each slot keeps one write token (two hours). A released slot keeps its lessee's token, so the same agent leasing it again needs no control-plane call. An idle slot gets its token revoked and a fresh one minted in the background. A token is never handed to a different agent. - Notes fanout. Lineage notes use git's 2-character fanout layout. Notes written flat before it migrate a slice per flush. After the scale runs, stock git found a note on 5,894 of 5,894 landing commits.
FT_LAG_PROBE(load tests only) makes ColonyDO log event-loop lag and stream it ascolony.lagMs.
Measured on a preview (packages/bench/results/2026-10-12-scale/) with synthetic API agents (no model calls, review off, no work time, 64 regions), 120 s runs:
| Agents | Landings/min | Time to land p50 / p90 | Platform errors |
|---|---|---|---|
| 1,000 | 2,924 | 13.0 / 17.0 s | 0 |
| 2,000 | 2,714 | 14.8 / 19.5 s | 0 |
| 5,000 | 2,778 | 12.5 / 17.4 s | 0 from load; 78 of 50,368 calls failed in the first 33 s: the load generator's own DNS and connection errors, and one reset when the preview's code was updated (live-n5000.json) |
These runs predate the event record (see Replay). Their driver held a live feed open, so regions were already emitting field events to the colony; the record adds one batched insert per published batch. Before these changes, 1,000 agents reached 515 landings/min and the run stopped at 60 s with a 38% error rate. The next limit is TrunkDO, the single writer: every 3.4 to 3.9 s commit cycle hits the 200-proposal cap, so throughput no longer grows with agents, and about 6 s of queueing dominates time to land.
Round 4 (packages/bench/results/2026-10-13-round4/: preview p18, one colony with 64 regions, the same driver) measured the trunk writer's new modes at 5,000 agents. The git path landed 1,821 a minute: 3.9 s of each 5.1 s cycle went to reading the changed blobs from workspaces (2.2 s on 2026-10-12; the code is the same, Artifacts read latency is not). Inline changes remove that read: the cycle took 2.8 s, 3,842 changes landed a minute, and time to land fell from 19.1 to 10.7 s at p50. Asynchronous publishing moves the push out of the cycle (cycles of 1.8 to 2.5 s, below the 200-proposal cap, and pushes of 6 to 11 s beside them, each carrying 120 to 360 landings; median time to land about 8 s) but did not raise throughput (2,759 and 3,776 a minute): ColonyDO, which answers every take and proposal and the trunk writer's call for inline contents, became the limit, and it slows as the colony's history grows. Two of its costs were fixed on the way: with asynchronous publishing, every take's check for unpublished landings on its files sorted the landings table and parsed the newest 500; it now walks only the landings between the two heads, by an index on trunk_sha, and extends its last answer when only the colony's head moved (takes refused for room fell from 10,201 to 237 in a run).
The live bench (packages/bench/src/live.ts) leaves its colony and record in place and writes a run block: agents joined, trunk commits, changes and intents landed, the span from the run's first landing to its last, landings and changes a minute over it, and time to land at p50 and p95. Its agents call the REST API, which the per-session call budget does not cover (that budget applies to MCP sessions). Production's scale colony is such a run.
Replay #
ColonyDO records every event it publishes (publish in ColonyDO, EventRecorder in colony/events.ts) into a capped events table: a sequence number, a time, the event type and the event, the newest 100,000 kept. Field marks are not recorded (the replay derives the field from intents, claims and landings); metrics are sampled every 3 s while events flow, plus one trailing sample. Recording happens where events are published, so it does not depend on who is watching or how live delivery is batched; regions therefore keep emitting while a colony works, even with no viewer.
GET /api/colonies/:id/events?after=<seq>&limit=<n> pages the record. A colony that ran before recording existed answers with a timeline rebuilt from what it stores (colony/replay.ts: rules, intents with their times, agents, proposals and their decide outcomes, decisions, landings with their files, revisions and directives), labeled reconstructed; claim times and the deaths of reaped agents are estimated. The web app's /c/:id?replay=1 feeds the record through the same reducer as live events, at 1× to 16× with a scrubber; director mode (?director=1) works on top of it.
The colony page holds thousands of agents: agents are canvas dots, a map of more than 2,000 files is laid out again at most once a second while files keep arriving, a seek backward in a record of more than 20,000 events waits until the scrubber rests (250 ms), and proposals are pruned once per batch of events.
New projects (ft new --spec) #
A spec (see demo/new-project-spec.md) declares the project's intents, the files each one builds on, the spec sections every agent receives, an optional model per intent, the architecture rules, and acceptance tests. ft new creates a colony with an empty trunk, sets its policy first when the spec declares one (policy: protectedPaths, commentProof; POST /decide/policy, so no agent can change a protected path before it holds), adds the rules, and posts the intents in dependency order (parents and priorities make foundations land first). An in-platform agent waits until the files its intent builds on have landed, reads them, and writes files that must parse, keep every rule, use only TypeScript that Node can strip, and import only files and names that exist; each file gets up to three attempts with the failure as feedback. demo/run_new_project.rb runs the flow and verifies the result from a plain git clone.
Plain-git access #
A colony's trunk can be read with stock git and no account: git clone https://<host>/git/<colony>.git, then git fetch origin refs/notes/firetrident:refs/notes/firetrident for the lineage notes (api/git-proxy.ts, mounted at /git/*).
- Only
GET /git/<colony>.git/info/refs?service=git-upload-packandPOST /git/<colony>.git/git-upload-packare forwarded, to<colony>--trunkinft-trunk.git-receive-pack(push) gets 403 with a plain-text body that git prints asremote:lines: the colony page, how a change reaches trunk (declare or take an intent, then propose), the one-line MCP add command and/connect; every other path is 404./connect(core/connect.ts, shared by the site,ft connect,start.mdandllms.txtthroughscripts/connect_docs.ts) builds each client's setup, the public read-only MCP first and the token variant second;GET /api/colonies/:id/skill(api/skill.ts) serves a colony's SKILL.md. - Only colonies listed in
FT_PUBLIC_GIT(comma-separated) are served; empty or unset turns the proxy off. The name in the path must be a colony slug (lowercase letters, digits and single hyphens, at most 48 characters), so no other repo is reachable. - Read-only by construction: the Worker mints a read token for the trunk with
mintToken(30 minutes, replaced 5 minutes before expiry, cached per isolate) and adds it upstream. Clients never see it. When the remote refuses the cached token, the ref advertisement retries once with a fresh one; a 401 is never passed on, because git would ask for a password. - Protocol v2 passes through (
Git-Protocol), and v0 still works. Bodies stream both ways with their content types. Gzip request bodies (git compresses requests over 1 KB) are decoded in the Worker, and a request body is capped at 4 MiB both as sent and after decoding (413). /git/*is inrun_worker_first: without it, static assets answer GET requests under/git/with the UI. So is/, matched exactly: a request for/that preferstext/markdowngetsstart.md, and every/response carriesLink: </start.md>; rel="service-doc"andVary: Accept. The page holds a static summary inside its root element (the pitch, the production runs, the clone, notes and verify commands, the public MCP command) that the app replaces when it loads, and/llms.txtand/start.mdanswer five questions with one measured number and one link each.GET /api/aboutnames the serving build: the commit (FT_BUILD, set by the deploy script), the version id and tag (the version metadata binding), the deploy time, and the names of the flags that are on. Every answer passes throughfront/headers.tsfirst: it adds the security headers an answer lacks (static assets get the same from_headers, generated from the same values) andCache-Control: no-storeto JSON that sets no caching of its own, so no cache serves a stale colony state; the key endpoint, the git proxy and static assets keep their own caching, and only headers change, so streams pass through unread.
Who authors the commits. By default the landing commit's author and committer are FireTrident, with a Co-authored-by line for each agent where FT_CREDIT_TRAILERS covers the colony. A colony can name a person instead (POST /api/colonies/:id/commit-author {name, email}, ft commit-author): that person is the author of every landing commit, FireTrident stays the committer, so signatures are unchanged, and agents get no trailers; the lineage note still names every agent, its model and the review receipt. A commit that lands an imported issue carries Refs: owner/repo#n (see "GitHub issues").
Measured on a preview (2026-10-12) with a Hono trunk imported at depth 200 (583 files, 2.4 MB pack) and one landing: clone 1.9 s median over 5 runs; clone, notes fetch and git log --notes=firetrident -3 together 2.9 s median.
Review from a chat client #
POST /mcp/review is a second MCP endpoint, for people, so a person can work through escalations in Claude, ChatGPT or any MCP host (mcp/review/, mcp/apps/). It takes the admin token only: it refuses without FT_ADMIN_TOKEN configured (even with FT_OPEN_WRITES), refuses the MCP token, and refuses when the two tokens are the same value. Its tools are review_queue (what waits for a person, as GET /api/colonies/:id/review), review_card (one change: the lines to look at, the masked review view, and who said each thing, built by the same code as the web card) and decide (approve or return with a reason, through ColonyDO.decide). The two reading tools declare MCP Apps views (specification 2026-01-26: _meta.ui.resourceUri on a ui://firetrident/... resource of type text/html;profile=mcp-app): one self-contained page with an empty content security policy that writes agent text as text. A model never decides: review_card gives the view a single-use code in the result's _meta (an HMAC over colony, change, head, the decision allowed, a nonce and a 10-minute expiry), which never appears in the text or data a model reads; decide refuses without a valid code, and a spent code voids every older code for that change. A host without the view gets the full card as text and the web review link. Add it to Claude Code with claude mcp add --transport http firetrident-review https://<host>/mcp/review --header 'Authorization: Bearer ${FT_ADMIN_TOKEN}' (single quotes: Claude Code stores the variable's name and reads it when it connects).
GitHub issues #
POST /api/colonies/:id/issues/import {repo: "owner/name", labels?, limit?, closeOnLand?, close?} (admin token; ft issues import <colony> owner/name) reads a repository's open issues from GitHub's REST API, without a token for public repositories or with FT_GITHUB_TOKEN, and declares one intent per issue, skipping pull requests and issues already imported (keyed by owner/repo#number). Issue text is written by strangers, so an imported intent's source is external: every gate reads it as an agent's request (it never waives a danger gate or earns autonomy), agents see its text quoted line by line under a line saying a stranger wrote it (at most 4,000 characters), and it records where it came from. The lineage note, the landing card and the Intents tab name the issue, and the landing commit carries Refs: owner/repo#n. Writing back is off unless the import set closeOnLand and FT_GITHUB_TOKEN can change that repository's issues: then FireTrident posts one comment linking the landing, and closes the issue only with close.
Trunk certification #
Review decides whether a change may land; trunk certification checks that trunk still works. After landings the colony runs the project's own tests on trunk and records the result per commit (packages/worker/src/certify/).
- When.
ColonyDO.onLanding(and FireTrident's own commits) only marks trunk as moved; no landing waits for tests. At most one certification runs at a time and it certifies trunk as it is when it starts, so when landings come faster than runs only the newest trunk is certified; runs start at leastminIntervalMs(2 s) apart (schedule.ts). - What runs. Every
*.test.tsand*.test.jsfile (undertest/or anywhere else) that importsnode:testand reaches no package and no Node built-in the runner lacks (file system, processes, sockets); others are listed with the reason. OneTrunkDO.certifySnapshotcall lists trunk's.ts/.js/.mjs/.jsonfiles at one commit, andTrunkDO.readBlobsreads the test files and, level by level, what they import (at most 400 modules, 4 MB, 512 KB a file). TypeScript is transpiled with sucrase's typescript transform the way Node runs it: types erased, imports that are notimport typekept, and enums, namespaces and parameter properties failing to load as under type stripping. Imports resolve exactly as written, so a missing file fails its importer, as undernode --test. - How. All test files run in one Dynamic Worker (
env.LOADER.load,nodejs_compat,globalOutbound: null, no env or bindings, CPU and wall-clock limits; the report comes back as a stream read to at most 4 MB and stopped at the run's deadline), each with its own copy of the modules it reaches (keyst0/src/http.ts,t1/...), so module state does not pass from one file to the next. That is less isolation thannode --test, which gives each file a process: the files of one run share globals (globalThisand the built-in objects), so a file that changes one can affect the files after it, and when a copy per file would exceed 6 MB of code, all files share one copy.node:testimports go to a shim (shim.ts): test/it/describe/suite with nesting, the (name, options, fn) forms, skip/todo/only, async functions and done callbacks,t.testsubtests, before/after/beforeEach/afterEach at file, suite and test level,t.planwitht.assert,t.mock(the runtime's) and per-test timeouts (10 s);node:assertandnode:assert/strictare the runtime's own. Counts follownode --test(tests = pass + fail + cancelled + skipped + todo). Tests run one at a time, and start from the request rather than while modules load (module evaluation allows no timers), so a file that awaits its own tests at top level never finishes loading. A file whose runner cannot start (for example after an earlier file broke a global the shim uses) counts as a failing test, so it can never drop out of a green run unnoticed; when the very first file's runner fails, before any repository code ran, the run is an error rather than a red that would declare a repair. - Reuse. A test file's results are keyed by what it reaches (paths and blob ids of its import closure, the runner, the per-test limit): a run executes only the files whose key changed and reuses the rest, and a commit whose tested files all match an earlier run reuses that verdict (
reusedFrom). Results are kept per file only from runs where each file had its own module copy, never from a run that shared one, and never for a file that failed to load or ran out of time. - Results. Stored per commit in ColonyDO (
green,red,errororskipped), published as acertifyevent (so replays show it), and added to the certified landing's lineage note ascertification(TrunkDO rewrites the note; its next notes flush pushes it, beside the landing cycles). Green setsgreenShaand moves the trunk repository'srefs/heads/certifiedto the commit (TrunkDO.markCertified: forward only, compare-and-swap on the remote; a reused verdict counts), so plain git can check out the newest commit on which every test file certification could run passed (deploy, release and pin from it;mainholds every landing, tested or not); red keeps the last green. A run is not green when it lost tests since the newest green run: a test file that ran then and did not run now (left out, unloadable or deleted), or a test that passed then and is now skipped, todo or gone. The lost files and tests are listed as failures, so the repair loop picks them up. The trunk bar shows a check or a cross on the head ("60/60 tests", "5 failing", names and errors on hover);ft status,ft watch,ft why,ft certifyand the Why tab show the same. - Culprit finding (
certify/culprit.ts,FT_CULPRITS). For up to 10 failing test files, a red run compares each file's import closure at the red commit with the last green one and asks the per-path landing index for the landings since that changed those paths (for a file new since green, the landings that added or last changed it and its closure), within 3 s and failing open; per file it keeps the oldest landing since the last green commit (read from the colony's landing log,certify/since.ts, because the path index keeps only the newest three) beside the newest two. The result'ssuspects(landing, proposal, intent, paths, commit) appear inft certify, on the certification card withft revert <colony> <prp>, and, fenced as data, in the repair intent's why. Each suspect carrieslabels(first-since-green,latest, andfirst-redonce the first-red search names its landing), and no cap drops a first-since-green or first-red suspect. - First red commit (
certify/bisect.ts,FT_BISECT:onor a list of colony ids). After a red result is stored and published, the colony searches in the background for the first commit its failing tests fail on: it takes the landings between the last green commit and the red one from the trunk writer's landing log (TrunkDO.landingRange, at most 512), runs only the failing test files (lost tests excluded) and their import closures at the red commit first and then halves the range, each run in the same no-network Dynamic Worker, at most 8 runs, 20 s a run and 60 s in all. A found commit names its landing, the intents landed in it and the proposals in it whose writes touch the failing tests' closures (from the landing's lineage note, with the suspects as the fallback); the result gainsfirstRed, abisectevent says so, and the open repair intent's why gains a fenced paragraph naming it (edited in place, so no change in flight is sent back). With no green commit to start from, tests that pass alone at the red commit, an error or a bound reached, it endsinconclusivewith the reason. It never delays a landing or the result; a colony restarted mid-search leaves that result withoutfirstRed. When the found landing holds several proposals, the colony then isolates the culprit (certify/isolate.ts): halves first (at most 3 runs; both halves passing is reported as an interaction), then one path-intersecting proposal left out at a time (at most 6), accepting one only if the failing tests pass without it, every other test in those files keeps its outcome and nothing fails to load; then a revert probe on the red commit with that proposal's writes reverted (skipped if work has landed on its files since). At most 10 runs and 60 s in all; anything else endsinconclusiveand the landing-level answer stands. The card,ft certifyand the repair intent show it, the suspect gains the labelisolated, andft revertis marked proven only when the probe turned the tests green. - Tests before landing (
certify/preland.ts,FT_PRELAND:onor a list of colony ids; per colony, the certify policy'sprelandPathsglobs, empty meaning every change that reaches a test file). In the decide stage, before the gate and never inside a trunk cycle, a covered change's files are laid over the trunk index at its base (inline contents or workspace blobs), the test files whose import closure reaches a file it writes or adds are selected (at most 5, nearest first) and run in certification's sealed Dynamic Worker, and each is judged against trunk's stored result for that file's closure at the base: a test that passed on trunk and fails with the change sends it back to its agent naming the test and its error; a test already failing on trunk is not held against it. A time-out (2 s), a runner error, no trunk result to compare with, or three runs already going (after a 1 s wait) let the change go on as before, recorded as "tests before landing: not checked" with the reason. A colony with review off takes the decide path for the tests alone. The run goes into the change's evidence and its lineage note (preland), soft why, the Why tab and the review card show it. Read-set validation still runs at landing: a change tested on its base can still go stale. - Repair loop. A red result with no repair intent open declares one (
repair.ts): "Make the failing tests pass:", source certify, priority 10 (spec intents use 1 to 5), footprint the source files the failing tests load (nearest first, at most 6), the failing test files and their other imports as "Builds on" context (the tests are the specification and are not to be edited; a writer that thinks a test is wrong gives the intent back with the reason), the failing tests with their errors and the spec context of the intents that wrote those files in itswhy. At most one is open, and at mostrepairsPerHour(3) are declared in an hour. A repair owes the tests it was declared for (certify_repairs): a run satisfies it only when each of them ran and passed, and until then the run is red and the repair stays open. Withdrawing a repair accepts its losses. Agents take it like any other need. A test module that will not compile (a syntax error, a name declared twice) stops a Worker from starting, so when the test Worker fails to start, probe Workers find the module (certify/startfail.ts), the files that load it fail with its error, and the rest run within the run's time limit: one bad file makes the run red, naming it, never an error for the whole run. - Test delta per landing (no flag;
certify/landingtests.ts,core/testdelta.ts,GET /api/colonies/:id/landings/:ref/tests, public). Each landing card (the Why tab, the note card,ft why, both MCPwhytools) shows its tests at its base against the first green certification that includes it ("433/434 at base, 434/434 certified, 0 lost"), lost tests by name, whose tests ran (inherited from the repository, written by agents with their models, edits a person approved), whether an earlier test's import closure reaches its files (amber "No earlier test reaches this change" when none does), what landed it and its spend as model calls this platform made. It is computed when a card asks, within 5 s, and cached per certified commit; a colony with no tests this platform can run says so. - Audit sample, baseline stream and blind canaries (no flag;
colony/audit.ts,decide/audit.ts,decide/canary.ts). People see ordinary landings, not only exceptions: a share of the changes that landed without a person (policyauditRate, 3%) goes to a person after landing, never blocking it, sampled per confirming layer (the first landing of each layer always, then the share, a hash of the landing deciding ties); the review queue shows one after every five escalations and up to ten when no escalation waits, each marked "already landed by" its rule. A person answers fine or should not have landed (with a reason): the latter offers the revert, or does it withrevert: true, and revokes the region's earned autonomy if autonomy landed it. WithcanaryRateon, sealed known-bad changes (built from the vaccine classes that reach a person without a model) join the human queue looking like any escalation; a decision on one reveals it and scores it, and it never lands. Measured per confirming layer with Wilson 95% intervals, plus people's approval rate drift:GET /api/colonies/:id/audit(public),/metricsaudit, the How this colony decides card,ft audit. - A change waiting for a person holds nothing (
FT_PERSON_WAIT;colony/personwait.ts,agents/personwait.ts). A change escalated to a person, or held as a merge, gives up its claims and heartbeats stop refreshing them, so other agents can take open work on the same files; an in-platform agent whose change is inline ends that job (awaiting_person) and takes other work, recording the outcome when the person decides (a git-path job keeps waiting, since its next push would overwrite the commit). Approved later, the change lands by the ordinary rules, and a merge lock holds it for the person again if trunk moved under it. Found by the contention test: without it, two changes waiting for a person kept every other agent off a shared module. - Trunk health (no flag;
certify/health.ts). Testing after landing trades a green trunk for throughput, and this measures the cost:GET /api/colonies/:id/metricsservestrunkHealth(red episodes per 100 landings, time red and its share, time to green at p50, p90 and max, landings built on red and after red was known, fixes counted apart, agent time and spend on repairs, culprit precision against the first red commit, and the share of landings certified within 5, 15 and 60 minutes). It shows on the first screen's Certified row and inft status; a colony with no runnable tests says so instead of zeros.scripts/coordination.rbdefinition 18 computes the same from the public record. - Switches.
FT_TRUNK_TESTS=onfor a deployment (bindingLOADER); per colony,POST /api/colonies/:id/certify/policy {enabled, repair, repairsPerHour, timeoutMs, testTimeoutMs, minIntervalMs}.GET /api/colonies/:id/certifications[/:sha]reads results;POST /api/colonies/:id/certifycertifies the current trunk now. - Checked.
test/certify/run.test.tsruns fixture projects through the pipeline in Node and matchesnode --testcount for count;test/do/certify/runs real Dynamic Workers;sandbox.test.tsthere shows a test file'sfetchand socket refused and itsprocess.envand Worker env empty (with the sandbox removed, the same file reaches the network). On the two production shortlinks trunks the counts equal a plain clone'snode --test: 60 of 60, and 67 tests with 62 passing (the same five failures). - Measured on a preview (2026-10-12). Certifying the 60-test shortlinks project took 121 ms from start to result (trunk index 26 ms, reads and transpiling 53 ms, Dynamic Worker 42 ms). Landing latency, two colonies side by side, 6 agents each landing a new test file (so every certification has new tests to run), 75 s runs in alternating order: time to land p50 1.26 and 1.23 s with certification on, 1.37 and 1.42 s off (p90 1.78 and 1.72 s on, 2.34 and 2.30 s off); certifications took 160 ms at p50 with suites up to 3,917 tests in 392 files. Without per-file reuse, rerunning every file added about 0.1 s to mean time to land under that load.
- On production (2026-10-12). Certifying
shortlinks-review(the second run of the new-project spec, made before certification existed) took 267 ms and found the five failures a plain clone shows. The repair loop then ran on its own: the first repair intent landed 11.5 minutes later and left one router test failing, so the colony declared a second; one attempt at it was returned by the second opinion for compile errors and two were given back, and the fourth landed 67 minutes after the first certification. Trunk was certified green at 67 of 67, and a plain clone agrees (acceptance 7 of 7). Evidence:packages/bench/results/2026-10-12-prod/shortlinks-review-certify-*.
Configuration and deployment #
- Tuning variables:
FT_COCHANGE_WEIGHT(co-change repulsion weight, 5 in production),FT_LATERAL_INHIBITION(inhibition threshold as a fraction of a fresh claim, oroff),FT_IMPORT_WEIGHT(import repulsion in take; off when absent or 0)FT_REVALIDATE(import-name revalidation:onor a list of colony ids; off when absent)FT_DELETION_GUARD(the deletion guard),FT_INLINE(inline changes through the API and MCP),FT_VIRTUAL_CHECKOUT(agents read trunk object by object and propose inline),FT_ASYNC_PUBLISH(asynchronous publishing),FT_CULPRITS(suspects on red certifications),FT_BISECT(the first red commit),FT_EXPORT_GUARD(the export guard),FT_TWINS(twin intents),FT_PRELAND(tests before landing),FT_QUOTED_ACCEPTANCE(quoted acceptance),FT_A2A(the Agent2Agent handoff),FT_INTENT_SPEND(model spend per intent),FT_CONTEST_JURY(a jury for contests),FT_INTENT_CHAIN_GATE(agent-declared chains held for a person),FT_TEXT_MERGE(text merge),FT_GOAL_CONFLICT(the goal-conflict check),FT_MEGAFILE(the megafile alarm),FT_FIELD_GUIDE(the field guide) ,FT_RESOLVER(the conflict resolver, withFT_RESOLVER_MODEL),FT_MERGE_INSERTIONS(insertion merges),FT_HEDGE(hedged model calls),FT_RETURN_HUNKS(returns quote what beat them),FT_LESSON_SCOPE(lesson scopes),FT_HEADS_UP(the heads-up and watermark),FT_CREDIT_TRAILERS(co-author and approver trailers),FT_REPRODUCE(review by reproduction),FT_ESCALATION_BATCH(escalation batching),FT_PERSON_WAIT(a change waiting for a person holds no claims),FT_ALREADY_ON_TRUNK(a change already on trunk closes without a commit),FT_RECIPE_MODULES(a person's own recipe module, run in a sealed Dynamic Worker),FT_REVIEW_VIEW(the review view) andFT_MODEL_STEPUP(the stronger model after two returns): eachonor a list of colony ids, off when absent.FT_AGENT_CALL_BUDGET(onor<rate>,<burst>) is global.FT_PUBLIC_MCPlists the colonies the public read-only MCP serves, likeFT_PUBLIC_GIT. The production config (packages/worker/wrangler.jsonc, its top-levelvars) turns onFT_TRUNK_TESTS,FT_INLINE,FT_VIRTUAL_CHECKOUT,FT_CULPRITS,FT_BISECT,FT_DELETION_GUARD,FT_TEXT_MERGE,FT_GOAL_CONFLICT,FT_MEGAFILE,FT_FIELD_GUIDE,FT_RESOLVER,FT_AGENT_CALL_BUDGET,FT_MERGE_INSERTIONS,FT_MODEL_STEPUP,FT_TWINS,FT_QUOTED_ACCEPTANCE,FT_A2A,FT_INTENT_SPEND,FT_HEDGE,FT_RETURN_HUNKS,FT_LESSON_SCOPE,FT_HEADS_UP,FT_CREDIT_TRAILERS,FT_REPRODUCE,FT_ESCALATION_BATCH,FT_PERSON_WAIT,FT_ALREADY_ON_TRUNK,FT_PRELAND,FT_RED_BRAKE,FT_REVIEW_VIEW,FT_INHIBIT_STRONGEST,FT_AGE_CAPandFT_ACCOUNTS; the stage config (env.staging) turns on the same 33. Production also signs lineage notes (FT_NOTE_KEY) and setsFT_PUBLIC_GITandFT_PUBLIC_MCPto its showcase colonies (hono,shortlinks,byo-demo,scale,order-backend,signed-notes,first-red); staging serves three more over git (mosaic,rename,smuggle). Off in both: asynchronous publishing, import-name revalidation, import repulsion, the export guard, tests before landing, the contest jury, the intent chain gate, module recipes and the review view (the export guard, tests before landing and the jury have a measured cost, and testing after landing is the default by choice). A deployment runs the config it was last deployed with, andGET /api/aboutlists the flags the serving build has on; mechanisms.md has every flag with its value in each config. A changed flag reaches each Durable Object when it restarts on the new deployment, so for a few minutes a colony object and its trunk writer can disagree; a colony's status shows its trunk writer's publishing mode (publish.on). Optional secrets:FT_ACCESS_TEAM_DOMAINandFT_ACCESS_AUD(verify approvers behind Cloudflare Access; staging sets both) andFT_GITHUB_TOKEN(GitHub issue import past the anonymous rate limit, and write-back where an import asks for it).FT_TEST_PUSH_DELAY_MSholds every push open for that long; it is a test hook for previews and is never set in production. - What each mechanism buys and costs, measured with it off and on, with each run linked: mechanisms.md, What each mechanism buys. The landing page's Try panel (
web/landing/tryit.ts) calls the same core functions the trunk writer, the decide stage and the colony call, with production's options; it never touches the API. FT_TRUNK_TESTS:oncertifies trunk after landings with the colony's own tests (see "Trunk certification"); needs theLOADERWorker Loader binding (Dynamic Workers).FT_PUBLIC_GIT: colonies served read-only over plain git (see "Plain-git access"); production listshono,shortlinks,byo-demo,scale,order-backend,signed-notes,first-red. A preview sets its own withnpx wrangler preview --name <x> --var FT_PUBLIC_GIT:<colony>.- Secrets:
FT_ADMIN_TOKEN(mutating API routes),FT_MCP_TOKEN(/mcpand/sse),FT_NOTE_KEY(the Ed25519 key that signs lineage notes; optional),ANTHROPIC_API_KEY(optional Claude agents). Without its token, the API refuses writes and/mcprefuses connections (fail closed);FT_OPEN_WRITES=1lifts that for local development only and is never set on a deployment. A preview gets its own withnpx wrangler preview secret put FT_ADMIN_TOKEN --name <x>. wrangler.jsonchas apreviewsblock that mirrors every production binding, sonpx wrangler preview --name <x>gives an isolated deployment with its own Durable Objects. Previews share the Artifacts namespaces with production, so a preview colony must not reuse a production colony's name.