Skip to content

DNA

Reference material, synced from the compiler repo’s spec/. The guide is the gentler path in.

DNA is the part of a Hale application that governs how the application changes. It ships as a library inside the toolchain (vendor/dna, the dna/core seed of the hale repository) and as the hale dna commands. This file specifies what the library and the commands promise; the design is GH #521 and its successor #566; docs/src/dna/ is the guide.

An organism has three memories, and every row kind belongs to exactly one (GH #646).

Memory Holds Lives in Written by
Structure what the organism is: positions, routes, workflow definitions and grants as authored, the law, the app, the models catalog the codebase the editor, under review
Record how the organism changed and was allowed to: mutations and their reviews, practices proposed and ratified, authority contracted or revoked, connections, provisioning, people, the adoption of the ledger git, refs/dna/*, one signed commit per row, cloned and synced the Board, the organism’s pipeline, a head from its clone
Ledger what the organism did today: intents, tasks, assignments, decisions, bills and receipts, money reserved and settled, schedules declared and their occurrences, concerns, handoffs, effect claims, liveness, the application’s readings memory: Postgres, in the record’s own schema dna_<identity> every writer under its own role — the nodes as the spine’s, each head as its own — through memory’s insert function, the gate (GH #1026)

The routing table is memory_of in the core (routing.hl): the case, intent, task, decision, completion, exception, schedule, receipt, handoff, pressure, instance, effect, report, lease, claim, reading (an application’s events, #987) and reflex (what a reflex made the body do, #988) families are the Ledger’s, as are concern.requested / .raised / .refused, grant.reserved / .released / .fenced / .reservation_refused (money; grant.refused — a grant born wider than its ceiling — is authority and the record’s), spend.reserved / .settled / .compensated, body.claimed / .released / .credential_*, model.called, knowledge.consulted, optimize.refused, budget.exhausted, and a workflow execution’s own facts (workflow.admitted / .refused / .ask_refused / .settled, step.registered / .activated / .completed / .failed, work.settled, attempt.admitted / .outcome), and a leg’s rows on an attempt (attempt.claimed, .outcome_requested / .outcome_refused, .allowance_requested / .allowance_granted / .allowance_refused, .unresolved, .released). Every other kind, and any kind a build does not know, is the record’s. Routing is versioned and is a fact of the record: version 0 is every kind in the record; version 1 is the table; an organism is on the version its last ledger.adopted row names, and on 0 again after ledger.abandoned. There is no dual-write: a kind has one home, and a writer that cannot reach it fails rather than writing elsewhere.

RoutedJournal is the Journal an organism and the host stand on: the record’s journal and the Ledger’s read as one merged sequence (a row’s seq as read is its merged position; rows name each other by id), appended to by kind under the record’s routing. The Ledger is MemoryLedger (dna/core/memory_spine.hl) over PqLedger (Postgres, the record’s dna_<identity> schema, one insert conditioned on the tail, the claim kinds effect.requested, spend.reserved and task.born unique by (kind, entity)), opened on first use as the role its process’s environment names (Memory, below). There is no service in front of memory (GH #985): a part that reads or writes it opens Postgres itself, and the role’s grants are the trust boundary. The organization’s Ledger carries the body lease and the epoch the host handed it (HALE_DNA_LEASE, HALE_DNA_LEASE_TOKEN): an append under a lease that is not held, is held at another token, or has expired lands nothing (fenced: …).

Adoption is explicit, per organism and one-way. hale dna ledger adopt, with the record synced, appends ledger.adopting {routing, by} to the record: a request, which the spine carries out on its tick (The spine, below). It copies every operational row of the record up to that row into the Ledger keyed by its commit (so a copy interrupted anywhere is rerun, never repaired), records the cutover, carries the running body’s lease into memory at its token (unless memory already holds a lease under that key), and appends ledger.adopted {routing, checkpoint, rows, by}: the checkpoint is the ledger.adopting row’s commit, the record head the copy was taken at, and from that commit on operational kinds never go into the record. Historical operational rows stay in git, read-only; readers of history (history, the projections, the export) read both memories as one. hale dna ledger abandon --why appends ledger.abandoning {by, why}; the spine empties the Ledger and appends ledger.abandoned {by, why}, and the organism is on routing 0 again with nothing lost. Until a body with memory runs, an adoption or abandonment waits in the record. hale dna run on an adopted record refuses to start without HALE_DNA_MEMORY_DSN_SPINE (#646 decision 3: a body without its memory admits nothing); dev applies memory’s schema and hands the host the spine’s DSN. hale dna ledger [status] says the routing, whether memory is named here, and the Ledger’s row count and cutover; hale dna ledger rows prints the Ledger’s rows as JSON lines (seq, kind, entity, body, author, writer — the role that wrote it, so a head writing as host is told apart from the body — prev, digest), read under the head’s role or the spine’s.

Writers write directly; the store is the gate (GH #1026). A head — the CLI, hale dna ui, the API — writes a Ledger row itself, straight into memory under its own role, and a node writes the organism’s rows under the spine’s; nothing sits in front of memory and no process admits anything. Every Ledger row lands through memory’s insert function, ledger_append(expected, kind, entity, body, author, lease, epoch, request), a definer function run as the schema’s owner, and the calling role is who wrote it (session_user, kept in the row’s writer column beside lease and epoch). It refuses, returning the reason the verb prints:

  1. a kind that is not the Ledger’s (ledger_kind, the routing table above as data): it is appended to the record instead;
  2. a person the record retired (org_retired, below) — checked before the author, since a retirement also ends the person’s memberships, and the refusal says so;
  3. an author the calling role does not hold. memory_principals, owner-only, maps each role to whom it writes as: * any author and no person checks (the spine’s role, and the schema’s owner); @ any person (the head’s role over a record with one owner); an owner’s name, that owner’s members — the persons who hold its organization node (each owner’s head role over a shared record, The owners’ roles, below); host for any head;
  4. a task.transfer_accepted for a Task not offered to an owner, or in the name of someone who is not the member of the owner it is offered to;
  5. task.done, task.failed, completion.linked and completion.excepted in any name but the Task’s assignee’s;
  6. a row naming a lease — the body’s, which its organism carries — whose claim is not live at that token in claims right now: fenced, with whose it is and at what epoch;
  7. a revision a decision was read at that the Ledger has moved past (stale_revision; a task.transfer_accepted lands at the tail whatever its expected, GH #1052, since check 4 reads the transfer against the Ledger as it stands), and a claim kind whose entity is already taken (claimed).

Checks 3 to 5 are not the role’s own business, so they apply to every role but a *. Appends are serialized on the schema, and the row’s digest extends the chain as event_digest does. A write carries a request id minted when it is made (ledger_requests): a write retried after an answer that was lost is answered as it landed, once. A head’s verb says it is done when the row lands, and says why when it is refused; nothing is pending and there is nothing to wait on. A head with no memory named writes nothing, naming the checkpoint the organism’s operations live behind since — never into git instead. A write decided at a revision needs the Ledger’s revision read, and without memory its verb refuses (the ledger's revision was not read). Nothing on a head is authoritative, and no head executes work offline (#646 decision 2). On routing 0 an operational row goes into the record, as it always did. An adoption and an abandonment are the record’s ledger.adopting and ledger.abandoning rows above, carried out by a node under a claim on the asking row (Claims by id, below).

The org chart in memory (GH #1026, #1123). The checks above read what the spine projects, and the org chart is the graph’s (The repository’s graph, below): org_members(person, owner) is a view over the graph’s live holds edges whose position side is an organization node — a person holding organization:<o> is o’s member, and owner is the organization’s name — so it moves with the graph’s own projection and nothing rewrites it; org_retired(person, seq) is the record’s person.retired rows, projected in the same per-row transaction as the graph (and emptied with it on a rebuild). The head reads both, so a verb that checks before writing — a retired person, a transfer’s owner — says so in its own words; the store’s check is the gate, the head’s the message.

The owners’ roles (GH #1026). Over a shared record each owner’s heads write the Ledger as that owner’s role, <schema>_head_<owner>, which the migration creates for every owner the owners’ keys name (HALE_DNA_OWNER_KEYS, <owner>=<key> …, in the owner’s environment at migration) and prints as HALE_DNA_MEMORY_DSN_HEAD_<OWNER>=<dsn>; the plain head role of a shared record reads and writes as no one. A role is who a head is: memory holds no signature bytes, and the keys stay the record’s until the vault holds them (#989). The host is a principal: it writes its own rows as host, under the spine’s role or an owner’s, and its body.* rows carry owner in their body when the record names one, so a row by host still says whose body it was. Who a person is remains the head’s to establish — its issuer under dna.principal = oidc.

Admission, as a reader checks it (GH #1026). A row of the record counts only when dna::row_admissible(record, row, trust) says so: under dna.trust = signed its commit carries a signature the record’s trust verifies (git’s keyring or allowed-signers file), and an unsigned row, or one signed with a key the record does not know, is refused; under local trust every writer is trusted. The projection runs it on every row it applies and moves its stamp past a refused row without projecting anything of it. A Ledger row needs no reader’s check: the insert function refused it before it landed.

Evidence and the export across two memories (stage 4, #653). Receipts are placed by what they evidence: evidence.* (a verification step’s output, a diff document — evidence of a mutation) and review.reasoned are the record’s; receipt.* — a bill filed, its class, its disclosure, its reads, its holds, its redaction — are the Ledger’s, whatever the class, and the body itself is where it always was (a git blob for an internal body, memory’s sealed store for a protected one; only the digest is ever a row). A redaction of a git receipt after adoption is therefore a Ledger row, and the body it names may be a blob filed before adoption (a head’s redaction of a protected body is a row of the record, which the spine reads to erase it): sync applies redactions and candidate drops over both memories read as one, so the treatment holds across the split. The books export is a supported ledger query: it reads hale dna ledger rows beside the record’s rows and never SQL of its own. Recovery between the two memories needs no transaction spanning them: every step that touches both writes its intent in the memory that owns the decision, acts, and writes its outcome, and the restart scans the merged journal — a task settles on its mutation’s outcome, a settled review births its task once, an effect claim is resolved by evidence or gated as unknown, a retirement’s reassignments are reapplied, a filed body without its row is filed again — exactly as they did over one journal, because the routed journal is that journal. The continuity gate is a pre-split record built with the real git journal — unfinished work, a reservation, a practice bound to a handed task, a retired participant, redacted evidence, a pending handoff, a schedule — adopted through the same steps a live organism takes, its projections read before and after, and a redaction after adoption removing a body filed before it (dna_ledger.rs).

Handoffs between records (stage 5, #662). A handoff crosses between records as an envelope through an Exchange, the core’s contract for one record’s mailbox in another: peer_identity, deliver (idempotent by the envelope’s kind and entity — its entity is handoff:<id> and the id is the handoff’s origin, peer, kind and subject, so a delivery repeated after an interruption is one envelope), delivered (whether the peer already holds it), received (every mailbox of this record). The exchange is git’s alone: mailbox refs in a private peer cache of the other record’s remote, refs/dna/exchange/<sender>, beside the identity each record publishes under refs/dna/identity. hale dna connect refuses an http:// or https:// url: a connection exchanges through the other record’s git remote. The exchange keeps the contract #646 set: durable delivery — a handoff published here whose envelope the peer does not hold is delivered again, once, by hale dna handoff sync from the envelope kept with its handoff.published row; a delivery that cannot reach the peer at all records nothing and says to deliver again; the disclosure scope is the connection’s classes, checked before anything is delivered and again before anything is admitted; settlement only on the receiver’s acceptance — the origin’s task moves to transfer_accepted when the receiver’s handoff.accepted envelope reaches it through the exchange, never when a delivery returned.

Coordination in memory (stage 2, #651). On routing 1 every lease is a row of memory’s claims table, swapped by its token: MemoryLeases is Coordination over current(key) and put(key, lease, expected), and a put lands only when the stored token is the one expected (0 for an absent lease). GitLeases — the record’s coordination — takes a mutation lease from the record’s cells on routing 0 and from memory on routing 1, reading the routing from the record whenever its head moved: an adopted organism’s leases move with it, and nothing in its wiring changes. The body lease moves the same way: on routing 1 it is the row body (owner/<owner> over a shared record), the fence renews the row every third of its life with every call bounded, a stale token — a host that lost the lease to another — lands nothing, hale dna body reads it there, and refs/dna/lease/* is not written. A head takes and fences a lease under its own role. Effect claims are unique rows of the Ledger ((kind, entity)), so the contended-claim row the git rule needed does not arise. Memory lost after activation is the case the git lease already handles: the renewal fails, the fence keeps the lease it last proved until the margin before its expiry and then stops the organism, and status says THE LEDGER IS UNREACHABLE and that nothing is admitted until it answers.

Memory is Postgres, and only Postgres (GH #985). Its stores are the core’s — dna/core/memory_store.hl (the graph, Pq), memory_ledger.hl (PqLedger, PqLeaseStore), memory_protected.hl (PqProtected), memory_embed.hl, memory_schema.hl (the schema, the roles, the grants and the functions) and memory_spine.hl (Memory, MemoryLedger, MemoryLeases, MemoryKnowledge, MemoryVault, LedgerAdoption) — and they open Postgres through pond’s driver, pinned under dna/core/pond and vendored beside the core at vendor/dna/pond. The projection tail, MemoryProjection, is not in the core: it lives in dna/operations/memory_tail.hl, beside the command codecs it decodes admitted facts with, and runs on the host’s tick (The spine, below).

  • Memory is per record. Each record has its own schema, dna_<identity> (the identity lower-cased, any other character _, at most 56 characters of it), and two roles of its own, dna_<identity>_spine and dna_<identity>_head, cut to fit Postgres’s 63-byte names: one role granted on every record’s schema would read every other record’s evidence on the same server. A role’s password is the vault’s postgres-<role> (The organism’s secrets), drawn when the role is declared and read at use; the migration sets each role’s password from the vault (ALTER ROLE when it exists), so the vault stays the one source. A role’s DSN carries no password: it names the entry, postgres://<role>@host:port/db?sslmode=…&vault=postgres-<role>, and the connection reveals it into its SCRAM exchange’s @secret parameter, in the one statement that uses it (GH #989). The seed compose database’s owner DSN names postgres-owner-<seed> the same way. An operator’s DSN with a password in it is presented as given.
  • Three DSNs. HALE_DNA_MEMORY_DSN_OWNER is the schema owner’s, used only to migrate: by hale dna memory migrate [dir], by hale dna dev before it starts the host, by hale dna upgrade when it is set, and by a provisioned body, whose env file carries it for the hale dna dev its unit runs. Without it, memory migrate and dev use the database dna/compose.yaml brings up. hale dna memory migrate prints two lines, HALE_DNA_MEMORY_DSN_SPINE=<dsn> and HALE_DNA_MEMORY_DSN_HEAD=<dsn>: the owner’s host and database, each role’s name and credentials. The host that runs the organism is handed HALE_DNA_MEMORY_DSN_SPINE alone, and the organization inherits it: dev sets it from the migration, and hale dna run takes it from its own environment; both take HALE_DNA_MEMORY_DSN_OWNER and HALE_DNA_MEMORY_DSN_HEAD out of the host’s. With no database to migrate, dev says why and starts the host without the owner’s DSN. A head — the CLI, hale dna ui, the API — opens memory with HALE_DNA_MEMORY_DSN_HEAD; a process given the spine’s DSN uses that instead, and none holds both.
  • The migration runs in one transaction under an advisory lock on the schema: the vector and pgcrypto extensions, the schema and every store’s tables, the record’s claim on the schema (knowledge_meta.record; a schema that names another record is refused), the projection protocol, the receipt functions and the receipt key, both roles and their grants, and memory_meta.schema_version. It is idempotent. It refuses a knowledge store left in public from before stores were scoped (drop its tables or the database; the projection rebuilds from the record), and a schema a newer toolchain migrated.
  • The version fence. The schema version is 8 (GH #1026: 2 made the lease table claims and the graph’s projection one transaction per record row — migrating a version-1 memory renames the table in place, its rows kept; 3 adds the Ledger’s gate, ledger_append, with the org chart it checks and the roles’ principals; GH #1085: 4 adds the repository’s graph, graph_nodes, graph_edges and graph_members; GH #946: 5 adds hats, hats by digest, which moves no projection; GH #1123: 6 makes org_members a view over the graph’s holds edges, where it was a table the spine rewrote from the retired owners map, and moves no projection either; GH #987: 7 adds the reading family to the routing table the insert function reads, which moves none; GH #988: 8 adds the reflex family the same way. Each protocol move empties the graph once for the projectors to rebuild). A store’s open selects the record’s schema and refuses one at another version, or at none, naming both and hale dna memory migrate with the owner’s DSN; it also refuses a schema whose claim names another record (it is not read). The host checks the version on the spine’s DSN before it starts anything, and refuses to start (exit 2) on a mismatch.
  • Grants. A head’s role — the head’s, or an owner’s — has SELECT on the ledger, knowledge, org-chart and meta tables (memory_meta, ledger_rows, ledger_meta, ledger_requests, claims, knowledge_*, graph_*, org_members (a view), org_retired), INSERT and UPDATE on claims only, to take and fence a claim, and EXECUTE on receipt_file, receipt_read and ledger_append: a head writes the Ledger through its gate and no table directly. The spine’s role reads and writes its schema’s tables (SELECT, INSERT, UPDATE, DELETE, TRUNCATE) except the owner-only memory_keys, memory_principals, protected_receipts and protected_redactions, and has EXECUTE on every function; it has no DDL. Every Ledger row but an adoption’s copy goes through ledger_append.
  • Connections are bounded. Each process holds one memory handle (Memory: the Ledger and the leases), opened on first use and closed when the process ends; everything in a process that reads the Ledger or a lease takes that handle rather than building its own, and each Pq store closes its connection when it dissolves. A host holds a fixed handful of connections however long it runs.

The spine (GH #1026). The spine is not a process: it is the program any number of identical nodes run, and a node holds nothing — its state is the stores, its code is the genome. On each tick (HOST_TICK, 1 s) every host that runs a body applies the record into the graph and the org chart (MemoryProjection), carries out an adoption or abandonment the record asks for (LedgerAdoption, under a claim on the asking row), and erases the protected bodies the record redacted (MemoryVault.complete_redactions). Nothing coordinates the nodes; the stores serialize them — the projection a row at a time by compare-and-swap, an adoption by its claim, an erasure by digest (it is idempotent). There is no spine lease and nothing is admitted: a writer writes, and memory’s insert function is the gate (Writers write directly, above). Sync before project: when the record has a remote, a node projects only as far as the head the remote holds after that tick’s sync, and not at all on a tick whose sync did not complete, so a stamped row is always a shared one — a node that projected a row it never shared and then died would leave every other node waiting on a commit none of them can receive. With no remote there is one clone, and nothing to wait on. A host with no HALE_DNA_MEMORY_DSN_SPINE says so and runs with no memory: nothing is projected. What failed on a tick is said once, when it changes, and tried again on the next.

Claims by id (GH #1026). Every non-idempotent act a node takes is claimed by id, with an expiry, in memory before it is taken, so any number of nodes may run one organism over one record with no coordinator. The table is claims(key, holder, token, until, present); the body lease is one of its rows. MemoryLeases.claim(key, holder, ttl) takes a key that is free, expired or already this holder’s, with one conditional write — of two holders racing for a key one lands and the other finds it taken — and answers with the claim as it stands. Expiry is the database’s clock, never a node’s: the write computes until from Postgres’s now() and compares against it, so nodes whose clocks disagree agree on when a claim ends. A renewal by its holder keeps the token; any other take raises it. release_claim(key, holder) gives it back on completion, its token kept so a late renewal is refused. A holder that dies leaves its claim to expire and another finishes the act. Without memory (no DSN) there is no one to race and a claim is granted. The substrate names its node by HALE_DNA_NODE (the host sets it to the body’s holder, user@host:<clone>; otherwise this machine and process) and claims before it spends: an ask is claimed as plan/<intent>, and a human case the leader is asked to plan as plan/case:<case>, for claim_ttl (300 s) before the leader is asked, and, once claimed, the record is read again, so an ask the claim’s previous holder admitted meanwhile is answered with that admission and the claim given back; a node that finds the claim taken answers planning elsewhere: <holder> holds plan/<intent> and writes nothing, and the ask comes back to it with the host’s relay until it is answered or the claim expires (a case is held and driven again by redrive; memory that cannot be reached is said as such, not planned: memory could not be reached to claim …). The claim is given back once the plan’s admission is recorded (the case handed). The optimize pass takes no claim: it is a scheduled occurrence, one execution under its key (Schedules). A claim a node acts on is a claim.taken <key> {holder, token, until} row and its return a claim.released <key> {holder} row (the Ledger’s), so the record shows who took what, and a second node’s take after the first one’s expiry; a claim taken without memory is not recorded.

The genome pull (GH #1026). Nodes converge on the genome by pulling; there is no push deploy for the organism’s own nodes (the heart’s deploy is another thing). At start a host fetches the forge’s default branch (the record’s remote, its HEAD), moves its genome forward to it when it descends from what is checked out, and checks and builds it — the application’s artifact, the organization and, under dev, the application, each through the build cache. A genome that does not check or build is a node.build_failed <holder> {sha, why, by, owner} row; the node returns its checkout to the last genome that built (.hale/dna/genome.built), builds that, and serves it. A node that is up records node.started <holder> {sha, by, owner}. On its tick, every HALE_DNA_GENOME_POLL seconds (300 by default; 0 never), it fetches again, into refs/dna/remote/genome (never FETCH_HEAD, which any other fetch in the clone rewrites); when the forge’s head is neither what it runs nor a sha that did not build here, it stops cleanly — the organization and the expression drain on SIGTERM, the body lease is given back (body.released {why: "genome"}) — and exits with NODE_RESTART (75); its unit (Restart=always) starts it again, and it builds the new genome at start. It never replaces itself in place. A forge head that did not build here is said once and waited out. Each poll writes .hale/dna/genome.json: sha, failed, polls. hale dna status carries a genome: line: the sha each node runs, from its last node.started, and a newer one that did not build since.

The Journal is a git branch, refs/dna/journal, in the governed repository:

  • One commit per event. The commit’s tree holds journal.jsonl, every event so far, one JSON object per line: seq, kind, entity, body, author. The commit’s subject is <kind> <entity>.
  • The commit DAG is the chain. An event’s digest is its commit; its prev is the parent; the first event has no parent. Integrity is git’s: the ref resolves, and its commit count is the event count.
  • Append is compare-and-swap on the ref. A writer builds the next commit on the head it read and updates the ref with that head as the expected old value. A writer that lost the race reloads and, when it was appending at the tail, re-appends at the new tail; seq is the position in the record, never a promise made before the append. A lost race is not a failure: the append goes round with the new tail until it lands, within a bound only a wedged repository reaches.
  • Authorship is git’s. The organism’s own events carry its configured author; a human’s facts (a verdict, an intent through the CLI, a host’s crash accounting) carry the git identity of whoever ran the command. Commit signing, where the repository requires it, applies unchanged.
  • Receipts are blobs under refs/dna/receipts/<sha256>, filed by the digest of their content: a verification step’s output, a diff document. Events name receipts by digest. A receipt has a data class (GH #606): a public or internal body is such a blob; a customer or confidential body (protected_class) never is. It is kept in memory alone, sealed there — by the organism through Dna.file_evidence(text, class, by), by a head through hale dna receipt file --class, both through MemoryVault — and the record gets receipt.classified <digest> {class, by, store}. Memory seals it, and no process holds the key (GH #985). The receipt key lives in memory, in memory_keys, written once at migration from the owner’s HALE_DNA_RECEIPT_KEY (sixteen characters at least); a different key at a later migration is refused, because bodies sealed under the first would no longer open. Three SECURITY DEFINER functions, run as the schema’s owner, are the whole surface of the sealed table protected_receipts: receipt_file(digest, class, body, at) seals a body with pgcrypto (OpenPGP, AES-256) under the key, and refuses a class that is not protected, a digest that is not the body’s, and a digest memory erased under a redaction, whoever files it; receipt_read(digest) opens one; receipt_erase(digest, at) deletes the body and keeps its digest in protected_redactions. Heads and the spine may execute receipt_file and receipt_read; only the spine may execute receipt_erase. Memory with no receipt key keeps no protected evidence and says so. A dump of the database carries the key (memory_keys) and the ciphertext together, so a dump is as sensitive as the evidence itself; #989 revisits where the key is held. A query that fails drops the connection, dials again and tries once more, so a database restart does not leave every protected read and erase failing (#637). Without memory, the body is withheld: receipt.withheld <digest> {class, by, why}, and nothing holds it. The record keeps the digest and the class either way, so sync never carries a protected body into a clone. Reading one is an act in the reader’s name for a purpose (hale dna receipt show <digest> --purpose <p> [--as <who>]): it is refused unless a receipt.disclosed <digest> {recipient, purpose, by} row names both, a refusal appends receipt.read_refused, and an answer is given only once its receipt.read row is in the record — a read the record cannot hold discloses nothing. A body kept while the record refuses its receipt.classified row is reported so, to be filed again. Dna.evidence_class(digest) is the class a request carries when it puts the body in a prompt, so a hosted model refuses it. This is the third trust profile #606 names: the readers of a clone hold digests and classes, never protected bodies. Under local trust a reader’s name is attribution, as --as is; a verified principal is #612’s. Retention (part 2). receipt.held <digest> {by, why} stands until receipt.hold_released, and refuses redaction while it stands. A redaction (Dna.redact_evidence(digest, by, why, policy), hale dna receipt redact <digest> --why --policy) appends receipt.redacted <digest> {by, why, policy, class, store} and then removes the body: a git receipt’s ref is deleted (Receipts.erase); a protected body is erased from memory through receipt_erase, by the spine — a head’s redaction of a body memory keeps records receipt.redacted (store: memory) and the spine erases the body on its tick, as it erases every body the record redacted and memory still holds; a withheld one had no body. The redaction is in the record before a byte is erased, appended exactly at the revision the hold was read at: a redaction the record refuses erases nothing, a hold that arrived in between refuses it, and a redaction recorded before an erase that failed is completed on a later tick or by redacting again. A body the record says was redacted is not filed again — Dna.file_evidence answers its digest and keeps nothing, and hale dna receipt file and receipt_file refuse it — and hale dna receipt redact appends a git receipt’s redaction only at the head it read the hold at and, once the organism has adopted the ledger, only at the ledger revision it read the hold at (the request’s expected), refusing “the record moved” otherwise (#636). A store that cannot read the body (its read failed, as opposed to finding none) records nothing and reports nothing erased: redact again once it answers (protected_erase_step). The record keeps the digest, so provenance survives and a reader learns the body is gone — a read of a redacted digest is refused … was redacted: by … under …, and hale dna history says so under a redacted prompt. Every sync deletes redacted receipts’ refs from the clone and the remote, so a clone that fetched one drops it at its next sync; the blob stays in each object store until git collects it (git gc --prune=now), and a copy disclosed or cloned outside the record cannot be recalled. The projection retires an idea whose receipt was redacted instead of stopping.
  • Leases are blobs under refs/dna/lease/<key> (: in a key becomes /), holder, token, expires, present on four lines, compare-and-swapped on the ref. Tokens are monotonic per key.
  • Effects are requested before they run, and the claim is exclusive (GH #604 rule 3). effect.requested <key> is appended before a dispatch and effect.result <key> once after; a retry finds the request and does not dispatch again. Two writers may both append a request for one key; only the writer whose row is the first request dispatches, the other’s row stays as a contended claim and it does not act: every gate (worktree.open, commit, apply, rollback) refuses in flight while another request holds the claim with no result yet (#635). A request with no result when the organism restarts is resolved by evidence where evidence exists (an apply:<candidate> whose genome is at the candidate is ok; a worktree.open with no worktree is failed) and marked unknown where it does not; the gate refuses an unknown effect until a person resolves it — hale dna effect resolve <key> --outcome ok|failed, one row in their name, the last result for the key being its status.
  • Sync. hale dna sync (and the host, every tick) fetches the remote’s record into refs/dna/remote/journal, reconciles, and pushes. Local ahead: push. Remote ahead: fast-forward. Diverged: the local-only events are re-appended on top of the remote’s head, bodies and authors unchanged, seq their new position, then pushed; a push the remote refuses is fetched and reconciled again. A fetch is a read (GH #961, #1072): two at once in one clone — the host’s tick and a verb beside it — race on git’s lock of the ref they update, and the one that loses is tried again, up to five times, waiting 100 ms more before each (the other already moved the ref at least as far). A fetch that still fails is a read that did not happen and refuses the sync, the connect or the handoff with git’s reason; only git’s own “couldn’t find remote ref” is a remote that holds nothing under the family. The reconciled chain is built beside the ref and swapped in with one compare-and-swap: the ref never points at a record missing a row it held a moment before, so a reader’s position only grows, a writer that reloads after a lost race finds its own rows, and a clone that appended while the chain was built is not overwritten — the swap fails and the reconcile goes round. A local-only event with no row refuses the reconcile rather than being skipped. A fact’s identity in the record is its content and its place among rows of the same content, never its seq (a reconcile renumbers, and a node raising one concern three times writes three identical rows, each a fact): a host relays and handles by that identity, so a renumbered fact is not relayed twice and a repeated one is not relayed once. A reconcile rebuilds only what this clone may sign (GH #639). Under dna.trust = signed every local-only row is read before any chain moves: a row this clone signed is rebuilt and re-signed, and its commit names the original (Rebuilt-From: <commit> in the message’s body); a row never signed is rebuilt unsigned, so a reconcile upgrades nobody’s provenance; a row signed with another key refuses the whole reconcile — the row and the key it was signed with are named, every local row and commit stays where it is, the remote is untouched, and the refusal says what keeps the work (that row’s writer syncs first, since a writer rebuilds its own rows; or the remote is fast-forwarded once it holds the row). A fast-forward is never refused. Under local trust nothing is read and every row is rebuilt as before. A remote with no record yet is the local one’s push, not “up to date”: the first sync after an ordinary git push origin main carries the record, so no one has to push refs/dna/* by hand for a second clone to have the organization’s history. Receipts and candidates travel by refspec both ways; mailboxes are received. The remote is dna.remote in git config, or origin. A plain clone has no record until it syncs.
  • Candidates are kept. The gateway points refs/dna/candidates/<mutation> at a candidate when it commits it, and sync carries the pointers with the record, so a refused or revised change stays readable as a diff after its worktree is gone: hale dna candidates lists them with their Review’s state, hale dna candidates <mutation> shows one from its Review’s base, and hale dna candidates drop <mutation> --why <why> appends candidate.dropped <mutation> {by, why} and stops keeping it — here, at the remote, and at every clone on its next sync, the way a redaction removes a receipt. The commits stay in each object store until git collects them.
  • An apply expresses the candidate, tree and all. An approval applies exactly the reviewed candidate, or nothing: the candidate’s worktree head is the pinned digest, the genome’s head is the base the review was against, and the genome has nothing uncommitted — tracked changes anywhere, and every input of the candidate’s build that the tree does not hold. The graph is the candidate’s, read by the compiler in the candidate’s worktree (hale inputs <seed>: the seed’s .hl files and those of every directory they import, transitively; the hale.toml of each such directory and every C source it declares under [ffi]), because a reviewed change that adds an import makes files an input the moment it lands; any file of those kinds the genome holds untracked in one of the graph’s directories, or that the graph names outright, is dirt whether git ignores it or quotes it. What no build reads — a binary, a log, another program’s source — is not dirt. A graph that cannot be inspected is dirt: an inspection that failed authorises nothing. A dirty genome is mutation.refused before any effect row exists, the work untouched; the gateway checks it once more around the git call and reads the head back, so an apply that reports success is the candidate. Approving again an approved and unapplied candidate re-runs the whole gate (mutation.apply_retried, then review.settled again so a waiting hale dna review hears it): the Review readmits only the same approval in full — digest, standing (a required position, or with none the authority), independence, cited evidence, the word approve — and refuses any other verdict; a Review approved and never applied is rehydrated settled, so the road survives a restart.
  • A person’s job (GH #596 W; GH #604 rules 4 and 5). A plan of kind person hands the Task on: the Work is done as far as the organism is concerned and the record keeps task.handed <task> — work, assignee (the person the plan named), by, narrative; nothing is mutated. The person reports it done with hale dna task done <id> [--as <who>] [--note …], a task.done row in their name and nothing else. Completion is checked at admission: a handed Task with an assignee is closed only in the assignee’s name; anyone else is refused and told to reassign. hale dna task reassign <id> --to <who> appends task.reassigned (from, to, by) and the Task stays handed. hale dna retire <who> [--to <successor>] stops new work reaching a person: every handed Task they hold is transferred as its own task.reassigned row and person.retired <who> records it (by, to, transferred). From then no new work reaches them: a job the leader plans for them is handed to that successor, with retired_assignee on the task.handed row — followed on through each successor who retired in turn to the first person still working, unassigned when the chain ends in a retirement that named none or comes back on itself — and a Task is never reassigned to them, nor are they named anyone’s successor; refused while they hold work and no successor is named — pending work is accounted for, never dropped. Refused for a Task that is not handed: one the organism is working, or has settled, is not a person’s to close. A completion meets its acceptance condition. When the plan names the obligation a person’s job discharges, the organism binds the acceptance practice in force at hand-off: task.handed carries acceptance (the digest of the practice acceptance/<obligation> that is ratified and not retired, “” when none) and evidence_required (its text says evidence: required). A later change of practice never changes a case already handed. Such a Task closes only with --evidence <digest>, a receipt the record holds, appended before task.done as completion.linked (task, evidence, by, practice), or with --exception <why> --authorized-by <who>, appended as completion.excepted (task, why, authorized_by, by, practice) — only when <who> has authorized it in their own name first: hale dna task authorize <id> --exception <why> [--as <who>] appends exception.authorized <task> {task, why, by, practice}, refused for the Task’s assignee and, when the bound practice names who may (exceptions by: <names>), for anyone else, and refused outright when the bound practice cannot be read here (absent from the clone, unreadable, redacted) — an unreadable practice is not one that names no one; naming someone is never an authorization. The completion carries the exception exactly as it was authorized under the bound practice: a different --exception is refused, and needs an authorization of its own. A note alone is refused, and so is an exception without an authorizer, a self-authorized one, or both at once. Either may accompany any person’s completion. task.done keeps its body; the projection shows a done Task’s evidence or exception, and a person’s completion with neither as human-reported, because a note never verifies anything.
  • A person proposes a practice (GH #602). hale dna practice propose <name> --text <text> [--because <what prompted it>] [--supersedes <digest>] [--as <who>] carries a PracticeRequest (request_id, name, text, by, because, supersedes) as a practice.requested row, which a node relays onto the topic dna.practice.requested (The nerves). The organization proposes it as knowledge of kind practice, author org, bound to org, with the ordinary Board Review k:<digest> (decided, it is ratified by an execution of practice-ratify: GH #995, The workflow catalog), and journals practice.proposed <request_id> (name, digest, review_id, by, because, supersedes); a request without a name or a text is practice.refused. Nothing is in force until the Board ratifies it. Roles are authorities, not identities. The person who proposed a practice may ratify it: the proposal is in their name (by), the verdict carries the Board’s authority, and the record keeps both, so one person holding several roles — the Board, and the hands doing the work the practice describes — proposes as one and decides as the other. The Review’s independence rule refuses the candidate’s author, which for a practice is the organization (org), never the proposer; and a governance proposal requiring the Board’s authority (a fill, an opening, a graph proposal) admits its author’s verdict too, so the Board ratifies what it asked for. A change to the work never does, whatever authority it requires. This is the design, not an artifact; an organization that wants the proposer barred from ratifying their own proposal needs a review policy that says so, and none ships. The text arrives byte for byte, newlines included: the host escapes an argument’s own backslashes and newlines in its argument list and unescapes every value it reads. hale dna practice lists every named practice with its state (in force, awaiting the Board, declined, retired), its first line, who proposed it and why, and requests not yet heard.
  • Cross-record handoff (GH #615). A replica of one record is inside the horizon: sync carries all of it, to equally trusted readers. A handoff crosses a horizon into a separate record, and a connection is its only path; nothing selective leaves a record by sync. hale dna connect <record-url> --name <name> --as <position> --purpose <purpose> --classes <internal,customer,confidential> [--by <who>] (the url is the other record’s git remote; an http:// or https:// url is refused as a service url) reads the other record’s identity — its genesis, the root commit of its journal, which every record publishes as a blob under refs/dna/identity when it syncs (until a push of it to the remote as it is now has succeeded — dna.identitypublished names that remote and blob — each sync asks the remote and pushes it, never forced, and sync says when it could not) — and nothing else of it; it refuses this record’s own, and appends connection.proposed (name, url, peer, position, purpose, classes, by, review_id) with a Board Review c-<name>-<n>. The connection is in force once a board verdict approves that Review from someone other than its proposer — the host then settles the Review in the approver’s name — and until hale dna disconnect <name> --why <why> appends connection.closed. hale dna handoff <name> task <id> | receipt <digest> [--note …] [--as <who>] writes one handoff.received envelope into the other record’s mailbox for this record, refs/dna/exchange/<this record's identity> — a chain of rows only this record writes, pushed there under compare-and-swap. Neither record reads the other’s journal: this clone’s cache for a connection (.hale/dna/peers/<name>.git) holds the other record’s identity and this record’s own mailbox there, nothing else. The envelope carries handoff (h and twelve hex digits of the origin genesis, kind and subject, so a fact crosses once), origin_record, origin_url, origin_author, origin_row (the source row’s digest), lineage (the subject’s rows here, as kind#seq), purpose, position, via, kind, subject, class, fact, note. This record then appends handoff.published (with peer_row, the commit it landed as) and, for a Task, task.transfer_requested. A handoff retried after a crash completes what the first try left (#637): an envelope already in the mailbox is reused, never written twice, and a published Task missing its task.transfer_requested gets it. Only a handed Task crosses. A receipt crosses as its digest and class; its body never leaves this record. A fact whose class the connection does not carry is refused at the edge as handoff.refused, with nothing written across. The receiving record reads its own mailboxes (sync fetches refs/dna/exchange/* from its remote; a row claiming another origin than the mailbox it is in is ignored) and admits what arrives under its own policy: hale dna handoff lists a received handoff as admitted only under a connection in force back to its origin record that carries its class, and hale dna handoff accept <id> [--as <who>], only for an admitted one, appends handoff.accepted to its own journal (with the envelope’s origin, lineage, purpose and fact) and sends a handoff.accepted envelope into the origin’s mailbox for it. hale dna handoff sync reads, for every connection in force, the envelopes the other record sent to this record’s mailbox: each acceptance of a handoff published through it is admitted once, as handoff.accepted_by_peer and, for a Task, task.transfer_accepted, each row checked on its own, so a crash between the two is completed at the next sync rather than suppressing it (#637). The Task settles only then, the rule retirement follows (GH #604 rule 5). A closed connection is not read, so history stays in both records and nothing further is admitted.
  • An attributed external decision (GH #616). A decision made by someone who does not run Hale — a manager, a client, an accountant — enters as a fact reported by a position inside the horizon, never as that party’s verdict. hale dna task decide <id> --decided-by <party> --via <channel> --evidence <digest> [--note …] [--as <reporter>] appends one decision.reported row on the Task in the reporter’s name: reporter, decider, channel, evidence, scope (the Task), obligation, practice, policy (the practice’s digest), accepted, why, note. It is refused with nothing appended when the Task is not handed, when the reporter is not its assignee (GH #604 rule 4), when the decider is the reporter, when a field is missing, or when the evidence is not a receipt the record holds. hale dna receipt file <path> [--class internal|customer|confidential] files one: internal text under refs/dna/receipts/ with a receipt.filed row (by, name, bytes, class, store), a protected class sealed in memory alone (receipt.classified). Whether a report settles the Task is the obligation’s acceptance policy: a practice named acceptance/<obligation>, ratified by the Board and not retired, whose text says reported decisions: allowed. The obligation is the class of obligation a person’s job discharges; the leader’s plan names it (obligation:) and task.handed carries it. The policy is the practice bound when the Task was handed (task.handed.acceptance), so a later change of practice changes no case already handed — only a Task handed before practices were bound reads the one in force; it is read at admission and its answer kept in the row, so a later change of practice never rewrites what was admitted. Accepted, the Task is decided, and the projection shows the report as such: who reported, who decided, through what, on what evidence. Otherwise the Task stays handed, hale dna board lists it under tasks waiting with the reason, and its assignee closes it with hale dna task done. Without an obligation class or a practice in force, a report never suffices.
  • Schedules (GH #610, GH #1143). When a workflow runs and who convenes it — nothing else. A schedule points at a definition of the catalog (The workflow catalog), never at intent text: Schedule { id, definition, args, every_ms | cron, convener, paused, last_occurrence }, plus the state the record rebuilds (from, task, fired, last_minute). There is no ask and no requires, no schedule.fired or schedule.admitted row, and no claim before an ask: an occurrence is an execution, and its key is its idempotence. Declared by ratification. A ratified practice whose receipt carries schedule (a JSON object as text) declares it in its ratify step; the ratification stands when the declaration is refused — the step is done, ratified by <who>; its schedule was refused (<why>), and the refusal is its own schedule.refused row naming the hole. hale dna schedule declare <id> (--every <n>ms|s|m|h|d | --cron <expr>) --definition <id> --convener <position> [--args <json>] [--as <who>] asks it of the organization through the store: a schedule.requested row the host relays, answered once under the request’s id by schedule.answered {schedule, declared, why, by} (the CLI takes a bare position name as position:<name>). Declaring through the store is the Board’s, or the convener’s own holder’s: with memory naming who holds the Board, a requester who holds neither it nor the convener is answered declared: false with why, and nothing is declared; while nobody holds the Board, or no memory is named, the verified requester declares, as a Review’s fallback decides by its required authority. A declaration is schedule.declared <id> {every_ms, cron, definition, args, convener, from}, written when new or changed — declaring the same schedule unchanged writes nothing — and from is the occurrence it was declared at (a cron with no match in the week before counts from the declaration’s own time): nothing at or before it is due. It is refused, a schedule.refused <id> {why} row and the reason, when it has no id or one with @ or a space; names neither an interval nor a cron, or both; names a malformed cron (five fields, minute hour day-of-month month day-of-week; *, a, a-b, */n, lists; ranges checked — at declaration, never when it would first fire); names a convener that is not position:<name>, or one that cannot reach the definition’s first store; names a definition not in the catalog, one whose expansion the catalog would refuse, or one with a step the organization does not perform; or carries args that are not a JSON object naming every input the definition takes. Reach: the organism’s own positions (leader, editor) reach every store its workflows write; a position of the graph reaches it through who holds it — a person, or an organization’s members — as memory’s routing says, and in this slice being held is the whole of reach: no per-position grant on a store is read; with no memory named, or memory not answering, a graph convener is refused, nothing assumed. A position nobody holds is a hole: the refusal says “nobody holds position:

    : it convenes nothing until someone does (hale dna fill <p> <holder>)” and the row carries hole: position:<p>, the hole #1091’s path fills. Declaring also writes the schedule into the graph (The repository’s graph): a definition:<id> node, once, and a convenes(position, definition) edge whose body carries the cadence (every 1d, every 90s, cron <expr>) and schedule of every schedule declared for that pair (; - and space-joined). A re-declaration for another pair writes the pair it left again, and retires its edge (graph.retired) when no schedule is left on it. Occurrences. The org program’s loop ticks with the wall clock in milliseconds (request_tick(std::time::nanos(std::time::current()) / 1000000), never a monotonic clock), queued through the organization’s keyed TickRequested handler. Synchronous tick is for that handler or isolated callers with no incoming bus work; a live loop uses request_tick so incoming work cannot enter during journal refresh. An occurrence’s time is its cadence’s: an interval’s step from the epoch (floor(now / every_ms) * every_ms), or a cron’s latest matching minute in UTC (day-of-month and day-of-week both restricted: either; looked for over the last week), a cron evaluated once per minute. A tick in an occurrence’s period admits it as an execution of the definition’s newest revision through its first store, with the ask id sched:<id>@<occurrence time>, args as its inputs, and the convener as its position: the admission is the convener’s owner’s (to, GH #664; the organism’s own positions are the organization’s), and workflow.admitted carries it as convener. On a shared record a body whose organization does not own the convener leaves the schedule to the one that does, writing nothing. That key is the idempotence: a schedule’s state is rebuilt from the record (from its declaration’s from, the admissions under its key, its skipped, refused and missed rows, and its pauses), so a restart, or a second ask of the same occurrence, is one execution. The occurrences whose period passed with no tick — the organism was down — are one schedule.missed <id> {first, last, count, why} row per run of them, and nothing else in this slice. A refused admission is schedule.refused <id> {occurrence, why}. Concurrency is the definition’s (WorkflowDef.concurrency): under skip, the default, an occurrence while the last execution is open (admitted, not yet workflow.settled) is a schedule.skipped <id> {occurrence, task} row, never silent; under overlap it is admitted beside it. Amendments. hale dna schedule pause <id> / resume <id> append schedule.paused {by} / schedule.resumed {by, at} (at in wall milliseconds) in the caller’s name from any clone; the organism reads them at its tick (a git journal is re-read from its ref at most every 5s). A paused schedule is not due: its occurrences pass unfired and unmissed, and a resume goes on from the next one — a resume read at a restart counts misses from its at. hale dna schedule lists each as the record has it — <id> [live|paused] every <n>ms|cron `<expr>` (UTC) — <definition>, convened by <position> · occurred N, skipped N, missed N · last <task>, occurred counting the admissions under its key. Not in this slice, by name: terms (an edge’s until firing a definition), turnarounds (a row’s age firing one), calendars and offsets beyond UTC, per-person availability, derived cadence holes (ingest proposing that a process has no cadence), on_miss: late and a queue concurrency, declaring a refused schedule again once its hole is filled, and who fires on a multi-node spine — the tick is the body’s that holds the lease, and a memory compare-and-swap on the occurrence key is owed when spine nodes multiply.

  • The optimize pass (GH #596 O). The pass is an execution of optimize-walk (GH #995: The workflow catalog) and nothing else. Its cadence is a schedule (above) that the seeded operating practice operating/optimize-cadence declares once the Board ratifies it — {"id": "optimize", "every_ms": 86400000, "definition": "optimize-walk", "args": "{}", "convener": "position:leader"} — convened by the leader, who walks the machinery, so it needs no one seated to be declared; a different cadence is an amendment the Board ratifies. The tick is the body’s that holds the lease, and an occurrence asked twice is one pass. Its one step, walk, first asks the budget — the pass is model-backed work, and none is routed on an exhausted window: optimize.refused <org> (“the pass waits for the next occurrence”) and the step fails saying so — then reads the record’s structural signals — asks planned and how many took the defaults, concerns raised, grant contractions, verdicts refused, mutations rolled back — and asks the leader (OptimizeRequested, keyed by org_id, once per incarnation) to walk the machinery, not the work. The leader answers with one small proposal or none (OrgReviewed), and the substrate journals org.reviewed <org> either way, with the signals it read and the execution it answers (task, carried on OptimizeRequested and OrgReviewed, so passes that overlap never answer each other), which settles its walk step: “if the state is clean, say so”. A proposal enters as an ask in the leader’s name and takes the whole road — planned, proposed, reviewed by the Board as an organization change.
  • Grants layer by containment (GH #596). The substrate may hold a ceiling: the grant above the child’s — the organism’s, the Board’s to widen. A child’s boundary reads its grant through the ceiling as it is now at every assessment: the classes both allow, the smaller magnitude, the stricter review (intersect_grants). Grants contract on their own (two failures in a row halve the ceiling), so a child born within a ceiling of 100 is bound by 50 the moment its parent contracts, and the record says so (grant.contracted <parent> {boundary, from, to, epoch, binds, note}, appended before the contraction takes effect). An organism born over the record restores the last contraction of its ceiling — its epoch and pre review whatever the authored ceiling now says, and the stricter of the recorded and authored magnitudes — before anything is admitted, as it restores a revocation. A contraction whose row the record refuses still binds live, and is appended again before every reserve and admits; until the record holds it, nothing is admitted. A child born wider than its ceiling is the early error: grant.refused <child> names what is wider, and the ceiling binds it from birth; that row is authority, the record’s, and is not the row a refused spend writes. Law layers by adoption: every position lives under the org program’s main, which adopts the organism’s law, so a department’s law can only add to it.
  • Grants delegate resources (GH #605). A Grant also carries amounts (per-operation ceilings, "USD 500.00 EUR 200"; a currency not named may not be spent), window_amounts and window (day, week or none), counterparties and routes (space-separated; none named, none allowed), expires (unix seconds; 0 never) and an authority_epoch. Every field defaults to none, so a grant written before them spends nothing. They contain like the rest: the currencies both grant at the smaller ceilings, the smaller window limit, the parties and routes both name, the earlier expiry (never is later than any, so a child that never expires under a ceiling that does is wider in time), and the sum of the two epochs (intersect_grants); resource_gap names what is wider, and the birth refusal carries it. permits(grant, spend, now) checks a Spend { id, amount, currency, counterparty, route } predicate by predicate and names the field that fails (expires, operation, currency, amount, counterparty, route). Dna.reserve(spend) admits a spend against the effective grant and appends spend.reserved <op> {child, amount, currency, counterparty, route, ceiling, epoch, at, funder, account} (GH #668: the allocation is the row’s entity, a claim kind the Ledger’s unique constraint reserves once, and the row names who pays — a grant’s funder, <owner>/<account>, inherited from the ceiling when the child names none; a child naming another funder than its ceiling’s is born wider, funder … not granted above); a reservation from before this, grant.reserved <child> {op, …}, is read the same way. The child’s own window counts its reservations and the ceiling’s window counts every child’s under it. The remainder is read at a revision and the row is appended with Journal.append_exact at that revision — a writer that moved the record in between makes it stale, never re-appended at the tail — so two children cannot each spend the same remainder. A refusal is grant.reservation_refused <child> naming the field — money, the Ledger’s, never grant.refused. Dna.settle_spend(op, spent) appends spend.settled <op> {child, attempt, spent}, one per attempt: every attempt’s actual consumption is retained and the window counts their sum from then on (a second settlement is attempt 2, not a refusal). Under contention a settlement is once: the row is appended with append_exact at the revision the last settlement was read at, and one another writer landed in between is read as the answer, never doubled. Money that came back is a compensation someone authorized, Dna.compensate_spend(op, amount, by) appending spend.compensated <op> {child, attempt, amount, by} — never an implied rollback. A purchase two children (or two owners of a shared record) fund is two reservations, each its own: one may settle and the other not, and the partial success is visible as such; no atomicity across them is assumed. A contraction advances the epoch, and so does Dna.revoke_grant(by) (the parent’s only; nothing is left granted). A revocation is recorded before it takes effect, grant.revoked <child> {by, parent, epoch} — one the record refuses did not happen — and an organism born over the record restores it before anything is admitted, so a restart never restores revoked authority. Dna.admits(admission) refuses an admission made under an older epoch, or whose grant has expired by the time it is carried out (grant.fenced). The generated law adds money_only_through_the_substrate: forbid reaches(positions, effects(money)) avoiding substrate, with effect money; declared in the core; an organization’s existing dna/org/law.hl is project-owned and gains the clause by hand. The money budget is distinct from the model budget.
  • The principal source (GH #612, #989). Who acts where a verdict, an intent or a task completion enters comes from git config dna.principal. A CLI verb on a clone is its operator’s, as --as and the git identity say. A head’s principal path is the only way in: a peer on the head’s socket is the local account the kernel vouches for, mapped by dna.unix.member; every HTTP caller is an identity provider’s subject under oidc. Trusted-local — a head’s session under its own identity (the launch token, hale dna ui with no principal source) — is a fixture’s mode alone: a head serves it only under HALE_DNA_TRUSTED_LOCAL=1, and refuses to start without either it or dna.principal = oidc. Local mode is OIDC too: hale dna head (or, from a checkout, dna/face/start.sh) starts the stub provider (dna/oidc: the discovery document, a JWKS, an authorization endpoint that signs in local-sub, and a token endpoint that exchanges a code or mints for a subject, every ID token ES256 under its published test key) and signs the person running it in as local-sub, mapped to $USER. Under oidc a head serves nothing without a verified token: /api/* answers 401, / redirects to /auth/login. A session comes from OpenID Connect’s authorization-code flow against dna.oidc.issuer (https, or plain http to 127.0.0.1 or localhost exactly — the URL’s host is parsed, userinfo refused, never matched by prefix): the head discovers the issuer’s endpoints, refuses a discovery document whose authorization_endpoint or token_endpoint is not https (plain http only when the issuer itself is on this machine; discovery_refusal, #635), sends the browser to its authorize endpoint with a random state and nonce (std::os::getrandom), and exchanges the returned code at the token endpoint itself with the client’s secret (dna.oidc.client, its secret the vault’s oidc-client-<client>, revealed on the one line that builds the token request, dna.oidc.redirect as the callback, else the head’s own /auth/callback). Every ID token is verified: its ES256 signature under a key of the issuer’s JWKS — the head reads the discovery document and its jwks_uri once, at start, and does not start without them; an RS256 token is refused, the standard library verifying ES256 only (es256_refusal) — then claims_refusal checks the issuer, the client among the audience, the expiry and the nonce. A caller that is not a browser presents its ID token as Authorization: Bearer, checked the same way without a nonce (bearer_refusal). A command reaches the head’s surface (HeadCommands) over its http::Rpc exposure (GH #1135, #1417; spec/api.md § The HTTP transport), POST /call/<member>, on the port the head’s launcher gives it (HALE_DNA_COMMANDS_PORT, never derived from the reads’ port; none given, none served, and a relayed command is commands_unavailable), as a POSTed line of the wire under the caller’s bearer; no program forwards a line. The head’s bearer source (dna/api/bearer.hl, OidcBearer) verifies the token as above and names the principal oidc:<subject>; the binding’s roles map that subject to its person by dna.oidc.member as they map a socket peer by dna.unix.member, the gates are the same, and the receipt says principal_mode: oidc, the person, and the positions they hold (principal_positions). A fixture’s trusted-local head names its launch token the head’s own account (LaunchBearer, uid:<n>), the account its local session acts as. The face (dna/api/project_service) is itself a client of the issuer under HALE_DNA_OIDC_ISSUER, _CLIENT and _MEMBER (the <subject>=<person> who signs in there), configures a project it attaches for that issuer, and hands the session’s ID token to the project’s API child as the bearer — on the child’s reads, and on POST …/commands, which it proxies to the child’s binding transport (a GET there is 405: a lookup is the CommandLookup call) — and the child verifies it itself. A child serving its own shell relays that shell’s POST …/commands the same way (its ID token under OIDC, the launch token in a trusted-local session). Either relay keeps the CSRF guard a cookie session needs — the exact Origin, X-Hale-Command: 1, JSON — before the session becomes a bearer; a caller’s own bearer is not asked. An OIDC child is ready when it refuses an unauthenticated read carrying the digest of the proof the face gave it (X-Hale-Child), so a process squatting on the child’s port is never handed a token. An issuer on the loopback is anyone’s who can bind its port, so its key is pinned: a head under a loopback issuer does not start without dna.oidc.key (the issuer’s SubjectPublicKeyInfo) and trusts that key alone. In local mode the stub signs under a key made for the launch, its client secret is the vault’s oidc-client-<client>, which the stub and the head each read there (never the environment or argv), the face pins the key into a project it attaches (restarting a child that pinned another), only the browser that opened the URL the face prints (its 0600 launch token) may start or finish a sign-in, and the face answers only as 127.0.0.1 or localhost on its port. The subject — never an email alone — maps to a member through a reviewed mapping, git config --add dna.oidc.member "<subject>=<name>"; an unmapped subject gets no session. A service is a principal too: a program with no person behind it (an application reading a project’s ratified practices) asks the issuer with the OAuth client_credentials grant under its own client id and secret — the stub (dna/oidc) answers for the ids services names (HALE_DNA_OIDC_SERVICES to dna/oidc/serve), each secret the vault’s entry oidc-service-<id> (std::secret’s vault: source; the entry is the secret’s exact bytes, no trailing newline), read at every ask and refused when there is none — with an ES256 token whose subject is the client id and whose audience is the issuer’s relying party. A token verifies only in that shape (ES256, aud the head’s client): an identity provider whose client_credentials tokens are RS256, or name another audience, is not supported. The record maps it by git config --local --add dna.oidc.service "<subject>=<service>". A subject is a person or a service, never both: the face refuses a bearer whose subject the record maps as a member (a person signs in, and a service’s token cannot be told from an ID token), and the child refuses a subject mapped both ways. The face accepts the token as Authorization: Bearer on the project’s API routes only (/api/hale/v1/applications…; on its own routes a bearer is 401), verifies it as it verifies an ID token, refuses a subject the attached project maps as no service (403), hands it to the child as the bearer (its commands reach the binding’s gates like a person’s), and never gives it a session or a cookie. The child reads as the principal service:<service>, never a person’s name, so a read a person alone may make (a Review’s exact candidate) is refused it; its only POST is a command, and a command or a lookup is a person’s: the handler refuses an issuer’s subject that maps to no person (forbidden), whatever its row requires, and never records it as the account the head runs under. A service holds no position, so no row’s role is held by it. A sign-in’s state is used once, expires in ten minutes, and is bound to the browser that started it by an HttpOnly; SameSite=Lax dna_signin cookie: a callback carrying the state from any other browser is refused and leaves the sign-in for the browser that started it (so a callback URL handed to someone else cannot sign them in); a session is a random 256-bit id in an HttpOnly; SameSite=Lax cookie (Secure when the callback is https) and lasts eight hours or until /auth/logout. With a session, a verdict acts as the member (--as, and --authority board when dna.oidc.board names them, reviewer otherwise) and an intent is asked by them (hale dna task create --as), the row’s author too; the form’s own as field is ignored. The head never acts in its own name. It speaks plain HTTP: TLS is a reverse proxy in front of it. Rows arriving by sync remain admitted under dna.trust (GH #604 rule 6); a synced row claiming a rank is not a sign-in.
  • The nerves: the record’s requests reach the organism (GH #986). A request is a row first, always. hale dna task create appends intent.requested (the body: outcome, from, to), a verdict appends review.verdict (the body: the verdict), concern raise appends concern.requested, pressure raise appends pressure.requested and practice propose appends practice.requested, each in the appender’s git identity, from any clone. A one-shot verb never publishes. A node — the host under hale dna run or dev, which is its program’s main locus for as long as it runs — relays every request row it holds that the record has not answered onto the nerves on its tick, admitting each under dna.trust (GH #604 rule 6), and again every 30 s while it stays unanswered. Beside a live organism a verb waits for the answer in the record; from a clone with none it syncs and says where the answer will be. A request is answered by its own answer (GH #689): an intent, a verdict’s review and a practice request have an entity of their own, and a concern — whose entity is its source, shared by every concern from it — carries a request id that the organization writes into the concern.raised answering it. That row is one object — the concern’s words, its severity, which occurrence it is, and the request it answers — so the id is a member of the row rather than a shape inside its text: a worker who writes observed [request c1] in a log has written text, not metadata, and the host compares the whole id, so an answer to c10 is not an answer to c1. A row written before that rule carries no such field and is matched by counting, as this host did before. One request is one concern: a request delivered twice — the host relays a row again while it looks unanswered — writes one concern.raised, and the count that turns concerns into a knowledge proposal counts distinct requests, never redeliveries. Two requests raising the same words are two concerns; one id used for a second, different concern — other words, or the same words at another severity — is refused once, in concern.refused, which is that request’s answer and carries its id. Pressure is answered the same way (GH #986): a pressure.requested row carries a request id, and the pressure.raised answering it is an object — the signal’s words, count (which time its source has raised pressure) and request; a pressure row from before carries <what> x<n> and is read as that. A publish is confirmed by its answer in the record, never by the transport (GH #682): a row still unanswered 30 s after it was relayed is relayed again, whatever the transport said. The organism admits an intent once by its id — an intent offered again after its Task was born answers with that Task and journals nothing — a repeated verdict at a settled Review is answered as already settled, and a practice, a concern or a pressure request is answered once by its id. The host’s own observation report is a row too: observation.requested <mutation> (mutation_id, outcome, model_hash, detail), relayed as ExpressionObserved until the organism answers it with expression.observed; a node inside its observation window relays while it waits for that answer. A refusal the host writes for an unverified row answers the request it refuses: for a request with an id of its own (a concern’s, a pressure signal’s) it is one object, why and request. local, the default, trusts every writer to the record — the operator’s profile; signed admits a row only when its commit carries a signature git verifies (git verify-commit; git’s keyring or allowed-signers file is the list of who may act), and a row that does not is refused in the record in the host’s name (intent.refused, review.refused, concern.refused: “unverified writer”) and never relayed. Under signed, rows this clone writes are signed commits. Signing is one mechanism for this edge, not the boundary itself: a hosted head is admitted through a reviewed mapping of principals to authority and acts as the user, never as itself. The organism’s answers (intent.offered, task.born, review.settled, review.refused) return the same way. The body writes intent.offered, so the asker is not its author: the row’s body names who asked — <outcome> (from alice), an optimizer — and a head’s ask carries the person the head identified. A row is answered when a later row of the answering kind names its entity. hale dna task create --no-wait appends and returns, on either path: beside a live organism once the row is appended, from a clone with no organism once the row is appended and synced. “Beside a live organism” is the record’s body lease: live, and held by this clone’s body.

The nerves (GH #986) are NATS JetStream, through pond’s client and std::bus adapter, vendored under dna/core/pond/realtime/nats.

  • Bindings where the publishes are. The host binds every DNA fact topic it publishes (IntentOffered, ReviewVerdict, ExpressionObserved, PressureRaised, ConcernRaised, PracticeRequested, HoldRequested, PositionOpenRequested, GraphReviewRequested, ScheduleRequested, KnowledgeNodeRequested, KnowledgeBindingRequested, KnowledgeEdgeRequested, WorkSubmit, WorkAllowanceAsk, RowLanded) to nats::NatsAdapter, and owns the connection, a nats::NatsConn placed pinned; its relay is a NodeRelay child whose publishes go out through those bindings. The generated organization reads them through its own pinned connection: a message that arrives is handed to every topic of its subject the organization subscribes, whatever its bindings list (which init writes for most of them, for its own publishes). The payload is the bus’s own encoding: both ends are Hale programs declaring the same topic, and nothing converts it. A one-shot verb instantiates no main locus, so it binds nothing and connects to nothing.
  • Subjects are the topics’ declared subjects under the organization’s token — the one memory names its schema with (dna_<record id>): <org>.dna.<family>.<event> (dna_4f….dna.intent.offered); an application’s own events are <org>.app.<app>.<event> (The heart’s events), and what a node tells the heads is <org>.head.<event> (Rows landing). The connection puts the token on what leaves and takes it off what arrives, so a topic keeps its declared subject.
  • One stream per organization, DNA_<ID> over <org>.>, on disk, kept a week (limits retention: what was said is kept and read from a position, not deleted once read). Every publish is acknowledged by the stream. The organization reads through its durable pull consumer spine (from the stream’s start when it is first made, from where it left off after that), filtered to <org>.dna.>, so a fact published while it was down or restarting reaches it when it is back. Over a shared record each owner is a space of its own: every owner runs a body and an organization (GH #664), and an owner’s host publishes under <org>.<owner>. and its organization reads through the durable spine_<owner>, filtered to <org>.<owner>.dna.> — so no organization pulls a fact another owner’s host published, and refuses it, which the relay would take as the answer. The host names its owner from the clone (dna.owner, the name of its organization node) and hands it to the organization (HALE_DNA_OWNER); an owner’s name in a subject or a durable keeps its letters, digits, - and _, anything else _.
  • Credentials, one per family, each a user on the server: owner creates the stream and is held by no process that runs the organism; spine publishes and reads <org>.dna.> and an owner’s <org>.<owner>.dna.> (the host and the organization), reads <org>.app.> through the durable heart (the host), and publishes <org>.head.> and an owner’s <org>.<owner>.head.>; head subscribes and publishes nothing; the application the record attaches holds a user of its own, app-<name> (The application’s account), which publishes on <org>.app.<name>.> alone and reads nothing of DNA’s. hale dna nerves migrate [dir] creates or updates the stream with the owner’s URL (HALE_DNA_NATS_URL_OWNER, or the nerves service of dna/compose.yaml, brought up) and prints HALE_DNA_NATS_ORG, one HALE_DNA_NATS_URL_<ROLE> per role — nats://host:port alone — and HALE_DNA_NATS_VAULT_REFLEXES, and for the attached application HALE_DNA_NATS_USER_APP and HALE_DNA_NATS_VAULT_APP: the users and vault names a program that is not the host presents; hale dna dev runs it before it starts the host and hands the host the spine’s URL and the token, never the owner’s nor the head’s; hale dna run takes the two from its environment and has the owner’s and the head’s removed from it. hale dna nerves drop [dir] deletes the stream, and everything it held, with the owner’s URL — beside dropping memory’s schema, when an organization is torn down.
  • The application’s account (GH #989). The application the record attaches is named by its project: the project’s name as a subject token (lower-case letters and digits, anything else _, at most 32), recorded as name on the application.attached row when it lands. A row recorded before that field resolves by the project’s directory name: a record is data already written, not an API kept compatible. An application publishes under its project’s name. Its user is app-<name>, its password the vault’s nats-<org>-app-<name> and dna/nats.conf‘s $NATS_APP_<NAME>_PASSWORD, and it may publish on <org>.app.<name>.> and nowhere else; no other application’s subjects, nor the reflexes’. It is provisioned when application.attached lands (hale dna init lands it, then provisions the organism’s secrets) and on every upgrade that finds it missing. hale dna application remove [project] [--as <who>] writes application.detached {name, by} to the record, the membership’s source, and revokes the account: its user leaves dna/nats.conf, its password the include and its entry the vault, and compose’s nerves is restarted, so a connection it holds is closed and none opens again. An application named reflexes holds no account: those subjects are the reflexes’ alone.
  • The nerves’ passwords (GH #989) are the vault’s, one per role and seed, nats-<org>-<role> (The organism’s secrets). A part that connects holds std::secret::Credential { vault: … } beside its user, and pond’s client reveals it on the one line that writes CONNECT — never a field, a return value, a URL or the environment. No URL carries a password: userinfo in a server’s URL is dropped. A role whose entry the vault lacks is refused (nerves migrate, the host, the reflexes); nothing connects with a default.
    • The server’s files. dna/nats.conf is a tracked seed file with no secret in it: it includes ./nats.secrets.conf and names each user’s password as its variable ($NATS_<ROLE>_PASSWORD, and the attached application’s $NATS_APP_<NAME>_PASSWORD). dna/nats.secrets.conf, written by the bootstrap from the same draw as the vault’s entries, is the one file that holds a password: mode 600, ignored by the project’s .gitignore (which init and upgrade give the line), mounted into compose’s nerves service.
    • The guard. The writer refuses a secrets file git tracks or would track.
    • Rotation. init and upgrade draw every role’s password anew (a regenerated seed rotates them); a server compose is running is restarted to read them, and a restart that fails is said. upgrade rewrites dna/compose.yaml, which mounts the secrets file. A server of the operator’s (HALE_DNA_NATS_URL_OWNER) takes the conf and the secrets file again after each rotation.
  • Liveness. At start the host waits for the organization to read its facts: its durable consumer has a pull outstanding. It says so (the organization reads its facts from the nerves (DNA_<ID>)) or that it did not within 20 s; the facts wait in the stream either way. A restarted or rolled-back organization is confirmed the same way. Without nerves (HALE_DNA_NATS_URL_SPINE or HALE_DNA_NATS_ORG unset) the host runs and says the organization hears nothing the record asks.
  • A lost connection. The connection keeps every publish until the stream acknowledges it, writes it again after a reconnect, and a publish unacknowledged past its window violates the connection’s delivery closure. The violation collapses it to its owner, which does not restart the connection in place: the node starts again as a fresh process, its connection and its organization’s both new. The host appends violation.recorded (adapter_undeliverable, subject nerves, owner its holder, detail why), stops its organization and the expression, gives the body lease back (body.released, why: nerves) and exits NODE_RESTART (75, the genome pull’s code: one restart code for the organism), and its unit (Restart=always, which restarts on any exit) starts it again. The organization’s program exits 75 on its own connection’s collapse, and the host that supervises it ends with it. The row first is what makes a loss recoverable: the next node relays every request still unanswered.
  • The heart’s events (GH #987). An application’s one hookup to DNA is its events. It declares each as a topic of its own, under its own subject app.<app>.<event>, <app> its project’s name (The application’s account), binds it to pond’s NatsAdapter with a codec that writes one JSON object, and publishes it through a pinned NatsConn as its own user (HALE_DNA_NATS_USER_APP), presenting the vault’s entry HALE_DNA_NATS_VAULT_APP names, under the organization’s prefix, acknowledged by the stream. It imports nothing of DNA. The event names itself: its body is one JSON object as std::json::valid_object admits it (well-formed UTF-8, unique unescaped keys, at most 64 top-level members, nesting at most 64 deep) with a string id of the application’s choosing (letters, digits and -_.:, at most 128 bytes, as the application’s and the event’s names are). The node’s host reads <org>.app.> through the durable heart on its own connection, at most 64 unacknowledged at once. There is one durable for the organization, whichever owner’s host pulls, so each event goes to one host, and the prefix is the organization’s alone. The connection hands each event over as it arrived and does not acknowledge it (pond’s NatsConn.untyped), since no part of the organism declares an application’s events. The host lands it as reading.recorded, entity <app>/<event>/<id>, body app, event, id, subject and payload (the event’s JSON, at most 64 KiB), before anything acts on it. A reading is a signal, never a fact: it acts on nothing itself. What the spine does with one is a row of its own: an application’s concern becomes concern.requested (The application side), a reflex’s firing is read with the others for its target (The senses and the reflexes). What the organism does to the application goes the other way, through the API the application exposes.
    • Duplicates. A second arrival of an entity already recorded, whether a redelivery or a replay, is refused as a duplicate and lands nothing. Whether it is recorded is decided at the revisions the row is appended at, so two hosts land it once.
    • Shared records. Over a shared record that keeps its rows in git (routing 0), each owner’s clone appends on its own, and only the ledger can decide an id across owners. There readings wait in the stream until the ledger is adopted, and the host says so once. They wait at most as long as the stream keeps them (a week).
    • Not a reading. A message that names no application and event, or whose body is not such an object, is not a reading: the host says why and records nothing.
    • Acknowledgement. The host acknowledges the durable’s message once its row has landed, or once it has been refused as a duplicate or as not a reading. One whose row could not be written stays unacknowledged and comes again, so a host that stops between the hand-off and the row loses nothing — within the stream’s retention: an event not landed within a week (NERVES_MAX_AGE_SECS) ages out of the stream unrecorded. One whose write keeps failing comes again every ack wait (30 s) and holds one of the durable’s 64 places.
  • Rows landing. For every row its view of the organism gains, a node publishes head.row.landed (RowLanded: the row’s seq, kind and entity; under an owner’s prefix over a shared record), at most 256 on one tick; its first look only takes its place. It is the heads’ family, outside dna., so no organization’s durable pulls it; the stream keeps it like everything under <org>., and the head’s push is what it is for.
  • Stopping. A node is its program’s main locus, and SIGTERM drains it (GH #1039): its supervision reads draining, stops the organization (which drains the same way), gives the lease back and ends. hale dna run and dev give the node a 30 s grace for it (LOTUS_DRAIN_GRACE_MS, unless the environment already sets one). The core imports no NATS, because a program holding pond’s connection drains on SIGTERM: an API that imports the core keeps the default action. The head holds its own connection and drains: its watcher stops and the server stops accepting, at once.
  • One transport: the nerves (GH #986). Every fact between the organism’s parts travels over the nerves, and an application’s concern is one of its own events (The heart’s events; The application side). An application — an instance a node starts, the expression dev starts — inherits the application’s server, its own user and the vault name of that user’s credential, and the organization’s token (HALE_DNA_NATS_URL_APP, HALE_DNA_NATS_USER_APP, HALE_DNA_NATS_VAULT_APP, HALE_DNA_NATS_ORG; dev keeps them from nerves migrate, a node is started with them), and none of the organism’s other credentials: the spine’s, the owner’s, the head’s and the reflexes’ NATS URLs and memory DSNs are taken out of what it inherits. A node materializes vendor/dna in its clone for its toolchain, so an instance can import pond’s NATS client from it. Local mode is the seed compose’s nerves.

.hale/dna/ holds only what is not the record: the status projection, worktrees, scratch inputs to the toolchain. Deleting it loses nothing the record holds.

Every kind names its memory. The memory column is the routing table under version 1 (memory_of in routing.hl); on routing 0 every kind is the record’s, and a kind no build knows is the record’s.

kind memory what it is
application.attached record the application the organism oversees: its entrypoint, its artifact, the toolchain, and its name, which its broker account carries
application.detached record the attached application removed (hale dna application remove): its name and who; its broker account is revoked
structure.observed record the compiler’s model of one of its parts, at init
graph.node record a node of the repository’s graph, entity <kind>:<name> (The repository’s graph)
graph.edge record a hyperedge of it, entity its id: kind, members {role, node} in order, via, outside
graph.retired record the node or edge the entity names leaves the graph; a ratified graph proposal’s receipt may carry one too (a re-ingest’s retirement), with the same effect
graph.requested record hale dna ingest asks the running organization to open the Review it filed (request_id, review_id, by), relayed as GraphReviewRequested (dna.graph.requested)
graph.proposed / graph.refused record the organization opened that Review (routed, its signers told), or could not; graph.refused <row id> is also memory’s refusal of a graph row it would not project, in the record or in a ratified proposal (kind, why, row, by: memory)
graph.ingested record hale dna ingest read the repository at a commit (entity the commit; added, changed, retired, the review_id of the one proposal it filed, "" when nothing differed)
hold.requested record someone asks the organization to propose a holder for a position — a person or an organization — or a member for an organization (hale dna fill, or Commands::fill, whose row carries the command’s fields after the CLI’s)
batch.proposed record a batch of changes hale dna apply (or vacate, or pack apply) proposed as one family Review in the group changes (digest, review_id, by, steps, pack for a pack: <name>@<version> sha256:<digest> <source>, and planned_on for one planned on pending proposals, <review id> <digest> a line; through Commands::apply_batch, the command’s fields after those); its graph.requested, under the same id, asks the organization to open the Review
position.requested record someone asks the organization to open a position at run time, with its mandate (hale dna position open: position, text, mandate, under, by; Commands::open_position adds the command’s fields after them), relayed as PositionOpenRequested (dna.position.requested)
position.proposed / position.refused record the organization proposed the position’s node and its mandate as one family Review, group positions (digest, review_id), or why not (a name the graph already states, no mandate, an under that names no node the graph states: the CLI resolves a bare --under <name> to the seed, work part, position or process the graph states by that name)
hold.proposed / hold.refused record the organization proposed it to the Board (digest, review_id), or why not; hold.refused <hold id> is also memory’s refusal of a hold it would not project (why, row, by: memory)
responsibility.proposed record a one-line responsibility inferred for a part, not yet ratified
law.deferred record a clause init could not certify
intent.requested ledger an ask from a clone with no organization running
intent.offered / intent.refused ledger the outcome an ask was admitted for, or the refusal
intent.unrecovered ledger an intent offered before a restart that no admission names; never re-offered
review.verdict record a verdict appended in the reviewer’s name, from a clone or a forge
task.born ledger the work an intent or a settled review made
task.handed ledger handed to a person: assignee, obligation, acceptance
case.admitted ledger a human Work’s case, its own handed Task (card 16): parent Task, Work and attempt, objective, assignee, obligation, acceptance, required evidence, its origin (owner), disposition (to) and performer (version 2, card 17) — before its task.born / task.handed, which say what it recorded; its completion (task.done, decision.reported) is the attempt’s outcome (card 17)
task.reassigned ledger the assignment moved to someone else
task.<state> ledger a case’s states, to done or failed; an admitted execution’s root is settled by workflow.settled, its workflow’s row (card 18)
mutation.requested record which exact Work and attempt asked for the Mutation, with the request as asked; written before mutation.proposed (card 14); for a leg’s candidate also the leg (leg), its commit (leg_commit) and the receipt that is its patch (patch, GH #1156)
mutation.proposed record a change proposed for a Task: class, objective, target, base
mutation.worktree record the sandbox opened for it, and removed
mutation.located record the files found, and the grant they were found under
mutation.candidate record the commit the editor produced
mutation.<disposition> record what the autonomy boundary decided: review, stage, escalate, release, deny
mutation.topology record the diff names a plan or the manifest: re-classed for the Board
mutation.applied record the candidate applied to the genome
mutation.apply_retried record the apply ran again on a settled review
mutation.retained record kept after its observation window
mutation.rolled_back / mutation.rejected / mutation.revise record the genome back at the base, refused after review, or sent back for another pass
mutation.refused / mutation.failed record not applied (the candidate moved, the gate said no), or the change did not survive its own verification
effect.requested / effect.result ledger the exclusive claim on an effect key, and its outcome
effect.relayed ledger an attempt a leg relay answered pending: a leg’s to claim at the head, once per attempt
evidence.<step> record a verification step’s output, kept by the digest the row names
evidence.magnitude record the measured magnitude of the change
review.requested record the Review: question, authority, candidate, disposition, evidence, diffs
review.routed record what a routed Review requires: definition, signers (<position>=<holder>,…), gates, fallback; written when it opens and again when its holders change (the latest is the requirement)
review.signed record an approval a routed Review admitted that settled nothing: by, awaiting
review.settled / review.refused record the verdict that decided it, or why one was not admitted
review.reasoned record the deciding verdict’s comment — a person’s note or the Leader’s reasoning — right after review.settled (hale dna review <id> renders it as why:)
candidate.dropped record a kept candidate is no longer kept, here and at every clone’s sync
knowledge.proposed / .ratified / .declined / .refused record a practice through its review
knowledge.retired record a practice superseded by a later version
library.family.refused record a library family approved while a member was refused (its predecessor was replaced); names the refused members
knowledge.consulted ledger what a piece of work looked up today
grant.contracted record authority narrowed, and what it leaves
grant.revoked record the parent took the authority back
grant.refused record a grant born wider than its ceiling: authority
grant.reservation_refused ledger a spend the window would not admit, naming the field: money
grant.reserved / grant.released ledger a spend admitted against the window, and the reservation settled at what was spent
grant.fenced ledger an admission refused because the grant’s epoch moved since
budget.exhausted ledger the window’s model allowance is spent
model.called ledger a model call and its evidence
model.rule record the model mapping’s rule for a selector (default, position:<name>, contract:<Name>, task:<id>): the size it needs and the sizes an override may move it to (size, permit, by); the latest row per selector is the rule (dna/core/model_map.hl)
model.category record which model fills a category <mode>.<size> today, whether it returns tool calls and its price (model, tool_calls, input_micros_per_1k, output_micros_per_1k, by); the latest row per category stands
optimize.refused ledger the organization’s pass over itself did not run
node.started / node.build_failed record a node runs a genome, by its sha; a genome did not check or build, and the node stayed on the last that did (sha, why)
violation.recorded record a closure the organism absorbed (GH #989), recorded by the part that owns it, which goes on: {kind, subject, owner, detail}. adapter_undeliverable: a node’s connection to the nerves collapsed, a publish the stream did not acknowledge in its window (subject nerves, owner the node’s holder); the node stops and exits 75 for its unit to start it again. lease_unsettled: a leg’s lease expired with no outcome (subject the attempt, owner spine), once per lease, before the attempt is asked again. pulse_stopped: the heart’s pull on its durable refused while the connection held (subject heart, owner the spine’s holder), once per outage; the durable is made again. The board shows the latest five
observation.requested / observation.refused record the host’s observation report as a row (mutation_id, outcome, model_hash, detail), relayed until expression.observed answers it; refused, unrelayed, when the row is not verified under signed trust (GH #986)
claim.taken / claim.released ledger a node took a claim by id before acting — plan/<intent>, plan/case:<case> — and gave it back: holder, and for a take its token and until
attempt.claimed ledger a leg’s claim on an admitted, outstanding attempt, taken at the head (GH #946): the lease (holder, token, until), the principal that took it (principal_mode, principal_name: whose outcome the lease admits), the attempt’s task, work and performer kind, and the command that took it
attempt.outcome_requested ledger the outcome a leg handed back under its lease: the disposition, the result (result, result_ref), the receipts it filed (evidence_ref), the calls it made (calls), the hat it wore (hat_digest, hat_head, hat_watermark, prompt_digest, renderer) and the command (request); relayed until attempt.outcome or attempt.outcome_refused answers it
attempt.outcome_refused ledger why the owner would not settle a leg’s outcome (why, holder, token, request)
attempt.allowance_requested ledger a leg’s ask for its attempt’s spend, under its lease (holder, token, request, at, and the command); relayed until attempt.allowance_granted or attempt.allowance_refused answers it (GH #1131)
attempt.allowance_granted ledger the one gate admitted a leg’s spend: what the attempt may cost (allowance_micros, -1 when nothing bounds it), the ceiling, the window’s spend, the lease and the request
attempt.allowance_refused ledger the one gate admitted no spend (why): on a leg’s attempt (naming the request), the editor’s attempt, or review:<id> for the leader’s Review
attempt.released ledger a leg gave its lease back without an outcome (holder, token, why, and the command); the attempt is another leg’s to claim (GH #946)
friction.filed record what got in a position’s way (position, attempt_id, text, and the command), filed on the attempt it was met on, else on the position; nobody admits it, it is a fact (GH #946)
org.reviewed record that pass’s own answer
person.retired record someone left, and who took their work
body.claimed / body.released ledger who is running this record, by the lease’s token
body.provisioned record a machine made able to run it
body.credential_missing / body.credential_present ledger whether the model’s key is in the vault where the body runs
secret.rotated record a credential set or rotated — the name and the place, never the value
schedule.requested / schedule.answered ledger a schedule asked of the organization (hale dna schedule declare), relayed by the host; its answer under the request’s id: declared, why
schedule.declared / schedule.refused ledger a schedule a ratified practice or a request declares (every_ms, cron, definition, args, convener, from); or one that would not be declared (why, and hole for a convener nobody holds), or an occurrence whose admission was refused (occurrence, why)
schedule.skipped / schedule.missed ledger an occurrence not admitted while the last execution is open (occurrence, task); occurrences whose period passed with no tick (first, last, count, why)
schedule.paused / schedule.resumed ledger paused and resumed by hand, in your name
receipt.classified / receipt.withheld ledger a protected body memory keeps sealed, or one withheld because no memory could keep it
receipt.disclosed ledger a reader authorized, for a purpose
receipt.read / receipt.read_refused ledger a read in the reader’s name, or its refusal
receipt.held / receipt.hold_released ledger a hold that refuses redaction, and its release
receipt.redacted ledger the body removed, the digest kept
concern.requested / concern.raised ledger a concern from a part about the part above it; the request carries its own request id; the answer is one object (what, severity, occurrence, request, and since GH #995 the parent it is routed to and the concern-escalate execution that raised it, task), so a concern’s words are never read as metadata, and one request is one concern, however often it is delivered
concern.refused ledger one the organization would not admit
practice.read record what the hat read once a proposal was ratified, at which head (digest, target, ratified_at, hat, head, included, task): practice-ratify’s hat step (GH #995)
concern.proposed record three raises became a proposal — at concern-escalate’s raise step once the source crossed the threshold, including the execution a birth admits for a source over it, unproposed
pressure.requested / pressure.raised ledger a signal from a source; the request carries its own request id and a node relays it until answered (GH #986); the answer is one object (what, count, request) and is written once per request — a row from before is <what> x<n>
pressure.remeasured ledger the declared fitness signals, measured again after the change
appendage.proposed record an organ the organization proposes for itself
expression.restart_requested record the organization asks for a change to be expressed
expression.restarted / expression.deployed record the shape and build the new expression reports, or what a deployment gateway expressed and judged
expression.observed / expression.crashed record the observation window’s outcome; crashed names the instance and node on a fleet
fleet.deploy record a genome revision expressed through the fleet’s nodes
instance.up / instance.exited ledger a node’s report on one instance of the plan
github.pr / github.commented record the pull request a Review opened, and the settlement commented back
gate.observed record a gate’s check run at the candidate as the forge reported it: gate, sha, conclusion, url, forge, run; written only by the host’s forge sync, once per run (GH #1161)
ledger.adopting / ledger.adopted / ledger.abandoned record the move of the day’s work into the ledger, its checkpoint, and its undoing

Their bodies are documented in the guide’s reference chapter; the set grows by ordinary change, and a reader that meets an unknown kind must keep walking.

A recursive workflow can be written as data (dna/core/workflow_definition.hl; the execution contract it serves is dna/WORKFLOW-CONTRACT.md). A WorkflowCatalog holds definitions: define(id, revision, stores, title) declares a workflow whose ordered steps each write the one store its word names — stores holds one word per step, record, forge, genome, heart, graph, nerves, memory, vault or host, so the step count is the word count (GH #995). A step is where a fact is written: reading, or waiting on another writer, is part of the step whose fact it serves, never a step of its own; leaf(workflow, revision, step, key, work, attempts) adds a required member carrying the content of one Work request and its retry allowance; child(workflow, revision, step, key, child, child_revision) adds a required member that invokes another definition. Each returns "" or why it was refused (a revision defined twice, a member for an undefined workflow). Definitions refer to each other by id and revision, in two flat collections.

Before anything is built, expand checks every definition reachable from the root once, counting each one’s Works and nesting height from its children’s and saturating at the limits: a catalog whose expansion would be enormous is refused from those counts, in one pass over the definitions, without materializing a node of it. The check also holds for the root, which is depth 1, and for the limits themselves (a limit below 1 admits nothing and is refused).

expand(root_task, id, revision, limits) binds one execution: the whole finite tree as BoundNodes (tasks, steps, works) with their ids — a child Task is <parent>.s<i>.<key>, a Workflow <task>/wf<revision>, a Step <workflow>/s<i>, a Work <step>/<key> — each Work carrying its authored request content and allowance (bound_request makes its WorkRequest), and each Task its definition, parent, spawning step, depth and definition path. It refuses the whole expansion, binding nothing and naming what broke, when a definition is missing (by id and revision), a workflow has no steps, a step has no members, a member names a step outside its workflow’s steps, a step declares no store (-), two (record+forge: declares two stores (record, forge)) or a word that is no store, a step writes or reads a part that is not built yet (named with its issue: the heart, GH #987; the vault, GH #989; a host the organization provisions, which only hale dna body provision reaches today), a member key is not [a-z0-9-]+ or repeats in its step, a member is neither a leaf nor a child, a workflow invokes itself directly or through its children, or an AdmissionLimits value is exceeded: max_depth (8), max_steps (32), max_members (32), max_attempts (8, the largest allowance a leaf may bind) and max_works (256), the defaults in parentheses, supplied by the caller and returned with the Expansion. Two invocations of one definition are two executions with their own ids; a later revision changes nothing an earlier revision expands to. A definition id is [a-z0-9][a-z0-9-]*, checked when it is defined and when it is read, so an id can never carry the delimiters the definition path is written with. encode() writes every definition and member as one JSON document (format: dna.workflow-definitions/3: each workflow’s steps, its stores, and how it occurs on a schedule — concurrency and takes) and decode(text) reads one back, refusing a document that does not close, another format, an id outside the grammar, an already defined revision, a document that defines one revision twice, a workflow whose stores do not name one per step, or a concurrency that is not skip or overlap (an absent one is skip), and adding nothing then. latest(id) is the newest revision the catalog holds: what a new Task binds.

How a definition occurs when a schedule points at it (GH #1143, Schedules) is the definition’s: occurs(id, revision, concurrency, takes) sets WorkflowDef.concurrency — skip (the default: an occurrence while an execution of it is open is skipped) or overlap (admitted beside it) — and takes, the input names, space-separated, that a schedule’s args must carry; it refuses another concurrency, or a definition the catalog does not hold. The baseline’s ask-edit@1 and ask-person@1 take objective. hale dna definitions prints occurs: <concurrency> while open[, takes <inputs>] under a definition only when it says more than the default.

encode_bound() writes one bound execution as a recipe (format: dna.workflow-recipe/1; an admission nests it as an object of its own, never escaped into a string) carrying the node count it was written with, and decode_bound(text) reads one back. It refuses another format, a missing node array, a count that disagrees with what the document carries, a node without an identity or of no known kind, a node whose number was written as text or whose text was written as a number, and a document that does not close: every object, array and string closed by its own delimiter, with nothing after the value. That is a closure scan rather than a JSON grammar validator — it does not check escape sequences or number syntax — which is what reading back one encoder’s own output needs; a document from outside the system wants a real parser in front of it. A whole final node also ends in }, so the last character says nothing about the document. Every refusal binds nothing: half a recipe is not a smaller execution. Nothing yet executes a bound expansion.

DNA ships a catalog of definitions, each a chain in which every step writes one store (dna::baseline_definitions(), in dna/core/workflow_definition.hl). A project’s dna/org/workflows.hl is generated — written by hale dna new and rewritten to the current shape by every hale dna upgrade, whatever an earlier toolchain wrote — and returns it plus the project’s own (fn workflows() -> dna::WorkflowCatalog), which live in dna/org/own_workflows.hl (fn own_workflows(catalog): project-owned, written only where there is none, never rewritten; what it answers when a definition is refused is kept on the catalog, refused, which hale dna definitions leads with and fails on, never a smaller catalog; an upgrade that rewrites a workflows.hl holding definitions of its own names each line it dropped, for the project to move into own_workflows.hl), and the generated organization admits from it (catalog: workflows() on its dna::Dna; a body given no catalog takes the baseline at birth). The baseline is the vendored toolchain’s, so an upgrade supersedes it by revision: a Task born under a revision finishes under it — its recipe was bound at admission — and a new Task binds the newest (latest).

id steps (the store each writes)
ask-triage offered · classify · plan (record each)
change-deliver candidate (record) · review (forge) · verdict (record) · apply (genome) · deploy (heart) · settle (record)
practice-ratify ratify · hat (record each)
deploy-observe deploy (heart) · settle (record)
concern-escalate raise (record)
optimize-walk walk (record)
secret-rotate rotate (vault) · recorded (record)
body-provision provision · start (host) · lease (memory) · observed (record)
ask-edit, ask-person, ask-judge, ask-position the ask’s one leaf: an edit prepared for review, a person’s job, a judgment handed back as an Assessment, or a change made by the position it is routed to (record)

Every step writes a fact of its own. What #995 listed as steps that write none is folded into the step whose fact it serves: practice-ratify’s verdict is the Review’s row, so ratify waits for it and writes the ratification or the decline; memory’s projection writes the idea and its binding, so hat waits for it and records the row it was fenced to; the request and the Review are written by whoever proposes (a person’s request, a concern, the record’s birth); a concern’s routing is a field of its concern.raised row and its proposal is written in the same step at the threshold; the optimize pass’s walk reads what its org.reviewed row records; deploy-observe’s pulse is what settle waits for, and its rollback is the failure edge, not a step: the happy path writes nothing to the genome.

A step the organization performs itself is one leaf that requires: "organism". The engine relays its attempt to the assembly serving the scope (OrganismRelay, kind organism: StepRequested, answered pending), which performs the step and reports the attempt’s outcome (Dna.on_step_requested). A step ensures its rows — performed again after a redelivery it writes none twice — and one that waits (a verdict, the leader’s word, memory’s projection) is kept and performed again when what it waits on may have arrived: a verdict, the leader’s answer, the tick. The organization performs the steps this toolchain knows, by definition and key: practice-ratify/ratify, practice-ratify/hat, concern-escalate/raise, optimize-walk/walk. A definition with an organism step it does not know — a path not moved onto the catalog yet (ask-triage, whose ask is still admitted as ask-edit or ask-person after the leader plans it), or a revision from a newer toolchain — is refused at admission naming the step, never failed when it runs, as a definition on a part not built is refused naming the part; both are workflow.refused rows and nothing else. The organization performs a step only of a definition admitted directly, so one that invokes a definition with an organism step anywhere below it is refused at admission too. A ratification the catalog refuses is answered once per incarnation — one workflow.refused row — and asked again after a restart, so a catalog fixed in between ratifies it, and nothing retries at every reconciliation. hale dna definitions lists what an admission would refuse each definition for.

  • practice-ratify carries every knowledge proposal the Board decided: when a verdict settles a proposal’s Review, the organization admits one execution for it (ratify:<digest>), whoever proposed it — a person (hale dna practice propose, proposed by the organization as before), a concern, the record’s birth (the purpose, the seeded practices, a repository’s holes and practices), a holder asked for. ratify writes knowledge.ratified (and knowledge.retired for what it supersedes, or knowledge.refused) under an approval, and knowledge.declined otherwise, which fails the chain; it is done only when the whole consequence landed, so a retirement the record refused is written again. hat fails the chain, saying so, when no memory is named (nothing would come to wait for); otherwise it waits for memory’s projection to hold the idea ratified, then reads the target’s package at memory’s head and writes practice.read <digest> (target, ratified_at, hat, head, included, task): what the hat read, at which head. No verdict path writes a ratification of its own — a Review’s verdict or a decision by command (review.command_decided) alike; a settled verdict whose ratification never ran is admitted the same way at the next reconciliation.
  • concern-escalate is a concern: hale dna concerns, and the organization’s own, are admitted as concern:<source>:<request> (a redelivery of the request is the same execution; the same request with other words is concern.refused, as before). Its one step, raise, writes concern.raised <source> (what, severity, occurrence, request, the parent it is routed to, task) and, once the source crossed the threshold, proposes it as knowledge by the rule that always did. A source over the threshold with no proposal and no execution running for it is escalated once at birth by an execution that raises nothing more (its raises are the record’s).
  • optimize-walk is the optimize pass, and occurs on the schedule operating/optimize-cadence declares (The optimize pass): walk asks the budget first — an exhausted one is optimize.refused and fails the step, and the pass waits for the next occurrence — then reads the machinery’s signals, asks the leader once per incarnation and is done at its org.reviewed row, which names the execution (task).

The record is the local-mode floor; the ledger is the runtime. Every step is a few rows of the day’s work (step.*, attempt.*, work.settled, effect.*). Until hale dna ledger adopt, each is a commit on refs/dna/journal, so an execution costs tens of commits: a Board deciding a repository’s 35 seeded proposals waits some 15 to 18 seconds for their ratifications on a git-backed record (measured), where memory takes the same rows in about a second. A git-backed record is what one person working alone starts on; an organization runs with its ledger adopted. hale dna review <group> approve|reject writes every verdict of the group first and then waits for the answers once, bounded by the group’s size. hale dna review all approve|reject decides every group with a pending Review in the order a new organization is ratified (purpose, design, operating, using, library, holes, practices, positions, changes, holds, ingest), each group waited for before the next, since a hold waits on the position it sits on. hale dna init --with <pack> [--set <param>=<value>]… [--as <who>] runs init, then hale dna pack apply --on-holes on the toolchain’s nursery: the pack proposed as one batch beside init’s own proposals and planned on the holes init just proposed, so it may fill them (the toolchain’s founder pack is fill * <founder>, every seat left vacant), and one review all approve ratifies the starting organization, its seats included: the holes in holes, then the pack in changes. Planned on the record alone, a fill of a hole is refused (the position is a proposal until the Board ratifies it), which is why onboarding took a second batch and a second verdict. The holes stay one Review each, and the pack one family: init’s proposals are not folded into the pack, since a hole’s graph rows (its unfold from the purpose or the part, the reviews edges, its equipment) are not steps a batch writes, and plain init stays as it is. What the batch stands on is named in it instead (planned_on, under the batch of changes below), and its ratification waits for that ground.

The declared purpose is a proposal like any other. hale dna init proposes it in the record’s one seed call (host verb graph-ingest, a repository’s or an application’s: kind purpose, author org, for org, provenance: declared) with the Board’s Review under the group purpose, which hale dna review lists first and hale dna review purpose approve decides; the generated organization holds no Review of its own for it.

The graph read again (hale dna ingest [project] [--at <rev>]). init reads the repository once; a later commit is read again with the same ingest, the commit (HEAD by default) checked out beside the record under .hale/dna/ingest/ with the repository’s own directory name and removed after. The ingest also states a tool node per operation of every surface the repository serves (tool:<Surface>::<op>: its contract, the roles it requires, and its effect class, uncertain while a description says nothing of an operation’s effects). What differs from the graph the record states (its rows and its ratified graph proposals, latest body per id), over the kinds an ingest owns (purpose, axiom, process, seed, contract, noun, deployment, gate, document, witness, tool; meets, names, refers, constrains, runs, gates, witnesses, and an unfold between two of them), is filed as ONE graph proposal in the group ingest: added and changed nodes and edges with their bodies, then what is gone as graph.retired rows, edges before nodes. A graph.ingested row names the commit either way; the same difference at the same commit is proposed once, and nothing differing proposes nothing. The host files the proposal and its Review, and a graph.requested row, relayed like any request, has the running organization route and open that Review (graph.proposed), so the Board’s verdict settles it there; an organization started later finds it in the record. The proposal’s author is hale dna ingest, and a verdict the record holds before the organization opens the Review is weighed when it opens, in record order: the first decisive verdict decides, as it would have live. An attached application’s own re-ingest is not built yet.

hale dna definitions [project] [--json] lists the catalog dna/org/workflows.hl returns, built beside a one-line main as the model catalog’s probe is: every definition with its revision and title, each step with its key and store, what a newer revision supersedes, and what a definition is refused at admission for; --json is the catalog’s own document. The head serves the same catalog to the face’s definitions views (ops::ProjectWorkflowCatalog: what hale dna definitions --json reads, re-read when either file or the vendored catalog changes; the baseline alone for a project with no workflows.hl; unavailable, with the reason, when it does not build).

What a workflow execution leaves behind is written as a fact of the day’s work (dna/core/workflow_events.hl). Seven kinds carry an execution: workflow.admitted names the Task, the definition and revision, the inputs and their receipt, the bound recipe, the limits that were applied and what the expansion came to; step.registered names a step’s whole required set before any of it is dispatched; attempt.admitted carries one attempt’s id and the WorkRequest it was admitted with; attempt.outcome its disposition, result and evidence; and work.settled, step.completed / step.failed and workflow.settled settle each level to the one above it, a child workflow naming the parent Task and spawning step it answers. An admission also names the engine (wf1), and every fact a version. A decoder refuses a version it does not know, another engine’s admission or refusal, a fact that does not name the entity it is about, a fact that says nothing where a number belongs (an absent revision is not revision 0), and a fact whose own identities contradict each other: an attempt id that is not its Work and number, an attempt carrying a request for another Work or another attempt, a Work settling under a Step it is not part of or on another Work’s attempt, a step failing on a member it never had, a child naming its parent but not the step that spawned it, an admission deeper or wider than the limits it says were applied. A fact is read whole or not at all. The identities an attempt admission carries are written once and derived from the fact, so its id, its number and its embedded request cannot disagree.

step.activated and workflow.refused have codecs of their own, like the rest; a routing row is not a durable shape. A fact a transition committed carries the scope, key and proposal id it was committed under, appended with it, so the committed proposal can be reconstructed from the journal after a restart and compared against a later reuse of that id.

These kinds are the ledger’s under split routing and the record’s on routing 0, like every other operational kind.

An attempt’s id is its Work and its number (<work>/a<n>, numbered from 0 as §4 has it: the first attempt is a0 and a retry is a1), so a retry is a new id and a re-sent admission is the same one. A transition’s proposal id (§9 of dna/WORKFLOW-CONTRACT.md) is what the transition does, not a counter or a clock: the same id carrying the same scope, key, kind, entity and body is that transition proposed again, which the committer answers from its journal; the same id carrying anything else is a conflict, and transition_conflict names which part differs.

The state an execution is in is read from those facts and nothing else (dna/core/workflow_projection.hl). replay(projection, kind, entity, body) applies one row and answers one of three things: the fact was applied, it is a replay of what the projection already holds, or it is refused. Applying a fact asks nothing of the world: the entry point carries @no_syscall, which the compiler checks through everything it reaches, and the projection holds no journal, gateway or model to call. The contract guards syscall-class effects; having nothing to call is what rules out the rest.

The admitted recipe is the authority. An admission carries the whole bound tree under its Task, and every later fact is checked against it: a Step is one the recipe binds for that Task at that index; a registration names exactly the members the recipe bound under that Step, each by key, kind and entity, none missing and none invented; a Work’s retry allowance is the one bound for it; a child is admitted only as the Task its parent bound under that key, from a step that is registered and active, and answers only as that Task. A child’s admission carries the subtree its parent bound under it, exactly: the same nodes, each the same in every field, none added, none left out, none changed. It activates a binding; it never redefines the execution its parent admitted. An attempt asks for what the Work is: its request’s content — objective, context, bindings, output contract, data class, requires, target, cost ceiling — is the bound Work’s, on the first attempt and on every retry; who performs it is the application’s choice. A fact the recipe does not know is refused, however well-formed.

Every transition has its basis. A Work settles done only on a current attempt that recorded done; it settles failed only on a failed attempt with no allowance left, or once its Step has failed or its Task is cancelled, under which a member records its outcome and stops (the drain policy); it settles cancelled only under a cancelled Task (the fence). A retry is the next attempt, admitted only after the last one recorded a failure and only within the allowance. A Step activates only after the step before it completed, completes only when every member it registered has settled done, and fails only on a member that failed. A Task settles done only when every Step the recipe binds for it completed, and failed only after a Step failed and every responsibility it admitted has settled: the root settles last. Cancellation stops further admission in the cancelled Task and in everything under it — no attempt, no step, no child is admitted below a cancelled ancestor — while what was already admitted still records its outcome and settles under the fence. The walk to the root goes as deep as the caller’s max_depth admitted — nothing here has a ceiling of its own — and an ancestry that does not resolve refuses admission rather than permitting it. A failed ancestor is not a cancelled one: under the drain policy a member keeps its responsibility until its own outcome.

A fact is one row. The row’s entity is the fact’s own identity, and a step.completed row carries a completion. A fact whose identity is already recorded is a replay when it is the same bytes and a conflict otherwise, checked before the state, so a row from history is recognized as such after the state has moved past it. The join is by identity, never by counting: two answers from one member are one member. A Work’s result is the output of the attempt it settled on, readable only once it has settled; what an outstanding or superseded attempt said is never the Work’s result.

An application admits a defined workflow through the assembly (Dna.admit_workflow(WorkflowAsk)): which definition and revision, with what inputs, under an identity of the caller’s. The durable fact precedes everything. The id is minted, the definition expanded under the assembly’s admission_limits (its catalog holds what the application defined), and the admission — engine, bound recipe, limits, counts, the ask’s identity — appended with exact compare-and-append before anything could run; only then the legacy task.born summary, so the tooling of the day lists the Task. A refusal, by the catalog or by the record, is a workflow.refused row under the id it minted and nothing else: no summary, no child, no work request. Dna.run_workflow(WorkflowAsk) is the admission followed by the execution’s start (card 18, below).

The ask’s id is the admission’s identity. Asked again — relayed after a lost answer, retried — it is one execution, found in the record by that id. One decision, one revision: the record is read once, at a revision the body captures; whether the ask already landed and whether a candidate id is already an execution’s or a refusal’s are decided against that reading; and the append is exact at that same revision. An exact append protects the revision it was given, not a decision taken at an older one, so when the record has moved — stale, or the store’s unique claim on the id — the whole decision is taken again from a fresh reading. A taken id is skipped before any append. Two bodies contending for one Task id cannot both take it, and one ask admitted by another body between this body’s reading and its append is found on the next reading, not admitted twice.

Only the owner of the position admits (GH #664): the same rule ask applies, applied before an id is minted, so a body over a shared record that names no owner admits no workflow either — it answers the asker and writes nothing, the ask being the owner’s to admit. A refusal, whether the catalog’s or the store’s, is written as the versioned workflow.refused and the write is checked: when the record will not take even that, the answer says the refusal went unrecorded rather than presenting it as recorded. A refusal bound to a Task id carries the admission’s own guarantee — it is appended exactly at the revision that found the id free, and when the record has moved it is read again: the ask may have landed from another body, in which case that execution is the answer, and the id may be another execution’s now, in which case the refusal is not a Task’s at all. A refusal that reaches no Task — the id taken meanwhile, every id it would mint already taken, the record moved under it too often — is a fact of its own kind, workflow.ask_refused, under the ask’s own identity and a decision ordinal (<ask>#<n>), which can share no identity with any Task under any owner’s name; the projection holds it beside the executions, never as one. The same ask refused again for the same reason is the same decision, answered without another row; refused for another reason, it is the next ordinal. That decision, too, is one reading of the record: the decisions already taken on the ask are counted at a captured revision, the ordinal chosen against that count, and the append is exact at that revision, so another body deciding on the same ask meanwhile moves the record and the decision is taken again — its decision for the same reason becomes this body’s answer, and for another reason takes the ordinal ahead of this body’s. Nothing is appended as a changed body under a recorded identity, and a refusal never attaches to an execution another body admitted. A restart counts admitted and refused ids among those minted, so an id is never minted twice, and an admitted Task is never resumed as legacy edit work, with or without its summary row: the admission is the one positive discriminator, and its recovery belongs to the engine’s own cards.

The durable Work owner admits an attempt (attempt.admitted) and then asks for it to run through AttemptExecutor (dna/core/ workflow_execution.hl), which runs one admitted attempt once. Nothing runs before the admission is in the record as admitted: the attempt’s identity, its performer kind and every field of its request are checked against the recorded admission. An attempt whose outcome is recorded is not run again; the recorded outcome is the answer, claim or no claim. The decision to run is one reading of the record: refreshed, read at a captured revision — the admission, whether the attempt is settled, whether its execution is claimed — and the claim appended exactly at that revision, with the effect claim of the existing effect records, keyed by the attempt. When the record has moved the decision is taken again: an outcome recorded meanwhile is the answer and the performer is not called; a claim taken meanwhile is attached to; only a claim that lands is followed by a performer call. A request that arrives while the attempt is running attaches to its pending outcome instead of starting a second performer, and a claim the record refuses runs nothing. The performer the admission names runs once. Every reply, the one that comes back from the call and the one that comes later, is judged first for whose it is: it must be about this attempt (a reply that echoes an attempt_id must echo this one) and from that performer (the identity the kind selects), and a reply that is neither is not this attempt’s pending answer either — it is refused before anything is made of it, and the attempt stays outstanding under its claim. A reply that is this attempt’s and not terminal leaves it outstanding and records nothing; a terminal one must say something the outcome contract carries — done, failed, declined or timeout — or it is refused without a row and without closing the claim. The later reply is settled through the same path, under the same rules. An accepted outcome is persisted as attempt.outcome with an exact append before anything is answered, and a reply for an attempt whose outcome is already recorded changes nothing; an outcome the record will not take is reported unrecorded, never as done, and nothing advances on it.

The executor retries nothing (one retry owner: the Work’s lifecycle) and advances nothing (the Work settles on the outcome in a later card). It makes no claim that external effects happen once: a performer that acted and died before its outcome was saved is a later card’s. The WorkSystem retries nothing of its own (card 18): it routes each admitted attempt to the performer of the kind the admission names.

The spine’s whole API to a leg is three things (GH #946): the hat as a read, dna.attempt.claim, and dna.attempt.outcome. A leg holds nothing between tasks and has no database role; the owner still admits and settles. Once the organism has adopted the ledger the attempt kinds live there: the head reads them and writes a leg’s rows there under its role (HALE_DNA_MEMORY_DSN_HEAD; memory’s insert function is the gate, keyed by the request), and refuses the commands and the hat only when no memory is named to it.

  • The hat (GET …/dna/context?id=<work>; dna/operations/context.hl) is one Work’s context as structure, never a prompt: the position’s identity — the graph’s position:<name> id, from the performer kind the attempt admitted last names, or the kind the request selects before one is, or, for a Work whose requirement names position:<name> (ask-position), that position — and its charter (the record’s graph.node text for that id, "" until the graph names it); read under OIDC, it is the lease holder’s — the person the record maps the bearer to, whose attempt.claimed row is the Work’s current, unexpired, unreleased lease; any other caller is refused 403 lease_required; the practices ratified for the Work’s targets (targets: its path, its codebase’s languages, system:dna for an organization change, the position — The hat’s target set), from memory under the reader’s role, resolved to text with their ids; the knowledge bindings; the output contract and the hands it gives (dna/core/contracts.hl): the tools a performer may use, each {name, description, input_schema, effect, role, family} from the toolchain’s built-ins (dna/core/tools.hl), the tool grant (their names) and the validators among them, which the harness runs before handing the outcome back (a Patch gives read edit check test fmt patch, validated by check test fmt; an Assessment the read-only record_status record_history org_chart; a contract not yet described, none); the model the mapping resolves for the Work (dna/core/model_map.hl: the mode its hands make (think with no hands, tools with hands and no workspace hand, do with a workspace hand: an Assessment is a tools Work; a tools or do category is filled only by a model that returns tool calls), the size of its most specific model.rule, a task’s override only inside the permitted sizes, the model the model.category row names, the rule that decided it, or why no model fills the category); the data class; the history of the execution the Work belongs to, as facts (the head replays only that execution’s facts, its root task’s id and every id under it, child tasks included, and refuses context_read_limit past 512 facts, 256 bound nodes, 512 KiB of bodies or a 64 KiB body; the record’s length is no bound); the record head and memory’s projection watermark it was rendered at (-1 without memory, and practices_status says so); and the digest, sha256 over the canonical body with the digest itself left out. Rendered twice at one head it is one digest; a row that moves the head moves it. Two attempts of one task may wear different hats, correctly: what is forbidden is unrecorded variation, so the hat reads no clock, no environment value and no random id, and replay renders from the recorded hat, never the live graph. Rendering is a leg’s, and the renderer’s version is evidence of its own. The practices are structure — {id, name, text, kind, author} from one snapshot of memory, the watermark read in the same transaction as the ranked bound — never rendered lines split again; and every hat is composed by one function (hat_compose), whether built by the owner for an edit it asks (the attempt carries the hat’s digest as its context_digest; the practices reach the editor as its brief, the ask stays the ask) or by a head for a leg, is kept in memory by its digest (hats, insert if absent).
  • The tool loop (dna/core/legs/tool_loop.hl). A model performer whose hat carries hands and whose model.mode is tools or do offers the hands to the model as tools (ModelRequest.tools, the chat API’s function shape, with the turns since the prompt in ModelRequest.messages) and runs every tool_calls entry the reply asks for (ModelResult.tool_calls, each {id, name, arguments}) on the leg’s side, sending each result back as a tool message, until a reply carries no tool call. A do Work works in a scratch clone of the leg’s repository at HEAD; its answer stands once the hat’s validators pass on every touched directory holding .hl source (a changed file’s own directory: a nested seed is its own), a refusal being one more turn; a Patch is then one commit on the base, handed back as result_ref: commit:<sha> with the commit’s patch as the first receipt. A hand outside the hat’s tool_grant, or a path outside the worktree or into its .git (a .. or .git component in any case or spelling, a control character, or a symbolic link anywhere on the way), is refused as the call’s answer; a changed file git can only name quoted is refused by validate rather than passed unchecked. record_history reads the execution its id belongs to (/dna/workflows?id=<task>). A RecordedModel keys a loop’s request on its tools and turns too and keeps the tool calls it returned, so a replay follows the same turns. Each model call is a calls entry (its response digest covers its tool calls) and each tool call a row of one receipt, {"tool_calls": [{turn, name, arguments_digest, ok, output_digest, output_bytes, error}]}. The loop is bounded by max_turns (16) and the attempt’s allowance.
  • A person’s session runs the same hands one call at a time: hale dna work hand --attempt <a> --name <hand> --args <json> (and hale mcp’s hale_dna_hand) reads the attempt’s hat from the head and runs the named hand through the same bench, a file hand in a clone of the repository kept per attempt under .hale/dna/work/ with its base recorded beside it; submit --from-worktree runs the hat’s validators over the touched directories, refuses on a failure, and otherwise submits one commit on that base as result_ref: commit:<sha> with its patch as the first receipt, then removes the kept clone.
  • The claim is taken at the head. The filter names the performer kind and identity, the capabilities the leg has, the data classes it may see, the owners it works for and a TTL. The identity is a position (position:<name>, #<n> for one worker of several) the record knows, and one the principal’s person holds (GH #1162): by the record’s live holds edges — a graph.edge row, or one a ratified graph proposal carries (hale dna fill) — the edge, its position and its holder not retired, memory not having refused it (hold.refused), the person not retired; or, under a record that declares dna.trust = local, any position. The socket’s roles read the same edges (HoldsReader): a person holds a position in their own name, or as a member of an organization holding it (GH #1123, as the route reads a firm-held position); a membership alone is no seat, so it grants neither position nor any other role. Otherwise the claim is refused naming the person and the position, and so is a renewal once the person no longer holds it (a release stays open) and friction filed as a position the person does not hold. Without local trust the organization’s own kind positions (position:agent, leader, …) are held only by a holds edge naming them. The head picks the first admitted attempt of that kind that is outstanding — asked to run (effect.requested attempt:<id>), no outcome — was relayed to legs (effect.relayed attempt:<id>: the owner records it when a leg relay answered the attempt pending, and only then — an edit the organism’s editor holds, or a case a person does, is outstanding the same way and is never a leg’s, GH #1158; a row the record would not take is recorded when the next ask redelivers the attempt, and a leg is refused it until then), fits the filter, and is not held by another leg under a live lease; takes memory’s claim attempt:<id> for the performer with the TTL under the head’s role (GH #1026: the store decides between two legs racing; without memory nothing races); and appends attempt.claimed naming the lease and the principal that took it. The claim is not fenced on the record head the leg read — memory’s conditional insert is the race, so a leg a row behind is not turned away with stale_subject — and a claim memory took that ends in no row is given back. A leg that names no data class is handed nothing: no class is no class. The receipt returns the task and the lease as a value. Nothing fitting, or everything held, is a refusal: a receipt with the reason — which names the class or the owner that stood in the way — and no row, since legs ask often and an empty answer is not a fact.
  • The outcome comes back under the lease, with the hat the leg wore (its digest, the head and watermark it was rendered at, the digest of what was rendered, the renderer’s version — the five together or none). The head refuses — a receipt with the reason, no row — an attempt never admitted or never asked to run, one already settled or already handed back (duplicate: one outcome under one lease), a lease that is not this holder’s at this token now, by the recorded claim and, with memory, by memory’s (stale), and an outcome from a principal other than the one that took the lease; files the receipts (a protected body in memory alone, the rest as git receipts); and appends attempt.outcome_requested. A node relays the row onto the nerves (WorkSubmit, dna.work.submit) until the record answers it. The owner checks the same lease, journals the calls the leg made as model.called rows on the attempt id — tokens per task hold out of process — and settles the attempt through the path every reply takes: attempt.outcome by exact append, naming the request it answers and carrying result_ref and the hat, the Work and the task after it; a submission it will not settle — an attempt never relayed to a leg (no effect.relayed), a stale lease, a second outcome after settlement — is an attempt.outcome_refused row naming why and the request, which answers it, so the relay stops. A Patch Work’s outcome (the request’s output_contract is Patch, disposition done) is a candidate change, and the owner makes it one (GH #1156): the outcome names the leg’s commit (result_ref: commit:<sha>) and files that commit’s own patch among its receipts (git format-patch, whose From <sha> line names it; the legs’ git hand exports it, hands.git.patch). The owner mints a Mutation bound to the attempt (mutation.requested carrying the binding, the leg, its commit and the receipt), opens its gateway worktree at the genome’s head, applies the patch there — only under the seed the Task’s class edits, every path counted (a rename’s source too), and never the organization’s own seed unless the class is organization — and commits, verifies and opens the Review exactly as for the editor’s candidate, the leg its author. The patch is the candidate: what it carries is what is verified and reviewed, and verification runs the candidate’s own tests on the owner, as it does the editor’s. The attempt settles done on the prepared candidate (<m>: candidate prepared (<disposition>) from commit:<sha>), or failed with the Mutation’s failure — no commit named, no receipt that is its patch, a patch that does not apply at the head, one that edits outside the seed, a candidate denied, a Review that could not open — so the Work’s allowance decides what follows. A submission relayed again finds the Mutation bound to the attempt and makes no second; one found still in flight (the owner restarted while making it) is failed as interrupted. The outcome names the bound Mutation’s commit, whichever lease handed it back. A candidate whose seed holds no Hale source at the base or at the candidate is not denied for a check with nothing to judge (hale_source false in its evidence) and is never staged or released by a grant: it is reviewed before anything applies. A LegRelay performer answers pending for the kinds a program hands to legs, so the attempt waits for one instead of running in process (stage 1 of the legs; the in-process performers go as each stage lands), and RelayReplay is its reconciler: an owner that restarts with a leg’s attempt in flight leaves it to its claim — nothing is asked of it while the lease is live — and asks it again once the lease expires with no outcome (effect.redelivered, once per lease), when a leg claims it anew.
  • The spend (GH #1131). Before its first model call a leg asks the spine, which holds the one budget (Models), for its attempt’s spend: dna.attempt.allowance under its lease (the attempt, the holder, the token), which the head admits as it admits a renewal — the same checks — and writes as attempt.allowance_requested (holder, token, request, at, and the command). A node relays it onto the nerves (WorkAllowanceAsk, dna.work.allowance) until the record answers it. The owner checks the lease as of the head’s admission (this holder, this token, live, the attempt relayed to a leg and unsettled) and asks the one gate with the Work’s own cost ceiling: attempt.allowance_granted (allowance_micros, ceiling_micros, spent_micros, window_micros, window, holder, token, request, at) or attempt.allowance_refused (why, …), naming the request. The ask reads back requested until then, granted with allowance_micros, or refused with the reason. The model performer asks before its first call and waits up to 60 s for the answer; refused, or unanswered, it makes no call and hands back declined, naming why; granted, it makes no call — a retry included — whose cost is known before it is sent and is past what is left of the allowance (ModelBackend.price_micros: a price per call, a recorded answer; the attempt is declined), nor any once its calls have spent it. A cost learned only from the answer (a price per token, a harness’s summary) can cross the allowance: that call is made whole, so a leg passes its grant by at most one call. At settle the owner takes an outcome whose calls cost more than the lease was granted — anything, when it asked for nothing — as the attempt failed, naming the overrun (no Patch Work’s candidate is made from it), so the Work’s allowance of attempts bounds the repeats; the calls are journaled all the same, for what was spent is spent.
  • The verbs (hale dna work, GH #946 slice 4; dna/core/legs, vendored as vendor/dna/legs) are a leg as API clients: login asks the head (GET /.well-known/hale-dna, unauthenticated under OIDC: principal, issuer, client) who signs its people in, and where that is the local stub (a loopback issuer, client dna-local) mints an ID token for the subject the record’s dna.oidc.member maps to --as <person>, the client secret read from the vault entry oidc-client-dna-local and never printed, and prints export HALE_DNA_ID_TOKEN=…; for any other issuer it prints the sign-in URL and exits 1; next is dna.attempt.claim for a position (--as position:<name>, the graph’s id, never a free string; a claim matches attempts by performer kind, --kind, and a position the graph states that names no kind of its own claims as an agent and also takes the Works routed to it, ask-position; a refusal names the kinds outstanding and the flag), brief the hat read — or rendered in the leg, text, prompt or agent, recording the hat digest, the digest of what was rendered and the renderer’s version (legs-render/2) — renew is dna.attempt.renew (the lease extended, the token kept: another attempt.claimed row), allowance is dna.attempt.allowance read back until the spine answers it (--wait, 60 s: what an external harness asks before its first model call), submit is dna.attempt.outcome, settle that command read back, release is dna.attempt.release (attempt.released; the attempt is another leg’s to claim), friction is dna.friction.file (friction.filed, a fact nobody admits), and hand runs one of the hat’s hands on this machine (A person’s session, above). Each prints one JSON object and holds nothing afterwards; submit and release are keyed on the lease, so run twice they are one act; next mints a fresh id per call (a claim by its holder renews); renew is counted by the caller. A leg’s identity is a position the record knows — position:<name> from the graph (a graph.node row, or a position a ratified graph proposal states: a hole hale dna init proposed, a position hale dna position open opened; read through the record the command is admitted over, and ended by a reviewed retirement in a later ratified proposal) or the organization’s own, / allowed in the name as the graph spells a process’s positions (position:api/dev), with #<n> for one worker of several — never free text, at the head as at the verb. While an outcome under a lease awaits the owner the lease is neither renewed nor given back, and the attempt awaits no other leg; without memory the record numbers the leases (the next claim row is the next token). Exit codes: 0 admitted, 1 refused (the receipt printed), unreachable, or a run that ends unsettled or unresolved; 2 usage, or a catalog with a performer declaring no effect class. run is one cycle through the project’s performers (dna/org/work.hl, generated at init): a person’s leg renders the brief and leaves the outcome to submit; a deterministic performer wins for the work kinds it takes and settles; the model performer (legs::ModelPerformer) is the catalog’s router behind the performer interface, out of process, one task per run: the brief rendered as a prompt, the answer the result, and every call the router answered handed back as evidence (the adapter that answered, backend, reported model, tokens, cost, wall time under the prompt and context digests), which the owner journals as model.called rows on the attempt through the encoder its in-process calls take; a rate-limited call is backed off inside the attempt (bounded, doubling, the lease held) and each wait is a row of evidence of its own, so the attempt’s cost in time is in the record beside its cost in tokens; the lease is renewed before each wait. The model is sent the render alone (the hat is structure), and the hat’s digest is the context digest on every row. --performer names the performer instead of the catalog’s choice; a performer never answers a kind it does not take, forced or not. Every performer declares an effect class — effect_free (answered, touched nothing), idempotent (may be run again to the same end) or uncertain (an agent’s seat with tools: may have acted once already) — with no default: a catalog with a performer declaring none is refused by the verbs that claim or hand back (exit 2) and read by the rest. The class rides on the claim (effect_class), which the head admits only if the Work admits the class — the class decides what a failed settle becomes, never what a performer may do, so a Work that is answered admits every class and an edit admits no effect_free; the hat says so in effects — is recorded on attempt.claimed (a renewal carries it on), must be named again by an outcome under the lease, and is evidence on the outcome (effect_class on attempt.outcome_requested). A settle that fails (the outcome cannot be handed back or read back, was refused, or the owner refused it) on an effect_free or idempotent performer is unsettled: the lease is given back when still the leg’s, and the loop runs the slot again after a rest, performing anew. On an uncertain performer nothing is retried by a program: the leg marks the attempt unresolved at the head — an outcome of disposition unresolved, its calls as evidence, admitted past the lease’s end — which lands attempt.unresolved and effect.result unknown on the attempt; friction names the attempt, the holder, the lease and the resolution; the leg answers unresolved (exit 1) and the loop stops that slot. A marked attempt awaits no leg (its holder included) until a person resolves it (hale dna effect resolve attempt:<id> --outcome ok|failed), on which the owner settles it. An uncertain claim whose lease lapses with no outcome is never asked again either: the owner records effect.result unknown itself; such a claim’s lease is an hour by default. The model leg judges a refusal by whether the backend may have acted (ModelResult.made, false only for a refusal before anything was sent or run): behind an uncertain performer one that may have is unresolved, never retried or failed over; behind the other two a failed outcome; a rate-limited call is asked again only when it was never made. loop --parallel N is a worker: N supervised child processes, each run as its own holder position:<name>#<n> (two workers of one position never share a lease), each answer one JSON line as the child ends, started again at once after a task and after a per-slot doubling backoff when idle (nothing claimed, a person’s, given back); --once runs each once. The leg is its program’s main locus: SIGTERM/SIGINT drains the loop, which ends its children (TERM, then KILL after a grace) and reaps them before it ends; --drain (a marker beside the leg) ends a running loop once its children have. Nothing is held between tasks; a child ended mid-task leaves a lease that expires. A project initialised with no backend configured gets NoModel: its agent Works are a person’s. The catalog and the tape (models.hl, tape.hl) stay in the core while the editor and leader call them in process. A performer is handed the brief and the hands as interfaces — git in scratch, the forge (it decides, a leg never merges), the toolchain; deploy and the heart’s API refuse, naming #987 — and answers a performance (a result struct until #732). The verb is the project’s own program, built beside the vendored seed under .hale/dna/legs by the host; hale mcp exposes it as hale_dna_work, and a Work’s hands as hale_dna_hand. The commands go over the head’s api socket (GH #1104 piece 5): each verb a call on a member of the head’s surface — Commands::claim, Commands::renew, Commands::outcome, Commands::release_attempt, Commands::file_friction, requiring position; Commands::lookup for settle — with the receipt on the value channel as the record wrote it; the reads (the record head, the hat, the socket’s path in /capabilities) stay HTTP. The principal is the peer’s credentials mapped to a person by the record (dna.unix.member; a record that declares dna.trust = local, as hale dna new declares for the one it makes, is that person’s — they hold every position — and one that declares nothing, or another trust, leaves it to the graph’s holds edges); the lease’s holder is the position the leg works as (--as, #<n> for a worker, --worker <n>), never a free flag; a peer whose person holds no position is told so at attach, and a verb outside the caller’s slice is unknown, exit 1. The leg takes the socket from --socket, HALE_DNA_SOCKET, or the head’s capabilities.

A command is one act per request id: sent again under the same id, a command that landed is found by it, before anything else is checked, and answered with its receipt. A head answers snapshot_changed when its record moved while it read it, and command_busy when the record moved through every read its admission allows itself. Neither admits the command, though a pass may have left what the command’s own retry takes back or finds (a claim is given back; a renewal extends the same lease; receipts are filed by their digest). So a leg asks the same command again, 200ms apart, under the same id, until ten seconds have passed, whatever the command, and reads the hat again the same way. A record several legs and the owner write moves often. command_outcome_unknown is not asked again: the leg reads the command back by its id. A hat it still cannot read gives the lease back.

One active step and its leaf Works are resident loci (dna/core/workflow_runtime.hl), in the shape the lifetime proof established: a StepRun is an accepted child no parent releases, born from its owner’s handler, alive after its run() and every handler return, advancing from its own handlers keyed by its id, ending with terminate;. It owns its WorkRuns the same way, one per leaf, born from the handler that heard the activation land. Neither holds a journal: each proposes every transition to the one WorkflowRuntime over the bus — TransitionProposed, keyed by scope — holds one proposal at a time, and acts only on that proposal’s answer, and only on ok. A refusal dispatches nothing.

WorkflowRuntime is the committer, and the executor, over one journal. A proposal is decided at one reading: the runtime refreshes, applies what the record holds beyond what its projection has seen and remembers each committed proposal’s id and revision, and that reading’s revision is the one the decision is taken at and the one the append is exact at — never a second look. A proposal already committed is answered again from what was committed, if it is the same proposal; the same id carrying another transition is a conflict, refused with nothing appended. Otherwise the transition is validated against the projection without touching it (a dry run of card 06’s rules), appended exactly at that revision, and applied for real once the append has landed. When the record has moved under the decision — another runtime committed the same transition, or anything else — the append is stale and the decision is taken again against the new state: the same transition committed meanwhile is answered from its row, and one row stands. An append the record refuses answers a refusal and moves nothing, so the same transition proposed again is decided against the state as it is. A proposal the state refuses lands nothing: the record is not the place a transition is found invalid. An answer says who refused (basis: state, record or conflict), because the residents treat them differently. The body of a proposal carries the reference the row will be committed under, and it must be the proposal’s own scope, key and id, whole, or the proposal is refused as a conflict with itself before the state sees it; a committed proposal is rebuilt from its row and compared field by field with one sent again — the same, whole, is a replay answered from the row; anything else under that id, another key included, is a conflict. A Work asks the runtime to run its admitted attempt by id (AttemptRequested); the runtime reads the admission from the record and runs it through the one-attempt path above, and every reply — the one that came back from the call and the one that came later, through settle — reaches the Work the same way (AttemptReplied, keyed by the Work), so an immediate and a delayed reply are one reducer and one completion path.

The barrier holds as the contract has it. A StepRun registers its whole member set (step.registered), activates (step.activated), and only then births its leaves; a member answers once, by its key (MemberSettled, keyed by the step): a member that answered again and an answer from no member of the step change nothing. Two required leaves complete the step in either order; one immediate reply and one delayed one leave it waiting, alive, with the outstanding Work reachable. A member that failed fails the step at once (step.failed, naming the member) and a step whose every member settled done completes it (step.completed); either publishes one terminal outcome (StepSettled, keyed by the step) and settles the step. A member is its key and the Work bound under it: a settlement naming a required key for another Work is no member’s and changes nothing. After that a sibling’s later reply is recorded — its attempt’s outcome and its Work’s settlement land as facts — and reopens nothing; the StepRun stays until every member it dispatched has settled, then reclaims itself, and a WorkRun reclaims itself once its settlement is answered. A leaf with allowance left is retried before it fails: each attempt is admitted as a proposal of its own, and a retry the record will not take — the step failed meanwhile — settles the Work failed on the attempt that failed, the drain. A refusal by the record is no outcome: a Work or a Step whose append the record refused holds its transition, tells nobody it settled, reclaims nothing, and proposes the same transition again when the record resumes (RecordResumed, published by whoever knows the record is writable again) or, for a Work, when its attempt’s reply reaches it again; a member has answered its step only once its settlement is in the record. A Work whose first admission the state refused has nothing admitted, no claim and no effect to await: under a Step that has durably settled it retires (MemberRetired, keyed by the step), which is not a settlement — it answers nothing — and the step counts it toward its drain and reclaims itself once every admitted responsibility settled; a first admission the record refused is proposed again when the step settles, so the state says whether it can still be admitted. One the state refuses while its step runs stays, unattempted and reachable, until it is fenced. Nothing here orders steps, spawns child workflows or survives a restart; those are later cards.

One admitted execution is a WorkflowRun, resident like its steps: it holds every leaf of the execution (copied at birth), births step 0, and births step i+1 only from the handler that hears step i has drained — StepDrained, keyed by the task, which a StepRun publishes as it leaves, once every member it admitted has settled or retired — behind that step’s committed completion. No ordering lives in the workflow that the record does not enforce: the projection refuses a step’s activation while the step before has not completed, and the activation’s own identity (<step>@activated) is one row at the committer, so a duplicate completion cannot start the next step twice; a drain message for a step the workflow has advanced past, or for none it has active, changes nothing. When the last step drained completed the workflow proposes workflow.settled done; a failed step drains first, then the workflow proposes workflow.settled failed, and later steps are never born. Both are proposals like any other: decided at one reading, held when the record refuses them, proposed again on RecordResumed.

A cancellation is asked of a workflow by task (WorkflowCancelRequested) and is a proposal too: workflow.settled cancelled, which the state takes at any point before the execution settled and refuses after. Once it landed the workflow says so (WorkflowSettled, keyed by the task) and births nothing further; its active step hears it and fences what it dispatched (StepFenced, keyed by the step) — a step with no outcome yet is cancelled (no row of its own; the task’s row is its basis), and a step that had failed and is draining keeps its failure: the fence is not a second outcome. The fence is the one message a Work acts on for a cancellation (a cancelled step settlement is for the owner; a Work ignores it), so a Work that retires or settles on it acts, and ends, exactly once. Its Works hear the fence: an admitted attempt still awaiting its reply settles cancelled (the projection allows that settlement only under a cancelled task); a first admission the record refused is proposed again and, refused by the state, retires; one the state had refused retires. The fence itself is in the record, not in a notification: the executor reads the execution’s durable cancellation — the task’s workflow.settled, or an ancestor’s, walked through the admissions — at the same reading its execution claim is exact at, and an attempt not yet claimed under a cancelled execution does not run, however the notification lagged, while one claimed before the cancellation still records its outcome. Nothing external is undone — a reply that comes later is recorded as the attempt’s outcome and reopens nothing. The workflow leaves once the fenced step has drained. An activation the record refuses dispatches nothing: the step holds it, no leaf is born, and the record’s resumption lands it once. Nothing here spawns child workflows or survives a restart; those are later cards.

One admitted Task is a TaskRun, resident like the rest: it holds the bound recipe, decoded into its own catalog at birth, and from it the leaves and children of every step, and births the one WorkflowRun that runs them. The executions owner (Executions, the Task owner the contract names, one per scope) accepts every TaskRun and never releases one: a root asked of it (ExecutionAsked) with its admission already in the record, and a child asked by its parent Task. One execution is one Task: a Task asked twice is born once. The same engine runs every level; there is no other path for a child.

A step that dispatches a child member asks its own Task (ChildRequested, keyed by the parent task), which holds the recipe the child is bound in; the Task cuts the subtree the recipe bound under that child, exactly, and asks the owner to admit and run it (ChildAdmitRequested, keyed by scope). A request that names a child the recipe does not bind under that step and key is ignored. The child proposes its own admission first — workflow.admitted with the subtree, its parent, its spawning step and its member key, which the projection accepts only for what the parent bound, from a step that is registered, active and unsettled — and runs only once that landed; an admission the record refused is held and proposed again when the record resumes, or when the spawning step settles or is fenced — so the state, not the child, says whether it can still be admitted, and under a failed step it retires on that answer; one the state refused retires under the settled or fenced spawning step, as a leaf does. A fence that reaches a child while its admission answer is out is kept, with its reason, and applied when the answer comes: an admission that landed starts and is cancelled at once through its own settlement. A spawning step that settles or is fenced while the answer is out owes the child one re-decision, taken on that answer if it is a refusal by the record: the admission is proposed again once, and the state says — neither ordering of the two messages is assumed, and a record that keeps refusing is not spun against. A child that settled tells the step that spawned it once (ChildSettled, keyed by that step) after its row landed, and the step counts the exact child bound under the key: a settlement for another Task under the key, for a key that is no child of the step, or delivered again changes nothing; a failed child fails the step at once, as a failed leaf does. The Task leaves once its workflow has (WorkflowLeft), and tells the step that spawned it so (ChildLeft, keyed by that step): the step’s drain waits for a child that settled to have left, not for its settlement, so no ancestor reclaims itself ahead of a descendant still holding something — a Work whose settlement the record refused, say. A leave from a child that has not answered is nobody’s: a leave follows a settlement. A step fenced by its Task’s cancellation fences a running child the same way: the child’s Task asks its own workflow to cancel, which settles cancelled through the same proposal, so the fence reaches every level below and each level drains and reclaims from the leaves upward.

Restore is not a mode. An execution asked of the executions owner over the record a crash left — or fresh: the same ask — first asks the runtime what the record holds about it (ExecutionStateAsked, answered from the projection): one the record has settled is over — its settlement is announced and the workflow leaves, birthing nothing — except a cancelled one whose admitted Works have not settled: cancelled is not cancelled-and-drained, and the current step is born fenced, so those Works settle cancelled, the step drains, and the workflow leaves behind it. A cancellation heard while the record is being asked waits for the answer: an execution the record has settled is not cancelled, and one still open is cancelled first — and then, as whenever a cancellation lands with no step active, the record is asked again what it still holds admitted and unsettled, and that is drained through the current step, born fenced, before the workflow leaves. One still open proposes its transitions exactly as a fresh one does, and the committer answers what the record already holds as a replay, so every Task, Step, Work and Attempt id a restored incarnation uses is the record’s, never minted again — a completed record rebooted gains no row and runs nothing, and a cancelled one ends the restored execution before anything is born.

The one thing a restart adds is redelivery, and the runtime derives it from its own incarnation, never from a request: a request for an attempt names the attempt and the Work it is an attempt of, and the runtime knows which attempts it claimed itself and which it already redelivered. Every request is decided against the record at one reading: an attempt whose outcome is recorded is answered from it and never runs again, and a claim the dead incarnation left open under it is closed with it, durably, before the Work is answered — on every path that answers from a recorded outcome: a request that finds it, one that runs into it on its own reading after seeing the attempt unclaimed, one that attaches to a running attempt and finds it, and a reply — and a close the record refuses leaves the Work holding its responsibility, told so, until a later request or reply closes it: a Work whose request was refused without an outcome asks again when the record resumes; one never claimed runs through the ordinary path, which claims first; one claimed by this incarnation, or redelivered by it, is running, and the request attaches, so a duplicate dispatch starts no second invocation; one claimed and never answered by a claimant this incarnation is not was claimed by an incarnation nobody will hear from again — unless its effect resulted unknown (a restart the existing recovery reconciled no further), which stays visibly unresolved, nothing running on it until card 12c’s reconciliation says what happened. Otherwise its dispatch is issued again with the same attempt id and no new claim — a transport redelivery, not a new attempt — and that decision is a row (effect.redelivered) appended exactly at the reading that saw the claim, no outcome and no fence, so an outcome or a cancellation landing meanwhile makes it stale and the decision is taken again. A reply from the old process for the still-current attempt is accepted and recorded like any reply, and whichever reply arrives second changes nothing. The admission’s performer kind is durable: the restored incarnation runs the attempt on that kind. What an invocation that died did before it died is a later card’s, as is cross-process delivery.

A restart rebuilds an execution from the record alone. The executions owner is asked with nothing but the task’s id and which performer kind runs each leaf (ExecutionAsked); it asks the runtime for the admission as the record holds it (AdmissionAsked), and the runtime answers from the row, read whole, and from its projection’s word that the row was admitted (AdmissionAnswered): the definition and the bound revision, the recipe, the limits. The Task is born from that answer, so an execution restored after a restart runs what it was admitted with — its bound recipe — whatever the catalog offers now; a definition changed to a new revision between shutdown and restart binds new admissions and touches no old execution. A task the record never admitted, one admitted with a recipe that cannot be read, or one whose admission the projection refused is not resumed: the owner says so (ExecutionRefused, with why) and nothing is born or invented for it. From there the residents re-propose through the transition code live execution uses, and the record decides what replays and what runs: a step the record completed is not reborn and the next step starts once; a child the record settled announces its settlement and leaves, and its parent’s step completes on it; a member still waiting is recovered where the record left it — its admitted attempt redelivered, under card 12c’s reconciliation — and completed members do not rerun; the grandchild settles, then the child, then the root. Only unfinished responsibilities exist in memory after a restart, and every id and every required-member set is the record’s.

The admission recovered is the one the projection accepted — the body it recorded when it applied the row — never the last row in the record that names the task: a later row the projection refused (a re-admission under another revision, a row whose body names another task) is not the execution, and a task the projection holds no admission of is refused with the projection’s reason for the last row it refused, from a dry replay of that row. A child of another Task is not recovered by asking for it: the answer says whose child it is, under which step and key, and the child comes back through its parent, in its bound place. The workflow identity an execution runs under is the one its recipe bound, read from the record and never re-derived; the projection refuses an admission whose recipe binds the task under any workflow but <task>/wf<revision> (§4 of the contract), so the recipe and the identity never say different things. Every question the owner puts to the record carries an identity of its own (AdmissionAsked.ask_id), held outstanding until answered; an answer is taken only to a question this owner holds outstanding, for the task it asked about, and only once — an answer nobody asked for, however positive, and an answer under a question already answered, are strays and birth nothing. A leaf asks the record what it holds about it before proposing anything (WorkStateAsked / WorkStateAnswered): a settled Work has answered and leaves; a Work with an admitted attempt resumes that attempt under its number, id and performer kind as the record has them — the kinds a restart is asked with bind only attempts not yet admitted — and a Work never attempted admits its first attempt under the asked kind. The state question carries the Work’s scope and an identity of its own (WorkStateAsked.ask_id), held outstanding until answered; the answer carries both back, and the Work takes only the answer to its one outstanding question — its scope, its identity — once: a same-named Work in another scope’s record (ids are minted within a record) is not this Work, and a stale or unsolicited answer is no state. A fence heard while the state question is outstanding is remembered and decided on after the answer: an admitted attempt is re-proposed and, admitted under the fence, settles cancelled; a Work never attempted proposes its first admission, which the state refuses under the cancelled Task, and retires. No settlement ever names no attempt.

Workflow execution: uncertain external effects

Section titled “Workflow execution: uncertain external effects”

An attempt’s performer may have acted before the outcome was saved: the incarnation claimed the attempt, invoked the adapter, and died before the outcome row landed. Recovery reconciles before it decides on a redelivery. The work system carries an effect adapter’s reconciliation beside each of its performers, one per kind as the performers are (Reconciler, default NoEvidence): the adapter of the admitted kind is asked, for the identity that kind selects, about an attempt whose claim an incarnation that died left open, and it says whether it can tell, whether it acted, what it recorded when it did, or that acting again is documented safe. Its answer names the kind and identity it speaks for, and evidence that does not name the admitted performer’s is no evidence about the attempt — another adapter’s attestation that nothing acted, or its documented idempotence, does not authorize replaying this performer. An adapter that acted hands back the outcome it recorded, as it recorded it — judged for whose it is exactly as any reply is, the performer it names and the attempt it names if it names one, never relabelled — and that is the attempt’s outcome, recorded and its claim closed; nothing is performed twice. An adapter that attests it never acted, or whose effect is documented idempotent, has the dispatch issued again (effect.redelivered, saying which). An adapter that cannot say leaves the claim resulted unknown, durably and visibly (effect.result unknown (reconciled after a restart: …), the existing rule-3 mark, appended exactly at the reading and under the fence; the record moving under that append — an outcome, a resolution, a takeover, or something unrelated — has the decision taken again at the new reading, so a resolution that landed meanwhile is the outcome and an unrelated move just lands the unknown row after it), and the attempt waits — asked again it still waits, nothing is replayed blind — until a person resolves it (hale dna effect resolve <key> --outcome ok|failed); then the resolution is the attempt’s outcome, recorded so the Work settles on it. The assembly’s own restart rule, which results the other effect families by evidence or marks them unknown, leaves an attempt’s claim to the runtime’s adapter. One flat leaf; no exactly-once external execution is promised and no compensation is invented: what is promised is that recovery never performs an effect twice on its own say-so, and never pretends to know what it cannot.

Workflow execution: retries under a restart, and the fence

Section titled “Workflow execution: retries under a restart, and the fence”

A retry is the next numbered attempt of a Work, and it is admitted only after the record holds the outcome that permits it — a failed attempt with allowance left, the allowance the recipe bound — as card 06’s rules have it; its request and performer kind are the admission’s, durable, so nothing is planned again for it. Under a restart the retry is the record’s: a reboot proposes attempt zero, is answered from its recorded failure, proposes attempt one, which the record already holds and replays, and asks for it — redelivered if the incarnation that claimed it died — and never mints attempt two. A late success for attempt zero is answered from its recorded failure and changes nothing; the Work’s current attempt is attempt one. An admission the record refuses invokes no performer and spends no retry: the Work holds it, with its number, and proposes it again when the record resumes.

The runtime runs under a lease that is rows of its own record (lease.taken, carrying the fencing token — a per-key epoch allocated under the exact append that takes it — the holder and the expiry; lease.renewed, which moves the expiry and keeps the token; lease.released; each a JSON body, so a holder reads back as it was written and an empty holder is refused): a key, a holder, the token the holder was granted, and the clock it judges the lease by. The token is the row’s own, never its position: a routed reader that starts fresh reads the record’s rows before the ledger’s and finds the same lease a reader that was live found, and the lease is the take with the highest token, renewed and released by rows that name it. Ownership is read at the revision a write is exact at, so the fence is atomic with the write by construction: a takeover is a row, it moves the revision, and a stale holder’s exact append fails and is decided again — at the new revision the record names the new holder, and the write is fenced. That holds for every write the runtime makes: a transition it commits, a claim, a redelivery, an outcome it records, a claim it closes. A holder whose lease expired — unclaimed, re-taken by another, or re-taken under its own name by a restarted incarnation with a new token — commits nothing, however live it feels, and answers fenced, which a resident holds as it holds a refusal by the record and proposes again once the record resumes under a live token; a request that only attaches to a running attempt writes nothing and is not fenced. Two takes of a free lease at once land one: the other is stale, decided again and refused. The cell leases (MemLeases, GitLeases) stay for what they hold — a body, a mutation — where the operation they fence is not an append to this record. A runtime with no lease key is unfenced: a standalone runtime over a memory record.

An edit leaf of a workflow execution is performed by the assembly — its editor, its gateway, its record — never by a router performer. The engine reaches it through a fifth performer kind, edit: the EditRelay publishes the attempt (EditRequested, keyed by the runtime’s scope, which is the organism’s id) and answers pending; the assembly performs or waits, and reports the outcome for that exact attempt (AttemptReported), which the runtime settles as it settles a late reply — judged, recorded, answered to its Work and only its Work; a report whose outcome names another attempt than the report does, or none, is nobody’s and settles nothing. The Work settles as any Work does, its step counts it, and the workflow settles the root: no Work of an admitted execution settles its Task, and a Mutation’s review outcome never settles it either — one root terminal writer.

Two output contracts, never reinterpreted as each other. Patch — the default for an edit leaf — is a candidate prepared for review: the attempt is performed once, the Mutation it produces is bound to it (card 14), and the Work is done when the candidate is in review, escalated or released, failed when preparation failed before a candidate; a later verdict changes nothing about it. Applied (or Approved) is the candidate approved and applied: the Work performs nothing and waits on one exact candidate: the Work its request names in target, as <step>/<key> within the workflow (s0/e) or a whole Work id, resolved to the Mutation bound (card 14) to the attempt that Work’s settlement accepted — a first attempt that failed to prepare and was retried is not it. A Work naming none fails at once, with why, since the dependency is explicit and never a Task-wide or Work-wide guess; one whose producer settled other than done fails, there being no candidate. The producer’s settlement and its candidate’s review and application are prerequisites that land in either order, and each landing is an occasion to look again: a waiting attempt is evaluated afresh — by the same resolution — when any Work of the engine ends (RunLives), when a review settles, when the record resumes, on redrive, and whenever the assembly is asked; it is answered once, when its state is an outcome. The same resolution serves an ask again after a restart, which waits once. It is answered from that candidate’s durable state alone: rejected or sent back is failed; approved and applied (or retained) is done; approved and rolled back is failed; approved but not applied — the apply gate refused, an application pending or unresolved — is no outcome yet, and the Work keeps waiting, to be answered when the application lands, on a first approval or on an approval again. An attempt asked again — a redelivery after a restart — is answered from the Mutation bound to it and edits nothing twice, so the edit kind’s reconciliation is a replay. A request row the record refuses starts nothing: the attempt is held, and redrive puts it again. A published report is not a durable one: the assembly keeps every report until the record is seen to hold the attempt’s outcome, and puts a kept one again when the record resumes or on redrive — the validated outcome re-persisted, never the edit performed again.

A human leaf of a workflow execution is a case for a person. The engine reaches the assembly through the human performer kind: the HumanRelay publishes the attempt (CaseRequested, keyed by the runtime’s scope, the organism’s id) and answers pending, and the attempt stays out until the person reports the case done. The assembly admits the case as its own handed Task, <task>.s<i>.<key> — the Work’s step index and member key under its Task, the child-Task form of §4 — stable across redeliveries and naming its exact parent Work and attempt by construction. The admission (case.admitted, versioned and self-sufficient: parent Task, Work and attempt, objective, assignee, obligation, acceptance practice, required evidence, who said so) lands first, exact at the revision read, and only then the rows the tooling of the day reads — task.born <case>, then task.handed <case> (or task.transfer_requested, for another owner’s person) — so hale dna task done <case> --as <who> addresses that case as it addresses a job today, and the same gate applies. Assignee, obligation, acceptance practice and required evidence bind as a job’s do: in an organism the leader plans for, the leader says who and under what obligation; without one the case is unassigned. A retired assignee’s successor, a transfer to another owner, and reassignment keep their semantics.

What the leader proposed — whom, under what obligation — is one thing; what binds is decided from the reading the admission is exact at: who is admissible now (a retired person’s successor), which acceptance practice is in force now, whose the assignee is. A reading gone stale under the append repeats the decision, not only the write, so a retirement that landed between sends the case to the successor, never to the person retired. The admission names its origin (owner) and its disposition (to: the owner whose person takes it, empty for the origin’s own), and the compatibility rows say what the admission recorded, never what the body writing them is.

The hand-off settles nothing: the parent Work stays pending, and the root is neither closed nor marked by it. Two human Works in one step are two cases, each completable on its own. An attempt asked again — a redelivery after a restart, a later attempt of the same Work — finds the case by its stable identity and admits no second, and asks the leader nothing: the terms bound at admission stand, and a case already awaiting the leader’s word is not asked for twice. A restart that fell between the admission and either compatibility row completes them from the admission, in that order, and never plans, resumes or pends a case as edit work: a case is a person’s, never an executable root, never legacy edit work. Only the recorded owner completes them: a transfer to another owner is theirs to hand once a member of theirs accepts it (task.transfer_accepted, as a job’s today), and another owner’s restart neither hands it nor completes the offer. An admission the record refuses holds the attempt for redrive; a compatibility row the record refuses holds the case, completed when the record resumes or on redrive without a restart, and the hand-off is announced only once it is durable.

A case is completed by its person through the tooling of the day — hale dna task done <case> --as <who>, with the evidence its bound acceptance condition wants (completion.linked) or an exception someone else authorized (completion.excepted), or a decision someone else made, reported by the assignee and accepted under the bound practice (decision.reported, GH #616) — and that accepted fact is the outcome of the attempt the case was admitted for (card 17). The assembly reads the completion from the record (case_completion, completion.hl) and reports it to the runtime of its scope as it reports an edit leaf’s outcome: judged, recorded as attempt.outcome and answered to its Work by the runtime’s own barrier, so the Work settles done, the step counts it, and a completion the record already holds an outcome for changes nothing. The report is kept until the record is seen to hold the outcome, as an edit’s is.

The completion is judged against what the admission bound — the assignee (moved by task.reassigned), whether evidence is required, which practice — never against a practice in force later: a practice relaxed after the hand-off relaxes nothing, while a case handed after the relaxation closes under the new one; evidence linked under another practice is none, and a decision accepted under another policy than the bound one, or under none, or naming another case or none as its scope, is none. An exception applies to the person on the completion row: recorded in their name and authorized by someone other than them — a case reassigned to its exception’s authorizer does not close under their own authorization. A completion the bound condition refuses — not the assignee’s, no evidence linked under the bound practice where it is required, an exception that is not the closer’s or that the closer authorized, a decision reported by someone other than the assignee — connects nothing and is noted once; a later valid one connects. The first valid completion is the outcome; a second is a duplicate and settles nothing twice. One case’s completion advances nothing of another’s.

It is observed wherever the assembly reads the record: live, at the organism’s tick, which examines the cases whenever the record has rows past the revision they were last examined at — by that revision, never by whether the tick’s own refresh moved, since another handler (a redelivery’s) reads the record without examining every case; when the record resumes; on redrive; at a restart of the assembly (which reads what the record holds); and at a restart of the execution, whose redelivery asks for the attempt again and is answered with the outcome the record holds. A case keeps its parent step waiting through a restart of the execution: nothing settles it but its completion. Only the recorded owner connects a completion — the Work runs in the origin’s execution. The tooling’s own gate (hale dna task done, evidence, exceptions, reported decisions) is unchanged.

What the sections above promise is held, as one oracle, by dna/tests/workflow_conformance_test.hl over the canonical three-level example (dna/WORKFLOW-CONTRACT.md §6), in every supported mode, with a unix listen binding declared so every publish is queued as in a bound organism. A review traces each promise to it; what it does not cover is not promised.

Delivery. A leaf’s attempt is admitted, claimed and delivered to its performer once per claim. A performer answers at once or later; a later reply names the attempt and the performer, and reaches the runtime whenever it comes — after the grandchild’s step, before the root’s own leaf, in any order. A reply for an attempt whose outcome is recorded is answered from the record and writes nothing, whether it repeats the recorded reply or arrives for an attempt the Work has retried past. A reply from another identity than the admitted performer’s is nobody’s and is refused. A reply for an attempt the record never admitted or never claimed is nobody’s.

Failure. A leaf that fails its allowance fails its step; the step fails its execution; a child’s failure fails the step that invoked it, up to the root. Nothing later activates and nothing further is requested. A leaf still out under a failed step keeps its responsibility: its outcome is recorded when it comes, it reopens nothing, and the execution above it settles failed only after it settled (the drain policy, contract §6).

Cancellation. A root’s cancellation fences everything below it: what was admitted records its outcome; nothing further is admitted or claimed at any depth; the tree drains and reclaims from the leaves up; a late reply for a fenced attempt is still recorded, as the attempt’s outcome, and reopens nothing. A root’s own residents learn of a cancellation committed by another hand through a restart or through the fence on their next transition, not by being told.

Restart. The record is the execution. An incarnation booted over it — in process, a new runtime over the same git record, a new routed record over the same two memories, or a new process — rebuilds only what is unfinished, under the original ids: no execution is admitted again, no step registers or activates twice, no completed member runs again. An attempt claimed by the incarnation that died is reconciled before anything is delivered: a durable adapter that recorded the effect answers from its store and the attempt is not performed again; a performer that truthfully has not acted is delivered the attempt again, once, under the same id, and the record says so (effect.redelivered). The completed record rebooted gains no row and asks no adapter. Every fact of an execution is routed to the ledger (one memory), so a reconstruction of the routed record keeps their order.

Fencing. Under a lease, a holder whose token is not live commits nothing: its replies are refused and written nowhere; with the lease back, the same replies go through and the run completes as every other.

Reclamation. A settled execution dissolves every resident of its tree, leaves first; repeated completed runs leave nothing alive. What grows is the record.

Logical exactly-once, apart from external-effect reconciliation. One effect.requested per attempt, ever; one attempt.outcome; one work.settled on that attempt; one settlement per execution. Whether the external effect happened once is the adapter’s to say: an adapter whose store holds the invocation is not asked again; one that cannot say leaves the claim resulted unknown and the attempt waits for a person (card 12c); one documented idempotent is replayed. The record distinguishes the cases: a redelivered attempt has its effect.redelivered row and, in the adapter’s store, two invocations under one id.

The process boundary. dna/tests/conformance/runner.hl is the public assembly (Dna.run_workflow over a GitJournal) with a durable service adapter whose store is a file; the fixture builds it with the hale under test, runs it as a separate process, kills it with SIGKILL the moment C1’s attempt is admitted, the moment it is claimed, and the moment its adapter has recorded the effect but before the outcome landed, and runs it again over the same record each time: the assembly resumes the admitted root, the attempt keeps its one admission and one claim, the redelivery is recorded once, the store is the outcome, D runs once, the root settles last. Run once more over the completed record, it runs nothing.

Journal (ordered append with an expected revision, read by index, chain verification), Coordination (leases with fencing tokens) and Receipts (content-addressed store and read) are interfaces in the core. The git-backed implementations are the ones an assembly wires for an organism; the in-memory ones exist for tests. Protected bodies go through MemoryVault to memory’s sealed store instead (ProtectedBodies, PqProtected, which files, reads and erases only through receipt_file, receipt_read and receipt_erase; GH #606, #985).

The record has an API of its own (GH #646 stage 0). Record is the one boundary the git-backed GitJournal, GitReceipts and GitLeases and the host’s sync, reconcile, body lease, identity and exchange mailboxes all stand on, with a vocabulary that never names the tool: chains of rows appended under compare-and-swap at a head (journal; exchange/<identity> for a mailbox; remote/journal for what the remote held at the last receive), a row built beside every chain and swapped in later, bodies kept by the digest of their content, cells swapped by version (a lease, the published identity), pointers naming an object the repository already holds (candidates/<mutation>, revisions/<rev>), families received from and shared with a remote, the signer of a row, and the dna.* settings. GitRecord is the one implementation over git plumbing — the only file in the core and the host that spells git for the record; the genome’s own git in workspace.hl, verification.hl, org.hl and the host’s genome.hl is the Structure, not the record. MemRecord is the one for fixtures. hale dna init and upgrade seed the record through the host (graph-ingest, design-upgrade): the driver keeps no record code of its own.

A reload never shortens what a reader holds (GH #748). The chain only grows — a reconcile builds beside the ref and swaps in one whole chain, so the ref never points at a record missing a row it held a moment before — and a reader is held to the same rule. A chain that reads back shorter than the rows a GitJournal already holds, or empty, or with no head at all, is a read of the record that failed, never a shorter record: git unable to run under load, the repository unreachable for a moment, a tool that could not be forked. Every read is made before a row is dropped, and one that comes back short leaves the rows, the head and the text as they were, counts itself (reads_refused, with last_error naming the read), and answers refresh() with “nothing moved” — so the next refresh tries again, and an append whose reload could not read the chain ends as the i/o failure it is rather than re-offering a stale head two hundred times. The view a reader holds is therefore stale at worst. It used to be emptied first and refilled from those reads, so one failed read left an organism holding no record at all, in silence, and everything it derives from a scan of its own record began again from nothing: how often a source has raised a concern, which Review is open, which Task is in flight.

A head read tells an absent chain from a read that failed (GH #961). Record.head(chain) answers a Head: ok when the read happened, then absent for a chain that does not exist or id for its head; a read that did not happen is ok: false with why, and no caller takes it for an absent chain. GitRecord reads absent only from rev-parse -q --verify exiting 1 with nothing on either stream. Every caller that compares a head it read earlier — an admission fence, an API response’s final check, a writer’s compare-and-swap — refuses on a failed read with the reason (record_unavailable, or an io: append error), never treating it as a Record that did not move. A CLI verb says the record could not be read rather than that there is none, record-head answers none only for a record known absent (the driver seeds on it), and a receipt is not written again on a read that did not happen. Both used to be the empty string. hale dna sync refuses, with the chain untouched, when either head cannot be read, or when a round of its reconcile reads either head behind where the previous round left it; it pulls into an empty clone only while the ref is still absent.

Every external dependency of the DNA is declared the same way, one interface in the core with one implementation over the real thing and one in memory: Infrastructure (a body’s database, supervisor and credentials; GH #647) and Transport (how a head reaches a body) in infrastructure.hl, Forge (a code-review host; GH #648) in forge.hl. Their memory implementations exist; the host still drives compose, systemd, ssh and gh directly until each is moved behind its interface.

The organism is an organization written in Hale (GH #566 F2): a program at dna/org that hale dna init generates and hale dna run runs. The application it oversees is not modified by init and contains none of it; it is observed like any Hale binary. The manifest declares two environments, the application’s and the organization’s ([claims] no_base = true; each adopts its own law).

  • Positions are loci; routing is the bus. Board (the human authority: intent enters through it, escalations and reports leave through it, it owns every grant), Leader (a model-backed position holding the project’s grant: it decides the Reviews inside the grant by reading the source diff and the semantic diff, and its verdict is a model call with evidence), and the substrate Dna (the record, the gateways, verification, the editing position, the Reviews). Its work system hands the agent kind to the legs’ relay (agent: dna::LegRelay, with agent_reconciler: dna::RelayReplay beside it): a judgment or an analysis is answered pending and a leg claims it through hale dna work (GH #946); no agent performer runs in process, and the law’s positions group names the relay. A Review is announced as a typed ReviewRequested fact carrying what a deciding position needs.
  • Authorities are ranked: board (4, maintainer is its older name), leader (3), supervisor (2), reviewer (1). A claimed authority satisfies a requirement of its rank or below; an unknown name satisfies only itself.
  • Who decides. OrgPolicy: a change that touches law, widens effects or crosses ownership, or is of class organization, constitutional, process-policy or topology, requires the Board; a change outside the grant (disposition escalate) requires the Board; everything else inside the grant requires the Leader. That required authority is the fallback: it decides only a Review whose route defines no signers.
  • Signers from the route (GH #1089). A Review is opened by a definition, and the definition says what it requires: change-deliver’s Review (a mutation’s, a prepared source’s) requires what hale dna route computes for the paths its candidate changes against its base; practice-ratify’s (every proposal the Board decides — a practice, a hold, a hole, a binding, an edge) requires the board position under the purpose. The organization asks memory’s graph when it opens the Review and records the answer once as review.routed <review> {definition, signers, gates}; that row is what the Review requires from then on, a restart included. signers is every position the route names with the holders it has then, <position>=<holder>,<holder> … — a position an organization holds is held, for signing, by that organization’s members (GH #1123: the route expands the organization into its members, each once), so any one of them signs for it; gates is the gates guarding the change, and fallback whether the route left some path to the fallback. A route that names no held position defines no signers: with no memory named, or before anyone holds a signing position (the Board’s own first holder is ratified this way), the Review keeps its required authority. A change-deliver Review’s fallback is recorded too, and final — review.routed with no signers and why (no memory is named, no position the route names is held), which hale dna review <id> shows; a practice-ratify Review writes nothing on its fallback and is routed again each time it is rebuilt (a restart), so a Board seated since signs it, and rebuilding a record’s proposals appends nothing. The Review is not opened — and reconciliation asks again — when memory is named and does not answer, when git cannot name the paths the candidate changes (no base, or a failed diff), or when the record does not take its routing; so no Review opens on its claimed authority alone because memory blinked, and no change routes as one that touches nothing. The route is recorded before a post-review candidate is applied. With signers, a verdict is admitted only from a holder of a required position, whatever authority it claims (reviewer <who> holds no position this Review requires (<positions>)) — or, where the route left a path to the fallback, from one claiming the Review’s own required authority; a verdict counts toward every requirement it meets. The author is still refused, except on a governance Review (a practice, a hold, an opening, a graph proposal) requiring the Board, which admits its author’s verdict; a mutation’s Review refuses its author whatever authority it requires; one rejection or revision from a required position settles it; approval settles only once every required position has approved — one holder of two positions signs for both — the required authority where the route left it a path, and every gate has a passing run at the candidate, as the forge reported it. An approval that settles nothing is answered all the same, review.signed <review> {by, awaiting}, and hale dna review <id> shows what it awaits. Holders are routed again: while a routed Review is open the organization reads who holds its positions on each reconciliation, and when that changed — a position filled after it opened, a holder retired — writes review.routed again (the latest row is the requirement) and the Review takes the new holders; the positions stay the route’s, and what was signed stays signed. The Leader never decides a routed Review. A gate’s run is the forge’s (GH #1161): a gate is a workflow’s job, and its run is the job’s check run at the candidate, which the host reads from the forge (Forge.checks: GitHub’s check runs for the commit, a file forge’s <n>.checks in fixtures) when it syncs the forge, and records once per run as gate.observed <review> {gate, sha, conclusion, url, forge, run} — completed runs of the gates the Review’s route names, matched by job name (a matrix leg job (…) and a reusable workflow’s job / … are the job’s). The host’s forge sync writes that row, and it is the only run a Review weighs, and only from the forge its github.pr row names: a run at another commit, one whose conclusion is not success (skipped included), another gate’s, or another forge’s counts for nothing, and a verdict cites no run (a receipt anyone files is no gate’s word; --evidence is refused). The row is trusted as every record row is, by who may write the record. When such a row lands, the organization tells the Review (ReviewRouted, on its reconciliation, once per row), and the approvals it holds are weighed again: with every gate’s passing run in, it settles on the last of them. A Review rebuilt at a restart admits the plain verdicts the record holds for it again, so the approvals given before the runs survive it. A prepared source’s Review (an organization change) takes the route’s positions and no gate. A command verdict (command_id) is decided once by the same rules: redelivered after a lost response, or asked of the Review a restart rebuilds from review.command_decided, it answers the decision it made and writes nothing. The typed dna.review.verdict command applies the same standing, over the latest review.routed row, in every Review profile (a practice, a binding, an edge, a prepared source), naming its reviewer by the principal; its refusal is forbidden on the wire, and the admission’s result carries a why naming what the Review requires, as the Review’s own refusal does; the plain path (hale dna review <id> approve --as <who>) names its reviewer as the row’s writer says, so under dna.trust = signed the position check is as strong as the writer’s signature.
  • Owners (stage B2, GH #664; the graph’s, GH #1123). A shared record’s org chart has one owner per organism position: the firm whose controller admits intents for it. The org chart is the graph’s (The repository’s graph): a firm is an organization node, it owns a position by holding it (holds(position:org, organization:acme), holds(position:org/collections, organization:north)), and a person is its member by holding the organization (holds(organization:acme, alice)). A position no organization holds takes the owner of its nearest held ancestor by path (org/collections/late takes org/collections’s, up to org), and one under no held ancestor is unowned — admitted by no controller. A graph in which no organization holds anything is one owner, the organization itself, and nothing about a single-owner organism changes. The organism’s Ownership is built from memory’s graph_ownership() (JSON [node, holder] pairs of every live holds edge an organization is in; memory not answering is no ownership, so a view that may have moved is dropped), read again at rehydrate and on every reconciliation, so the Ledger’s gate (org_members) and every admission read one projection. A body over a shared record says which organization it is (git config dna.owner acme, the bare name of its organization node, carried as HALE_DNA_OWNER); a body that names its organization admits nothing until memory says what it holds — it fails closed. The host’s run and dev read which organizations exist straight from the record’s graph.node rows of kind organization (the record is the membership’s source), so a shared record is known as one without memory: over a record that states organizations, a body that names none does not run, nor one that names an organization the record does not state; and run of a body that names its organization needs the ledger adopted. The host’s admission helpers read memory as a head does (HALE_DNA_MEMORY_DSN_HEAD, else the spine’s DSN). A body admits an intent (Intent.to, the position it is for; “” is org) only for a position its organization owns, refusing one offered to it for another’s by name (intent.refused: “not this organization’s to admit”). A head never offers such an intent to the body beside it: hale dna task create --to writes it to the record for the owner’s controller, the host relays only intent.requested rows for positions its owner holds, and status lists the rest as [unadmitted] with the owner. Ownership and membership change like any other hold: proposed (hale dna fill <position>|organization:<name> <holder>|organization:<name>) and ratified by the Board (practice-ratify, which requires the board position); no quorum of owners approves anything — every approval is the route’s (Signers from the route), and a position an organization holds is signed by any of its members (a Review’s wire form keeps its approvers field, always empty). There is no verb yet to propose a new organization node: a fixture or an operator writes its graph.node row. Ids and effect claims carry their owner (GH #666): a controller mints in its own namespace — acme:t3, acme:m2 — and claims an apply as acme:apply:<candidate>, so two controllers never contend for one id, each restores its count from its own births alone, and the ledger’s unique constraint on a claim kind decides a first claim with no coordination between them: a task.born the Ledger answers claimed is minted again under the next id, and nothing is refused. With one owner nothing is prefixed. Work that crosses owners is a transfer inside the one ledger (GH #667), #615’s rule applied between owners: a plan that hands a job to another owner’s member appends task.transfer_requested <task> {owner, to, assignee, …} in place of task.handed, and the Task waits; a member of that owner accepts it (hale dna task accept <id> --as <who>, task.transfer_accepted, which memory takes only in a member of to‘s name) and their controller then appends task.handed in its own name, naming transferred_from and who accepted, so completion is admitted in the assignee’s name as always. Nothing settles on the request alone. Every owner runs a body over a shared record, and every body projects, carries out and erases, none coordinating (The spine). Each owner’s heads write as their owner’s role, which writes only in its owner’s members’ names (The owners’ roles); who the person is remains the head’s to establish — its issuer under dna.principal = oidc. With one owner, the head’s role writes as any person. The organization that owns org is ownership.host_owner in the API’s organization read (empty with one owner), which builds ownership from memory under the head’s role; the owners map’s host = <owner> (GH #669) went with the map. An organization source published by a named source owner is published only when that owner owns org in the graph.
  • The foundational law (dna/org/law.hl, generated, extendable, never weakened): nothing applies except through the substrate (forbid reaches(positions, effects(genome_apply)) avoiding substrate); the editing position reaches neither repo_write, worktree_io, genome_apply nor the Knowledge; the Leader’s verdict reaches the genome only through the substrate; credentials are sealed. The claim engine follows bus edges, so a position that could reach an effect through a published fact is a build failure with the path as its witness.
  • Hosts. hale dna run builds and runs the organization with iris attached; hale dna dev runs the application under the same host too, rebuilds and restarts it on an apply, records expression.restarted in the host’s name, watches the window and reports on the nerves. The host writes org.pid and app.pid under .hale/dna. Under run alone a restart request is logged, not answered: expressing an application deployed elsewhere is a deployment gateway’s job.
  • One body per owner: the body lease, and its epoch (GH #665). A record admits one body per owner at a time — one body when the organization is the one owner (bounded attachment; the initial controller model). Over a shared record (Owners, above) the lease is a row of memory’s claims table per owner, owner/<owner>, taken like the single body’s body row, so two firms’ controllers run side by side over one record and a firm’s second host waits on its own firm’s lease alone; a shared record runs no body until its ledger is adopted. The lease’s token is an epoch: it rises when the lease is taken (a takeover, a forced claim, an expiry) and holds through renewals, and the host hands the body it starts its lease and epoch (HALE_DNA_LEASE, HALE_DNA_LEASE_TOKEN), which the body’s MemoryLedger checks before every append: a write under a lease that is not held, is held at another epoch, or has expired lands nothing (fenced: …), so a controller that survived a partition writes nothing after its replacement took over, whatever it still believes. A head’s or a host’s write is a request, which the spine admits. Failover is within an owner; no owner’s positions pass to another on a timeout. The rest of this entry describes the lease as one body’s; it holds per owner. A record admits one body of each. The host takes refs/dna/lease/body before it builds or runs anything — at the record’s remote when it has one, so two hosts on two clones fence each other through the remote itself (the lease blob is pushed with --force-with-lease, git’s compare-and-swap), in the clone otherwise. The blob is a GitLeases lease (holder\ntoken\nexpires\npresent), the holder user@host:<clone>, so the same clone restarting is the same holder and takes its lease back at once; a second clone is refused with who holds it and exits 3. The lease lives 30s and is renewed when a third of that is gone; the host asserts it at the top of every tick, before it relays, restarts or applies, and stops itself (exit 3, the organization with it) when the lease is someone else’s or released. It proves the lease again, renewing it, the moment before it starts a process — the organization and the expression at startup and at every restart — and once more before relaying after the tick’s sync: a build or a sync that outlasts a takeover starts and relays nothing, and the host exits 3. The renewal and the fence do not wait on the host. Beside the host runs the body fence (the host binary’s body-fence), which renews the lease every 10s with every git call bounded to 8s — and to the deadline it last proved (the lease’s expiry less 5s, .hale/dna/body.deadline), so the calls of one renewal together cannot outlast the lease — and writes what it proved to .hale/dna/body.fence.status, each line prefixed with its host’s pid; the host reads its own lines there instead of renewing. The fence’s mandate is .hale/dna/body.fence, naming its host’s pid: a fence whose mandate is gone or names another host (a host restarted on the same clone) stops and kills nothing. When the lease is someone else’s or released, when the fence cannot prove it within 5s of its expiry, or when the host is gone, the fence kills the organization and the expression (by their pid files), each with every process it started — the tree and the session frozen, then killed. Every process the host starts begins a session of its own (dna::in_new_session: setsid(1), or perl’s POSIX::setsid where there is none), and a tool keeps its session through everything a process group does not survive: run_tool gives each tool a group of its own, and a tool whose organism died is reparented, but both stay in the session its organism began, and only a tool that calls setsid itself leaves it (#970). Then every process carrying the body’s mark (HALE_DNA_BODY=<holder>#<token>#<host pid>#, set in the environment of everything the host starts and inherited by every tool they start) — the second net, for that setsid tool, where the machine discloses environments — and says why (the host does the same, before it stops the fence, on every exit it takes once it holds the lease: a failed build, a refused start, its organization’s exit), and the host exits 3 when it next looks. The session reaches a reparented tool on every POSIX machine; the mark needs the machine to disclose another process’s environment, and what a machine will give differs (#638). body_scan says which it has: proc — Linux’s /proc/<pid>/environ; ps — ps -E, proved at the time of asking on a child of the call’s, since the answer is what the kernel discloses about another process and not whether the flag parses (macOS shows a process its own environment and, with SIP, no one else’s — GitHub’s runners do show it); or none, where the fence still stops the sessions — every tool the organization and the expression started, orphaned or not — and only a tool that began a session of its own is out of reach, which the host says at startup (#970). A host blocked in a sync, a build or an observation window therefore cannot keep its organism executing past the lease; while the remote cannot be reached the lease is kept unrenewed until then: a partitioned body executes nothing past its TTL. Taking the lease is a row (body.claimed <holder> {token, forced, by}), giving it up at exit another (body.released). hale dna body reads the lease; hale dna body claim --force releases a live lease that belongs to a body that is gone, as a body.claimed row with forced: true in the forcer’s name (that body stops when it next asserts; the next hale dna run takes the lease); hale dna body release [--force] releases it explicitly.
  • The combination is detected, never stored. An organism is one point in a space of independent capabilities — where the body runs, whether a hosted head exists, whether a fleet expresses it, which records it is connected to — declared by its pieces. hale dna profile prints one line per axis from the pieces alone: the record’s remote, the body lease and its last tick, [dna] fleet, the knowledge store’s environment, dna.trust, dna.github; the status projection carries a profile: and a body: line. hale dna new --profile local|remote-body [--remote <url>] [--body <user@host>] sets pieces (a remote, dna.body); the profile name is not written anywhere. Profiles are examples of combinations, not a closed set; the toolchain refuses only what breaks a commitment (a second body on one record).
  • The body on a server. hale dna body provision <user@host> [--dsn <url>] [--dir <path>] [--dry-run] makes a body over ssh, in order: the toolchain hale.lock pins (installed with the site’s installer at that version, verified, or stop), the record’s remote cloned (fetched into a clone that exists), vendor/dna and the record brought up, memory’s database from dna/compose.yaml or a DSN (which goes into the body’s env file as HALE_DNA_MEMORY_DSN_OWNER, never the record: the unit’s hale dna dev migrates with it and hands the host only the spine’s DSN), and a systemd user unit hale-dna-<project>-<record> supervising hale dna dev . --no-iris — on one server the body is the organization and the application under one host — with Restart=always (GH #1026): a failure flows up one more level, and a node that exits to take a moved genome is started again on it (The genome pull). It stops before writing anything when the record has no remote or one local to this machine, when ssh cannot reach the host, or when the host lacks git, curl, systemd, or (without a DSN) docker compose. It records dna.body and dna.body.dir here and a body.provisioned <user@host> {dir, toolchain, knowledge, by} row. The default directory is $HOME/dna/<project> on the body, expanded by the body’s shell (the unit’s WorkingDirectory=%h/dna/<project> is the same place). The script can carry the DSN, so its file here is created empty with mode 600 before it is written, and removed once ssh has read it. --dry-run prints the exact script. hale dna body start|stop|logs [--body …] reach the unit over ssh; stop succeeds when the unit is no longer active, and says so. Postgres, Docker, ssh and systemd are the reference setup; the definition admits other implementations, and names where they plug in (GH #647): Infrastructure — a body’s knowledge database, its supervisor (a unit installed, started, stopped, asked, read) and its credentials (put by name, never read back) — and Transport — how a head reaches a body (probe, execute) — are interfaces in the core. ReferenceInfrastructure in the host is compose, a systemd user unit and the env file, reached through SshTransport (HALE_DNA_TRANSPORT=ssh, the default) or LocalTransport (local: every script runs in this machine’s shell under HALE_DNA_TRANSPORT_HOME, which is how the fixtures exercise a body with nothing stubbed on PATH for ssh); MemInfrastructure and MemTransport are the core’s fixtures. The provisioning script is composed from the database’s and the supervisor’s own fragments, so a body without systemd gets another supervisor’s unit by implementation, not by if. Nothing in the host spells ssh, systemctl, journalctl or docker compose outside that one implementation.
  • Secrets. hale dna secret set <NAME> [--body <user@host>] reads the value from stdin — never argv (a NAME=value argument is refused), never the record — and fills that name’s slot of the vault (The organism’s secrets; mode 600, the newest value) on the body over ssh’s stdin, or on this machine. A name is a slot the organism requires — FORGE_TOKEN, OIDC_CLIENT_SECRET, or a credential dna/org/models.hl or dna/org/work.hl names (HostedCredential { key }; the body’s own credential check reads models.hl alone, so a legs key never stands in for the organization’s) — and any other is refused. No process’s environment carries the value: the part that sends it reads it from the vault. secret rotate <NAME> is the same for a name already set. The record gets secret.rotated <NAME> {where, by} and nothing else. At start the host checks the credentials its catalog names (HostedCredential { key } in dna/org/models.hl): when none is in its slot of the vault (model-<key>, the one source a model key has; no environment variable is read) it says so and appends body.credential_missing model {any_of}; status and board carry “no credential for the model” until a start finds one and appends body.credential_present.
  • What a body holds on a machine is bound to its record (#635). <record> in the unit’s and the env file’s names is the first twelve hex digits of the record’s identity (its journal’s genesis), so two records whose directories share a name on one machine never share a unit or a credential. Everything else a body reads lives in its clone: the dna.* git configuration (the OIDC member mapping among it) and the knowledge schema, whose ownership is checked against the record (GH #613); the hosted head and the node serve the clone they run in.

A change of class organization edits the organization’s own seed (Mutation.seed, dna/org), through the same pipeline as a change to the application: a worktree, the editing position confined to that seed, verification of that seed (its own base artifact cut at the same moment), the semantic diff of the organization (positions are loci, routes are subscriptions and publications, capabilities are effect classes), a Review that is the Board’s. Applying it restarts the organization (the restart request names the seed; the host answers one for dna/org by rebuilding and restarting the organization itself, which records expression.restarted at birth); the window then judges the new organization.

Growth is initiative, not reflex: when one source raises pressure appendage_threshold times, the assembly journals appendage.proposed and — with initiative on — proposes a growth Mutation of the organization’s seed (appendage.candidate names it). Nothing is grown until the Board approves the candidate commit.

hale dna board is the Board’s queue: the Reviews only it can settle, the proposals, the last report. hale dna report appends report.filed, in the name of whoever asked, summarizing the record since the previous report (proposed, reviewed, applied, retained, rolled back, rejected, escalated, pressure, proposals, model calls and cost, settlements). hale dna pressure lists the pressure raised and answered; hale dna pressure raise <source> <what> appends one pressure.requested row, which a node relays on PressureRaised (dna.pressure.raised) until the organization answers it.

SourceEditor.perform plans the files an objective is about — every listed file the objective names, else the quick tier’s plan from the listing, one file per line; only a listed file is ever a target — edits each (one model call per file, the request naming the file as target), formats and checks the seed that holds the edited files, and, when the check fails, tries again with the diagnostics in the prompt, up to max_tries in all (default 3). Each try is an attempt id (<work>/a<n>), so every try’s model calls are evidence in the record. The result names the files changed and the tries taken; a proposal that does not check within the bound is a failed Attempt with the last diagnostics.

The listing is the .hl files under the grant’s root, by path from it, four directories deep; hidden directories and the organization’s own (dna, vendor) are not listed, so a grant over a repository’s root reaches the seeds under it (api/todo.hl). When no file is found the Mutation fails naming the cause, not the grant alone: the grant lists no .hl file (no model is asked), the model could not be asked, or the model named a file the listing does not hold (SourceEditor.located_why).

Which seed is checked. A root that is itself a seed (it holds a .hl file of its own: an application’s organization; the root hale.toml that hale dna init writes in repository mode is the organization’s environments, not source, and does not make a seed) is checked and formatted whole. Otherwise (a repository, whose code sits under its seeds) each edited file’s seed is its nearest ancestor directory under the root that holds a hale.toml and a .hl file, else the file’s own directory; each seed is formatted and checked once, in order of the first file edited in it, the first failure’s diagnostics (named from the seed, not the worktree) feed the retry. The candidate’s verification follows the same rule: when the Mutation’s seed is the repository’s root (.) and the root is not a seed, fmt --check, check, verify, test and the base topology run in the seed of the first file the candidate changed.

The organization’s models are a catalog in source (GH #583 M1): dna/org/models.hl, generated by init and owned by the project.

  • The seam. ModelBackend (identity, adapter, price_micros, model_name, allows, capacity_bytes, complete) is what every adapter implements; every call publishes ModelCalled, which the substrate journals as model.called with the time of the call (at). One vocabulary (GH #1131): a row’s adapter is the adapter that answered the call — openai-chat, anthropic-messages, local, harness, fake, recorded; empty when the router permitted no backend, so none answered — whether the call was made in process or by a leg (the router records it, last_adapter); legs is only a leg’s wait row, a call that was not made (refused says why, waited_ms for how long). The prompt is a receipt (GH #611): the evidence carries the prompt and the context as sent, and the substrate files each as a receipt in the record (refs/dna/receipts) under the prompt_digest / context_digest the row names — only a body that hashes to its digest, and receipts are content-addressed, so an identical brief is stored once — recording bodies: stored (or none for a call with neither). An organism whose receipts are not the record’s (a file store: a fixture, an embedded use with no record) files nothing and records bodies: none (receipts are not the record's: file). A customer-class call files no body: bodies: withheld (data class customer), and its digests are all the record keeps. hale dna history <attempt> renders, under each model.called row of that attempt, prompt <digest> (<n> bytes), exactly those bytes from the receipt, and end of prompt (the same for the context) — never a re-composition; a withheld body says so, and a body this clone lacks says hale dna sync fetches it. A wider history (hale dna history m1) names the calls and leaves their bodies to each attempt’s own, since an editor’s prompt carries whole files. The row says stored only when the store gives the body back. Routers return data, never a backend. Adapters: OpenAiChat (the OpenAI chat shape: OpenAI, OpenRouter, vLLM, Ollama; adapter: openai-chat), AnthropicMessages (the native Messages API: system beside messages, max_tokens required and defaulted to 4096, the reply’s text blocks joined, anthropic-version sent; adapter: anthropic-messages), LocalModel (the OpenAI shape to this machine; no credential, no external_model, any data class), FakeModel (scripted: answer, answer_file, answers_dir, answer_role; fail_after refuses after that many calls; turns_dir answers a request that offers tools from <dir>/<turn>.json, a tool loop’s scripted turns). A request names the model its mapping resolved (ModelRequest.model, else the backend’s own), evidenced as requested_model; it carries metadata (the attempt, the position and the holder: attribution only), which OpenAiChat sends only with send_metadata: true, and an idempotency_key, sent as Idempotency-Key (a model leg’s is <attempt>:call:<turn>). A model leg declines a Work whose mapping named a model and refused it (a tools or do Work on a model that returns no tool calls) before it asks for spend or calls anything. A hosted adapter’s complete carries external_model; its credential is a sealed HostedCredential that names its key (key, whose vault slot is model-<key>; GH #989, the vault is the one source a model key has, and no environment variable is read) and its scheme — bearer (an Authorization: Bearer header) or x-api-key — and never returns the bytes: the credential reads its slot and composes the header in the one statement that sends the request, no adapter does. An API error is refused as http <status> <the API's message>; a transport failure as http 0 <kind> <detail>.
  • The harness. HarnessModel runs an installed coding harness (command: claude, or codex with output: text and exec --skip-git-repo-check --sandbox workspace-write, because a non-interactive codex is read-only by default and would answer without ever editing its export) per request with its own tools on, as a backend that works_in_place: for a source-editing request the editor makes an EXPORT of the Mutation’s worktree (a plain directory with the same files, never .git, never .hale), runs the harness with the export as its cwd and the whole objective as its prompt, and imports the export’s diff back under the grant — a file that differs or is new is written through the tools, one that is gone is removed, a change outside the grant is counted (outside_grant) and left behind; files_changed is derived from that diff, never from the harness’s answer. A file the worktree does not have is new whatever it holds, empty included. A written file’s parent directories are made under the grant first, so a harness may add a module in a directory the worktree does not have yet, and an in-grant write or removal that fails is the attempt’s failure (the import was incomplete: …), never a candidate carrying part of the change. Then the same fmt, check, retry (the diagnostics in the next prompt) and assessment as the file-by-file flow. An answer-only request (a review) runs in an empty directory of its own. complete carries external_model and harness_run; evidence is one model.called row per call with the harness’s own cost summary (total_cost_usd, usage), tool_grant: harness @export, and confinement=<kind> in params.
  • The boundary is stated exactly. The export is not a sandbox: a process whose cwd is the export can still reach whatever the operator’s account can. What DNA guarantees is that the genome is unreachable from the harness process. A Confinement (kind, available, wrap(argv, cwd, mask)) supplies it: Bubblewrap (Linux: the filesystem as the operator sees it — the harness’s own home state, the toolchain, the network — with the repository and the worktree replaced by empty tmpfs mounts, --die-with-parent) or NoConfinement. The genome is the model’s own, not the request’s: HarnessModel { genome, genome_env } reads it at birth (the host exports HALE_DNA_GENOME), every call masks it, and a request’s mask adds what that call knows besides — the worktree an editor exported. Every verb the host runs is given HALE_DNA_GENOME, not only the organization under run and dev: hale dna models probes the catalog in a process of its own, and a confined harness with nothing to mask is refused. An answer-only role (a review, a classification, the catalog’s probe) carries no request mask, so a genome on the model itself is what makes the guarantee true for them. A confinement with nothing to mask is refused, not run, so the claim and the fact cannot diverge. A harness whose confinement is unavailable is refused (unconfined harness not allowed) unless the org chart’s allow_unconfined says otherwise; the evidence records which it was, and masked=<n> how many paths the boundary covered. On every platform the assembly also checks that the repository’s head and the worktree’s head did not move during the attempt and fails the Mutation (mutation.failed: the genome moved during the attempt) if they did: not a boundary, but a bypass becomes a loud refusal. Outside the genome the harness has exactly the operator’s account, which is what running it by hand has. The Repository interface names its root for the mask.
  • The tape. RecordedModel { dir, mode, inner } wraps any backend (dir_env / mode_env name environment variables it reads into dir / mode at birth, so a catalog function computes no string: dna/FRICTION.md F.17). The key is sha256 over the fields that identify a request — role, the inner’s name, the model the request asks for (the one its mapping resolved, else the inner’s), the prompt and context digests, the data class, the grant normalized (… @grant: its path is where it ran, not what it was) and, for a backend that works in place, a digest of the workspace’s starting tree; a tool loop’s request adds digests of its tools and its turns so far, and its entry keeps the tool calls the model asked for, so a replay follows the same turns. An identical request made again in one run — a retried Work planned a second time — is keyed by its occurrence as well (occurrence=n for the n-th; the first keeps the plain key), so a recording that got two answers replays both in order, and a miss on a later occurrence says it was never recorded. record makes the tape’s directory before it writes anything into it, and an entry is counted only once every file of it has landed: a patch or an entry that could not be written is a refusal (cannot write the tape: …), because an entry claiming a patch that is not there replays as a miss on a request the tape appears to hold. record forwards to inner (whose evidence is the call’s) and writes <key>.json with every keyed field in clear and the answer, and for a workspace <key>.patch: what the backend changed there, before any import, so a replay reproduces the edit and everything after it runs for real. replay answers from the directory (evidence adapter: recorded, params: tape=<key> mode=replay), applies the patch to the workspace, and refuses a miss naming the request and, when an entry shares the prompt digest, the fields that differed (tape miss <key> (role …, backend …, …): nearest <key> differs in: context_digest tree). Replay is pure; record reaches whatever inner reaches. Anything that varies between runs must stay out of the key, and recording finds what does: the editor therefore strips the worktree’s path from the diagnostics it feeds back. The tape is a checked-in fixture, not a store. It proves how the organization handles recorded outcomes; model quality is only ever tested by a fresh run.
  • The fixture. dna/acceptance/trio: a gateway (the root seed), an api and a worker under a plan of four instances on two nodes with routes between them and claims across them (require_publishes on the gateway and the api, forbid_reaches gateway → worker avoiding the api). Its organization, with every backend a RecordedModel over the real adapter (dna/acceptance/trio.fixture/catalog.hl; mode from HALE_DNA_TAPE, the tape from HALE_DNA_TAPE_DIR), is driven by scripted asks end to end: a change to the gateway approved and deployed to both nodes, the organization grown by a supervisor under pressure from the worker and restarted with it, and a change to one service that still checks but breaks the fleet’s claim denied — every model call answered from the tape, keyless. It is the acceptance for every later change to the organization; HALE_DNA_TAPE=record with a key re-records it.
  • The catalog is source. A backend is a constructor function (frontier(), fast(), desk() …); a position’s router is a function composed from them (leader_models(), editor_models(), agent_models(), the model leg’s, behind the performer in dna/org/work.hl); the organization’s main takes each position’s router from the catalog and names no adapter inline. A new position is one more function; a provider switch is one file; hale check validates it and the law keeps the concrete types at every boundary.
  • Discovery. init and new look at the environment (ANTHROPIC_API_KEY, OPENAI_API_KEY) and PATH (ollama, whose first listed model becomes the desk model; claude, else codex, becomes harness()), write the catalog for what is there (AnthropicMessages with claude-opus-5 / claude-haiku-4-5 when its key is present, else OpenAiChat with gpt-4o / gpt-4o-mini, a placeholder key when none is set; with a harness the editor’s and the agent’s quick tier is the harness, and with no key every model-backed slot is), and print what each position was given. Nothing found is fine: a hosted backend without its key is not permitted, and every Review waits for the Board. upgrade writes a catalog for an organization that predates it and says what to point at it; it never edits the organization’s main.
  • The budget has one owner. The catalog declares org_budget(), a BudgetPolicy (window: day, week or none; allowance_micros, 0 = unmetered) as data. The substrate owns the one Budget: every model.called row is accounted at its time, the counters are rehydrated from the record at birth, and when a window is exhausted intent is refused (intent.refused: budget exhausted (spent … of … micro-dollars this day in … call(s))), budget.exhausted is journaled once per window (detail, spent_micros, allowance_micros, window, at), the Board is told, and a Review is not announced to the model-backed positions — it waits for the Board, which needs no model. A ModelRouter counts what its calls cost and has no allowance of its own.
  • One gate for spend (GH #1131). Budget.admit(ceiling, now) is the gate every model-backed spend passes, in process and out: the editor’s attempt and the leader’s Review are admitted by it before a call is made, and a leg asks it for its attempt (Legs). It refuses an exhausted window and otherwise admits the smaller of the work’s own cost ceiling and what the window has left (-1 when neither bounds it: an unmetered budget, no ceiling). A refusal is the same row wherever it is asked — attempt.allowance_refused <entity> (why, ceiling_micros, spent_micros, window_micros, window, at) on the editor’s attempt, which is declined and edits nothing, on review:<id> for the leader’s Review, which waits for the Board, or on a leg’s attempt. Intent admission and the optimize pass ask it too (their answers stay intent.refused and optimize.refused). Nothing is reserved, so the window can be overrun, by a bound: legs admitted against one remainder may together pass the window’s allowance by at most the sum of the allowances they were granted plus one call each — a leg makes no call whose known price is past what is left of its grant, nor any once its calls have spent it, and only a call whose cost is learned from its answer can cross it; the owner settles such an attempt failed, naming the overrun, and journals the calls. In process the gate admits the editor’s attempt and the leader’s Review whole: their calls within are not held to an allowance, so their spend is bounded only by the window being open when they are admitted.
  • Tokens per task (GH #946). The attempt id on a model.called row is the tag its cost is attributed by: <ask>/plan to the task born of the ask (the task.born row whose body starts <ask>: ); <work>/a<n> to the Work’s task — a wf1 attempt’s admission names the task and the performer kind, a Mutation’s request names the task (the editor’s; an older record says it in the proposal’s task <id> summary); <review>/review to the task the Review’s request names, or its Mutation’s, or knowledge for a knowledge Review (k:<12hex>); optimize/<n> to org. The probe leaves no row. A call of no known shape is its own and no task’s. The projection (dna/operations/usage.hl, Usage) sums calls, input and output tokens and cost_micros under an id — the attempt itself, the ask, Work, Mutation or Review it is of, or the task it serves and every task under it (by workflow.admitted’s parent_task) — by position, keyed by the graph’s id for it, position:<name> (position:leader for a plan, a verdict and the optimize pass; position:editor for a Mutation’s attempts; position:<performer kind> for a wf1 attempt, until an attempt’s claim names the position that took it) and by <backend>/<model>. hale dna history <entity> prints usage of <entity>: <calls> call(s) · <in> in · <out> out · <cost> µ$ and the two breakdowns under the history of line, nothing when no call was made, and usage: … for the whole record with no entity; the API carries the same sums as usage on every execution, each of its attempts and every administered Task. The editor’s locate and plan calls carry the Work’s attempt id (the tape key excludes it).
  • hale dna models rule <selector> <size> [--permit <sizes>] appends model.rule <selector> in the setter’s name (--as): a selector is default, position:<name>, contract:<Name> or task:<id>, a size quick, standard or deep, and --permit the sizes a more specific selector may move it to. models category <mode>.<size> <model> [--tools] [--price-in N] [--price-out N] appends model.category (prices in micro-dollars per 1k tokens); a tools or do category without --tools is refused. models map lists the latest row per selector and per category. hale dna init seeds a starting mapping when a hosted model’s key is in the vault: model.rule default at standard (permitting every size), and every category <mode>.<size> filled by the hosted pair the catalog writes, quick by the fast model and standard and deep by the frontier one, each with tool calls and the catalog’s prices, in the name hale dna init; with no hosted key it seeds none. hale dna task create --size <size> appends model.rule task:<intent id>, an override that holds only inside the sizes the rule beneath it permits.
  • hale dna models builds the catalog beside a one-line main in .hale/dna/probe, runs its probe_catalog() from the project root with the organization’s environment, and prints one line per backend: the catalog name, the router-facing slot, the model, and the answer — ok, elapsed, cost and the first line of the reply; refused: …; or not permitted (no credential present). The organization is not started, the nerves not touched, and nothing is journaled: a probe is nobody’s Attempt.

The knowledge graph has two halves and one authority (GH #583 K1). The live half is memory’s, projected from the record by the spine (GH #985).

  • The record holds the decided half. A proposal is a document — canonical JSON with a fixed field order (kind, text, author, target, provenance) — filed as a receipt under its sha256, so a digest in the record always resolves to recoverable content. Dna.propose_knowledge(idea, target) files the receipt, appends knowledge.proposed <digest> (body: digest, review_id, kind, author, target, class, at), and births a Review pinned to that digest (review_id = k: + the digest’s first twelve hex digits; required_authority: board — ratified knowledge is the organization’s; the review.requested body carries knowledge_digest). The tower rule classifies the binding from who proposes and where it binds — a parent’s idea bound to a child is a goal, a child’s bound to a parent a concern, one’s own an initiative — and a lateral proposal (siblings) is refused with knowledge.refused and no Review. A verdict naming another digest is refused by the Review; a Leader’s does not satisfy the Board’s requirement. The authoritative event is knowledge.ratified <digest>, appended by the assembly on review.settled with approve (body: digest, review_id, outcome, decided_by, kind, target, class, at); any other outcome appends knowledge.declined. A pending knowledge Review is re-born from the record at birth like a mutation’s.
  • Binding changes preserve the item. The reviewed dna.knowledge.binding.bind@1 and .unbind@1 operations address one (idea, target, author) tuple. Independent explicit grants admit a knowledge.binding.requested fact. The Body records delivery before creating the canonical dna.knowledge-binding-change/1 candidate and its exact Review (knowledge_binding_digest, not knowledge_digest). The candidate pins the original command, grant and latest Record directive for the tuple. Approval produces knowledge.binding.bound or .unbound; an intervening tuple change or newly inactive bind subject produces a separate apply refusal. Each consequence belongs to the original request identity. Projection changes future package applicability, preserving the item receipt and earlier Work packages. A visible retired item permits exact unbinding. This is the reviewed command profile the API admits (The API, under The surface), not a universal Review requirement on the in-process Knowledge.bind primitive.
  • Relationship policy selects direct or reviewed admission. The existing dna.knowledge.edge.link@1 and .unlink@1 commands retain their exact directed (from, to, rel) identity and command encoding. A review grant instead admits knowledge.edge.requested, pinning the latest direct or reviewed directive for that tuple in a canonical dna.knowledge-edge-change/1 candidate. Its Review carries knowledge_edge_digest; the candidate is not a Knowledge node. The Body derives ownership from both endpoint receipts and requires its owner to admit all four author/target positions in this reference profile. An independent approval produces knowledge.edge.reviewed_linked or .reviewed_unlinked only while the pinned tuple basis and endpoint eligibility still hold. A changed basis or ineligible endpoint produces a separate apply refusal. A retired, ratified endpoint permits exact unlinking. Recovery follows the original admitted variant even if policy later changes mode. Native effects replay in Record order; approval alone does not establish a graph change. This policy does not add a universal Review requirement to the graph primitives.
  • Memory holds the live half. dna/core/memory_store.hl declares KnowledgeStore: scope / record, open, healthy / reopen, watermark (the next record seq to apply), begin_row(seq, prev, head) / commit_row / abort_row (one record row’s transaction, below), rebuild(watermark, head), upsert(idea, ratified_seq) (idempotent by id, which is the digest), retire, bind, unbind, link, unlink, idea(id), context_ids(target, budget) (accepted ideas bound to the target or any prefix of its path — goals flow down, initiatives stay local — in ratification order; target is one target or a space-separated set, Targets are paths or nodes, below), ranked_ids, count(what), and the structure and signals of K3, and the repository’s graph (below). Pq is Postgres (nine tables: knowledge_meta, knowledge_ideas, knowledge_bindings, knowledge_edges, knowledge_structure, knowledge_signals, graph_nodes, graph_edges, graph_members, which the migration creates and open only checks; node writes are upserts and removals delete exact tuples; rows come back as JSON built by the server, since the driver’s tab- and newline-separated rows cannot carry an ordinary paragraph; a signal counts once per record row, keyed on that row’s sequence). The projection is a consumer of the record: apply_record(store, journal, receipts) in dna/operations/memory_tail.hl walks the record’s rows from the watermark, resolves each knowledge digest to its receipt, upserts (ratified accepted with its binding; proposed and declined kept as such, never over a ratification). Each record row is one transaction (GH #1026): begin_row moves the stamp first — the watermark from n to n + 1, then graph_projection_head from row n - 1’s digest to row n’s, each by compare-and-swap on its knowledge_meta row — then the row’s effects, and commit_row lands them together; a failed effect lands nothing (abort_row), and the next pass begins the row again. A projector whose compare finds the stamp moved rolls back having written nothing and ends its pass (yielded): it blocked on the watermark’s row while another projector held it, and that projector committed the row. So any number of projectors may apply one record at once, with no coordinator, and converge on one graph, each row applied once; after a crash a projector resumes at the stamp. Nothing in memory becomes ratified except from a knowledge.ratified row: git is the sole authority; memory can lag, never disagree. The observed and inferred tier (K3) lives only in Postgres and is backed up like any Postgres; the ratified tier rebuilds from git.
  • The record is the scope of its memory. Memory is opened for one record, named by the record’s identity — the sha of the journal’s first commit, the same in every clone of the record and different for every record: that first commit carries a Record-Nonce: of 16 random bytes, so two projects created alike in the same second (same author, same scaffold, same row) are two records, not one organization to memory and the nerves (GH #1068); a record that cannot draw them writes no first row. A record made before keeps its identity (GitJournal.genesis(); scope(record) on the store before open, record() to read it back). Pq keeps one Postgres schema per record, dna_<identity>, selected for the session at open (with public behind it for the vector type), so two records on one database each see only their own ideas, bindings, structure, signals and watermark; hale dna dev’s compose database is one record’s anyway. The schema’s knowledge_meta carries a record row naming its owner, written by the migration and checked by the migration and by every open: a schema that names another record is refused (scope: schema … belongs to record …, not …), never read. A record with no rows yet has no identity: its projection opens under dna_unscoped, where nothing is ever applied, and moves to the record’s own schema once its first row lands.
  • The spine projects; readers read. MemoryProjection (dna/operations/memory_tail.hl) applies the record into the graph on the host’s tick, as the record’s spine role, on every node (The spine, above), so the projection advances without anyone asking. The fence compares heads by ancestry: memory’s stamp is a commit of the record, so every clone can place it. Nothing stamped, or the stamped head is this clone’s row at the stamp: the rows after it are the delta. This clone’s head an ancestor of the stamped head, or the stamped head a commit this clone has not received: another projector is ahead, and there is nothing to project here until the record is received (a head a clone cannot place is never taken for a replaced chain, or two nodes that append before they share would rebuild each other’s projection until they synced). Neither, of a head this clone holds: the chain was rebuilt beneath the projection (a reconcile re-appends local rows with new digests, GH #603), so the graph is rebuilt from row 0 — rebuild(watermark, head) empties it only while the stamp is still the one read, so of several projectors that see one replaced chain one rebuilds, and a projector rebuilds at most once per pass. A question memory cannot answer is a refusal, never an empty answer: a package built from failing reads, or from a projection behind the record, is indistinguishable from “there is no knowledge here”. The projection’s store is opened on the first tick rather than at birth, and the connection is asked on each tick afterwards (healthy), because an open that succeeded once is not a connection that still answers: a session dropped by a restart or a failover is re-established (reopen). A reader — MemoryKnowledge in the organism, the API as a head — never projects. A package (MemoryKnowledge.package_for(target, query): the ids included, their ideas with kind, text and author, the projection’s watermark as its revision, and a digest over the target and the ids — what is handed over, never the revision, which varies between runs and would make a tape unable to answer the same request twice) waits, bounded at 20 s (PACKAGE_WAIT_SECS), for the projection to pass the record’s last row that changes a package (an idea ratified or retired, a binding made or undone), and otherwise refuses: memory's projection is at row <n>, before the record's knowledge row <m>; the spine applies it on its tick. The organization’s knowledge package is read from memory under the spine role. The API reads the graph under the head’s role (The API, below).
  • The design, as proposals (GH #596 C). init seeds the toolchain’s practices about how a DNA organization works — the design principles, the evolution pattern, structure follows intent, standard equipment, the structural signals, signaling, the optimize pass, software delivery — as knowledge.proposed rows with receipts bound to org, one Review per practice (k:<12hex>, grouped design in hale dna review’s listing; hale dna review design approve|reject is a convenience that decides each pending one in turn, never a batch object in the record). A Review pins one digest and settles with one outcome, so the Board decides practice by practice, and nothing is ratified by the toolchain. Each receipt carries a stable name (design/<slug>) and the toolchain version. Supersession is the Board’s decision, not a side effect. Declining a proposal retires nothing (the projection keeps a ratified idea a later knowledge.declined names). A proposal whose receipt carries supersedes: <digest> retires that digest when it is ratified — the assembly appends knowledge.retired <old> — and leaves it in force when it is declined; a retirement proposal carries only that. A replacement is ratified only while what it replaces is still active: an approval of a proposal whose supersedes names a digest already retired is refused (knowledge.refused <digest>, with what retired it) rather than ratified — otherwise two replacements of one version would both be served, the first never retired. hale dna upgrade proposes each practice whose current text is not the latest proposed under its name, superseding the active version under that name (a pending or declined proposal is not a predecessor), one replacement at a time: a practice whose latest proposal is still before the Board waits (upgrade says so), and a refused one is proposed again against what is active now. The projection retires an idea by marking it not accepted: it leaves every package and stays readable by digest.
  • The operating practices, as proposals (GH #994). Beside the design, init seeds a second family, operating/<slug>: how the organism runs, which is what a leader plans within and a reviewer cites, where the design says how an organization is shaped — one store per step, row first, readings never act, legs hold nothing, deploy settles on pulse, the forge decides. Each is one paragraph, a rule a person applies. The family is seeded, listed (grouped operating; hale dna review operating approve|reject), decided and superseded exactly as the design, and upgrade seeds it into an organization whose record never held it. Its receipts carry provenance design, which marks a toolchain-seeded practice of either family; HALE_DNA_DESIGN_SUFFIX (fixtures only) appends to every practice of both. A receipt may also carry a schedule, which the practice declares when it is ratified (Schedules): operating/optimize-cadence carries the optimize pass’s, convened by the Board once a day (The optimize pass).
  • Targets are paths or nodes (seed/pull). A binding’s target is a locus path (org, org/<child>) or a graph node id (language:hale, system:dna, position:leader: The repository’s graph, below). The match rule is one query, ranked_ids and context_ids taking a space-separated set of targets: a word holding : is a node id and matches a binding to exactly that id (language:hale is not language:hale-x, and a prefix of an id is nothing); a word without one is a path and matches a binding to it or to any ancestor, as it always has. An accepted idea is in the package once however many words match it, and a set of one path is the tower rule unchanged. A node is bound from above, never beside: classify_binding calls a binding to a node a goal when its author is a path, so it is never lateral. A proposal or a binding request whose target is a node the record holds no graph.node row for (and no later graph.retired took back) is refused naming it: knowledge.refused for a proposal, knowledge.binding.refused with reason unknown_node for a binding request. The projection’s knowledge_bindings rows carry the target unchanged — no migration; a node id is just another value of the column — and ranked_ids keeps their ordering and budget.
  • The hat’s target set (seed/pull). The hat’s package is asked for a set, in this order (hat_knowledge_targets, dna/core/hat.hl): the Work’s locus path (hat_knowledge_target: org or org/…, else org); language:<l> for every written_in edge of the attached application (application.attached’s name, the node application:<name>), unless the Work is an organization change; then system:dna in their place when it is one — the Work’s Task admission carries the leader’s plan with kind organism (class organization), the same word edit_class_of reads to edit the organization’s own seed; and position:<name> for the position the hat names (hat_position_of), so a mandate bound to the position arrives with the Work. The hat’s JSON names them as targets (a space-separated string) and its digest covers them, so a hat is a different hat when its set is. The owner’s edit path asks the same set for its package, and PRACTICES (ratified knowledge for <targets>, package <digest>) names it. A reviewer’s brief is the same read: hale dna review <id> prints, under knowledge for <targets>, the names of the ideas ratified for the subject’s targets — the node a knowledge Review is bound to when it is one, system:dna for a mutation of class organization — ranked by the Review’s question and bounded by the package’s budget; without memory (HALE_DNA_MEMORY_DSN_HEAD) it says the knowledge was not read, and a subject with no node prints nothing.
  • The nodes knowledge is about, and the library (seed/pull, seed/library). init writes graph.node rows for language:hale (kind language, text Hale <toolchain version>…) and system:dna (kind system) and, for an attached application, application:<account name> (kind application, the name application.attached carries — the record named the application by that row alone before) with a written_in edge from it to the language (written_in:application:<name>|language:hale); a repository with no application gets the two nodes and no edge. Beside them it proposes the toolchain’s library: the book per chapter and the spec per section, one idea each. crates/hale-cli/build.rs generates the table (LIBRARY) from the tree at build time — no hand list, so a new chapter joins the library by existing: every docs/src/**/*.md is an idea named library/<path without docs/src and .md>, titled by the chapter’s first heading, and every ## section of spec/*.md is an idea named library/spec/<file>#<slug>, titled by the section’s heading, its text running to the next ## . An idea is bound to language:hale, except the DNA chapters (docs/src/dna/**, docs/src/parts/**, spec/dna.md, spec/model.md), which are bound to system:dna. Each is a knowledge.proposed row of kind idea, provenance toolchain, whose receipt carries the text, the title, the target, the toolchain version and the text’s digest (content_digest). The proposal’s target is the binding: ratifying it binds the idea there, so a library idea has no separate knowledge.binding.proposed row (a binding request needs a ratified subject).
  • A family is ratified by one Review (seed/library). The Board does not review hundreds of ideas one by one. The library is proposed as two families, one per node — library/language@<version> and library/design@<version> — and each family is one review.requested (group library, kind family) whose subject is the family’s own document, a receipt of kind family naming the ideas it ratifies, <name> <digest> a line, in the order they were proposed (the Review’s body carries the same list and a count). hale dna review lists a family as one entry; hale dna review <id> renders the ideas it ratifies; hale dna review library approve|reject decides each pending family in turn, and its verdict is the verdict of every idea it names. Approving writes, for each idea, the rows it would have had of its own — knowledge.ratified (kind, target and class, supersedes when the idea replaces one, the retirement of what it replaces) — and then the family’s own row, library.family.ratified <family digest> when every member was ratified, or library.family.refused <family digest> (with refused, a <name> <digest> line per refused member, and why) when the record refused any — a member whose predecessor another approved revision replaced — so the family is never reported ratified while incomplete; rejecting writes knowledge.declined for each and library.family.declined. A family’s rows land as one batch: the journal is read once, every member’s outcome is decided against that read (a member’s ratification is what a later member’s supersession sees), and the rows — the members’ in the family’s order, each retirement after the ratification it follows, then the family’s own row — are written by one append_batch at the revision they were decided at. A batch that finds the record moved is decided again against what landed, and a row already there is not written twice; a practice ratified alone is still its own one-row write. The family’s own row is what its practice-ratify execution waits for (its hat step is done once memory’s projection holds the last idea, ratified); the per-idea rows are the shape the lanes already project, unchanged. Practices (design, operating, using) keep one Review per practice, and the record stays append-only.
  • Upgrade is supersession (seed/library). upgrade proposes the library of the new toolchain version, for a record that has one, as new families (library/<node>@<new version>) whose ideas each name the active digest under their name in supersedes; approving the family ratifies the new ideas and retires the old, by the existing supersession rows. Until the Board approves it, nothing is retired: an application pinned to the old toolchain keeps the bindings of the version it was seeded at. A family whose earlier version still awaits the Board waits (upgrade says so), and a family already proposed at this version proposes nothing. A record seeded with --no-library gets no library from an upgrade. HALE_DNA_LIBRARY_VERSION and HALE_DNA_LIBRARY_SUFFIX (fixtures only) name another version and append to every text: how a test makes a later toolchain out of the one binary it has. init --no-library (and new --no-library) seeds none of the nodes, the edge or the ideas (the practices stay). language, system and application are node kinds and written_in(application, language) an edge kind (The repository’s graph).
  • The using practices (seed/library). A third seeded family, using/<slug>, beside design and operating: how a person or an agent works with the organism — propose-review-ratify, change-classes, ask-the-leader, cut-structure, read-the-record, evidence-first, bind-knowledge. One Review each, grouped using, seeded, decided and superseded exactly as the other two (hale dna review using approve|reject), and kept by --no-library: they are practices, not the library.
  • The charter (GH #596 L). init writes dna/org/charter.hl, a function returning text like purpose: the leader’s brief, saying that it is the organism’s architect — it proposes, the Board decides — and what it must know before it plans an ask or decides a Review. Project-owned; a change is a reviewed change to the organism. upgrade writes it for an organization from before it.
  • The leader reads its brief at both moments it thinks. The org chart hands the Leader its charter and purpose (the program’s own text) and a MemoryKnowledge; before a review and before a plan it composes its brief — CHARTER, PURPOSE, the LAW as the genome holds it at HEAD, and PRACTICES from the package for org when memory has any — and puts it above the diffs. The model call carries knowledge_bindings naming the package, so the evidence of every decision that read it says so.
  • HALE_DNA_DISCOVER=off makes init’s discovery find nothing — no key, no local model, no harness — so a fixture on a developer’s machine makes the organization CI makes: one whose leader has no model that answers, whose plans take the defaults, and which spends nothing. The CLI fixtures set it.
  • An ask is admitted as a workflow, in the one engine (card 18). Dna.ask(Intent) — intent through the Board, an optimizer’s — passes the Board’s gate, the owner’s (GH #664) and the budget’s, journals intent.offered, and admits a workflow for it (workflow.admitted, the one positive discriminator, under the intent’s id as the admission’s identity — offered again it is the same execution) which the engine the substrate owns for its scope runs. With planned: true on the substrate (the generated org chart says so) the leader’s word comes first: the substrate publishes PlanRequested for the ask, the leader answers TaskPlanned, and the admission binds that word in its inputs — the objective, the kind (organism, appendage, product or person), the class applied (an organism change is class organization whatever the plan called it: the Board’s), the target, whom and under what obligation, the narrative, planned. A person’s job is admitted under the ask-person definition (one human leaf, a case the substrate hands to whom the leader named, with no second word asked — cards 16 and 17 complete it); anything else under ask-edit (one edit leaf under the Patch contract, performed by the substrate’s editor as class and target the word names — the target only when it names a file the genome has under the seed the class edits; a model’s prose is not a path — else class application at the ask’s own target; card 15 reports its outcome). An ask for a judgment (hale dna task create --judgment, the head’s TaskCreate with kind: judgment, or the face’s task form; the intent row carries kind: judgment, and the receipt echoes it, GH #946, #1144) is the asker’s word: no plan is asked, and it is admitted under ask-judge — one leaf requiring judgment under the Assessment contract, which capability-first routing hands to an agent on the first attempt (the legs’ relay: a leg claims it through hale dna work) and to a person on the second. A change routed to a position (hale dna task create --to position:<name>; the intent row carries position, and to stays the ask’s owner, the organization) is the asker’s word too: no plan is asked, and it is admitted under ask-position — one leaf requiring position under the Patch contract, the routed position bound into its requirement at admission (position position:<name>). Such a Work is an agent’s, the legs’; a claim takes it only when its lease’s position is that one: a position the graph states, taken as an agent’s, claims with position and its own id among its capabilities, which no other claimant has: the head first drops any position or position:<name> word the claimant supplied, and grants them from the verified seat alone. Its hat is the routed position’s (charter, mandate, model rule), and its outcome is a leg’s Patch, applied, verified and reviewed as any. Class organization for a child that is not the organism is a contradiction and is refused before anything is admitted (intent.refused). The execution is asked of the engine with the performer kind of every leaf of the whole bound tree, named for the exact Work — its bound id, never its key, which two Works of different steps or workflows may share — the editor’s for an edit leaf, the human kind for a person’s, else the routing policy’s choice for the Work’s own request under the Task that owns it. The tooling’s answer to an ask (hale dna task create) is the execution whose admission names that ask as its request, never the next birth in the record, which may be another ask’s. An answer that names no kind and no class leaves the defaults standing — class application, the ask’s own target — and the admission says it did not parse. Without a leader the ask is an application change. An ask whose leader has not answered yet is in planning, and asked again meanwhile is not asked of the leader twice. No task.planned, task.pending or task.resumed row is written: the admission is the plan, and the execution’s facts are the engine’s rows.
  • Work survives a restart (GH #604, card 18). The record holds an execution’s admission before anything runs; a birth the record refuses stops there (intent.refused, workflow.refused), and nothing has run. On restart the substrate asks its engine again for every admitted root of its own that is not finished — settled AND drained: a cancellation settles the root before its admitted Works have settled, and a root whose tree (its own workflow’s Works and every child’s, by their admissions’ parent_task, and every admitted child itself, before its first attempt or after its last Work settled) still owes a settlement is asked for again, so the engine’s own recovery drains it (cards 12a, 13: the responsibilities still owed anywhere in the tree are counted, and a cancelled parent whose step holds only a child member drains the child through that step, born fenced). A child reborn under a cancelled ancestor reads that cancellation from the record at its own state question — a fence published before it existed reached nobody — proposes its own cancellation, and drains like any cancelled execution; a drained record is asked for nothing and written nothing. The engine restores each from its admission and the record’s facts — under the word bound at admission, never replanned; an attempt whose outcome is recorded is answered from it and never runs again; a case handed stays a person’s and waits. A Mutation’s recovery is derived from every row it has, from its first — its request row, or its proposal when no request names it: one whose last word is the request, the proposal, an open worktree or a located file was in flight and fails, once (mutation.failed); one with a candidate, a Review or an outcome after them is left to that. Its id is counted at the restart and never minted again. A Task with no admission — a row of the shape from before card 18 — is left where it is: nothing resumes it, plans it or settles it. An intent.offered with no admission naming it is noted (intent.unrecovered) and never re-offered: work may already have run.
  • Dev relies on docker compose. init writes dna/compose.yaml (its own compose project, name: hale-dna-<project>, so two seeds on one machine never share a container; the knowledge-db service, pgvector/pgvector:pg16, a named volume hale-dna-<project>-knowledge, a host port in 54xx). Each service’s host port (memory’s in 54xx, the nerves’ in 42xx, the senses’ in 93xx) is taken at seed time: a candidate from the project’s name, stepped past any port something on the machine already listens on, as dna::free_port takes a fixture’s, so a seed never publishes on one another server holds (a CI runner’s NATS on 4222). The file is part of the genome, and upgrade rewrites it to the template, keeping the ports the file already publishes, so a clone’s upgrade regenerates the same file. With no HALE_DNA_MEMORY_DSN_OWNER, hale dna dev runs docker compose -f dna/compose.yaml up -d --wait knowledge-db and derives the owner’s DSN from the published port and the vault’s postgres-owner-<project> (The organism’s secrets), which a database made with another password adopts first; an operator’s HALE_DNA_MEMORY_DSN_OWNER wins. It applies memory’s schema with the owner’s DSN and starts the host with the record’s spine DSN alone (Memory). With compose not on PATH, or no compose file, it says exactly which it needs and the host runs with no memory. hale dna run brings up no database: beyond one machine memory is a Postgres of the operator’s, migrated with hale dna memory migrate.
  • Knowledge changes later work (K2). The substrate holds a MemoryKnowledge (budget, 8 by default; memory opened under the spine’s role), the owner’s line to memory; the editor never holds one, which the law states (group knowledge = { dna::Knowledge, dna::MemoryKnowledge }, editors_never_learn). When a Mutation opens, the substrate asks memory for the package of the change’s target in the tower — org for an organization change, org/<child> for the application, org/<child>/<seed> for a seed inside it — and journals knowledge.consulted <mutation> (target, hat, digest, revision, included_n, included, error) whenever memory is named (HALE_DNA_MEMORY_DSN_SPINE), answer or not. The package becomes the hat the attempt wears (GH #946): built by the owner, kept in memory by its digest, and named on the request as its context_digest; the editor is briefed with the package’s ideas (SourceEditor.brief: what a model is shown is the ask, then a PRACTICES (ratified knowledge for <target>, package <digest>): block, one idea per line) while the record, the commit message and the Mutation keep the ask itself; and the request names the package (knowledge_bindings: package:<digest> <id>…), which every model call of the attempt carries into its model.called row. No memory named is an empty package that says so, and nothing waits on it; memory that cannot answer, or whose projection has not reached the record’s head within 20 s, is a package refused with the reason (The spine projects; readers read).
  • Concerns (K2). ConcernRaised (source, what, severity) is a child’s signal about the part above it: an application’s own concern.raised event, which the heart lands and the spine puts into the record (K4), or hale dna concern raise <source> <what…> [--severity N]’s, each a concern.requested row a node relays onto the nerves, and the substrate admits it as an execution of concern-escalate (GH #995: The workflow catalog) whose step journals concern.raised <source> (an object: what, severity, occurrence, request, parent, task), and when one source has raised it concern_threshold times (3) it becomes a knowledge proposal by that source bound to its parent path — a concern by the tower rule — through propose_knowledge, with concern.proposed <source> naming the digest, once. A source with no parent (no /) is refused with knowledge.refused concern:<source>. Which occurrence a concern is counted from the record, never from a number the organism keeps, so a restart continues the count where the record left it; a count that starts again is a record read short, not a restart (GH #748). An execution a restart interrupts is resumed with every other; a source over the threshold with no concern.proposed of its own and no concern-escalate running for it — rows that reached the record another way — is owed a proposal that nothing but a further concern would make. At birth every such source is escalated once, by an execution whose raise step finds its raises in the record (rehydrate); a source with no parent was refused where it was raised and is not refused again.
  • The repository’s graph (GH #1085). An organization and its product are one recursive hypergraph; the org chart and the deployed process model are two perspectives over it, neither its trunk. The record holds it as rows and memory projects it with everything else: one record, one projection, no second store. The vocabulary is dna/operations/graph.hl. Node kinds: purpose, axiom, process, seed, contract, noun, deployment, practice, gate, document, witness, position, work, organization (a firm, GH #1123), definition (a workflow catalog definition a schedule convenes, GH #1143), and what knowledge is about (seed/pull): language (language:hale), system (system:dna) and application (application:<name>), which init seeds and an ingest of a repository never reads; a node’s id is <kind>:<name>. Hyperedge kinds, arity two or more, each a list of members {role, node} whose first is its anchor: unfold(parent, child), meets(contract; server…, consumer…, carrier…), names(contract; noun…), refers(from, to), constrains(axiom; shaped…), runs(deployment; process…), gates(gate; guarded…), binds, witnesses(witness; about…), holds(position, holder), reviews(position, subject) — the subject a contract, a document, a seed or a deployment: what the position signs a change to, a design document being signed too — and convenes(position, definition) (GH #1143), a position convening a definition on a cadence, written when a schedule is declared (Schedules), its body carrying the cadence (every 1d, every 90s, cron <expr>) and the schedule; its position is a position node and its definition a definition node, and written_in(application, language) (seed/pull), the codebase an application is written in — an application node and a language node, keyed by both, the edge a hat reads its language targets from. A holds edge is a seat or a membership (GH #1123): holds(position:<p>, <person>) is a person’s seat, holds(position:<p>, organization:<o>) is organization o owning position p, and holds(organization:<o>, <person>) is the person’s membership of o; its position role takes a position or an organization node, its holder is a person or, for a position, an organization node, and an organization never holds another organization (Owners). A position has one owning organization and a person one: memory refuses a second of either as a hold.refused row, and hale dna fill refuses to propose it. A pair kind (unfold, refers, holds, reviews, convenes, written_in) is exactly its two members and is keyed by both, so an unfold is one edge per child; any other kind is keyed by its anchor, so there is one meets per contract and the latest row says who meets there. An edge’s id is <kind>:<anchor> or <kind>:<anchor>|<second>, and no name or holder holds a |, so an id reads one way. A role may require a node kind (a meets contract is a contract: node, a runs process a process:); a holder is a person or an organization node; meets also carries its transport (via) and the parties it reaches that are not nodes (outside, e.g. callers). Two kinds are never graph rows: a practice is a knowledge idea of kind practice — advice unless the Board ratifies it as law (its document says law: true; one ratified as advice stays advice) — and binds is its knowledge binding; a member names a practice as practice:<digest>. The rows are graph.node (entity the node’s id, body {kind, name, text?, source?}), graph.edge (entity the edge’s id, body {kind, members, via?, outside?}) and graph.retired (entity the id). The tail checks a graph row whole before memory takes it — the kind is the vocabulary’s, the anchor comes first, every member names a node of the kind its role requires and no node twice, the arity is two or more, and the entity is the id the body implies. A row that fails is refused as a row, as a refused hold is: memory appends graph.refused <row id> (kind, why, row, by: memory) once, says it on stderr, and the projection goes on past it, so one bad row never holds back every read waiting on the watermark. A graph row inside a ratified proposal is checked and refused the same way, at the row that ratified it, and the proposal’s other rows land. A store that fails to take a valid row still stops the projection at that row: that is the database’s failure, retried, not the row’s. So does a refusal the record cannot take (an I/O failure; a race with another writer is retried first): the row is never passed without its graph.refused or hold.refused, and the next pass writes it. A node or edge is replaced by a later row with its id (an edge’s members with it) and taken out by graph.retired; a retired node’s edges stay as the record left them, and the perspectives leave its memberships out (an edge whose anchor is gone is not shown). There is one org chart, and it is the graph: its position and organization nodes and their holds edges, proposed and ratified as holes and holds (below). Who owns a position and who is a firm’s member are holds edges, never a file: the owners map (dna/org/owners) is retired, not rendered (GH #1123) — hale dna init and new write none, and hale dna upgrade removes one, noting the lines it named to be stated again as holds edges, and takes the generated ownership: field out of dna/org/main.hl (or notes it where the file was edited by hand). The organization’s generated dna/org files and the organism’s program are written beside the graph, never its source, and nothing reads a position from them that the graph states. Perspectives are queries over memory: KnowledgeStore.graph_perspective("org") is the positions in the record’s order, each with the node it unfolds from (under), its holders and the contracts and documents it reviews; graph_perspective("processes") is the processes, every meets (contract, via, servers, consumers, carriers, outside), every runs (deployment, processes) and every convenes (position, definition, cadence, schedule), each as JSON the server builds. graph_nodes_count(kind) and graph_edges_count(kind) count them; practice counts the practices proposed or ratified and binds the ratified practices that bind something, one hyperedge each. Pq.graph_ownership() is JSON [node, holder] pairs of every live holds edge an organization is in (Owners).
  • Ingest: hale dna init on a repository (GH #1090). A directory with no Hale source of its own that is no workspace’s seed is a repository, not one application: init makes the organization there as it would for an application — vendor/dna, dna/org, the manifest with [claims] no_base and [environments.org] alone (there is no application entrypoint) — cuts no topology artifact, and seeds the record with what the repository holds, as the graph, through the host verb graph-ingest, then proposes the declared purpose (GH #995) (dna/operations/graph_ingest.hl). It never derives one perspective from the other. A directory holding .hl files is a seed: with a main locus it is an application, attached as before and read into the graph too (the next bullet), and without one it is refused as before. Ingest reads the tracked files (git ls-files, paths as git holds them with core.quotePath off) and recognizes: the purpose — the README.md’s title and first paragraph, and none without a README.md, so an empty tree is an empty graph; processes — the compose file’s services (compose.yaml, compose.yml, docker-compose.*), then the compose deployment that runs them (built) — processes first, since a perspective lists in the record’s order; seeds — each directory holding hale.toml, package.json, Cargo.toml, go.mod or pyproject.toml, the outermost only (a workspace’s members are its own), never the root; a process unfolds into the seed of its name; contracts — spec/*.yaml, spec/*.yml, spec/*.md and each directory under spec/vendor/; nouns — a YAML contract’s components.schemas and a markdown contract’s CREATE TABLEs, named by the contract; a noun is its name, so two contracts that name one merge into one node, which both names; then refers, from a contract’s local $ref: '#/components/schemas/<X>' (never another key that holds such a path, never a reference that leaves the contract) or REFERENCES <table>, only to a noun the same contract names — every noun of a contract is written before its refers; documents — every .md; gates — each CI job (.github/workflows/*), named <workflow>/<job name>, guarding the seeds, contracts and compose deployment its steps name; witnesses — each FRICTION.md entry (a ## heading) and each issue it links (<owner>/<repo>#<n>), about the seeds, contracts and documents it names. What structure cannot say is written in markdown, in two forms. A marked list item: a code span opening a list item names its kind — axiom, derived, practice or law — and its first bold span is its name; an axiom is a node under the purpose, and the repository files it links to are what it constrains. (derived, practice and law are practices, proposed at record birth, GH #1091.) A declaring table: a table whose columns include Served by and Consumed by declares one meets per row — the row’s first link to a contract is its contract, Over its transport — and one whose first column is Deployment and has Runs declares a named deployment and what it runs. In a cell a name in code is a process, else a seed, else a repository path; a link is the node its target is (relative to the document); plain words are a party outside the repository; a code span inside a link is the link’s text, not a name; an Over that names a process makes it the carrier. Links resolve to a contract, else a document, else the compose deployment, else the seed holding the path. Every row comes out once, a node before an edge that names it, and the host checks every graph.* row against the vocabulary before it appends any (record-seed does the same for a seed file), so an ingest the graph refuses leaves the record as it was. A YAML mapping’s children are indented as its first child line is, never by an assumed two. Input the conventions cannot take is refused, with the reason, never dropped: an axiom item with no bold name; any name the vocabulary refuses (a |, a control byte, longer than 512 bytes) — a witness’s title, a schema’s, an axiom’s; a meets row that links no contract; a runs row that names no deployment; a name in code in a declaring table that is no process, seed or file of the repository. Nothing is appended then, the purpose’s Review included (init seeds it and the graph in one graph-ingest call), so the record does not exist and init can be run again once the document is fixed. A job’s own name: names its gate and is not a word its steps say. init says what it read: graph <n> node(s), <m> edge(s): <count> <kind>, …; <p> proposal(s). A record that exists is not reseeded.
  • Ingest: hale dna init on an application (seed/structure-2). An application’s init enriches this one path; there is no second proposer and no starter chart. The artifact’s rows are unchanged (application.attached, structure.observed per locus, topic, binding and effect class, responsibility.proposed per locus: facts the Board reads, from which nothing is derived into the graph), and they land in the same graph-ingest call as the purpose’s proposal and the graph — graph-ingest --rows <file> --app <name> --description <file> — so a graph the vocabulary refuses leaves no record. The ingest then runs over the application’s workspace root as for a repository (README purpose, compose, spec/* contracts and their description roles, documents, CI gates, FRICTION witnesses, marked lists and declaring tables), with two additions the application’s own structure gives. The application is the root seed (seed:<name>, the project directory’s name, or the application’s directory when it sits below the workspace root, where the manifest walk finds it as before): the root that is never a seed for a repository is one here, a compose process of its name unfolds into it, a CI step naming . guards it, and an application with no README.md takes the purpose init declares (dna/org/purpose.hl) as its purpose node, so the board has a place. And each API surface it serves is a contract the seed meets as server: api:<Surface>, from the toolchain’s own export (hale check --api, handed to the host as a file), one node per surface; a stream served over a hub is stream:<Topic>. A surface whose lower-cased name is the stem of a spec/<stem>.md or .yaml contract (the stem rule that reads spec/<Stem>.description.json) is that contract’s node, not a second one; a contract a declaring table already gives a meets keeps the table’s row. The requires of the surface’s members, and of a stream, are the contract’s roles: the equipment of the positions that serve it (Standard equipment, below), never an organization and never authority. init says what it read on the same graph … line, and writes dna/org/structure.hl as for a repository. A fresh application whose export is empty has two nodes (its purpose and its seed) and no edge.
  • Holes as proposals (GH #1091). What a repository cannot imply and a delivery needs, init proposes at record birth, each on its own with one Board Review, in the same checked seed as the graph (so a repository whose holes cannot be proposed leaves no record either). The holes are derived from the graph’s edges, never enumerated; a part is a process, or a seed no process unfolds into (a UI, a validator); a cell naming a seed a process unfolds into (a link, a path inside it) names that process. init proposes: a board under the purpose; under each part that serves or consumes a contract (meets) — and under an application’s own seed whether or not it serves one — a reviewer whose proposal carries a reviews edge to the part’s seed and to every contract it serves or consumes; under each part whose seed a gate guards, or that serves a contract a gate validates (gates) — and an application’s own seed always — a dev, and a work item where a gate names it; a dev and a work item for each validated contract no part of the repository serves; under each deployment an operator with a reviews edge to the deployment; and each item a document marks derived, practice or law, proposed as advice (a law item’s question asks for law). The one structure policy is the project’s: dna/org/structure.hl, which init writes for a repository or an application unless one is there already, and whose fn operational_roles() -> String names, space-separated, the operational roles each deployment also gets (support, accounts, billing, on-call), proposed empty and signing nothing — "", none, by default. It is read once, when init seeds the record, so a project opts in by writing the file before init; a word that is no operational role refuses the seed, and a role named twice is proposed once. upgrade rewrites an existing file to the current shape, keeps the roles it names, and notes every other line the rewrite dropped. A work item is an unfold of its part (or its contract), named for the part — or for the contract’s file when the gate guards a contract (store.md), its whole path where a part already has that name; two parts delivering one contract give one work item, the first’s — its node’s done_when the first gate guarding it: the question proposes “done when <gate> passes”, and the Board amends that wording at ratification. A hole is a knowledge proposal: its receipt (kind: graph, rows) carries the graph rows it would add, each checked by the vocabulary before it is proposed; knowledge.proposed and review.requested (required_authority: board, group: holes or practices) are the rows a practice is proposed with. The existing review path ratifies it (knowledge.ratified); the projection, seeing a ratified document of kind graph, puts its rows into the graph at the row that proposed it (its knowledge.proposed), checking each again, so the org chart lists positions in the order the holes were proposed whatever order the Board ratifies them in; the proposal stays in memory as the idea it was, bound to nothing, so it never enters a context package. Ratifying them fills the org chart. hale dna review lists the groups holes, practices, holds, positions, changes and ingest together, as it does design and operating, and hale dna review <group> approve|reject decides each pending one in turn. A seeded group is listed, and decided by its name, in the order the record requested its Reviews (a position, then its mandate), never by digest. Opening a position at run time (hale dna position open <name> --mandate "<text>" [--text <what it is>] [--under <part>] [--as <who>]). The CLI refuses a position the record already states or proposes, resolves a bare --under to the seed, work part, position or process the graph states by that name, and appends position.requested in the asker’s name, relayed as PositionOpenRequested until it is answered. The organization refuses a name with a space or |, no mandate, no author, a name the graph states, or an under the graph does not state (position.refused); otherwise it proposes the position’s node (the body init writes for a hole, and an unfold edge from the part with --under) and its mandate as two members of one family Review in the group positions (position.proposed), which ratifying ratifies both. A batch of changes (hale dna apply <file> [--dry-run] [--as <who>], dna/core/batch.hl). Several changes to the structure as ONE Board Review, applied all or nothing. The file is one step a line, in the language the scenario runner reads: open <name> [under <node>] | <mandate>, fill <name> <holder>, fill * <holder>, vacate <name> <holder>, practice <name> | <text>, retire-practice <name>, nursery <source>, assign|unassign <name> reviews <kind>:<node>, close <name>, merge <name> into <name>, split <name> into <name> <name>, expose <kind>:<node> as contract:<name>, depend <kind>:<node> on contract:<name>, externalize seed:<name> and internalize contract:<name> as seed:<name>; # starts a comment, and a name is a position’s, with or without position:. The host plans the batch against the record (batch_plan): a line that is no step, a name with a space, | or tab, an open with no mandate, a position the graph states or the batch opens twice, an under that is neither a node the graph states nor a position the batch opens, a fill of a position the graph does not state and the batch does not open, a fill of a stated position with no ratified mandate, a hold the organization would then hold twice, or a vacate of a hold the record does not state, asked twice or filled in the same batch, or a practice named twice or already in force is refused whole, naming the step, and nothing is written. Opens are taken first, so a step may name a position a later line opens; every other step is taken in the order written, against the organization the steps before it leave. fill * <holder> is taken last: every position the organization would then state with a ratified mandate (or one the batch opens), no holder and no vacate in the batch is filled by the holder, in the chart’s order, and the step is written into the batch’s normalized steps as the fills it made, so the Review and the plan at ratification name each seat; a wildcard that finds no vacant seat is refused. Planned on pending proposals (--on-holes, batch_plan_on). hale dna apply <file> --on-holes and hale dna pack apply … --on-holes (what init --with passes) plan the batch on the holes hale dna init proposed that are not decided yet (batch_init_holes: each review.requested in the group holes authored by hale dna init whose digest has no knowledge.ratified, knowledge.declined or knowledge.refused): the record is read with a knowledge.ratified row for each appended (PlannedOn, a read-only journal), so a hole’s position, its edges and its mandate are stated for the plan. The pending proposals the plan stands on — a graph proposal one of whose row ids the batch’s rows or steps name, and a mandate whose target they name — are its planned_on, one <review id> <digest> a line, carried in the family document and in batch.proposed; the question adds ; planned on <n> pending Review(s), it is ratified after them, and the printed chart a line naming them. A pending proposal the batch does not touch (a work hole) is no ground of it. With no planned_on the family’s documents are byte for byte what they were. Ratifying a batch with planned_on (resume_family, batch_ground) first reads its ground: while one of them is not decided, the ratification waits (the verdict settles; its execution is performed again on the next verdict or tick), so approving the batch before its holes is not a refusal; when one was declined or refused, the batch is refused whole as stale (the batch is stale: the proposal it was planned on, k:…, was not ratified); when all are ratified, it is planned again against the record and its effects compared, as every batch is. A pack that opens a position init already proposes is refused when it is proposed ( `position:…` is a position the graph already states), not at ratification. The operations, as graph rows. assign adds a reviews edge (reviews:<position>|<node>; the node a contract, document, seed or deployment the graph states) and unassign retires it. close retires a position: its holds, what it reviews, the unfold edge it hangs from, then its node; never position:board, and never one the batch opens. merge a into b fills b with every holder of a it lacks and assigns it what a reviews, then closes a. split a into b c opens b and c, each with a’s ratified mandate to start from (the Board amends either later; refused when a has none), fills both with a‘s holders, assigns both what a reviews, and closes a. expose n as c states contract:c if the graph does not and writes its meets edge with n its server (refused when the contract is met already); depend n on c restates that edge with n a consumer. externalize seed:s: every contract the part serves is restated without it, the part’s name added to the edge’s outside parties (refused when nothing in the organism consumes the contract, or when the part serves none), every position unfolding from it closes, and its node retires. internalize c as seed:s states the part again and restates the edge with it the server and its name out of outside. A batch’s rows are each id once, at the last write its steps make (an assign after an unassign of the same edge stands; a retirement of an id only the batch added is dropped), filed as its nodes, then its edges (each restated meets once, at its last body), then its holds, then what it retires. The family carries effects, a digest of those rows and the mandates, retirements and nurseries it proposes; ratifying plans the batch again and refuses it as stale when the steps would now do anything else (a contract that gained a consumer meanwhile keeps it). Knowledge bound to a closed position stays bound to its node; rebinding it is not built. The org chart a batch is planned against also lists reviews <position> <node>, part seed:<name>, serves|consumes|carries <node> <contract> and outside <contract> <party>. Planned, the batch prints the org chart before and after (position <id>, mandate <id>, holds <id> <holder> lines, + where the batch adds one, - where it takes one out), and --dry-run stops there. Otherwise the host keeps the receipts and appends, in the asker’s name: one graph proposal (kind: graph, name batch/<request id>, target org) whose rows are the opened positions’ nodes, then their unfold edges, then the holds, then a graph.retired for each hold vacated; one mandate per opened position (mandate/<name>); one practice per practice step (kind practice, its name, target org, the proposer its author); each a member (knowledge.proposed, no Review of its own); the family (kind: family, its ideas the members in that order, and batch, the normalized steps), with its review.requested in the group changes under the Board; batch.proposed <request id> (digest, review_id, by, steps); and graph.requested <request id>, which a running organization answers by opening and routing the Review (graph.proposed), as it does a re-ingest. The same steps while a batch of them is pending are answered with that batch’s Review. The Board’s one verdict decides every member in one append (resume_family); an approval first plans the batch again against the record as it then stands, and a batch whose ground moved (a position it opens was opened since, one it fills was retired) is refused whole: every member knowledge.refused with why the batch is stale: …, the family library.family.refused, nothing of it projected. Memory still admits each ratified hold as it admits any (hold.refused for a holder it does not know), so the batch checks structure, and the store stays the gate on who may hold what. hale dna vacate <position> <holder> [--dry-run] [--as <who>] is a one-step batch: the holder leaves, the position stays. Every reader of holds takes a retirement carried by a ratified proposal as it takes a graph.retired row: the graph, memory’s projection, the head’s seats (HoldsReader) and the CLI’s and the head’s structure commands’ (held_state_of, dna/operations/structure_command.hl). The org chart a batch is planned against also lists practice <name> for each named practice in force. Packs and the nursery (hale dna pack list | show <name> | apply <name> [--set <param>=<value>]… [--from <dir|git-url[#rev]>] [--dry-run] [--as <who>] [project]). A pack is a batch with {param} placeholders; a nursery is a directory or a git repository whose nursery.toml lists its packs ([[pack]]: name, version, path, summary, params with each one’s default, "" for none). With no --from the nursery is the toolchain’s (dna/nursery, embedded: dev-team, small-startup, content-team, and founder, fill * {founder}, one person in every vacant seat, what init --with founder --set founder=<who> proposes); a git repository is cloned into $XDG_CACHE_HOME/hale/nursery/ and read at the revision after #, its default branch’s head without one. The CLI renders the pack: every parameter replaced by its --set value or its default, a line beginning ? kept only when every parameter it names has a value, and a parameter with neither refused by name. apply proposes the steps as a batch whose family and graph proposal are named pack/<name>@<version>, its question ratify adopting the pack <name>@<version> …, and batch.proposed carries pack: the name and version, sha256: of the pack’s text, and its source (toolchain <version>, the directory, or <url>@<commit>). hale dna pack retire <name> [--dry-run] [--as <who>] undoes the latest adoption of the pack the Board ratified, as one batch (pack/<name>/retired): every position it opened that is still stated closes (its holds and reviews with it), every hold it filled and assignment it made on a position that stays is taken back, and every practice it proposed that is in force retires (retire-practice <name>, a member of kind retirement superseding the practice, which ratifying retires); whatever has gone since is passed over. A pack supersedes nothing, so there is nothing to restore. A git nursery is trusted by the Board before its packs are adopted: hale dna nursery add <source> proposes the batch step nursery <source> (a knowledge idea of kind nursery, named by the source, target org), and once it is ratified the org chart lists nursery <source>; hale dna nursery list prints the ones in force. pack apply --from <git URL> is refused, before anything is fetched, while the URL is not one of them; the toolchain’s nursery and a directory need no trust, and reading a nursery (list, show) needs none either. Every mutation is a head command; the UI is a client. A change to the structure asked of the head is not a second code path: the head’s surface HeadCommands carries Commands::open_position (PositionOpening { request_id, record_head, position, text, mandate, under }), Commands::fill (HoldFill { request_id, record_head, position, holder }) and Commands::apply_batch (BatchApplication { request_id, record_head, steps, pack }), each requires: [position], and the read Commands::preview_batch (BatchDraft { steps }, ungated). The commands are operations of the composed head’s governance service (dna.position.open@1, dna.hold.fill@1, dna.batch.apply@1, dna/operations/structure_command.hl), admitted and recovered by request id as every command is, and their handlers write the rows the CLI verbs write, in the caller’s person’s name: position.requested and hold.requested with the CLI’s keys in the CLI’s order, and a batch’s receipts kept and its rows (members, review.requested, batch.proposed, graph.requested) landed together by one ref move, batch_filing’s bytes; each request row’s id is the one the CLI mints for the same ask by the same person at the same head (p, h or b and twelve hex digits of the digest of the head and the ask), and the command’s fields (command_format: dna.structure-command/1, command_id, command_payload, command_fingerprint, command_authority, command_authority_basis, command_record_head) follow the CLI’s keys in the request row (batch.proposed for a batch), so a relay and the organization read it as the CLI’s. Before a row the head checks what the CLI checks, over the same state: a position the record states or proposes is not opened again, a fill takes the CLI’s fill refusals word for word, and a batch is planned whole; a refusal is a receipt (state: refused, structure.why) and no row, and the same steps still pending are answered already_proposed with the Review they await. A command is conditioned on record_head (stale_subject once the Record moved) and is commands_unsupported once the Record’s ledger is adopted. The receipt’s structure branch (state, request_id, review_id, event_id, why, warning) reads requested until the organization answers the request row, then proposed with its Review or refused with why; the Board decides through Commands::verdict. vacate is a one-step batch through apply_batch. Commands::preview_batch plans a batch against the Record’s head and writes nothing: BatchPreview { ok, code, why, application_id, head, revision, steps, before, after, added, removed, unchanged, pending }, the org chart’s lines before and after and those the batch adds, takes out and keeps, one a line, and pending the Review the same steps await. The habitat UI is a client of the generated surface (hale api client --surface HeadCommands), and wires its affordances to these members. Mandates (seed/structure). Each position init proposes carries a mandate, proposed in the same seed right after its hole: a knowledge idea of kind mandate (family mandate, author: hale dna init, name mandate/<position name>, target the position’s node id, position:<name>), whose text states what the position decides, what it may not, what it cites, how it writes and what it escalates. review lists it under holes directly after its position (question: ratify the mandate of <p>: what it decides, what it may not, what it cites, how it writes, what it escalates), so hale dna review holes approve ratifies the position and then its mandate; the ordinary ratification binds the idea to the position’s node (a mandate is not a graph document, so it is bound, unlike a hole). A mandate ratified before its position lands is a binding whose node does not exist yet: nothing refuses it, and the hat finds it once the position does (hat_knowledge_targets names the performer’s position:<name>, so a mandate bound to it arrives in the hat’s package and brief). The texts are the toolchain’s (dna/operations/graph_mandates.hl), one per position kind the proposer knows (board; a part’s dev and reviewer; a deployment’s operator; each operational role), the part’s or deployment’s name substituted. The authority defaults are text in the mandate: which change classes and magnitudes the position may ratify alone, which need a second signer, which are the Board’s, in the law’s names (OrgPolicy: the classes organization, constitutional, process-policy and topology, and any change that touches law, widens effects or crosses ownership, are the Board’s; the rest, inside the grant, the Leader’s), cited and not re-implemented; the charter refines them. The organism’s own positions the hat already names (position:leader, position:editor, position:agent) get theirs at init and upgrade under design beside the design practices (design/mandate-<name>, a seeded practice with a target, proposed as a mandate bound to it and superseded on upgrade the same way). Standard equipment. A proposed position’s node body carries equipment, a JSON object with roles (the API roles the position is fitted with on fill) and nothing else for now. It is derived from the graph the ingest read and the toolchain’s own export: for a contract whose stem matches a spec/<Stem>.description.json (what hale api describe / hale check --api writes) case-insensitively, the ingest keeps the requires roles of its members (GraphIngest.roles, <contract>\t<role> lines; the description is the contract’s roles, not a node of its own; an application’s own surfaces and streams are contracts whose roles are read from the same export, § Ingest on an application). A role named like a position’s last segment (reviewer, dev, operator, an operational role) is that position’s equipment for the part that serves the contract (for operator and the operational roles, the deployment that runs the part); every other role (editor, reader, …) is the equipment of the part’s dev. A repository with no description proposes the same holes with empty equipment. The ruling that governs this: an API surface’s requires informs contract relationships and equipment; it never determines the organization and never grants authority. The reviews edge over the surface’s contract is what it was; no position exists because a role does. The seeded practice design/standard-equipment states the list: API roles now; the broker account and the vault names when an application attaches in repository mode. hale dna fill <position>|organization:<name> <holder>|organization:<name> [project] [--as <who>], once it has proposed the hold, prints equipment: <roles> when the position’s node names API roles (read from the node the record states or proposes); granting them, a token that carries those roles, is deferred, and fill grants nothing. It asks the running organization for a holder — a person’s seat, a firm owning a position (fill org/collections organization:north) or a person’s membership of a firm (fill organization:north bob), each a holds edge (GH #1123) — as hale dna practice propose asks for a practice (init alone writes proposals directly, since no organism exists yet): a hold.requested <id> {request_id, position, holder, by} row in the asker’s name, relayed as HoldRequested until the organization answers hold.proposed (digest, review_id) or hold.refused (why). The organization proposes it like any hole (group: holds), in the asker’s name. Whoever holds the Board may ratify it, the asker included: a governance proposal requiring the Board’s authority (the organization marks its knowledge Reviews governance) is the one exception to a Review’s independence from its author (a solo organization seats itself), which every change to the work keeps, a mutation requiring the Board included. The CLI checks first that the position or organization is one the record states or proposes and has not retired (graph.retired); a firm holding a position needs no person checks — only that the firm is an organization the record states or proposes, and that it holds a position, never another organization; a person must not be a name the organism writes as, must not have retired (person.retired), and, for a seat, must be a person the record knows — who wrote a row in their own name, or an organization’s member — under the same-holder rule. A membership’s holder need not have written a row before: joining an organization is how a person becomes known to a shared record. The store is the gate: a hold is checked again where memory projects it, however it reached the record — a seat whose holder the record never knew before the row (no row in their name, and no organization’s member) is not projected, nor is a retired holder’s, nor a membership of a second organization (a person belongs to at most one), and a part’s reviewer held by its dev’s holder is a warning on stderr under dna.trust = local (one person holds every role) and not projected under any other trust. A hold the store refuses is a row, once: hold.refused <hold id> (why, row, by: memory). A person retired after their hold was projected gives every seat and membership back where the retirement is projected. fill and practice propose name a request by its digest at the record’s head (h…, p…), so two requests in one millisecond never share an id.
  • A repository record runs its organization alone (GH #1091). When the record’s seed holds no Hale source of its own, hale dna dev and hale dna run check and build the organization only; there is no application to cut, build, express or restart, and dev says so.
  • Review routing (GH #1087). A change set goes to the positions that must sign it, read from the graph, never guessed: hale dna route [--json] (<path>… | --diff <range>) (the host verb route, reading memory as the head, over graph_perspective("routing"); the routing is dna/core/route.hl). A path is the repository’s: one given from a subdirectory is taken from there, and the organization’s root is the nearest directory holding dna/org (a seed’s own hale.toml is not it). A path names a node — a contract (its file, or a file under its directory), else a document, else a deployment (the file it was read from, when that is no document: compose.yaml), else the longest seed holding it — and the reviews edges say who signs, whatever a position is called. Any node: every position that reviews it — a part’s reviewer reviews its seed and the contracts it serves or consumes, a deployment’s operator the deployment (compose.yaml goes to compose/operator, against the gates guarding compose). A contract, beside those: the positions reviewing a consumer that is no part (a contract, a document, a deployment), and the board where the contract is law: a practice the Board ratified as law is bound to it (its receipt says law: true); one ratified as advice is not law, and the board does not sign for it. A position that reviews nothing — a derived dev, an operational role — signs nothing; one a reviews edge names signs, whatever its name. The evidence a verdict is given against is the run of every gate guarding a node the change touches. A consumer no position reviews is listed as such (unsigned), never dropped: a consuming process or seed with no position under it reviewing the contract; a path that names no node, or whose node no position signs (or operates), is left to the fallback — the Review’s own required authority, the task routers of GH #697 when they exist, which the route names. Text lists signed by, against (no gate guards it when no gate does), reviewed by no position and left to the fallback, <what it falls back to>, each signer with its holders and why it signs; --json is one object: signers (position, because), evidence (gate ids), unsigned (node, because), fallback (path, because), fallback_to. A Review takes its signers and gates from the same computation when it opens (Signers from the route, above).
  • Perspectives: hale dna show (GH #1086). hale dna show org | processes [--json] [project] is the host verb show. It takes --json and at most one project directory, and refuses any other flag, a second project or one that is not a directory. It reads memory under the head’s role alone (HALE_DNA_MEMORY_DSN_HEAD), never the spine’s, even in a process that holds both, scoped to the record, and prints what graph_perspective answers. Nothing is edited through it: a change moves through the record’s rows, and the spine projects it into both perspectives. A record with no rows is the empty perspective, with no memory asked, and stderr says hale dna init seeds a record and hale dna sync fetches one; with rows and no memory named it refuses, and says the graph is memory’s. The projection’s stamp is read before the graph, so the rows it reports never overstate what the graph holds. Text (dna/operations/graph_show.hl) shows a node by the last segment of its name. The org chart is nothing when there are no positions; otherwise purpose, then a line per node positions sit under, in the record’s order: a position under the purpose is a line of its own (board), and any other node is its name, padded in code points to the widest, then its positions’ roles. A role is the position’s name less its node’s (api/reviewer under api is reviewer), followed by (<the contracts and documents it reviews>) and, when it has holders, [<holders>]; a position under no node is under (unplaced), after the rest. Each role ends with {mandate: ratified|pending|none[; roles: <API roles>]}: ratified when a ratified idea of kind mandate is bound to the position’s node (memory), pending when one is proposed and undecided (read from the record), none otherwise; roles are the node’s equipment.roles, when it has any. --json carries each position’s roles and mandate (ratified, or empty) beside its holders. The process model is a line per meets — its outside parties and consumers, the arrow --<via>: <contract>-->, its servers or (nobody here), and (carried by <carriers>) after — then one <deployment> runs { <processes> } per runs, then one <position> convenes <definition> <cadence> per convenes (board convenes optimize-walk every 1d). --json prints one object: perspective, record_rows (the record’s length), projected_rows (memory’s watermark), stamp_valid and graph, the query’s answer as memory built it. How memory stands to the record is said on stderr, never in the perspective: behind it (memory has projected <m> of the record's <n> rows), past it (this clone is behind its remote: hale dna sync), or with a stamp that vouches for nothing (hale dna memory migrate). On voice’s record with its holes filled the two texts are GRAPH.md’s renderings, byte for byte (dna/tests/graph_show_test.hl).
  • Projections and ranking (K3). The tail also projects the record’s structure.observed rows (init’s loci, topics, bindings, effect classes and claims) into memory by kind and name, the latest row winning, and its pressure.raised and concern.raised rows into signals counted per (kind, source, what) with the last row’s seq (knowledge_structure, knowledge_signals), so the graph has what ideas bind to and what the fleet is saying. Retrieval is by binding and provenance first: the bounded set is the accepted ideas bound to the target or above it, and nothing outside it is retrieved. Inside it, a query — the objective, which the substrate passes from the ask, its first 2000 characters — ranks by similarity so the budget keeps the most relevant (KnowledgeStore.ranked_ids(target, budget, query_vec); no query is ratification order). The embedding is lexical — embed_text: a hashed bag of words in 64 dimensions, normalized, deterministic, rendered as a pgvector literal — so Pq ranks with <=> over a vector(64) column (the migration creates the vector extension and says so when the Postgres has none).
  • The application side (K4). A concern is one of the application’s own events (GH #986, #987). It declares it as its own topic, concern.raised under its own subject app.<app>., with a codec that writes one JSON object: its id, and the concern’s source, what and severity. It publishes it through pond’s NATS library with the application credential its node handed it, and imports nothing of the DNA. Its id is that concern’s alone: a repeat is a replay, refused. Its source is a /-path of letters, digits and -_., at most 128 bytes, one of whose parts is the application the subject names: an application speaks for its own parts, never another’s, and a concern that does not is refused (acknowledged, recorded nowhere). NatsPublisher, the publish-only adapter, dispatches nothing into the program: its artifact stays exact, so a fleet claim over it can still be certified (a NatsConn hands whatever arrives to __local_dispatch, which no checker can bound). The heart lands it as a reading first, reading.recorded <app>/concern.raised/<id>. The spine then appends concern.requested <source> (request the reading’s entity, source, what, severity, app) before the event is acknowledged, once for that request, whichever delivery reads it; a concern the record refuses leaves the event unacknowledged, to come again. A node relays concern.requested rows onto the nerves like intent.requested and review.verdict until the record holds the answer, and the organization journals concern.raised, admitted once by its request. hale dna concern raise writes the same concern.requested row by hand.
  • The learning scenario is the acceptance (K4), in the fixture: the worker on the second node observes its mail backlog and raises the concern three times; each travels as the worker’s own event → the heart’s reading → concern.requested → the relay → the nerves → concern.raised; the third makes a proposal by org/trio/worker bound to org/trio (a concern by the tower rule); the Board ratifies the exact digest (hale dna review k:… approve --authority board); the spine projects it into memory on its tick; and the next change to the trio consults memory (knowledge.consulted m1 with the digest and the hat’s digest), briefs the editor with the concern under the objective, and every model.called row of the attempt names the package and the digest. Something observed and ratified today informs the work done tomorrow, and the receipt says so.

The assembly names what fills each role; hale check sees the wiring.

  • Record — Journal: GitJournal (the branch), MemJournal (tests).
  • Board (the board field, a Membrane) — where humans see and decide: Board / LocalHumanMembrane, reached over the nerves on one machine; the record itself across clones (intent.requested, review.verdict rows, relayed by the host); GitHub, mirrored by the host when git config dna.github names owner/repo: every pending mutation Review becomes a pull request (github.pr; the candidate pushed to dna/<id>, the three views as the body), every GitHub review on it becomes a review.verdict row in the reviewer’s login (authority board when dna.github.board lists the login, else reviewer; each review once, keyed by login, commit and state), every settlement goes back as a comment (github.commented) and an approval pushes the genome — both once the Mutation is applied (or has failed), since a Review settles before its Mutation is applied. GitHub is a projection of the record and the record wins: a review whose head moved is refused here and shows as refused there. The forge is an implementation (GH #648): Forge in the core is the vocabulary of a code-review host and nothing of GitHub’s — open_review, verdicts (one per line: who, outcome, when, the forge’s own key so each is admitted once), comment, close_review, present, name. GitHubForge (gh) is one implementation; the FileForge (.hale/dna/forge/<n>.review, its verdicts the lines of <n>.verdicts) is the fixtures’; NoForge is a bare remote’s honest behaviour, and hale dna profile’s github: line names which the host found (HALE_DNA_FORGE, or dna.github). The forge’s pass is written against the interface; the rows it appends (github.pr, github.commented, review.verdict keyed by the forge’s key) do not change with the forge. Nothing in the core or the host spells gh outside that one implementation.
  • Deployment — Deployment: NoDeployment (a host expresses: hale dna dev), ShellDeployment { command, seed } (the command owns expressing and judging: express <candidate> <seed> returning 0 means up and healthy, rollback <base> <seed> restores; the exit code is the observation, expression.deployed records it), and LocalApplyDeployment (tests). With a gateway wired, an approval expresses and judges in the organization’s own handler and no host is asked; without one, expression.restart_requested asks the host.
  • Observation — for a shell gateway, the command’s exit; for a host, the window it watches; for a fleet, what its nodes report (below).

The skin’s (GH #989). A secret is provisioned when its owner is declared and consumed when it runs. Organism-level secrets are provisioned by hale dna init and upgrade, whether or not the part that uses one runs; per-member secrets are provisioned at that member’s admission, and are not on the organism’s list: an owner’s head role on a shared record (postgres-<schema>_head_<owner>) where the owner is declared to memory (memory migrate under HALE_DNA_OWNER_KEYS); a node’s own account, not yet built. The application the record attaches is the exception on the list: its broker account is drawn once application.attached lands, and revoked by application.detached (The application’s account). One skin-owned function (dna/host/secrets.hl, organism_secrets) holds the list, and one (provision_secrets) provisions it; every other part only consumes a secret, by its vault name, and never creates one.

vault name kind whose
postgres-dna_<identity>_spine, _head drawn memory’s roles (Memory)
postgres-owner-<seed>, when the seed has dna/compose.yaml drawn the compose database’s superuser
nats-<org>-<role>, for owner spine head reflexes drawn the nerves’ accounts (The nerves)
nats-<org>-app-<name>, while the record attaches the application drawn the application’s broker account (The application’s account)
oidc-client-<dna.oidc.client> (dna-local when unset) drawn for an issuer on the loopback, else a slot the head’s OIDC client
oidc-service-<service>, per dna.oidc.service drawn a service client (The principal source)
forge-token slot the forge’s token: with none, nothing is done at the forge, never as gh’s own login
model-<NAME>, per credential dna/org/models.hl or dna/org/work.hl names slot a model’s key (Models); hale dna secrets lists a work.hl key as the legs’ model key
  • Drawn or a slot. A secret the organism owns the value of is drawn from urandom (32 hex characters) into the vault when the vault lacks it; the nerves’ are drawn anew on every init and upgrade, since the server’s include is written from the same draw. The servers compose runs read theirs from files written from the same draw as the vault’s entries: dna/nats.secrets.conf for the nerves and dna/postgres.secrets for the database (its POSTGRES_PASSWORD_FILE), each mode 600, ignored, and refused when git tracks it or would. No committed file holds a secret. A secret a person supplies is a named empty entry that hale dna secret set <NAME> fills (FORGE_TOKEN, OIDC_CLIENT_SECRET, or a model key’s name); an entry that is empty is not held.
  • The vault. The local vault is std::secret::vault_local_dir() (HALE_VAULT_DIR, else the toolchain’s cache), each entry a file of the secret’s exact bytes, mode 600. A real vault (HALE_VAULT_ADDR) is provisioned out of band: there the bootstrap only checks, and says what the vault lacks. The local vault is one directory per user, not a boundary between the parts one user runs: any of them can read any entry by name. What keeps a part to its own credential is what it is handed (an application gets its own account’s vault name and none of the others’), not what it could read.
  • Presented, never held. A credential is presented by the adapter that puts it on the wire, and never exists as a String elsewhere: a part carries a std::secret::Credential { vault: … } (a sealed locus), and reveal_text() is called on the line that writes the secret out — pond’s NATS client on CONNECT, the head’s token request, the model’s request header. No field, URL, argument or environment variable carries one; memory’s DSNs name their role’s entry and pq presents it in its handshake. Two are not sealed yet: the migration sets each role’s password in the SQL it runs as the owner (CREATE / ALTER ROLE … PASSWORD, an SQL literal on pq’s ordinary query path), and the forge’s token reaches gh, which takes it no other way, as GH_TOKEN in that one child’s environment (through a file unlinked at once).
  • Fail closed. hale dna run (and dev, which starts it) refuses when the vault lacks a drawn secret, naming each; nerves migrate refuses a role with no entry; nothing falls back to a default.
  • hale dna secrets lists every secret the organism requires, whether the vault holds it, and the remedy for each missing one; never a value. The board says so for each missing one (a model key is reported by the body, since the vault that matters is the one on the machine that calls the model).

The genome’s topology is the fleet plan Hale already checks (spec/verification.md, “Fleet composition”), and the DNA expresses one arrangement of it:

  • The plan describes the workspace’s own services. Plan schema 1.2 lets an instance name a seed (relative to the plan) instead of an artifact; composition cuts the artifact from the seed first, so a candidate is checked as the fleet it would deploy. An instance may name the node that expresses it. [dna] fleet = "<name>" in hale.toml names which entry of [fleets] the DNA expresses.
  • The fleet is evidence. Verification runs hale fleet check --in <worktree> --if-declared on every candidate (evidence.fleet; fleet_clean, true when the workspace declares no fleet). A change to one service that breaks a claim the fleet makes over all of them is mutation.deny (“candidate breaks the fleet”) with the witness in the receipt. A candidate whose diff names a plan (*.plan.json) or the manifest (hale.toml) changes the fleet’s shape: it is re-classed topology (mutation.topology) whatever it was asked as, and OrgPolicy sends it to the Board.
  • A deploy is a row. fleet.deploy (entity: the Mutation, or the short revision for an operator’s deploy) carries the plan name, the revision, the seed the change edits ("" for the whole genome), the instances touched — every instance with a node whose seed is that directory — and the reason (apply, rollback, or the operator’s). The revision is pushed to refs/dna/revisions/<rev> on the record’s remote first, so every node can fetch it. Under hale dna run with [dna] fleet set, the host answers an application’s expression.restart_requested with a deploy row; hale dna deploy <revision> and hale dna rollback <mutation> write the same row by hand. A rollback’s row asks for the base; nothing is watched.
  • A node expresses. hale node <name> [--repo <clone>] [--fleet <name>] [--tick <ms>] runs in a clone of the governed repository. Every tick it syncs the record and reads the latest fleet.deploy; when that names a revision it does not express, it fetches the revision, checks it out, and for each instance the plan assigns to the node that the deploy touches (or that is not up) cuts the artifact, builds the seed, restarts the process (cwd the clone, LOTUS_OBS=1, HALE_DNA_NODE, HALE_DNA_INSTANCE, HALE_DNA_EXPRESSION) and appends instance.up as node/<name>: node, instance, revision, model hash, build digest, pid. An instance that exits appends instance.exited with its code. The expression identity per instance is therefore reported by the node that expresses it and joined to the plan by instance id. The node decides nothing. .hale/node/<name>/<instance>.pid is the only local state.
  • The window is over every touched instance. After an apply’s deploy row the host waits for every touched instance’s instance.up at the revision (180 s to settle), then for the observation window with no instance.exited at the revision among them. All up and none exited: Observation healthy with the model hash the instances reported. One exited: expression.crashed naming the instance and its node, Observation crashed with the same detail, and the organization rolls back — a rollback deploy row touching the same instances, which every node answers. Never up: expression.crashed “never expressed by …” and Observation build_failed.
  • hale dna fleet renders what the fleet expresses from the record: every instance of the plan, its node, whether it is up, the revision and model hash it last came up at, and the last deploy.

The senses are the organism’s perception, and the reflexes are its reactions that need no plan (GH #988). Both are their own parts: one telemetry store every part emits into, and a program beside it that reads it and says what fired onto the nerves. The spine records and routes; it never plans on a reading.

  • Readings. Every long-running part serves its readings as a Prometheus scrape target (std::metrics::Endpoint), namespace dna. It listens on the port its environment names in HALE_DNA_SENSES_PORT, else the one the seed names for it: the spine (the node’s host) on 9464, a node on 9465, the head on 9466 (dna/core/senses.hl). It binds every interface, or HALE_DNA_SENSES_HOST, from a dna::Senses its program’s main locus holds on a cooperative pool of its own (senses), whose shutdown ends the accept, so the part drains at once. A port another process holds is the server’s listen_failed, which Senses absorbs, as the only handler it has: the part runs on, unread, and says so.
    • Labels. Every series carries part=<name>, and a node’s node as well. No label is per event, per task or per revision: a series cannot be retired, so the set a part serves stays bounded by its parts and instances. A task joins senses, spend and receipts through its rows (a model.called row names its attempt, and the attempt its task), not through a label.
    • Series. dna_pulse_seconds{part} is each part’s tick. dna_instance_up{part, node, instance} is 1 while the instance runs and 0 once it exited. dna_instance_since_seconds{part, node, instance} says since when (a down transition writes its since first, an up its state first, so no scrape pairs a new state with an old since). dna_model_calls_total{part} counts the model calls whose rows the spine announces landed (with its nerves up).
    • A part never learns where its readings go.
  • hale node runs as the host program’s main locus (Host, verb node), the one place its Senses can be placed on a pool of its own. It is no spine: its nerves connection has no server and stops at once, and it takes no application’s events. It drains on SIGTERM.
  • The store. The seed’s dna/compose.yaml has a senses service: Prometheus, scraping what dna/senses.yml names over the host gateway (honor_labels, so a part’s own labels are the readings’), and keeping what it scraped for its retention (--storage.tsdb.retention.time, a week). Its port is the seed’s, in 93xx, on 127.0.0.1, taken as the compose file’s other ports are.
    • init writes both files. upgrade writes dna/senses.yml when it is missing, and says so when the compose file has no senses service or dna/nats.conf no reflexes user.
    • hale dna senses up [dir] brings the service up and prints HALE_DNA_SENSES_URL, its read URL.
    • hale dna dev runs it with the rest of compose, unless the environment names its own memory or nerves (an owner’s DSN or URL).
    • A reading never acts.
  • The reflexes (dna/reflexes) are a program on the private-services tier. They hold the store’s read URL and a publish-only credential, the reflexes user of the nerves (HALE_DNA_NATS_URL_REFLEXES, which hale dna nerves migrate prints). That user may publish on <org>.app.reflexes.> alone, and an application’s user may publish only under its own name, so no application can speak as the reflexes. They hold nothing else: never the record, memory or the vault. To the organism they are one more application.
    • The rule. One is built in, instance.down: an instance its node reads dna_instance_up 0, asked of the store each tick. It fires once per outage, reflex.fired { id, rule, node, target, action: "restart", since, fired_at } on <org>.app.reflexes.reflex.fired. The id names the outage (instance.down:<node>:<instance>:<since>, each segment kept to the heart’s id token).
    • The row. The heart lands the firing as a reading row first, reading.recorded reflexes/reflex.fired/<id> (The heart’s events), so a second publish of the same outage is refused as a duplicate. They remember what they published only while it is down.
    • Launch. The face’s launcher (hale dna head, start.sh) builds and starts them beside the stub OpenID provider when a project is attached, with that project’s store and nerves, and stops them with the launcher. A store or nerves that does not come up leaves the face running without them.
  • The node acts on the row. On its tick a node reads every firing that names it and has no reflex.acted row yet. It restarts that one instance at the latest deploy’s revision, through the path a fleet.deploy takes, touching no other instance and not marking the deploy expressed. It then appends reflex.acted (entity: the firing’s id; rule, target, action, outcome, node), whatever the outcome was: restarted, already up, or why not. A restart that did not take becomes a concern as well (concern.requested, request reflex-failed:<id>), not a quiet retry. A node restarts; any other action is refused on the row.
  • The spine’s read. The host that lands a firing, or finds it landed already, reads the firings for its node and target. A second firing within the window (HALE_DNA_REFLEX_WINDOW_SECS, 600 s) becomes a concern: concern.requested (source reflexes/<target>, request reflex:<id>). There is one such concern a window, however many firings fall in it, and it lands before the firing is acknowledged; a concern the record refuses leaves the firing unacknowledged, to come again. The organization raises it, concern.raised, and that is what a workflow answers to. Only the rule fires, only the node acts, and both leave rows.

Everything hale dna does beside the compiler that is DNA behaviour rather than manifest or scaffolding is the host, dna/host, embedded in the toolchain beside the core and the surface. It is two programs over one seed, one command per process (GH #1490):

  • the body, dna/host itself: run and dev (the supervision, in which the host is its program’s main locus and binds the nerves), node (the node agent, hale node) and body-fence (the renewer a body starts beside itself, from its own binary). Each runs until it is stopped.
  • the verb program, dna/host/verb: every other verb, one-shot — the projections (status, history, review, board, report, pressure, fleet), the writers (task create, a verdict, pressure raise, sync, deploy, rollback, github sync) and the rest. It imports the host’s seed and starts no locus that runs or handles the bus, so it runs to exit, and the memory-bound survey judges it as it judges a script (spec/verification.md).

hale dna <verb> resolves the project from the manifest (the root, the application’s seed, the fleet and its plan under [dna]), builds both programs once into the toolchain cache (side by side, the first time any host verb needs either), and execs the one the verb belongs to in place — the pid that ran hale dna <verb> is the host: <program> <verb> <root> <seed> <fleet> <plan> …, with HALE_BIN (the toolchain) and HALE_DNA_TOOLCHAIN in the environment. Either program refuses a verb that is the other’s. The host reads the record through the core’s GitJournal, appends in a person’s or a node’s name with the ref compare-and-swapped, and starts every child — the organization, the application, iris, an instance — through sh, detached, with its pid, its exit code and its log as files under .hale/dna/ or .hale/node/<name>/, echoing the log to the terminal a tick at a time. The compiler keeps init / new / upgrade (they embed the sources), hale fleet check and the plan schema, and the exec shims. The host decides nothing.

A seed is built once per content fingerprint

Section titled “A seed is built once per content fingerprint”

The host builds the organization’s seed before it starts it, and the application’s seed too under dev; it rebuilds both on a restart. A build is a pure function of what it reads, so the host does it once per content fingerprint and starts the stored binary thereafter. The cache is $XDG_CACHE_HOME/hale/dna-build (or ~/.cache/hale/dna-build), a sibling of the toolchain cache the host and the surface are themselves built into; it holds the 32 most recently used entries and prunes the rest. An entry is a directory, <fingerprint>/<seed name>, so the process keeps the seed’s own name.

A build never writes into the seed’s directory. The host passes hale build -o: with a cache the binary is built into a private directory beside the entries and renamed whole to its entry, and the entry’s path is what the host starts; without one (HALE_DNA_NO_BUILD_CACHE, no cache home, a fingerprint that could not be taken) it is built into .hale/dna/build/<seed>/<seed name> under the project root. Either way a checkout has no binary in it afterwards.

The fingerprint is a SHA-256 over:

  • every file the build reads, each as its path relative to the project and the SHA-256 of its bytes. The list is the compiler’s own — hale inputs <seed>, which resolves imports exactly as a build does — so a seed that grows an import, or a vendored core that is upgraded, is a different fingerprint without anything here having to be kept in step;
  • the project’s hale.toml and hale.lock;
  • the toolchain version, and the hale binary’s own size and mtime, so a compiler rebuilt in place without a version bump is a different fingerprint;
  • every environment variable that changes codegen;
  • the seed’s own path, because a binary carries the paths of the sources it was built from.

A hit is the entry used where it lies, and nothing else is done. A store is one rename of a whole directory into the cache, so a reader sees a whole entry or none, and a binary that is still running keeps its inode when its entry is replaced or pruned. Two hosts building one thing at once need no lock: the second rename finds the entry there and drops its own copy, and both are right. Anything that stops the fingerprint being taken whole — no cache home, hale inputs failing — is a miss, and a miss builds exactly as it did before. HALE_DNA_NO_BUILD_CACHE=1 makes every build a miss.

hale dna init / new / upgrade materialize vendor/dna from the DNA source embedded in the hale binary, so an organism runs the core that binary carries and not the one in any checkout. A version does not identify that source — two builds of one version can embed different dna/ source — so the toolchain names it (GH #726): EMBEDDED_DIGEST is a length-framed SHA-256 over the sorted (path, content) pairs of every embedded file (dna/core, dna/host, dna/host/verb, dna/operations, dna/organization_runtime, dna/organization_source, dna/ui, and pond’s driver and NATS client pinned under dna/core/pond), computed when the toolchain is built. It is:

  • the second line of hale --version (embedded dna: <first 16 hex>; the first line stays the version alone, since a provisioning script reads field 2 of it);
  • all 64 digits on stdout, alone, from hale dna --embedded-digest, and — with --from-tree <dir>, a checkout holding dna/core — the digest that checkout would embed, by the same algorithm. Unequal digests mean the binary predates the tree: what it materializes is the older core, and a test of an edit to the tree measures the wrong source. A missing directory is refused, never digested as a smaller set;
  • the first line of hale dna status, with the toolchain version, and a refusal to stay quiet when vendor/dna was materialized by another build (hale dna status --json is unchanged: the projection is the host’s);
  • recorded where a materialized tree can be asked about it: vendor/dna/README.md and .hale/dna/embedded.digest (hale <version> then embedded dna: <digest>), both toolchain-owned and git-ignored, both refreshed by upgrade. It is deliberately not written into the generated organization’s source: dna/org/main.hl is project-owned source that is reviewed, diffed and read by models, and a line that changes with every DNA edit belongs in neither a review nor a recorded baseline.

The build enforces its own snapshot: the digest is computed over the on-disk tree by the build script, the crate digests what the compiler actually embedded, and the two must agree — source edited while a build runs, or a file added without being embedded, fails the build rather than shipping a digest that names nothing.

hale dna ui [project] [--port N] serves the DNA surface in a browser from the record alone: a Hale program (dna/ui, embedded in the toolchain like the core and the host, built once into the toolchain cache) that answers every request by running one offline verb of hale dna in the project root and returning what it printed — the status projection (/api/status), the Board’s queue (/api/board), the pending Reviews and one Review’s three views (/api/reviews, /api/review/<id>), the fleet (/api/fleet), the history (/api/history[/<entity>]), pressure (/api/pressure). A verdict (POST /api/verdict), a task (POST /api/task/create) and a pressure signal (POST /api/pressure) are the CLI’s own verbs sent and not waited for: rows in the record, in the name the form gives, which a node relays. A path segment reaching the CLI is cleaned of separators and leading dashes, so a request cannot name a file or a flag. The surface reads nothing itself and decides nothing; with or without an organization up, it shows what the CLI shows. hale dna review <id> <verdict> --no-wait is the same non-blocking verdict from the terminal. Iris stays the observer: attached to the organization’s process it renders the org as the live topology it is.

The head (dna/api/project_service), which serves the face, is the surface’s counterpart for the operator’s machine. hale dna head [project] [--port P] [--api-port P] [--oidc-port P] [--commands-port P] [--source-drafts] starts it from an installed toolchain: the toolchain embeds the head’s seeds (dna/api, dna/api/practice_review, dna/api/project_service, dna/oidc, dna/oidc/serve, dna/reflexes), the face’s assets (dna/face/web) and the launcher (dna/face/start.sh) beside the host’s sources — not in the embedded DNA digest, which names what vendor/dna is materialized from — materializes them into the toolchain cache, whose key covers them, builds the four seeds there side by side under the cache’s build lock once per key, and execs the materialized launcher with every binary handed in (HALE_API_BIN, HALE_HEAD_BIN, HALE_OIDC_BIN, HALE_REFLEXES_BIN) and HALE_BIN the running toolchain. With a project and no HALE_DNA_MEMORY_DSN_HEAD, it derives the head’s memory DSN as hale dna memory migrate does, onto the launcher’s environment and never its output, when memory is up: an owner’s server (HALE_DNA_MEMORY_DSN_OWNER), or the project’s compose knowledge-db already running. Otherwise it says memory is not up, names hale dna dev, and starts the head with Knowledge unsupported; it brings no database up. From a checkout, dna/face/start.sh [project] is the same launcher, building each seed it is not handed from the checkout into temporary storage on every start. The head is one loopback process that serves the browser shell, keeps a registry one loopback process that serves the browser shell, keeps a registry of projects and a journal of receipts under a state directory, and proxies the Record’s read and command routes to a per-project API child it starts and replaces. Its own operations — create, init, attach, detach, forget, sync, publish, forge, the local body, the remote body, secrets by source name, the model probe, connections, handoffs — are the CLI’s verbs run detached with a pid, an exit code and a log as files, and each answers a receipt (recorded → admitted|refused → running → succeeded|failed|outcome_unknown) under a command-<digest> identity that replays on an identical retry and conflicts on a changed one. A run that passes its deadline is outcome_unknown when the verb reaches beyond this machine, never failed; the head appends nothing to any record itself. It is trusted-local and refuses an oidc project.

The head tells the browser when to read again, and the browser does not poll it (GH #986). GET /api/hale/v1/head/events is a text/event-stream the head writes event: changed to — data: rows when a row landed in the active project’s organism, data: head when the head’s own state moved: a receipt journaled, a run or a child started or ended, the API child answering. An event carries nothing to render; the face reads the API, the one source of truth, again, and once more on every reconnect. A comment line every 15 s keeps a quiet stream open and finds one whose browser left; a write that does not complete within a second closes its stream. At most 16 streams are held; one more is refused 503 busy. The rows come from the nerves (Rows landing): the head subscribes to the active project’s organization and every owner’s, as the head’s user, on HALE_DNA_NATS_URL_HEAD, else on the server of HALE_DNA_NATS_URL_OWNER (which it hands a body it starts), else on the project’s compose nerves service while hale dna dev has it up (asked for, never brought up; asked again every 5 s). With none, only the head’s own state is pushed. The application view still reads on a timer: the application’s readings (The heart’s events) do not reach it yet.

The API (dna/api, source-built; the head starts one per project) is a head of memory. It reads the Knowledge graph in its own process under the head’s role (HALE_DNA_MEMORY_DSN_HEAD), through one handle opened on the first read and held for the process’s life; a projection that has not reached the record’s head answers knowledge_projection_unavailable until the spine’s tick applies it, and without the DSN Knowledge reads are unsupported (knowledge_unsupported). It admits Knowledge commands — the *_knowledge members of its surface, requiring position, reached over its HTTP exposure like its other commands — into the record in the same process, under the explicit authority policy in the file HALE_DNA_KNOWLEDGE_COMMAND_POLICY names (dna.knowledge-authority/1, at most 64 KiB, decoded by KnowledgePolicyCodec in dna/operations/knowledge_policy.hl against the record’s identity and dna.trust). The policy is read once, when the API starts: an edit to the file does not change a running API’s basis, and a policy that does not decode stops the API (exit 2). dna/api/README.md describes the reads and the topics.

What is deliberately not here yet: a fleet-level semantic diff (a Review’s diff is the edited seed’s; the deploy row names the instances it reaches), pressure raised from services’ typed metrics (hale dna pressure raise is the spelling; nothing raises it for a node), and the organization as an instance of its own plan.

Every HALE_DNA_* variable the DNA reads, and the few others its programs read. The host, the CLI and the programs they start pass most of these to each other: a variable the CLI sets for a child is listed under the process that reads it. Variables that name a role’s memory or nerves are never inherited by a process that is not that role; the harness strips them from everything it starts (see Memory and The senses and the reflexes). crates/hale-cli/tests/env_knobs_are_documented.rs fails if a variable the tree names is missing here or in the runtime’s tables (runtime.md), or a row names one the tree no longer does.

A boolean is on for any value but empty or 0 unless its row says otherwise.

Variable Default Effect
HALE_DNA_DISCOVER=off discover off makes init’s discovery (harnesses on PATH, local model servers) find nothing; for fixtures. The api head passes it on to the init it runs.
HALE_DNA_TAPE=record|replay the catalog’s mode Whether a RecordedModel whose mode_env names it records answers or replays them; the trio fixture’s catalog and scripts/dna-cycle name it.
HALE_DNA_TAPE_DIR=<dir> the catalog’s dir The tape directory of a RecordedModel whose dir_env names it.
HALE_DNA_TOOLCHAIN=<path> the running hale The toolchain binary the host and the processes it starts use; hale dna sets it to the binary it is. It is part of a seed’s build fingerprint.
HALE_DNA_GENOME=<dir> unset The record root the organization takes its genome from; hale dna and the host set it for the organization they start, and the model adapters read it at birth like a credential names its source.
HALE_DNA_GENOME_POLL=<s> 300 How often the host polls the record’s remote for a changed genome, to restart the organization on it.
HALE_DNA_NO_BUILD_CACHE off Turns the content-fingerprint seed build cache off, so every seed is built.
HALE_DNA_FORGE=<kind> inferred Names the forge (github, the file forge) instead of inferring it from github: in the record’s profile or the remote.
HALE_DNA_TRANSPORT=<kind> ssh local runs every infrastructure script on this machine instead of over ssh; for fixtures.
HALE_DNA_TRANSPORT_HOME=<dir> unset With local, the body’s home directory.
HALE_DNA_GIT_TIMEOUT=<s> 8 Seconds a record fetch, push or ls-remote may take before it is killed.
HALE_DNA_NODE=<name> <hostname>:<pid> This node’s name in its claims (GH #1026).
HALE_DNA_OWNER=<org> unnamed The organization this body is, for the ownership graph.
HALE_DNA_LEASE=<name> unset The lease a body holds; set by the body for the processes it starts.
HALE_DNA_LEASE_TOKEN=<n> unset The lease’s fencing token; set with HALE_DNA_LEASE.
HALE_DNA_MEMORY_DSN_OWNER=<dsn> the compose Postgres A Postgres of your own instead of dna/compose.yaml’s: the owner role that migrates memory and creates the record’s roles. Read by memory migrate, dev, show and the fixtures that migrate or drop a record.
HALE_DNA_MEMORY_DSN_SPINE=<dsn> unset The spine role’s DSN, printed by memory migrate and handed to the process that is the spine.
HALE_DNA_MEMORY_DSN_HEAD=<dsn> unset The head role’s DSN, handed to the head and to nothing else.
HALE_DNA_OWNER_KEYS=<keys> one owner <owner>=<key> …, space-separated: the owners memory migrate writes for a shared record. The keys stay the record’s; memory never holds signature bytes.
HALE_DNA_RECEIPT_KEY=<key> unset Sixteen characters or more: the key evidence bodies are sealed under. The first key given is written; a different one later is refused, because bodies sealed under the first would no longer open.
HALE_DNA_NATS_URL_OWNER=<url> the compose NATS The owner user’s URL: it creates the organization’s stream (nerves migrate, dev).
HALE_DNA_NATS_URL_SPINE=<url> unset The spine user’s URL, handed to the process that is the spine.
HALE_DNA_NATS_URL_HEAD=<url> unset The head user’s URL: subscribes, publishes nothing.
HALE_DNA_NATS_URL_APP=<url> unset The application’s URL: publishes on its own subjects only.
HALE_DNA_NATS_URL_REFLEXES=<url> unset The reflexes’ URL: publishes on <org>.app.reflexes.> alone.
HALE_DNA_NATS_USER_APP=<user> unset The attached application’s own nerves user, app-<name> (hale dna nerves migrate prints it; GH #989).
HALE_DNA_NATS_VAULT_APP=<name> unset The vault entry holding the application user’s nerves password (hale dna nerves migrate prints it): presented on CONNECT, never carried in a URL (GH #989).
HALE_DNA_NATS_VAULT_REFLEXES=<name> unset The same for the reflexes user; dna/reflexes refuses to start without it.
HALE_DNA_COMMANDS_PORT=<port> unset The loopback port of the API child’s HTTP commands transport, given by dna/face/start.sh to the head and by the head to its API child; without it the head answers commands_unavailable (503).
HALE_DNA_NATS_ORG=<token> unset The organization’s token, the prefix of its subjects; dev hands it to the host and the host to the parts.
HALE_DNA_SENSES_URL=<url> unset The senses store’s read URL, for the reflexes.
HALE_DNA_SENSES_PORT=<n> the seed’s per-part port The port a part serves its scrape endpoint on; 0 serves nothing.
HALE_DNA_SENSES_HOST=<addr> 0.0.0.0 The address a part’s scrape endpoint listens on.
HALE_DNA_REFLEX_TICK_MS=<ms> 5000 How often the reflexes read the store.
HALE_DNA_REFLEX_WINDOW_SECS=<s> 600 How close two firings for one target are a pattern, not an accident.
HALE_DNA_ORGANIZATION_LAUNCH=<attempt> unset Set by the host on the organization process it starts for a workflow attempt: which attempt launched it.
HALE_DNA_RESTART_FOR=<mutation> unset Set by the host when it restarts an expression on a Mutation’s request; the expression journals expression.restarted for it (GH #529 D6).
HALE_DNA_EXPRESSION=<name> unset The expression that was restarted; set with HALE_DNA_RESTART_FOR.
HALE_DNA_DESIGN_SUFFIX=<text> unset Appended to the design practice hale dna new seeds, so a fixture can exercise upgrade’s supersession against real record history.
HALE_DNA_LIBRARY_VERSION=<version> the toolchain’s The version the library is proposed under (library/<node>@<version>, and on each idea): a fixture’s way to make a later toolchain out of the one binary it has, so upgrade’s supersession is exercised against real record history.
HALE_DNA_LIBRARY_SUFFIX=<text> unset Appended to every library idea’s text, so a later version’s ideas differ from the earlier’s (fixtures only).
HALE_DNA_LIBRARY_EARLIER_NAMES=1 unset The two moved chapters are proposed under their earlier names (library/api, library/shaping): a fixture’s way to make a record the earlier toolchain would have seeded, so an upgrade is seen finding them (fixtures only).
HALE_DNA_INSTANCE=<id> unset Set by a node on each instance it starts: which instance of its shape the process is.
HALE_DNA_BODY=<mark> unset The body’s mark: the host puts it in the environment of the processes it starts as a body (body_mark: holder, lease token, pid), and the body fence finds them by it.
HALE_DNA_SCAN_NEEDLE=<mark> unset Set by the body fence’s scan on a probe process: the mark it looks for in every other process’s environment (ps -E) to find the body’s processes.
HALE_DNA_SCAN_PROBE=yes unset Set on a probe process by the fence’s environment scan, which checks that this operating system shows one process another’s environment before relying on HALE_DNA_BODY.
HALE_VERSION=v<pin> unset Not read by the DNA: the host sets it for hale-lang.org’s install.sh when it installs the toolchain version a body is pinned to.
HALE_DNA_ONESHOT off A hale dna run application returns after the ping instead of staying up.
HALE_DNA_API=<url> http://127.0.0.1:8793 The head API a leg’s verbs (hale dna work …) talk to when --api is not given.
HALE_DNA_ID_TOKEN=<token> unset The bearer token a leg presents to the head; sent only over https:// or loopback, never plain http off this machine.
HALE_DNA_HEAD_STATE=<dir> ${XDG_STATE_HOME:-$HOME/.local/state}/hale/dna/head The project head’s state directory (its launch token and key).
HALE_DNA_COMMAND_POLICY=<path> unset The explicit authority policy the native command lanes and the governance verbs require; with none, they refuse.
HALE_DNA_KNOWLEDGE_COMMAND_POLICY=<path> empty policy The policy file for Knowledge commands.
HALE_DNA_ORGANIZATION_POLICY=<path> unset An optional, independent policy for organization-source proposals and Review writes; configuring evidence reads does not enable them.
HALE_DNA_TASK_POLICY=<path> unset The policy for task administration commands.
HALE_DNA_RPC_SOCKET=<path> the record’s socket under XDG_RUNTIME_DIR/hale/dna (else .hale/dna) Where the head serves its surface (HeadCommands, GH #1417) over unix::Rpc; the capabilities name it to the legs.
HALE_DNA_ORG_DRAFTS=1 off Lets the api accept organization-source drafts (1 only).
HALE_DNA_OIDC_ISSUER=<url> unset The project head’s OIDC issuer; plain http is accepted on this machine only. A project head needs the four HALE_DNA_OIDC_* below or it refuses to start.
HALE_DNA_OIDC_CLIENT=<id> unset The head’s client id at the issuer.
HALE_DNA_OIDC_MEMBER=<subject>=<member> unset Which member a subject is.
HALE_DNA_OIDC_KEY=<pin> unset The pin of the issuer’s signing key.
HALE_DNA_OIDC_KEY_FILE=<path> unset The stub issuer’s private signing key.
HALE_DNA_OIDC_KEY_X=<x> unset The x coordinate of the stub issuer’s public key.
HALE_DNA_OIDC_KEY_Y=<y> unset The y coordinate of the stub issuer’s public key.
HALE_DNA_OIDC_SERVICES=<clients> unset The service clients the stub issuer accepts.
HALE_DNA_CHILD_PROOF=<proof> unset The proof a face gave its child; the child answers an unauthenticated request with X-Hale-Child: <digest of the proof>, so the face knows its own child answered before it forwards a token.
HALE_DNA_TRUSTED_LOCAL=1 off The trusted-local session, a fixture’s mode alone (1 only). A project serves its heads under OIDC.
HALE_DNA_KEEP_SCRATCH off Fixtures and the suite’s sweep keep their scratch roots, for looking at what a fixture built.
HALE_DNA_SUITE_TAG=<tag> unset The stamp a DNA suite slice puts on the processes and scratch roots it starts, so its sweep collects only its own.
HALE_DNA_WAIT_SCALE=<n> 1 A whole number of at least 1 that widens every fixture wait.
HALE_API_BIN=<path> built from the checkout dna/face/start.sh: a prebuilt api binary to launch, as --api does. A binary handed in is checked before anything is built.
HALE_HEAD_BIN=<path> built from the checkout dna/face/start.sh: a prebuilt head (project service) binary to launch, as --head does.
HALE_OIDC_BIN=<path> built from the checkout dna/face/start.sh: a prebuilt stub OpenID provider (dna/oidc/serve) to start. hale dna head hands in the one it built.
HALE_REFLEXES_BIN=<path> built from the checkout dna/face/start.sh: a prebuilt reflexes binary (dna/reflexes) to start beside an attached project. hale dna head hands in the one it built.