DNA

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…

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.

Three memories

Section titled “Three memories”

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

Section titled “Memory”

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).

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 record

Section titled “The record”

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

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

.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.

Event kinds

Section titled “Event kinds”

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.

Workflow definitions

Section titled “Workflow definitions”

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.

The workflow catalog (GH #995)

Section titled “The workflow catalog (GH #995)”

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.

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).

Workflow facts

Section titled “Workflow facts”

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.

Workflow state

Section titled “Workflow state”

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.

Workflow admission

Section titled “Workflow admission”

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.

Workflow execution: one attempt

Section titled “Workflow execution: one attempt”

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.

Legs: the hat, the claim and the outcome

Section titled “Legs: the hat, the claim and the outcome”

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.

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.

Workflow execution: one step

Section titled “Workflow execution: one step”

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.

Workflow execution: ordered steps

Section titled “Workflow execution: ordered steps”

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.

Workflow execution: child workflows

Section titled “Workflow execution: child workflows”

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.

Workflow execution: restore

Section titled “Workflow execution: restore”

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.

Workflow execution: recovering the tree

Section titled “Workflow execution: recovering the tree”

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.

Workflow execution: edit outcomes

Section titled “Workflow execution: edit outcomes”

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.

Workflow execution: human cases

Section titled “Workflow execution: human cases”

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.

Workflow execution: human completion

Section titled “Workflow execution: human completion”

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.

Workflow execution: the baseline

Section titled “Workflow execution: the baseline”

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.

Storage interfaces

Section titled “Storage interfaces”

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 organization

Section titled “The organization”

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).

The organization evolves

Section titled “The organization evolves”

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.

The editing position

Section titled “The editing position”

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.

Models

Section titled “Models”

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

Knowledge

Section titled “Knowledge”

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).

Backends by role

Section titled “Backends by role”

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

The organism’s secrets

Section titled “The organism’s secrets”

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

The fleet

Section titled “The fleet”

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 senses and the reflexes

Section titled “The senses and the reflexes”

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.

The host is a Hale program

Section titled “The host is a Hale program”

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):

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:

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.

The embedded source has a name

Section titled “The embedded source has a name”

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 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.

The surface

Section titled “The surface”

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.

Environment

Section titled “Environment”

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.