DNA
Reference material, synced from the compiler repo’s
spec/. The guide is the gentler path in.
DNA is the part of a Hale application that governs how the application
changes. It ships as a library inside the toolchain (vendor/dna, the
dna/core seed of the hale repository) and as the hale dna commands.
This file specifies what the library and the commands promise; the
design is GH #521 and its successor #566; docs/src/dna/ is the guide.
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 | memory: Postgres, record_rows in the record’s schema dna_<identity>, one signed row per event, insert-only (GH #1590) |
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) |
Each kind’s placement 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. The placement is fixed,
part of the schema: it is not data in the record and is not versioned,
since every record has its Ledger from its first row (GH #1590). 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, as the table routes it. 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: …).
The Ledger is born with the record (GH #1590): memory’s migration
makes both, and nothing adopts or abandons a Ledger. Readers of history
(history, the projections, the export) read both memories as one.
hale dna run refuses to start without HALE_DNA_MEMORY_DSN_SPINE
(#646 decision 3: a body without its memory admits nothing); dev
derives the spine’s DSN from memory that is up and migrated and hands
it to the host, applying no schema (GH #1653). 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:
- a kind that is not the Ledger’s (
ledger_kind, the placement above, in the schema): it is appended to the record instead; - 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; - 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 itsorganizationnode (each owner’s head role over a shared record, The owners’ roles, below);hostfor any head; - a
task.transfer_acceptedfor 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; task.done,task.failed,completion.linkedandcompletion.exceptedin any name but the Task’s assignee’s;- a row naming a lease — the body’s, which its organism carries — whose
claim is not live at that token in
claimsright now:fenced, with whose it is and at what epoch; - a revision a decision was read at that the Ledger has moved past
(
stale_revision; atask.transfer_acceptedlands at the tail whatever itsexpected, 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).
An operational row never goes into the record: memory’s append
function refuses a Ledger kind there (GH #1590).
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 is a
Ledger row (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.
Handoffs between records (stage 5, #662; over the nerves, GH
#1590). A handoff crosses between records as a signed envelope through
an Exchange, the core’s contract (dna/core/exchange.hl): card(url, record) reads the card the other record publishes, deliver(url, peer, id, line) puts a signed envelope in its inbox, idempotent by the
envelope’s id (<handoff id>.<kind>; the handoff id is its origin, peer,
kind and subject, so a delivery repeated after an interruption is one
envelope), and received() is every envelope this record’s inbox holds.
NatsExchange (dna/host/connections.hl) is the nerves’ and
MemExchange the fixtures’. The contract #646 set holds: durable
delivery — an envelope is stored by the receiver’s exchange stream,
which keeps it until the receiver has drained it (no age limit), before
deliver returns; one delivered again by its id within a week is stored
once, and the receiver keeps one in memory whenever it arrived; a
delivery that cannot reach the peer records nothing and says to deliver
again; authorship — every envelope is signed
with the sending record’s own key, and a receiver admits one only under
a connection that pinned that key; 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 accepted envelope reaches it, never when a delivery
returned.
Coordination in memory (stage 2, #651). 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). The body lease is the row
body (owner/<owner> over a shared record): the fence renews it every
third of its life with every call bounded, a stale token — a host that
lost the lease to another — lands nothing, and hale dna body reads it
there. A head takes and fences a lease under its own role.
Effect claims are unique rows of the Ledger ((kind, entity)), so a
contended-claim row does not arise. Memory lost after activation: 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) — and they open Postgres through
pond’s driver, pinned under dna/core/pond and vendored beside the
core at vendor/dna/pond. The projection tail, MemoryProjection, is
not in the core: it lives in dna/operations/memory_tail.hl, beside
the command codecs it decodes admitted facts with, and runs on the
host’s tick (The spine, below).
- Memory is per record. Each record has its own schema,
dna_<identity>(the identity lower-cased, any other character_, at most 56 characters of it), and two roles of its own,dna_<identity>_spineanddna_<identity>_head, cut to fit Postgres’s 63-byte names: one role granted on every record’s schema would read every other record’s evidence on the same server. A role’s password is the vault’spostgres-<role>(The organism’s secrets), drawn when the role is declared and read at use; the migration sets each role’s password from the vault (ALTER ROLEwhen it exists), so the vault stays the one source. A role’s DSN carries no password: it names the entry,postgres://<role>@host:port/db?sslmode=…&vault=postgres-<role>, and the connection reveals it into its SCRAM exchange’s@secretparameter, in the one statement that uses it (GH #989). The owner’s DSN of the organism’s memory allocation in the local habitat namespostgres-owner-<id>the same way (The local habitat holds the organism’s infrastructure, below). An operator’s DSN with a password in it is presented as given. - Three DSNs.
HALE_DNA_MEMORY_DSN_OWNERis the schema owner’s, used only by the habitat’s acts that migrate (GH #1653): birth,hale habitat migrate [organism],hale dna memory migrate [dir],hale dna upgradewhen it is set, and a provisioned body’s provisioning script, which runshale habitat migrate .once with it. Without it, those acts bring up and migrate the organism’s memory allocation in the local habitat. A migration prints two lines,HALE_DNA_MEMORY_DSN_SPINE=<dsn>andHALE_DNA_MEMORY_DSN_HEAD=<dsn>: the owner’s host and database, each role’s name and credentials. The host that runs the organism is handedHALE_DNA_MEMORY_DSN_SPINEalone, and the organization inherits it:devderives it from memory that is up and migrated, applying no schema (memory-migrate --derive), andhale dna runtakes it from its own environment; both takeHALE_DNA_MEMORY_DSN_OWNERandHALE_DNA_MEMORY_DSN_HEADout of the host’s. With no memory up,devsays so, nameshale habitat migrate <organism>, and starts the host without memory. A head — the CLI,hale dna ui, the API — opens memory withHALE_DNA_MEMORY_DSN_HEAD; a process given the spine’s DSN uses that instead, and none holds both. The head’s DSN a migration answers is kept in the clone,.hale/dna/memory.env, beside the line that says where it was derived,HALE_DNA_MEMORY_FROM=ownerorHALE_DNA_MEMORY_FROM=habitat <project>/memory-<id>, and everyhale dnaverb in that clone given neither role’s DSN runs with it. The file holds the head role’s DSN alone (never the owner’s or the spine’s), is written mode 600 and never printed, and is written only where git ignores it (a graft andupgradekeep/.hale/in.gitignore); a verb refuses it when it is readable by group or others, namingchmod 600. A kept DSN derived from anything but the operator’s database or the organism’s own memory allocation (another habitat’s, a database an older toolchain migrated, a file with no origin line) is removed and refused, naming `hale habitat migrate`: no verb re-derives it, since none migrates as a side effect. - The migration runs in one transaction under an advisory lock on
the schema: the
vectorandpgcryptoextensions, the schema and every store’s tables, the record’s claim on the schema (knowledge_meta.record; a schema that names another record is refused), the projection protocol, the receipt functions and the receipt key, both roles and their grants, andmemory_meta.schema_version. It is idempotent. It refuses a knowledge store left inpublicfrom before stores were scoped (drop its tables or the database; the projection rebuilds from the record), and a schema a newer toolchain migrated. - The version fence. The schema version is 8 (GH #1026: 2 made the
lease table
claimsand the graph’s projection one transaction per record row — migrating a version-1 memory renames the table in place, its rows kept; 3 adds the Ledger’s gate,ledger_append, with the org chart it checks and the roles’ principals; GH #1085: 4 adds the repository’s graph,graph_nodes,graph_edgesandgraph_members; GH #946: 5 addshats, hats by digest, which moves no projection; GH #1123: 6 makesorg_membersa view over the graph’sholdsedges, where it was a table the spine rewrote from the retired owners map, and moves no projection either; GH #987: 7 adds thereadingfamily to the placement the insert function reads, which moves none; GH #988: 8 adds thereflexfamily the same way. Each protocol move empties the graph once for the projectors to rebuild). A store’sopenselects the record’s schema and refuses one at another version, or at none, naming both andhale habitat migrate <organism>(withHALE_DNA_MEMORY_DSN_OWNERfor a Postgres of the operator’s); it also refuses a schema whose claim names another record (it is not read). The host checks the version on the spine’s DSN before it starts anything, and refuses to start (exit 2) on a mismatch. - Grants. A head’s role — the head’s, or an owner’s — has
SELECTon the ledger, knowledge, org-chart and meta tables (memory_meta,ledger_rows,ledger_meta,ledger_requests,claims,knowledge_*,graph_*,org_members(a view),org_retired),INSERTandUPDATEonclaimsonly, to take and fence a claim, andEXECUTEonreceipt_file,receipt_readandledger_append: a head writes the Ledger through its gate and no table directly. The spine’s role reads and writes its schema’s tables (SELECT,INSERT,UPDATE,DELETE,TRUNCATE) except the owner-onlymemory_keys,memory_principals,protected_receiptsandprotected_redactions, and hasEXECUTEon every function; it has no DDL. Every Ledger row goes throughledger_append. - Connections are bounded. Each process holds one memory handle
(
Memory: the Ledger and the leases), opened on first use and closed when the process ends; everything in a process that reads the Ledger or a lease takes that handle rather than building its own, and eachPqstore closes its connection when it dissolves. A host holds a fixed handful of connections however long it runs.
The spine (GH #1026). The spine is not a process: it is the program
any number of identical nodes run, and a node holds nothing — its state
is the stores, its code is the genome. On each tick (HOST_TICK, 1 s)
every host that runs a body applies the record into the graph and the
org chart (MemoryProjection) 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 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). Every row a node projects is shared already: the record is memory’s,
read by every clone and body alike (GH #1590). 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/hale/remote/genome (the genome’s; 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 record is memory’s (GH #1590): the table record_rows in the
record’s schema dna_<identity>, read and written by every clone and
body alike, so nothing of it travels by git and there is nothing to
sync.
- One row per event:
seq,kind,entity,body,author, the writer’s role (writer, the session’s),prev,digest,sig. - The identity is the first row’s digest.
hale habitat graftcomposesrecord.genesis {genome, nonce}(the genome’s root commit, and a nonce so two projects made alike are two records), pins its digest inhale.toml([dna] record), provisions memory (the record’s Ledger with it), and appends it at 0, thenrecord.key {key}(the public half of the record’s key). A clone whose history does not hold the genome a record was born on does not open it (thehale.tomlnaming it was copied from another project). - The rows are read through the toolchain.
hale dna record-rows [project]prints the record’s rows in the line format (one JSON object per line:seq,kind,entity,body,author), oldest first, and exits 1 with the reason when the record cannot be read. It prints the record’s rows alone; the Ledger’s are memory’s, andhale dna historywalks both. The DNA’s tests and scripts read the record through it and never through its storage. - The chain is a prev-digest chain. A row’s digest is
sha256(prev|kind|entity|body), itsprevthe digest before it (genesisfor the first); a reader verifies the chain it loads. - Append is one function, insert-only. Every writer appends through
record_append(one row) orrecord_append_rows(a batch made visible together, every row checked before any lands),SECURITY DEFINERfunctions that take the schema’s advisory lock, refuse a Ledger kind, and land the row only on theprevand, when asked, the revision the writer read; no role may update, delete or truncate the record’s rows, or the Ledger’s. A writer that lost the race at the tail reads again and goes round with the new tail, within a bound only a wedged memory reaches; an exact append is one attempt. - Every row is signed as its writer signs. The organism’s parts
(
dna,spine,host, a node, a leg …) sign with the record’s key (Ed25519, in the vault asdna-exchange-<schema>, never leaving the credential) overhale-dna-row+ NUL + the digest; a person signs with their own ssh key (ssh-keygen -Y sign -n hale-dna-record, the key git’suser.signingkeynames undergpg.format ssh), and is unsigned with none configured. Underdna.trust = signeda person’s row is admitted when the key the record enrolled for them verifies it (hale dna signers:signer.enrolled {who, key}andsigner.revoked {who}rows, each counted only when signed by someone the record already admits; the first enrolment is the bootstrap). - A record an earlier toolchain kept on git (
refs/dna/journal) is not carried forward: nothing reads it, and a graft andhale dna upgraderefuse a clone that holds one, saying to start a fresh record (delete the old refs and the[dna] recordline, then graft again). - Receipts are kept in memory by the digest of their content: a
verification step’s output, a diff document. Events name receipts by
digest. A receipt has a data class (GH #606): a
publicorinternalbody is kept unsealed (receipt_keep(digest, class, body, at), which refuses a digest that is not the body’s and one memory erased under a redaction); acustomerorconfidentialbody (protected_class) is sealed. It is kept in memory alone, sealed there — by the organism throughDna.file_evidence(text, class, by), by a head throughhale dna receipt file --class, both throughMemoryVault— and the record getsreceipt.classified <digest> {class, by, store}. Memory seals it, and no process holds the key (GH #985). The receipt key lives in memory, inmemory_keys, written once at migration from the owner’sHALE_DNA_RECEIPT_KEY(sixteen characters at least); a different key at a later migration is refused, because bodies sealed under the first would no longer open. ThreeSECURITY DEFINERfunctions, run as the schema’s owner, are the whole surface of the sealed tableprotected_receipts:receipt_file(digest, class, body, at)seals a body with pgcrypto (OpenPGP, AES-256) under the key, and refuses a class that is not protected, a digest that is not the body’s, and a digest memory erased under a redaction, whoever files it;receipt_read(digest)opens one;receipt_erase(digest, at)deletes the body and keeps its digest inprotected_redactions. Heads and the spine may executereceipt_fileandreceipt_read; only the spine may executereceipt_erase. Memory with no receipt key keeps no protected evidence and says so. A dump of the database carries the key (memory_keys) and the ciphertext together, so a dump is as sensitive as the evidence itself; #989 revisits where the key is held. A query that fails drops the connection, dials again and tries once more, so a database restart does not leave every protected read and erase failing (#637). Without memory, the body is withheld:receipt.withheld <digest> {class, by, why}, and nothing holds it. The record keeps the digest and the class either way, and only the sealed table holds a protected body. Reading one is an act in the reader’s name for a purpose (hale dna receipt show <digest> --purpose <p> [--as <who>]): it is refused unless areceipt.disclosed <digest> {recipient, purpose, by}row names both, a refusal appendsreceipt.read_refused, and an answer is given only once itsreceipt.readrow is in the record — a read the record cannot hold discloses nothing. A body kept while the record refuses itsreceipt.classifiedrow is reported so, to be filed again.Dna.evidence_class(digest)is the class a request carries when it puts the body in a prompt, so a hosted model refuses it. This is the third trust profile #606 names: the readers of a clone hold digests and classes, never protected bodies. Under local trust a reader’s name is attribution, as--asis; a verified principal is #612’s. Retention (part 2).receipt.held <digest> {by, why}stands untilreceipt.hold_released, and refuses redaction while it stands. A redaction (Dna.redact_evidence(digest, by, why, policy),hale dna receipt redact <digest> --why --policy) appendsreceipt.redacted <digest> {by, why, policy, class, store}and then removes the body: an unsealed body is erased from memory at once (Receipts.erase); a protected body is erased from memory throughreceipt_erase, by the spine — a head’s redaction of a body memory keeps recordsreceipt.redacted(store: memory) and the spine erases the body on its tick, as it erases every body the record redacted and memory still holds; a withheld one had no body. The redaction is in the record before a byte is erased, appended exactly at the revision the hold was read at: a redaction the record refuses erases nothing, a hold that arrived in between refuses it, and a redaction recorded before an erase that failed is completed on a later tick or by redacting again. A body the record says was redacted is not filed again —Dna.file_evidenceanswers its digest and keeps nothing, andhale dna receipt fileandreceipt_filerefuse it — andhale dna receipt redactappends an unsealed receipt’s redaction only at the head it read the hold at and, once the organism has adopted the ledger, only at the ledger revision it read the hold at (the request’sexpected), refusing “the record moved” otherwise (#636). A store that cannot read the body (its read failed, as opposed to finding none) records nothing and reports nothing erased: redact again once it answers (protected_erase_step). The record keeps the digest, so provenance survives and a reader learns the body is gone — a read of a redacted digest is refused… was redacted: by … under …, andhale dna historysays so under a redacted prompt. A copy disclosed outside the record cannot be recalled. The projection retires an idea whose receipt was redacted instead of stopping. - Leases are rows of memory (
MemoryLeasesover theleasestable):holder,token,expires,present, put by token compare-and-swap. Tokens are monotonic per key. - Effects are requested before they run, and the claim is exclusive
(GH #604 rule 3).
effect.requested <key>is appended before a dispatch andeffect.result <key>once after; a retry finds the request and does not dispatch again. Two writers may both append a request for one key; only the writer whose row is the first request dispatches, the other’s row stays as a contended claim and it does not act: every gate (worktree.open,commit,apply,rollback) refusesin flightwhile another request holds the claim with no result yet (#635). A request with no result when the organism restarts is resolved by evidence where evidence exists (anapply:<candidate>whose genome is at the candidate isok; aworktree.openwith no worktree isfailed) and markedunknownwhere it does not; the gate refuses an unknown effect until a person resolves it —hale dna effect resolve <key> --outcome ok|failed, one row in their name, the last result for the key being its status. - There is nothing to sync (GH #1590). Every clone and body reads
and writes the one record in memory, so
hale dna sync(and the host’s tick) only applies what the record says of this clone — a dropped candidate’s branch deleted here. A fact’s identity in the record is its content and its place among rows of the same content, never itsseq(a node raising one concern three times writes three identical rows, each a fact): a host relays and handles by that identity, so a fact is not relayed twice and a repeated one is not relayed once. Another record’s envelopes arrive on the nerves. Candidates are branches of the genome and travel as branches do (below); the genome’s remote isdna.remotein git config, ororigin. A clone of the genome reaches the record through memory (its[dna] recordpin names the schema, andhale dna memory migrateorupgradewith the owner’s DSN keeps the head’s DSN in the clone). - Candidates are kept, as branches of the genome (GH #1590). The
gateway keeps a candidate when it commits it as the branch
dna/candidate/<mutation>(refs/heads/dna/candidate/<mutation>,dna::candidate_ref), and the record names its commit (mutation.candidate <mutation>, the sha), so a refused or revised change stays readable as a diff after its worktree is gone:hale dna candidateslists the branches with their Review’s state,hale dna candidates <mutation>shows one from its Review’s base, andhale dna candidates drop <mutation> --why <why>appendscandidate.dropped <mutation> {by, why}and deletes the branch here; every clone deletes its own when it reads the row. A candidate is code, and travels as any branch does: a person who pushes one to a forge deletes it there as any branch is deleted. The commits stay in each object store until git collects them. The readers know branches alone; nothing reads or moves a candidate an earlier toolchain kept under the record’s refs. - An apply expresses the candidate, tree and all. An approval
applies exactly the reviewed candidate, or nothing: the candidate’s
worktree head is the pinned digest, the genome’s head is the base the
review was against, and the genome has nothing uncommitted — tracked
changes anywhere, and every input of the candidate’s build that
the tree does not hold. The graph is the candidate’s, read by the
compiler in the candidate’s worktree (
hale inputs <seed>: the seed’s.hlfiles and those of every directory they import, transitively; thehale.tomlof each such directory and every C source it declares under[ffi]), because a reviewed change that adds an import makes files an input the moment it lands; any file of those kinds the genome holds untracked in one of the graph’s directories, or that the graph names outright, is dirt whether git ignores it or quotes it. What no build reads — a binary, a log, another program’s source — is not dirt. A graph that cannot be inspected is dirt: an inspection that failed authorises nothing. A dirty genome ismutation.refusedbefore any effect row exists, the work untouched; the gateway checks it once more around the git call and reads the head back, so an apply that reports success is the candidate. Approving again an approved and unapplied candidate re-runs the whole gate (mutation.apply_retried, thenreview.settledagain so a waitinghale dna reviewhears it): the Review readmits only the same approval in full — digest, standing (a required position, or with none the authority), independence, cited evidence, the wordapprove— and refuses any other verdict; a Review approved and never applied is rehydrated settled, so the road survives a restart. - A person’s job (GH #596 W; GH #604 rules 4 and 5). A plan of kind
personhands the Task on: the Work is done as far as the organism is concerned and the record keepstask.handed <task>—work,assignee(the person the plan named),by,narrative; nothing is mutated. The person reports it done withhale dna task done <id> [--as <who>] [--note …], atask.donerow in their name and nothing else. Completion is checked at admission: a handed Task with an assignee is closed only in the assignee’s name; anyone else is refused and told to reassign.hale dna task reassign <id> --to <who>appendstask.reassigned(from,to,by) and the Task stays handed.hale dna retire <who> [--to <successor>]stops new work reaching a person: every handed Task they hold is transferred as its owntask.reassignedrow andperson.retired <who>records it (by,to,transferred). From then no new work reaches them: a job the leader plans for them is handed to that successor, withretired_assigneeon thetask.handedrow — followed on through each successor who retired in turn to the first person still working, unassigned when the chain ends in a retirement that named none or comes back on itself — and a Task is never reassigned to them, nor are they named anyone’s successor; refused while they hold work and no successor is named — pending work is accounted for, never dropped. Refused for a Task that is not handed: one the organism is working, or has settled, is not a person’s to close. A completion meets its acceptance condition. When the plan names the obligation a person’s job discharges, the organism binds the acceptance practice in force at hand-off:task.handedcarriesacceptance(the digest of the practiceacceptance/<obligation>that is ratified and not retired, “” when none) andevidence_required(its text saysevidence: required). A later change of practice never changes a case already handed. Such a Task closes only with--evidence <digest>, a receipt the record holds, appended beforetask.doneascompletion.linked(task,evidence,by,practice), or with--exception <why> --authorized-by <who>, appended ascompletion.excepted(task,why,authorized_by,by,practice) — only when<who>has authorized it in their own name first:hale dna task authorize <id> --exception <why> [--as <who>]appendsexception.authorized <task> {task, why, by, practice}, refused for the Task’s assignee and, when the bound practice names who may (exceptions by: <names>), for anyone else, and refused outright when the bound practice cannot be read here (absent from the clone, unreadable, redacted) — an unreadable practice is not one that names no one; naming someone is never an authorization. The completion carries the exception exactly as it was authorized under the bound practice: a different--exceptionis refused, and needs an authorization of its own. A note alone is refused, and so is an exception without an authorizer, a self-authorized one, or both at once. Either may accompany any person’s completion.task.donekeeps its body; the projection shows a done Task’s evidence or exception, and a person’s completion with neither as human-reported, because a note never verifies anything. - A person proposes a practice (GH #602).
hale dna practice propose <name> --text <text> [--because <what prompted it>] [--supersedes <digest>] [--as <who>]carries aPracticeRequest(request_id,name,text,by,because,supersedes) as apractice.requestedrow, which a node relays onto the topicdna.practice.requested(The nerves). The organization proposes it as knowledge of kindpractice, authororg, bound toorg, with the ordinary Board Reviewk:<digest>(decided, it is ratified by an execution ofpractice-ratify: GH #995, The workflow catalog), and journalspractice.proposed <request_id>(name,digest,review_id,by,because,supersedes); a request without a name or a text ispractice.refused. Nothing is in force until the Board ratifies it.--supersedestakes the digest whole or as any prefix of it, ashale dna practiceabbreviates it (sha256:3122d), and the CLI resolves it before it requests anything (dna::superseded_digest): the row carries the full digest of the one practice in force it begins, and a prefix that begins no practice’s digest, several practices’ digests, or the digest of one not in force is refused there, by name, never accepted and then refused at ratification. Roles are authorities, not identities. The person who proposed a practice may ratify it: the proposal is in their name (by), the verdict carries the Board’s authority, and the record keeps both, so one person holding several roles — the Board, and the hands doing the work the practice describes — proposes as one and decides as the other. The Review’s independence rule refuses the candidate’s author, which for a practice is the organization (org), never the proposer; and a governance proposal requiring the Board’s authority (a fill, an opening, a graph proposal) admits its author’s verdict too, so the Board ratifies what it asked for. A change to the work never does, whatever authority it requires. This is the design, not an artifact; an organization that wants the proposer barred from ratifying their own proposal needs a review policy that says so, and none ships. The text arrives byte for byte, newlines included: the host escapes an argument’s own backslashes and newlines in its argument list and unescapes every value it reads.hale dna practicelists every named practice with its state (in force, awaiting the Board, declined, retired), its first line, who proposed it and why, and requests not yet heard. - Cross-record handoff (GH #615; over the nerves, GH #1590). A
replica of one record is inside the horizon: sync carries all of it,
to equally trusted readers. A handoff crosses a horizon into a
separate record, and a connection is its only path; nothing selective
leaves a record by sync.
- The card. Every record publishes a card on its nerves:
{identity, genome, key}— its identity, its genome’s root commit and the ed25519 public key its envelopes are signed with — signed by that key, the one message ofexchange.<org>.cardin its exchange stream (DNA_<ID>_EXCHANGEoverexchange.<org>.>).hale dna nerves migratecreates the stream, its inbox consumer (exchange, overexchange.<org>.in.>) and the card, purging the one before. The key’s seed is the SHA-256 of the vault’sdna-exchange-<org>, drawn by the graft andupgradelike the organism’s other secrets; it signs throughstd::secret::Credential.sign_ed25519, so neither the secret nor the seed leaves the vault’s credential. The record is the signer (a record-to-record act); the person who acted is named in the envelope. - Connecting.
hale dna connect <nerves-url> --record <identity> --name <name> --as <position> --purpose <purpose> --classes <internal,customer,confidential> [--by <who>]reads the other record’s card at its nerves (a direct get: connect needs the peer’s card, published by its nerves, and no process of it running), checks the card’s signature and that it is<identity>’s (the identity its genome pins,[dna] record), refuses this record’s own, and appendsconnection.proposed(name,url,peer,peer_key,peer_genome,position,purpose,classes,by,review_id,exchange: "nerves") with a Board Reviewc-<name>-<n>. The connection is in force once aboardverdict approves that Review from someone other than its proposer — the host then settles the Review in the approver’s name — and untilhale dna disconnect <name> --why <why>appendsconnection.closed. A connection proposed before the nerves exchange (exchange: "git", no key pinned) is not in force: it is proposed again. - The inbox key. The other record’s server holds a user
exchange(nats-<org>-exchangein the vault, the nerves allocation’snats.conf) that may publish intoexchange.<org>.in.>and read the card, and nothing else. Its operator hands it to a connected record’s operator, who keeps it under the same vault name; a sender whose vault lacks it connects without credentials, which an open server alone admits. The sender is proven by the envelope’s signature, not by the key. - Handing off.
hale dna handoff <name> task <id> | receipt <digest> [--note …] [--as <who>]signs one envelope (kind: received) and publishes it toexchange.<peer org>.in.<this org>.receivedwithNats-Msg-Idits id and the first 32 hex digits of the sha256 of its signature (so a retry of the same signed envelope is stored once, and no other holder of the inbox key can take its place by publishing first under an id it could predict); once the other record’s stream has stored it, this record appendshandoff.published(withpeer_row,<stream>#<seq>, and the signed envelope) and, for a Task,task.transfer_requested. The envelope’s body carrieshandoff(hand twelve hex digits of the origin’s identity, kind and subject),origin_record,origin_author,origin_row,lineage(the subject’s rows here, askind#seq),purpose,position,via,kind,subject,class,fact,note. A handoff retried after a crash completes what the first try left (#637): the envelope is delivered once by its id, and a published Task missing itstask.transfer_requestedgets it. Only a handed Task crosses. A receipt crosses as its digest and class; its body never leaves this record. A fact whose class the connection does not carry is refused at the edge ashandoff.refused, with nothing published. - The bounded inbox. The exchange stream is a work queue
(
retention: workqueue): an envelope leaves it once the receiver’s drain has acknowledged it, kept in memory or refused, and the card (read by direct get, never consumed) stays. What waits undrained is bounded, and the stream refuses what overflows it rather than dropping what it holds (discard: new): each sender’s subject holds at most 10 000 waiting envelopes of a kind (max_msgs_per_subject,discard_new_per_subject) and the stream 256 MiB, so a connected record cannot flood another’s disk while it is not reading, and a receiver that drains takes any number. A handoff refused as full records nothing here and says so. - Receiving. The receiving record’s verbs (
hale dna handoff,handoff accept,handoff sync) first drain its inbox into memory: each envelope addressed to this record, from the sender its subject names, whose signature holds under the key it carries, is kept once by its sender, its id and the key that signed it inexchange_rowsthroughexchange_keep(insert-only; memory schema 9), and acknowledged; anything else is acknowledged and refused. An envelope claiming a record this one has a live connection to, signed with any key but the one that connection pinned, is acknowledged and dropped unkept, and the drain says so on stderr; one from a record not yet connected is kept beside any other claiming the same id, so a forgery never takes the place of the sender’s own envelope, and only the one under the key a connection pins is admitted. The drain reaches its own nerves as the spine (HALE_DNA_NATS_URL_SPINE, else the owner’s server, else the organism’s nerves allocation if it is up) and says so when it reached none.hale dna handofflists a received handoff as admitted only under a connection in force back to its origin record that pinned the key the envelope is signed with and carries its class;hale dna handoff accept <id> [--as <who>], only for an admitted one, appendshandoff.acceptedto its own record (with the envelope’s origin, lineage, purpose and fact) and delivers anacceptedenvelope into the origin’s inbox; accepting it again delivers that envelope again.hale dna handoff syncadmits, for every connection in force, each acceptance the other record sent — under the pinned key — once, ashandoff.accepted_by_peerand, for a Task,task.transfer_accepted, each row checked on its own, so a crash between the two is completed at the next sync rather than suppressing it (#637). The Task settles only then, the rule retirement follows (GH #604 rule 5). A closed connection is not read, so history stays in both records and nothing further is admitted.
- The card. Every record publishes a card on its nerves:
- An attributed external decision (GH #616). A decision made by
someone who does not run Hale — a manager, a client, an accountant —
enters as a fact reported by a position inside the horizon, never as
that party’s verdict.
hale dna task decide <id> --decided-by <party> --via <channel> --evidence <digest> [--note …] [--as <reporter>]appends onedecision.reportedrow on the Task in the reporter’s name:reporter,decider,channel,evidence,scope(the Task),obligation,practice,policy(the practice’s digest),accepted,why,note. It is refused with nothing appended when the Task is not handed, when the reporter is not its assignee (GH #604 rule 4), when the decider is the reporter, when a field is missing, or when the evidence is not a receipt the record holds.hale dna receipt file <path> [--class internal|customer|confidential]files one: internal text kept in memory with areceipt.filedrow (by,name,bytes,class,store), a protected class sealed in memory alone (receipt.classified). Whether a report settles the Task is the obligation’s acceptance policy: a practice namedacceptance/<obligation>, ratified by the Board and not retired, whose text saysreported decisions: allowed. The obligation is the class of obligation a person’s job discharges; the leader’s plan names it (obligation:) andtask.handedcarries it. The policy is the practice bound when the Task was handed (task.handed.acceptance), so a later change of practice changes no case already handed — only a Task handed before practices were bound reads the one in force; it is read at admission and its answer kept in the row, so a later change of practice never rewrites what was admitted. Accepted, the Task isdecided, and the projection shows the report as such: who reported, who decided, through what, on what evidence. Otherwise the Task stays handed,hale dna boardlists it under tasks waiting with the reason, and its assignee closes it withhale dna task done. Without an obligation class or a practice in force, a report never suffices. - Schedules (GH #610, GH #1143). When a workflow runs and who
convenes it — nothing else. A schedule points at a definition of the
catalog (The workflow catalog), never at intent text:
Schedule { id, definition, args, every_ms | cron, convener, paused, last_occurrence }, plus the state the record rebuilds (from,task,fired,last_minute). There is noaskand norequires, noschedule.firedorschedule.admittedrow, and no claim before an ask: an occurrence is an execution, and its key is its idempotence. Declared by ratification. A ratified practice whose receipt carriesschedule(a JSON object as text), or whose text names a cadence and a step among its terms (A practice’s terms, below), declares it in itsratifystep, and so does each practice of a family (a pack) the Board ratifies whole; the ratification stands when the declaration is refused — the step is done,ratified by <who>; its schedule was refused (<why>), and the refusal is its ownschedule.refusedrow naming the hole.hale dna schedule declare <id> (--every <n>ms|s|m|h|d | --cron <expr>) --definition <id> --convener <position> [--args <json>] [--as <who>]asks it of the organization through the store: aschedule.requestedrow the host relays, answered once under the request’s id byschedule.answered {schedule, declared, why, by}(the CLI takes a bare position name asposition:<name>). Declaring through the store is the Board’s, or the convener’s own holder’s: with memory naming who holds the Board, a requester who holds neither it nor the convener is answereddeclared: falsewith why, and nothing is declared; while nobody holds the Board, or no memory is named, the verified requester declares, as a Review’s fallback decides by its required authority. A practice’s terms. A practice’s text may carry terms, words<key>=<value>(the value the rest of the word;practice_term,dna/core/practices.hl): the parameters of what the practice runs. Only a document of kindpracticeruns anything: an idea or a mandate whose text shows terms (an example, say) declares nothing. A practice whose terms nameevery=(an interval,500ms,30s,15m,1h,1d) andstep=<definition>declares, ratified, the schedule whose id is its name, of that definition, every that interval, convened by the position it is bound to (its target,position:<p>; a practice bound toorgnames no convener and is refused). Either way, by its receipt’sscheduleor by its terms, an occurrence’s inputs are{practice, name}(the ratified digest and the practice’s name) and whatever the receipt’sargsadd, so the step reads its parameters from the practice itself: an amendment the Board ratifies changes the terms, and the schedule is declared again with them. Upstream watches (below) is the first practice that runs a step this way. A declaration isschedule.declared <id> {every_ms, cron, definition, args, convener, from}, written when new or changed — declaring the same schedule unchanged writes nothing — andfromis the occurrence it was declared at (a cron with no match in the week before counts from the declaration’s own time): nothing at or before it is due. It is refused, aschedule.refused <id> {why}row and the reason, when it has no id or one with@or a space; names neither an interval nor a cron, or both; names a malformed cron (five fields, minute hour day-of-month month day-of-week;*,a,a-b,*/n, lists; ranges checked — at declaration, never when it would first fire); names a convener that is notposition:<name>, or one that cannot reach the definition’s first store; names a definition not in the catalog, one whose expansion the catalog would refuse, or one with a step the organization does not perform; or carriesargsthat are not a JSON object naming every input the definitiontakes. Reach: the organism’s own positions (leader,editor) reach every store its workflows write; a position of the graph reaches it through who holds it — a person, or an organization’s members — as memory’s routing says, and in this slice being held is the whole of reach: no per-position grant on a store is read; with no memory named, or memory not answering, a graph convener is refused, nothing assumed. A position nobody holds is a hole: the refusal says “nobody holds position:: it convenes nothing until someone does (
hale dna fill <p> <holder>)” and the row carrieshole: position:<p>, the hole #1091’s path fills. Declaring also writes the schedule into the graph (The repository’s graph): adefinition:<id>node, once, and aconvenes(position, definition)edge whose body carries thecadence(every 1d,every 90s,cron <expr>) andscheduleof every schedule declared for that pair (;- and space-joined). A re-declaration for another pair writes the pair it left again, and retires its edge (graph.retired) when no schedule is left on it. Occurrences. The org program’s loop ticks with the wall clock in milliseconds (request_tick(std::time::nanos(std::time::current()) / 1000000), never a monotonic clock), queued through the organization’s keyedTickRequestedhandler. Synchronoustickis for that handler or isolated callers with no incoming bus work; a live loop usesrequest_tickso incoming work cannot enter during journal refresh. An occurrence’s time is its cadence’s: an interval’s step from the epoch (floor(now / every_ms) * every_ms), or a cron’s latest matching minute in UTC (day-of-month and day-of-week both restricted: either; looked for over the last week), a cron evaluated once per minute. A tick in an occurrence’s period admits it as an execution of the definition’s newest revision through its first store, with the ask idsched:<id>@<occurrence time>,argsas its inputs, and the convener as its position: the admission is the convener’s owner’s (to, GH #664; the organism’s own positions are the organization’s), andworkflow.admittedcarries it asconvener. On a shared record a body whose organization does not own the convener leaves the schedule to the one that does, writing nothing. That key is the idempotence: a schedule’s state is rebuilt from the record (from its declaration’sfrom, the admissions under its key, its skipped, refused and missed rows, and its pauses), so a restart, or a second ask of the same occurrence, is one execution. The occurrences whose period passed with no tick — the organism was down — are oneschedule.missed <id> {first, last, count, why}row per run of them, and nothing else in this slice. A refused admission isschedule.refused <id> {occurrence, why}. Concurrency is the definition’s (WorkflowDef.concurrency): underskip, the default, an occurrence while the last execution is open (admitted, not yetworkflow.settled) is aschedule.skipped <id> {occurrence, task}row, never silent; underoverlapit is admitted beside it. Amendments.hale dna schedule pause <id>/resume <id>appendschedule.paused {by}/schedule.resumed {by, at}(atin wall milliseconds) in the caller’s name from any clone; the organism reads them at its tick (a git journal is re-read from its ref at most every 5s). A paused schedule is not due: its occurrences pass unfired and unmissed, and a resume goes on from the next one — a resume read at a restart counts misses from itsat.hale dna schedulelists each as the record has it —<id> [live|paused] every <n>ms|cron `<expr>` (UTC) — <definition>, convened by <position> · occurred N, skipped N, missed N · last <task>, occurred counting the admissions under its key. Not in this slice, by name: terms (an edge’suntilfiring a definition), turnarounds (a row’s age firing one), calendars and offsets beyond UTC, per-person availability, derived cadence holes (ingest proposing that a process has no cadence),on_miss: lateand aqueueconcurrency, declaring a refused schedule again once its hole is filled, and who fires on a multi-node spine — the tick is the body’s that holds the lease, and a memory compare-and-swap on the occurrence key is owed when spine nodes multiply. - The optimize pass (GH #596 O). The pass is an execution of
optimize-walk(GH #995: The workflow catalog) and nothing else. Its cadence is a schedule (above) that the seeded operating practiceoperating/optimize-cadencedeclares once the Board ratifies it —{"id": "optimize", "every_ms": 86400000, "definition": "optimize-walk", "args": "{}", "convener": "position:leader"}— convened by the leader, who walks the machinery, so it needs no one seated to be declared; a different cadence is an amendment the Board ratifies. The tick is the body’s that holds the lease, and an occurrence asked twice is one pass. Its one step,walk, first asks the budget — the pass is model-backed work, and none is routed on an exhausted window:optimize.refused <org>(“the pass waits for the next occurrence”) and the step fails saying so — then reads the record’s structural signals — asks planned and how many took the defaults, concerns raised, grant contractions, verdicts refused, mutations rolled back — and asks the leader (OptimizeRequested, keyed byorg_id, once per incarnation) to walk the machinery, not the work. The leader answers with one small proposal or none (OrgReviewed), and the substrate journalsorg.reviewed <org>either way, with the signals it read and the execution it answers (task, carried onOptimizeRequestedandOrgReviewed, so passes that overlap never answer each other), which settles itswalkstep: “if the state is clean, say so”. A proposal enters as an ask in the leader’s name and takes the whole road — planned, proposed, reviewed by the Board as an organization change. - Grants layer by containment (GH #596). The substrate may hold a
ceiling: the grant above the child’s — the organism’s, the Board’s to widen. A child’s boundary reads its grant through the ceiling as it is now at every assessment: the classes both allow, the smaller magnitude, the stricter review (intersect_grants). Grants contract on their own (two failures in a row halve the ceiling), so a child born within a ceiling of 100 is bound by 50 the moment its parent contracts, and the record says so (grant.contracted <parent>{boundary, from, to, epoch, binds, note}, appended before the contraction takes effect). An organism born over the record restores the last contraction of its ceiling — its epoch andprereview whatever the authored ceiling now says, and the stricter of the recorded and authored magnitudes — before anything is admitted, as it restores a revocation. A contraction whose row the record refuses still binds live, and is appended again before everyreserveandadmits; until the record holds it, nothing is admitted. A child born wider than its ceiling is the early error:grant.refused <child>names what is wider, and the ceiling binds it from birth; that row is authority, the record’s, and is not the row a refused spend writes. Law layers by adoption: every position lives under the org program’s main, which adopts the organism’s law, so a department’s law can only add to it. - Grants delegate resources (GH #605). A
Grantalso carriesamounts(per-operation ceilings,"USD 500.00 EUR 200"; a currency not named may not be spent),window_amountsandwindow(day,weekornone),counterpartiesandroutes(space-separated; none named, none allowed),expires(unix seconds; 0 never) and anauthority_epoch. Every field defaults to none, so a grant written before them spends nothing. They contain like the rest: the currencies both grant at the smaller ceilings, the smaller window limit, the parties and routes both name, the earlier expiry (never is later than any, so a child that never expires under a ceiling that does is wider in time), and the sum of the two epochs (intersect_grants);resource_gapnames what is wider, and the birth refusal carries it.permits(grant, spend, now)checks aSpend { id, amount, currency, counterparty, route }predicate by predicate and names the field that fails (expires,operation,currency,amount,counterparty,route).Dna.reserve(spend)admits a spend against the effective grant and appendsspend.reserved <op> {child, amount, currency, counterparty, route, ceiling, epoch, at, funder, account}(GH #668: the allocation is the row’s entity, a claim kind the Ledger’s unique constraint reserves once, and the row names who pays — a grant’sfunder,<owner>/<account>, inherited from the ceiling when the child names none; a child naming another funder than its ceiling’s is born wider,funder … not granted above); a reservation from before this,grant.reserved <child> {op, …}, is read the same way. The child’s own window counts its reservations and the ceiling’s window counts every child’s under it. The remainder is read at a revision and the row is appended withJournal.append_exactat that revision — a writer that moved the record in between makes it stale, never re-appended at the tail — so two children cannot each spend the same remainder. A refusal isgrant.reservation_refused <child>naming the field — money, the Ledger’s, nevergrant.refused.Dna.settle_spend(op, spent)appendsspend.settled <op> {child, attempt, spent}, one per attempt: every attempt’s actual consumption is retained and the window counts their sum from then on (a second settlement is attempt 2, not a refusal). Under contention a settlement is once: the row is appended withappend_exactat the revision the last settlement was read at, and one another writer landed in between is read as the answer, never doubled. Money that came back is a compensation someone authorized,Dna.compensate_spend(op, amount, by)appendingspend.compensated <op> {child, attempt, amount, by}— never an implied rollback. A purchase two children (or two owners of a shared record) fund is two reservations, each its own: one may settle and the other not, and the partial success is visible as such; no atomicity across them is assumed. A contraction advances the epoch, and so doesDna.revoke_grant(by)(the parent’s only; nothing is left granted). A revocation is recorded before it takes effect,grant.revoked <child> {by, parent, epoch}— one the record refuses did not happen — and an organism born over the record restores it before anything is admitted, so a restart never restores revoked authority.Dna.admits(admission)refuses an admission made under an older epoch, or whose grant has expired by the time it is carried out (grant.fenced). The generated law addsmoney_only_through_the_substrate: forbid reaches(positions, effects(money)) avoiding substrate, witheffect money;declared in the core; an organization’s existingdna/org/law.hlis project-owned and gains the clause by hand. The money budget is distinct from the model budget. - The principal source (GH #612, #989). Who acts where a verdict,
an intent or a task completion enters comes from
git config dna.principal. A CLI verb on a clone is its operator’s, as--asand the git identity say. A head’s principal path is the only way in: a peer on the head’s socket is the local account the kernel vouches for, mapped bydna.unix.member; every HTTP caller is an identity provider’s subject underoidc. Trusted-local — a head’s session under its own identity (the launch token,hale dna uiwith no principal source) — is a fixture’s mode alone: a head serves it only underHALE_DNA_TRUSTED_LOCAL=1, and refuses to start without either it ordna.principal = oidc. Local mode is OIDC too:hale dna head(or, from a checkout,dna/face/start.sh) starts the stub provider (dna/oidc: the discovery document, a JWKS, an authorization endpoint that signs inlocal-sub, and a token endpoint that exchanges a code or mints for a subject, every ID token ES256 under its published test key) and signs the person running it in aslocal-sub, mapped to$USER. Underoidca head serves nothing without a verified token:/api/*answers 401,/redirects to/auth/login. A session comes from OpenID Connect’s authorization-code flow againstdna.oidc.issuer(https, or plain http to127.0.0.1orlocalhostexactly, or to exactly the address the habitat declares,HALE_DNA_HABITAT_ADDRESS: a habitat on another machine of the LAN is named by its DNS and runs no TLS,spec/habitat.md§ A habitat on another machine — the URL’s host is parsed, userinfo refused, never matched by prefix, and no other host is admitted): the head discovers the issuer’s endpoints, refuses a discovery document whoseauthorization_endpointortoken_endpointis not https (plain http only when the issuer itself is on this machine or the habitat’s declared address;discovery_refusal, #635), sends the browser to its authorize endpoint with a randomstateandnonce(std::os::getrandom), and exchanges the returned code at the token endpoint itself with the client’s secret (dna.oidc.client, its secret the vault’soidc-client-<client>, revealed on the one line that builds the token request,dna.oidc.redirectas the callback, else the head’s own/auth/callback). Every ID token is verified: its ES256 signature under a key of the issuer’s JWKS — the head reads the discovery document and itsjwks_urionce, at start, and does not start without them; an RS256 token is refused, the standard library verifying ES256 only (es256_refusal) — thenclaims_refusalchecks the issuer, the client among the audience, the expiry and the nonce. A caller that is not a browser presents its ID token asAuthorization: Bearer, checked the same way without a nonce (bearer_refusal). A command reaches the head’s surface (HeadCommands) over itshttp::Rpcexposure (GH #1135, #1417;spec/api.md§ The HTTP transport),POST /call/<member>, on the port the head’s launcher gives it (HALE_DNA_COMMANDS_PORT, never derived from the reads’ port) or, none given, the port the system picks, which the API child reports to the head that started it (HALE_DNA_API_PORTS); until it has, a relayed command iscommands_unavailable, as a POSTed line of the wire under the caller’s bearer; no program forwards a line. The head’s bearer source (dna/api/bearer.hl,OidcBearer) verifies the token as above and names the principaloidc:<subject>; the binding’s roles map that subject to its person bydna.oidc.memberas they map a socket peer bydna.unix.member, the gates are the same, and the receipt saysprincipal_mode: oidc, the person, and the positions they hold (principal_positions). A fixture’s trusted-local head names its launch token the head’s own account (LaunchBearer,uid:<n>), the account its local session acts as. The face (dna/api/project_service) is itself a client of the issuer underHALE_DNA_OIDC_ISSUER,_CLIENTand_MEMBER(the<subject>=<person>who signs in there), configures a project it attaches for that issuer, and hands the session’s ID token to the project’s API child as the bearer — on the child’s reads, and onPOST …/commands, which it proxies to the child’s binding transport (aGETthere is 405: a lookup is theCommandLookupcall) — and the child verifies it itself. A child serving its own shell relays that shell’sPOST …/commandsthe same way (its ID token under OIDC, the launch token in a trusted-local session). Either relay keeps the CSRF guard a cookie session needs — the exactOrigin,X-Hale-Command: 1, JSON — before the session becomes a bearer; a caller’s own bearer is not asked. An OIDC child is ready when it refuses an unauthenticated read carrying the digest of the proof the face gave it (X-Hale-Child), so a process squatting on the child’s port is never handed a token. An issuer on the loopback (or on the habitat’s declared address) is anyone’s who can bind its port, so its key is pinned: a head under a loopback issuer does not start withoutdna.oidc.key(the issuer’s SubjectPublicKeyInfo) and trusts that key alone. In local mode the stub signs under a key made for the launch, its client secret is the vault’soidc-client-<client>, which the stub and the head each read there (never the environment or argv), the face pins the key into a project it attaches (restarting a child that pinned another), only the browser that opened the URL the face prints (its 0600 launch token) may start or finish a sign-in, and the face answers only as127.0.0.1orlocalhoston its port. The subject — never an email alone — maps to a member through a reviewed mapping,git config --add dna.oidc.member "<subject>=<name>"; an unmapped subject gets no session. A service is a principal too: a program with no person behind it (an application reading a project’s ratified practices) asks the issuer with the OAuthclient_credentialsgrant under its own client id and secret — the stub (dna/oidc) answers for the idsservicesnames (HALE_DNA_OIDC_SERVICEStodna/oidc/serve), each secret the vault’s entryoidc-service-<id>(std::secret’svault:source; the entry is the secret’s exact bytes, no trailing newline), read at every ask and refused when there is none — with an ES256 token whose subject is the client id and whose audience is the issuer’s relying party. A token verifies only in that shape (ES256,audthe head’s client): an identity provider whoseclient_credentialstokens are RS256, or name another audience, is not supported. The record maps it bygit config --local --add dna.oidc.service "<subject>=<service>". A subject is a person or a service, never both: the face refuses a bearer whose subject the record maps as a member (a person signs in, and a service’s token cannot be told from an ID token), and the child refuses a subject mapped both ways. The face accepts the token asAuthorization: Beareron the project’s API routes only (/api/hale/v1/applications…; on its own routes a bearer is 401), verifies it as it verifies an ID token, refuses a subject the attached project maps as no service (403), hands it to the child as the bearer (its commands reach the binding’s gates like a person’s), and never gives it a session or a cookie. The child reads as the principalservice:<service>, never a person’s name, so a read a person alone may make (a Review’s exact candidate) is refused it; its only POST is a command, and a command or a lookup is a person’s: the handler refuses an issuer’s subject that maps to no person (forbidden), whatever its row requires, and never records it as the account the head runs under. A service holds no position, so no row’s role is held by it. A sign-in’s state is used once, expires in ten minutes, and is bound to the browser that started it by anHttpOnly; SameSite=Laxdna_signincookie: a callback carrying the state from any other browser is refused and leaves the sign-in for the browser that started it (so a callback URL handed to someone else cannot sign them in); a session is a random 256-bit id in anHttpOnly; SameSite=Laxcookie (Securewhen the callback is https) and lasts eight hours or until/auth/logout. With a session, a verdict acts as the member (--as, and--authority boardwhendna.oidc.boardnames them,reviewerotherwise) and an intent is asked by them (hale dna task create --as), the row’s author too; the form’s ownasfield is ignored. The head never acts in its own name. It speaks plain HTTP: TLS is a reverse proxy in front of it. Rows arriving by sync remain admitted underdna.trust(GH #604 rule 6); a synced row claiming a rank is not a sign-in. - The nerves: the record’s requests reach the organism (GH #986).
A request is a row first, always.
hale dna task createappendsintent.requested(the body: outcome, from, to), a verdict appendsreview.verdict(the body: the verdict),concern raiseappendsconcern.requested,pressure raiseappendspressure.requestedandpractice proposeappendspractice.requested, each in the appender’s name and signed as they sign, from any clone. A one-shot verb never publishes. A node — the host underhale dna runordev, which is its program’smain locusfor as long as it runs — relays every request row it holds that the record has not answered onto the nerves on its tick, admitting each underdna.trust(GH #604 rule 6), and again every 30 s while it stays unanswered. Beside a live organism a verb waits for the answer in the record; from a clone with none it syncs and says where the answer will be. A request is answered by its own answer (GH #689): an intent, a verdict’s review and a practice request have an entity of their own, and a concern — whose entity is its source, shared by every concern from it — carries arequestid that the organization writes into theconcern.raisedanswering it. That row is one object — the concern’s words, its severity, which occurrence it is, and the request it answers — so the id is a member of the row rather than a shape inside its text: a worker who writesobserved [request c1] in a loghas written text, not metadata, and the host compares the whole id, so an answer toc10is not an answer toc1. A row written before that rule carries no such field and is matched by counting, as this host did before. One request is one concern: a request delivered twice — the host relays a row again while it looks unanswered — writes oneconcern.raised, and the count that turns concerns into a knowledge proposal counts distinct requests, never redeliveries. Two requests raising the same words are two concerns; one id used for a second, different concern — other words, or the same words at another severity — is refused once, inconcern.refused, which is that request’s answer and carries its id. Pressure is answered the same way (GH #986): apressure.requestedrow carries arequestid, and thepressure.raisedanswering it is an object — the signal’s words,count(which time its source has raised pressure) andrequest; a pressure row from before carries<what> x<n>and is read as that. A publish is confirmed by its answer in the record, never by the transport (GH #682): a row still unanswered 30 s after it was relayed is relayed again, whatever the transport said. The organism admits an intent once by its id — an intent offered again after its Task was born answers with that Task and journals nothing — a repeated verdict at a settled Review is answered as already settled, and a practice, a concern or a pressure request is answered once by its id. The host’s own observation report is a row too:observation.requested <mutation>(mutation_id,outcome,model_hash,detail), relayed asExpressionObserveduntil the organism answers it withexpression.observed; a node inside its observation window relays while it waits for that answer. A refusal the host writes for an unverified row answers the request it refuses: for a request with an id of its own (a concern’s, a pressure signal’s) it is one object,whyandrequest.local, the default, trusts every writer to the record — the operator’s profile;signedadmits a person’s row only when it carries a signature by the key the record enrolled for them (hale dna signers, The record), and an organism part’s only under the record’s key, and a row that does not is refused in the record in the host’s name (intent.refused,review.refused,concern.refused: “unverified writer”) and never relayed. Signing is one mechanism for this edge, not the boundary itself: a hosted head is admitted through a reviewed mapping of principals to authority and acts as the user, never as itself. The organism’s answers (intent.offered,task.born,review.settled,review.refused) return the same way. The body writesintent.offered, so the asker is not its author: the row’s body names who asked —<outcome> (from alice), an optimizer — and a head’s ask carries the person the head identified. A row is answered when a later row of the answering kind names its entity.hale dna task create --no-waitappends and returns, on either path, once the row is appended (the record is memory’s: there is nothing to sync); without--no-wait, an ask or a verdict made where no body anywhere holds the record’s lease is refused before anything is written. “Beside a live organism” is the record’s body lease: live, and held by this clone’s body.
The nerves (GH #986) are NATS JetStream, through pond’s client and
std::bus adapter, vendored under dna/core/pond/realtime/nats.
- Bindings where the publishes are. The host binds every DNA fact
topic it publishes (
IntentOffered,ReviewVerdict,ExpressionObserved,PressureRaised,ConcernRaised,PracticeRequested,HoldRequested,PositionOpenRequested,GraphReviewRequested,ScheduleRequested,KnowledgeNodeRequested,KnowledgeBindingRequested,KnowledgeEdgeRequested,WorkSubmit,WorkAllowanceAsk,RowLanded) tonats::NatsAdapter, and owns the connection, anats::NatsConnplacedpinned; its relay is aNodeRelaychild whose publishes go out through those bindings. The generated organization reads them through its own pinned connection: a message that arrives is handed to every topic of its subject the organization subscribes, whatever itsbindingslist (which the graft writes for most of them, for its own publishes). The payload is the bus’s own encoding: both ends are Hale programs declaring the same topic, and nothing converts it. A one-shot verb instantiates nomain locus, so it binds nothing and connects to nothing. - Subjects are the topics’ declared subjects under the
organization’s token — the one memory names its schema with
(
dna_<record id>):<org>.dna.<family>.<event>(dna_4f….dna.intent.offered); an application’s own events are<org>.app.<app>.<event>(The heart’s events), and what a node tells the heads is<org>.head.<event>(Rows landing). The connection puts the token on what leaves and takes it off what arrives, so a topic keeps its declared subject. - One stream per organization,
DNA_<ID>over<org>.>, on disk, kept a week (limits retention: what was said is kept and read from a position, not deleted once read). Every publish is acknowledged by the stream. The organization reads through its durable pull consumerspine(from the stream’s start when it is first made, from where it left off after that), filtered to<org>.dna.>, so a fact published while it was down or restarting reaches it when it is back. Over a shared record each owner is a space of its own: every owner runs a body and an organization (GH #664), and an owner’s host publishes under<org>.<owner>.and its organization reads through the durablespine_<owner>, filtered to<org>.<owner>.dna.>— so no organization pulls a fact another owner’s host published, and refuses it, which the relay would take as the answer. The host names its owner from the clone (dna.owner, the name of itsorganizationnode) and hands it to the organization (HALE_DNA_OWNER); an owner’s name in a subject or a durable keeps its letters, digits,-and_, anything else_. - Credentials, one per family, each a user on the server:
ownercreates the stream and is held by no process that runs the organism;spinepublishes and reads<org>.dna.>and an owner’s<org>.<owner>.dna.>(the host and the organization), reads<org>.app.>through the durableheart(the host), and publishes<org>.head.>and an owner’s<org>.<owner>.head.>;headsubscribes and publishes nothing; the application the record attaches holds a user of its own,app-<name>(The application’s account), which publishes on<org>.app.<name>.>alone and reads nothing of DNA’s. Birth,hale habitat migrate [organism]andhale dna nerves migrate [dir]create or update the stream with the owner’s URL (HALE_DNA_NATS_URL_OWNER, or the organism’s nerves allocation in the local habitat, brought up) and printHALE_DNA_NATS_ORG, oneHALE_DNA_NATS_URL_<ROLE>per role —nats://host:portalone — andHALE_DNA_NATS_VAULT_REFLEXES, and for the attached applicationHALE_DNA_NATS_USER_APPandHALE_DNA_NATS_VAULT_APP: the users and vault names a program that is not the host presents (GH #1653).hale dna nerves urls [dir]prints the same lines for nerves that are up, bringing nothing up and creating no stream (nerves-migrate --derive), and nameshale habitat migrate <organism>when they are not;hale dna devderives them the same way before it starts the host and hands the host the spine’s URL and the token, never the owner’s nor the head’s;hale dna runtakes the two from its environment and has the owner’s and the head’s removed from it.hale dna nerves drop [dir]deletes the stream, and everything it held, with the owner’s URL — beside dropping memory’s schema, when an organization is torn down. - The application’s account (GH #989). The application the record
attaches is named by its project: the project’s name as a subject
token (lower-case letters and digits, anything else
_, at most 32), recorded asnameon theapplication.attachedrow when it lands. A row recorded before that field resolves by the project’s directory name: a record is data already written, not an API kept compatible. An application publishes under its project’s name. Its user isapp-<name>, its password the vault’snats-<org>-app-<name>and the nerves allocation’s$NATS_APP_<NAME>_PASSWORD, and it may publish on<org>.app.<name>.>and nowhere else; no other application’s subjects, nor the reflexes’. It is provisioned whenapplication.attachedlands (hale habitat graftlands it, for an application and for a repository alike, then provisions the organism’s secrets) and on everyupgradethat finds it missing.hale dna application remove [project] [--as <who>]writesapplication.detached {name, by}to the record, the membership’s source, and revokes the account: its user leaves the nerves allocation’snats.conf, its password the include and its entry the vault, and thenerves-<id>allocation is restarted when it is up, so a connection it holds is closed and none opens again. An application namedreflexesholds no account: those subjects are the reflexes’ alone. - The nerves’ passwords (GH #989) are the vault’s, one per role
and seed,
nats-<org>-<role>(The organism’s secrets). A part that connects holdsstd::secret::Credential { vault: … }beside its user, and pond’s client reveals it on the one line that writesCONNECT— never a field, a return value, a URL or the environment. No URL carries a password: userinfo in a server’s URL is dropped. A role whose entry the vault lacks is refused (nerves migrate, the host, the reflexes); nothing connects with a default.- The server’s files live beside the nerves allocation in the
local habitat (
allocations/<id>/, mode 700; GH #1653), never in a checkout.nats.confholds no secret: itincludes./nats.secrets.confand names each user’s password as its variable ($NATS_<ROLE>_PASSWORD, and the attached application’s$NATS_APP_<NAME>_PASSWORD).nats.secrets.conf, written by the bootstrap from the same draw as the vault’s entries, is the one file that holds a password: mode 600, mounted withnats.confinto thenerves-<id>service. - Rotation. A graft and
upgradedraw every role’s password anew; anerves-<id>allocation that is up is restarted to read them, and a restart that fails is said. A server of the operator’s (HALE_DNA_NATS_URL_OWNER) takes the conf and the secrets file again after each rotation.
- The server’s files live beside the nerves allocation in the
local habitat (
- Liveness. At start the host waits for the organization to read
its facts: its durable consumer has a pull
outstanding. It says so (
the organization reads its facts from the nerves (DNA_<ID>)) or that it did not within 20 s; the facts wait in the stream either way. A restarted or rolled-back organization is confirmed the same way. Without nerves (HALE_DNA_NATS_URL_SPINEorHALE_DNA_NATS_ORGunset) the host runs and says the organization hears nothing the record asks. - A lost connection. The connection keeps every publish until the
stream acknowledges it, writes it again after a reconnect, and a
publish unacknowledged past its window violates the connection’s
deliveryclosure. The violation collapses it to its owner, which does not restart the connection in place: the node starts again as a fresh process, its connection and its organization’s both new. The host appendsviolation.recorded(adapter_undeliverable, subjectnerves, owner its holder,detailwhy), stops its organization and the expression, gives the body lease back (body.released,why: nerves) and exitsNODE_RESTART(75, the genome pull’s code: one restart code for the organism), and its unit (Restart=always, which restarts on any exit) starts it again. The organization’s program exits 75 on its own connection’s collapse, and the host that supervises it ends with it. The row first is what makes a loss recoverable: the next node relays every request still unanswered. - The heart’s events (GH #987). An application’s one hookup to DNA
is its events. It declares each as a topic of its own, under its own
subject
app.<app>.<event>,<app>its project’s name (The application’s account), binds it to pond’sNatsAdapterwith a codec that writes one JSON object, and publishes it through a pinnedNatsConnas its own user (HALE_DNA_NATS_USER_APP), presenting the vault’s entryHALE_DNA_NATS_VAULT_APPnames, under the organization’s prefix, acknowledged by the stream. It imports nothing of DNA. The event names itself: its body is one JSON object asstd::json::valid_objectadmits it (well-formed UTF-8, unique unescaped keys, at most 64 top-level members, nesting at most 64 deep) with a stringidof the application’s choosing (letters, digits and-_.:, at most 128 bytes, as the application’s and the event’s names are). The node’s host reads<org>.app.>through the durablehearton its own connection, at most 64 unacknowledged at once. There is one durable for the organization, whichever owner’s host pulls, so each event goes to one host, and the prefix is the organization’s alone. The connection hands each event over as it arrived and does not acknowledge it (pond’sNatsConn.untyped), since no part of the organism declares an application’s events. The host lands it asreading.recorded, entity<app>/<event>/<id>, bodyapp,event,id,subjectandpayload(the event’s JSON, at most 64 KiB), before anything acts on it. A reading is a signal, never a fact: it acts on nothing itself. What the spine does with one is a row of its own: an application’s concern becomesconcern.requested(The application side), a reflex’s firing is read with the others for its target (The senses and the reflexes). What the organism does to the application goes the other way, through the API the application exposes.- Duplicates. A second arrival of an entity already recorded, whether a redelivery or a replay, is refused as a duplicate and lands nothing. Whether it is recorded is decided at the revisions the row is appended at, so two hosts land it once.
- Not a reading. A message that names no application and event, or whose body is not such an object, is not a reading: the host says why and records nothing.
- Acknowledgement. The host acknowledges the durable’s message once
its row has landed, or once it has been refused as a duplicate or as
not a reading. One whose row could not be written stays
unacknowledged and comes again, so a host that stops between the
hand-off and the row loses nothing — within the stream’s retention:
an event not landed within a week (
NERVES_MAX_AGE_SECS) ages out of the stream unrecorded. One whose write keeps failing comes again every ack wait (30 s) and holds one of the durable’s 64 places.
- Rows landing. For every row its view of the organism gains, a
node publishes
head.row.landed(RowLanded: the row’sseq,kindandentity; under an owner’s prefix over a shared record), at most 256 on one tick; its first look only takes its place. It is the heads’ family, outsidedna., so no organization’s durable pulls it; the stream keeps it like everything under<org>., and the head’s push is what it is for. - Stopping. A node is its program’s
main locus, and SIGTERM drains it (GH #1039): its supervision readsdraining, stops the organization (which drains the same way), gives the lease back and ends. A node that ends on its own, as a genome that does not build ends it, gives the lease back the same way (body.released {why: "exit <code>"}) once what it started is gone, so a supervisor that starts it again does not leave a lease behind for each start; a lease it no longer holds is not touched (the release is a compare-and-swap on its token). A seed build that fails says why: its exit status (or the signal that ended it) and the last lines of its stderr.hale dna runanddevgive the node a 30 s grace for it (LOTUS_DRAIN_GRACE_MS, unless the environment already sets one). The core imports no NATS, because a program holding pond’s connection drains on SIGTERM: an API that imports the core keeps the default action. The head holds its own connection and drains: its watcher stops and the server stops accepting, at once. A request it is still relaying (a read proxied to its API child, a command relayed to its commands port) ends at the drain as well: the relay waits a slice at a time and, once the drain has begun, answers 503, retryable (head_api_unavailable,commands_unavailable), instead of waiting out the relay’s deadline past the drain’s grace. - One transport: the nerves (GH #986). Every fact between the
organism’s parts travels over the nerves, and
an application’s concern is one of its own events (The heart’s
events; The application side). An application — an instance a
node starts, the expression
devstarts — inherits the application’s server, its own user and the vault name of that user’s credential, and the organization’s token (HALE_DNA_NATS_URL_APP,HALE_DNA_NATS_USER_APP,HALE_DNA_NATS_VAULT_APP,HALE_DNA_NATS_ORG;devderives them asnerves urlsdoes, a node is started with them, and a repository’s compose services are handed them with the path of the password’s vault entry: The body runs a repository’s compose services), and none of the organism’s other credentials: the spine’s, the owner’s, the head’s and the reflexes’ NATS URLs and memory DSNs are taken out of what it inherits. A node materializesvendor/dnain its clone for its toolchain, so an instance can import pond’s NATS client from it. Local mode is the organism’s nerves allocation in the local habitat.
.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 (memory_of in routing.hl); 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 |
codebase.attached |
record | a foreign codebase the organism works on, attached with no application (hale dna codebase attach): name, repo (a url or a path), build and test (opaque command strings), branch (the default branch, main unless given), toolchain (go, hale or none, the default) and who; a later attach of the name states it anew |
codebase.detached |
record | the codebase removed (hale dna codebase detach): its name and who |
task.delivered |
ledger | a codebase task’s verdict, delivered (entity the task): via review (merged: review, leg_commit, merge_commit, edited, edit_receipt, comment_receipts: the pull request’s comments, each kept as a receipt, space-separated) or brief (its Review approved: review_id, by) |
task.refused |
ledger | a codebase task’s verdict, refused (entity the task): via review (closed without merging; comment_receipts as above) or brief (its Review not approved), with why |
structure.observed |
record | the compiler’s model of one of its parts, at the graft |
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 the graft 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 fleet seed’s source 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 fleet |
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) |
A codebase attached with no application
Section titled “A codebase attached with no application”hale dna codebase attach <name> --repo <url|path> --build <cmd> --test <cmd> [--toolchain go|hale|none] [--knowledge <dir>] [--branch <name>] [--as <who>]
writes codebase.attached (above); --knowledge (default docs/integrations/, a
directory inside the repository) declares where the codebase keeps an
watched source’s architecture note, <watch>.md, on its default branch
(The task a move becomes); detach <name> writes
codebase.detached; list (the default) reads the record’s attached
codebases. No application row is written and no broker account exists: a
codebase is a repository the organism works on, not a heart it senses.
The name is lowercase letters, digits, - and _. build and test
are opaque strings, run with sh in a worktree and nowhere else; empty
means no step. A path --repo is stored absolute. --toolchain declares
the toolchain hand a leg works the codebase with (anything but go,
hale or none is refused); none leaves build and test as the opaque
commands, and hale is recorded for the hale hand to select by.
A genome’s manifest may list its codebases, [[dna.codebase]] in its
hale.toml with the same fields (name, repo, branch, build,
test, toolchain); the habitat attaches each one at the organism’s graft
as this verb does, once (spec/habitat.md, Nurseries and saplings).
A go codebase’s leg holds a Go hand (legs::GoToolchain, beside
Toolchain in dna/core/legs/hands.hl, behind the GoHand interface and
Hands.go) with three tools added to the hat’s tool_grant: go_vet
(go vet ./...), go_test (go test over a package pattern, default
./...; a pattern that starts with - or holds other than path
characters is refused) and gofmt_check (gofmt -l .; any listed file is
the failure, empty is clean). Each runs in the leg’s worktree of the
codebase; its output is bounded and returned as the tool’s result, and the
call is a row of the tool-calls receipt like any other. A machine with no
go or gofmt on PATH answers that the Go toolchain is not installed
there, rather than failing obscurely. For a go codebase verify runs,
after build, gofmt -l, go vet and go test ./..., the first to fail
refusing as gofmt failed, go vet failed or go test failed; propose
runs the same three first and pushes nothing when one fails
(not pushed: …).
A leg working a codebase (legs::CodebaseWork, dna/core/legs/codebase.hl)
takes its worktree from the codebase’s repository, never the organism’s
own: a scratch clone at branch, on a new branch
leg/<codebase>/<task>. Its verify step is the codebase’s build and
then its test in that worktree (the first to fail refuses, with its
output). It proposes by committing, pushing the branch to the codebase’s
repository (origin) and opening a review through its ForgeHand:
GhForge { repo, base } names the codebase’s repository and default
branch (gh pr create --repo --base --head), and ForgeBridge puts the
core’s Forge behind the hand, so a fixture runs it over MemForge.
The forge decides; a leg never merges.
A task names a codebase with hale dna task create --to position:<name> --codebase <name> <outcome…> (the codebase must be attached, and the ask
routed to a position): the intent row, the IntentOffer and the Intent
carry codebase, and ask-position’s admission inputs record it. The
Work’s hat then carries codebase, the attached row as a JSON object
(name, repo, build, test, branch, toolchain; absent for a task naming none,
so such a hat’s digest is unchanged). A leg’s hand and submit --from-worktree on a hat that names one work a clone of the codebase’s
repository on leg/<codebase>/<task>, not the project’s checkout: the
check and test hands run the codebase’s build and test, the
validators are the two commands, and submit pushes the branch there,
opens the review through the leg’s forge hand and files the outcome as
result_ref: review:<ref>. The owner settles such an outcome done on that
reference and makes no Mutation of its own genome. The language hands (Go)
that make the commands typed are a later item.
Upstream watches
Section titled “Upstream watches”A watched upstream (an external service the organism depends on) is a
practice, never a manifest table or a record family of its own. The
standard pack’s template standard/upstream-watched/<name> is proposed
once per upstream (hale dna pack apply standard --set integration=<name> --set position=<owner> --set source=<source> --set kind=<kind> --set every=<cadence> [--set codebase=<name>]), ratified by the Board with the rest of its batch, and
bound to the owning position (its target position:<owner>). The batch
also proposes an idea, standard/upstream-source/<name>, the source as
knowledge, an external source with its URL, bound to the same position.
The practice’s text names its terms (A practice’s terms):
source=:http://,https://orfile://for a page or a feed; for an OpenAPI document, the surfacehale api importwrote and the genome commits (a path inside it), whose source is the one it was imported from (from openapi "…");kind=:changelog,docs,feedoropenapi;every=: the cadence, as a schedule takes it;codebase=: optional, an attached codebase whose client of the source a move is a task for (The task a move becomes); the pack’s defaultnoneis no codebase;step=source-check: the step its occurrences run.
Ratified, the practice declares the schedule its text names, its id the
practice’s name, of source-check, convened by its position: a position
nobody holds is schedule.refused with the hole, and the same terms
again write nothing (Schedules). Each occurrence’s inputs are
{practice, name}, and its check step (dna/core/source_check.hl)
reads its parameters from the practice’s receipt; a document that is not
a practice is refused (result refused). A practice that is not in
force (never ratified, or retired) checks nothing. One whose terms do
not stand (no position, a kind outside the four, a source of the wrong
shape) fails its occurrence saying why, its result refused.
The step reads the source and normalizes it:
changeloganddocs: a body whose first character is<is HTML, read as text with its markup stripped, a line per block element, and the volatile parts left out (head,script,style,noscript,template,svg,iframe,objectand comments); any other body is its lines. Each line has its common entities read, its white space collapsed and its ends trimmed, and blank lines are dropped.feed: the entries (RSSitem, Atomentry), a line each,title | link(theguidoridwithout a link); the feed’s own fields and dates are not read.openapi: the source document itself, canonical (hale api drift <surface> --document, run in the genome: the parsed OpenAPI document, JSON or YAML alike, as JSON with its keys sorted), with thedrift …lines ofhale api drift <surface>as its summary. The document, not the drift lines, is what is digested: a drift line names a row and a slot, so a second change to a row that already drifted reads the same. An exit of--documentother than 0, or of the drift check other than 0 or 1, is a failed read.
A fetch is one GET (std::http::get, 30 seconds, no redirects followed:
a status outside 2xx fails) or a file read, at most 4 MiB, in UTF-8. The
digest is the normalized text’s sha256, and the text is kept as a
receipt under it. The baseline is the receipt the previous occurrence
named: each occurrence’s outcome is the record of what it saw, and the
step reads the outcomes of the practice’s earlier occurrences (by its
name, so an amended practice keeps its baseline). An outcome done
names the snapshot it saw as its result (sha256:…); one failed
with the result unreached could not read the source. Against the
baseline:
- none: the outcome names the snapshot, and nothing else is written;
- the same digest: the same, nothing else;
- another digest: the unified diff of the baseline’s snapshot and this
text, labelled
<practice name> <from12>/<to12>, cut at a line past 16 KiB with a line saying so, is kept as a receipt; areading.recordedlands, entitysources/<watch>.changed/<task>(<watch>the last segment of the practice’s name),appsources,eventandsubject<watch>.changed, its payload{id, practice, name, kind, source, position, task, from, to, drift (openapi), receipt, diff}; an API’s receipt leads with its drift lines, then the diff of the canonical document; and the organization admits a concern (on_concern) from<position>/<watch>, so it is routed to the position, itsrequestthe reading’s entity and its words naming the reading and the receipt.
A read that fails is the occurrence’s own failure, result unreached.
The third in a row (SOURCE_UNREACHABLE_AFTER) also lands one
reading.recorded sources/<watch>.unreachable/<task> (payload {…, why, failures}) and no concern; the failures before and after it land
nothing, and an occurrence that reads the source again ends the run. A
step performed again (a restart before its outcome) that finds a reading
its task landed answers from that reading, without reading the source
again: a move’s outcome names the snapshot the reading moved to, and its
concern, asked again under the same request, names the reading’s
receipt, so a source that moved on since is the next occurrence’s move. The step
keeps no rows of its own: what each occurrence saw is its outcome, and
the snapshots and diffs are receipts.
The concern goes the way every concern goes (concern-escalate): raised
and routed, and proposed as knowledge at the threshold. Of itself a
concern is not a task; a practice that names an attached codebase=
files one for its move (below).
The task a move becomes
Section titled “The task a move becomes”A watched upstream whose practice names an attached codebase (a
codebase=<name> term, beside source= and kind=) is that codebase’s
client of an external service. Two things follow from a move, by the
organization, none by a person filing anything, and both are composed of
shapes that exist: the practice, a knowledge node, an ask. No row kind
and no record family is added.
The note. The codebase’s knowledge directory (--knowledge) holds
<watch>.md on its default branch, the source’s architecture note. At each
move the organization reads it (git show <branch>:<dir><watch>.md in the
repository, in a scratch clone when it is not on this machine) and
proposes it as a knowledge node of kind concept, named
integration/<watch>, authored by org and bound to the practice’s
position: the Board ratifies it as any knowledge (knowledge.proposed, its
Review). A proposal is by the digest of its document, so a note that did
not change is the proposal already in the record, and a note that changed
is a fresh proposal on the same node. A codebase with no such file notes
nothing.
The task. A move (the check that lands reading.recorded
sources/<watch>.changed/<task> and its concern, whether the check is
fresh or asked again) files, besides, one ask under position:<position>
naming the codebase, id source-<watch>-<reading's task> (an ask is
admitted once by its id): the ask-position workflow, Patch output, and
in the admission’s inputs an integration object {integration (the watch), practice (its name), codebase, source (its kind), reading, diff_receipt, diff, note_path, note_digest, note}, derived from the
reading, the concern’s receipt and the practice that convened the check
(the diff and the note inline cut at a line past 8000 bytes, the whole of
each in the receipt and the codebase). The hat carries it
(hat.integration, in the digest), and a prompt rendering names the move,
its diff and the note and the two ways to answer. A leg holding the
position claims it through the ordinary claim path (attempt.claimed;
hale dna work next --as position:<name>).
The answer. The leg reads the client through the codebase’s worktree
and answers one of two ways. A change opens a pull request at the
codebase’s forge (the Go hand and CodebaseWork.propose, A codebase
attached with no application); the review’s body ends in a
leg-commit: <sha> line naming the commit the leg left the branch at, and
the attempt settles done with result_ref: review:<ref>. A first line
beginning NO IMPACT over an unchanged worktree is a no-impact brief: the
attempt settles done with result_ref: brief:<digest> and the brief as
its result, no validator judging a tree nothing changed, and the
organization puts it to a Review of its own, not the forge’s: a knowledge
proposal (kind idea) bound to the position, its provenance naming the
task (no row of its own). Neither
touches anything but the codebase’s repository and the record: no Mutation
of the genome is asked. Either answer’s receipts are the architecture
note and the reading’s diff in full, one manifest of sources ({"sources": [{kind note|diff|file, path, digest}…]}: the note, the diff by its
receipt, and every file read through the read hand with the digest of
what it read), and the tool calls (a read row names its path).
The verdict. The forge decides a review’s fate and the organization
learns it through the Forge interface (dna/core/forge.hl):
review_state (open, merged\t<commit>, closed, unknown),
review_body and review_comments (who\tkey\ttext, newlines as \n),
beside the verdict, check and comment methods. The host’s forge sync
asks it for every codebase review not yet decided, per codebase (the
allocated Forgejo’s API for a codebase whose repository is there, by its
url; GitHub’s gh pr view for a github forge, the repository taken
from the codebase’s url; the file forge’s <n>.merged, <n>.said for
fixtures),
through codebase_intake (dna/core/codebase_reviews.hl), which returns
the rows and writes none:
- merged:
task.delivered {via: review, leg_commit, merge_commit, edited, edit_receipt, comment_receipts}. A review the team edited before merging is still delivered;editedis whethergit diff <leg_commit> <merge_commit>in the codebase is not empty, and that diff is theedit_receipt: what the organism learns from; - closed unmerged:
task.refused {via: review, why, comment_receipts}; - each comment on the review, once by its key, is a receipt on the task:
comment_receiptson the verdict row, in the forge’s order. A comment has no row kind of its own.
A no-impact brief’s Review is the verdict: approved, task.delivered {via: brief}; otherwise task.refused {via: brief}, written when the Review
settles (brief_verdict).
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 habitat graft 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) |
source-check |
check (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,
source-check/check. A definition with an
organism step it does not know — a path not moved onto the catalog yet
(ask-triage, whose ask is still admitted as ask-edit or ask-person
after the leader plans it), or a revision from a newer toolchain — is
refused at admission naming the step, never failed when it runs, as a
definition on a part not built is refused naming the part; both are
workflow.refused rows and nothing else. The organization performs a
step only of a definition admitted directly, so one that invokes a
definition with an organism step anywhere below it is refused at
admission too. A ratification the catalog refuses is answered once per
incarnation — one workflow.refused row — and asked again after a
restart, so a catalog fixed in between ratifies it, and nothing retries
at every reconciliation. hale dna definitions lists
what an admission would refuse each definition for.
- practice-ratify carries every knowledge proposal the Board
decided: when a verdict settles a proposal’s Review, the organization
admits one execution for it (
ratify:<digest>), whoever proposed it — a person (hale dna practice propose, proposed by the organization as before), a concern, the record’s birth (the purpose, the seeded practices, a repository’s holes and practices), a holder asked for.ratifywritesknowledge.ratified(andknowledge.retiredfor what it supersedes, orknowledge.refused) under an approval, andknowledge.declinedotherwise, which fails the chain; it is done only when the whole consequence landed, so a retirement the record refused is written again.hatfails the chain, saying so, when no memory is named (nothing would come to wait for); otherwise it waits for memory’s projection to hold the idea ratified, then reads the target’s package at memory’s head and writespractice.read <digest>(target,ratified_at,hat,head,included,task): what the hat read, at which head. No verdict path writes a ratification of its own — a Review’s verdict or a decision by command (review.command_decided) alike; a settled verdict whose ratification never ran is admitted the same way at the next reconciliation. - concern-escalate is a concern:
hale dnaconcerns, and the organization’s own, are admitted asconcern:<source>:<request>(a redelivery of the request is the same execution; the same request with other words isconcern.refused, as before). Its one step,raise, writesconcern.raised <source>(what,severity,occurrence,request, theparentit is routed to,task) and, once the source crossed the threshold, proposes it as knowledge by the rule that always did. A source over the threshold with no proposal and no execution running for it is escalated once at birth by an execution that raises nothing more (its raises are the record’s). - optimize-walk is the optimize pass, and occurs on the schedule
operating/optimize-cadencedeclares (The optimize pass):walkasks the budget first — an exhausted one isoptimize.refusedand fails the step, and the pass waits for the next occurrence — then reads the machinery’s signals, asks the leader once per incarnation and is done at itsorg.reviewedrow, which names the execution (task). - source-check is one check of an upstream a practice watches, and
occurs on the schedule the practice declares (Upstream watches);
it takes
practiceandname, and its parameters are the practice’s text.checkreads the source and lands what Upstream watches says, or finds the reading it landed when it is performed again.
The ledger is the runtime. Every step is a few rows of the day’s
work (step.*, attempt.*, work.settled, effect.*), each one row
of the Ledger in memory beside the record (GH #1590).
The declared purpose is a proposal like any other. hale habitat graft
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>]).
The graft 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, 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.
- The hat (
GET …/dna/context?id=<work>;dna/operations/context.hl) is one Work’s context as structure, never a prompt: the position’s identity — the graph’sposition:<name>id, from the performer kind the attempt admitted last names, or the kind the request selects before one is, or, for a Work whose requirement namesposition:<name>(ask-position), that position — and its charter (the record’sgraph.nodetext for that id,""until the graph names it); read under OIDC, it is the lease holder’s — the person the record maps the bearer to, whoseattempt.claimedrow is the Work’s current, unexpired, unreleased lease; any other caller is refused403 lease_required; the practices ratified for the Work’s targets (targets: its path, its codebase’s languages,system:dnafor an organization change, the position — The hat’s target set), from memory under the reader’s role, resolved to text with their ids; the knowledge bindings; the output contract and the hands it gives (dna/core/contracts.hl): the tools a performer may use, each{name, description, input_schema, effect, role, family}from the toolchain’s built-ins (dna/core/tools.hl), the tool grant (their names) and thevalidatorsamong them, which the harness runs before handing the outcome back (aPatchgivesread edit check test fmt patch, validated bycheck test fmt; anAssessmentthe read-onlyrecord_status record_history org_chart; a contract not yet described, none); the model the mapping resolves for the Work (dna/core/model_map.hl: the mode its hands make (thinkwith no hands,toolswith hands and no workspace hand,dowith a workspace hand: anAssessmentis atoolsWork; atoolsordocategory is filled only by a model that returns tool calls), the size of its most specificmodel.rule, a task’s override only inside the permitted sizes, the model themodel.categoryrow names, the rule that decided it, or why no model fills the category); the data class; the history of the execution the Work belongs to, as facts (the head replays only that execution’s facts, its root task’s id and every id under it, child tasks included, and refusescontext_read_limitpast 512 facts, 256 bound nodes, 512 KiB of bodies or a 64 KiB body; the record’s length is no bound); the record head and memory’s projection watermark it was rendered at (-1without memory, andpractices_statussays so); and the digest, sha256 over the canonical body with the digest itself left out. Rendered twice at one head it is one digest; a row that moves the head moves it. Two attempts of one task may wear different hats, correctly: what is forbidden is unrecorded variation, so the hat reads no clock, no environment value and no random id, and replay renders from the recorded hat, never the live graph. Rendering is a leg’s, and the renderer’s version is evidence of its own. The practices are structure —{id, name, text, kind, author}from one snapshot of memory, the watermark read in the same transaction as the ranked bound — never rendered lines split again; and every hat is composed by one function (hat_compose), whether built by the owner for an edit it asks (the attempt carries the hat’s digest as itscontext_digest; the practices reach the editor as its brief, the ask stays the ask) or by a head for a leg, is kept in memory by its digest (hats, insert if absent). - The tool loop (
dna/core/legs/tool_loop.hl). A model performer whose hat carries hands and whosemodel.modeistoolsordooffers the hands to the model as tools (ModelRequest.tools, the chat API’s function shape, with the turns since the prompt inModelRequest.messages) and runs everytool_callsentry the reply asks for (ModelResult.tool_calls, each{id, name, arguments}) on the leg’s side, sending each result back as atoolmessage, until a reply carries no tool call. AdoWork works in a scratch clone of the leg’s repository atHEAD; its answer stands once the hat’svalidatorspass on every touched directory holding.hlsource (a changed file’s own directory: a nested seed is its own), a refusal being one more turn; aPatchis then one commit on the base, handed back asresult_ref: commit:<sha>with the commit’s patch as the first receipt. A hand outside the hat’stool_grant, or a path outside the worktree or into its.git(a..or.gitcomponent in any case or spelling, a control character, or a symbolic link anywhere on the way), is refused as the call’s answer; a changed file git can only name quoted is refused byvalidaterather than passed unchecked.record_historyreads the execution its id belongs to (/dna/workflows?id=<task>). ARecordedModelkeys a loop’s request on its tools and turns too and keeps the tool calls it returned, so a replay follows the same turns. Each model call is acallsentry (its response digest covers its tool calls) and each tool call a row of one receipt,{"tool_calls": [{turn, name, arguments_digest, ok, output_digest, output_bytes, error}]}. The loop is bounded bymax_turns(16) and the attempt’s allowance. - A person’s session runs the same hands one call at a time:
hale dna work hand --attempt <a> --name <hand> --args <json>(andhale mcp’shale_dna_hand) reads the attempt’s hat from the head and runs the named hand through the same bench, a file hand in a clone of the repository kept per attempt under.hale/dna/work/with its base recorded beside it;submit --from-worktreeruns the hat’s validators over the touched directories, refuses on a failure, and otherwise submits one commit on that base asresult_ref: commit:<sha>with its patch as the first receipt, then removes the kept clone. - The claim is taken at the head. The filter names the performer
kind and identity, the capabilities the leg has, the data classes it
may see, the owners it works for and a TTL. The identity is a
position (
position:<name>,#<n>for one worker of several) the record knows, and one the principal’s person holds (GH #1162): by the record’s liveholdsedges — agraph.edgerow, or one a ratified graph proposal carries (hale dna fill) — the edge, its position and its holder not retired, memory not having refused it (hold.refused), the person not retired; or, under a record that declaresdna.trust = local, any position. The socket’s roles read the same edges (HoldsReader): a person holds a position in their own name, or as a member of an organization holding it (GH #1123, as the route reads a firm-held position); a membership alone is no seat, so it grants neitherpositionnor any other role. Otherwise the claim is refused naming the person and the position, and so is a renewal once the person no longer holds it (a release stays open) and friction filed as a position the person does not hold. Without local trust the organization’s own kind positions (position:agent,leader, …) are held only by aholdsedge naming them. The head picks the first admitted attempt of that kind that is outstanding — asked to run (effect.requested attempt:<id>), no outcome — was relayed to legs (effect.relayed attempt:<id>: the owner records it when a leg relay answered the attempt pending, and only then — an edit the organism’s editor holds, or a case a person does, is outstanding the same way and is never a leg’s, GH #1158; a row the record would not take is recorded when the next ask redelivers the attempt, and a leg is refused it until then), fits the filter, and is not held by another leg under a live lease; takes memory’s claimattempt:<id>for the performer with the TTL under the head’s role (GH #1026: the store decides between two legs racing; without memory nothing races); and appendsattempt.claimednaming the lease and the principal that took it. The claim is not fenced on the record head the leg read — memory’s conditional insert is the race, so a leg a row behind is not turned away withstale_subject— and a claim memory took that ends in no row is given back. A leg that names no data class is handed nothing: no class is no class. The receipt returns the task and the lease as a value. Nothing fitting, or everything held, is a refusal: a receipt with the reason — which names the class or the owner that stood in the way — and no row, since legs ask often and an empty answer is not a fact. - The outcome comes back under the lease, with the hat the leg
wore (its digest, the head and watermark it was rendered at, the
digest of what was rendered, the renderer’s version — the five
together or none). The head refuses — a receipt with the reason, no
row — an attempt never admitted or never asked to run, one already
settled or already handed back (
duplicate: one outcome under one lease), a lease that is not this holder’s at this token now, by the recorded claim and, with memory, by memory’s (stale), and an outcome from a principal other than the one that took the lease; files the receipts (a protected body in memory alone, the rest as git receipts); and appendsattempt.outcome_requested. A node relays the row onto the nerves (WorkSubmit,dna.work.submit) until the record answers it. The owner checks the same lease, journals the calls the leg made asmodel.calledrows on the attempt id — tokens per task hold out of process — and settles the attempt through the path every reply takes:attempt.outcomeby exact append, naming the request it answers and carryingresult_refand the hat, the Work and the task after it; a submission it will not settle — an attempt never relayed to a leg (noeffect.relayed), a stale lease, a second outcome after settlement — is anattempt.outcome_refusedrow naming why and the request, which answers it, so the relay stops. A Patch Work’s outcome (the request’soutput_contractisPatch, dispositiondone) is a candidate change, and the owner makes it one (GH #1156): the outcome names the leg’s commit (result_ref: commit:<sha>) and files that commit’s own patch among its receipts (git format-patch, whoseFrom <sha>line names it; the legs’ git hand exports it,hands.git.patch). The owner mints a Mutation bound to the attempt (mutation.requestedcarrying the binding, the leg, its commit and the receipt), opens its gateway worktree at the genome’s head, applies the patch there — only under the seed the Task’s class edits, every path counted (a rename’s source too), and never the organization’s own seed unless the class isorganization— and commits, verifies and opens the Review exactly as for the editor’s candidate, the leg its author. The patch is the candidate: what it carries is what is verified and reviewed, and verification runs the candidate’s own tests on the owner, as it does the editor’s. The attempt settlesdoneon the prepared candidate (<m>: candidate prepared (<disposition>) from commit:<sha>), orfailedwith the Mutation’s failure — no commit named, no receipt that is its patch, a patch that does not apply at the head, one that edits outside the seed, a candidate denied, a Review that could not open — so the Work’s allowance decides what follows. A submission relayed again finds the Mutation bound to the attempt and makes no second; one found still in flight (the owner restarted while making it) is failed as interrupted. The outcome names the bound Mutation’s commit, whichever lease handed it back. A candidate whose seed holds no Hale source at the base or at the candidate is not denied for a check with nothing to judge (hale_sourcefalse in its evidence) and is never staged or released by a grant: it is reviewed before anything applies. ALegRelayperformer answers pending for the kinds a program hands to legs, so the attempt waits for one instead of running in process (stage 1 of the legs; the in-process performers go as each stage lands), andRelayReplayis its reconciler: an owner that restarts with a leg’s attempt in flight leaves it to its claim — nothing is asked of it while the lease is live — and asks it again once the lease expires with no outcome (effect.redelivered, once per lease), when a leg claims it anew. - The spend (GH #1131). Before its first model call a leg asks the
spine, which holds the one budget (Models), for its attempt’s
spend:
dna.attempt.allowanceunder its lease (the attempt, the holder, the token), which the head admits as it admits a renewal — the same checks — and writes asattempt.allowance_requested(holder,token,request,at, and the command). A node relays it onto the nerves (WorkAllowanceAsk,dna.work.allowance) until the record answers it. The owner checks the lease as of the head’s admission (this holder, this token, live, the attempt relayed to a leg and unsettled) and asks the one gate with the Work’s own cost ceiling:attempt.allowance_granted(allowance_micros,ceiling_micros,spent_micros,window_micros,window,holder,token,request,at) orattempt.allowance_refused(why, …), naming the request. The ask reads backrequesteduntil then,grantedwithallowance_micros, orrefusedwith the reason. The model performer asks before its first call and waits up to 60 s for the answer; refused, or unanswered, it makes no call and hands backdeclined, naming why; granted, it makes no call — a retry included — whose cost is known before it is sent and is past what is left of the allowance (ModelBackend.price_micros: a price per call, a recorded answer; the attempt is declined), nor any once its calls have spent it. A cost learned only from the answer (a price per token, a harness’s summary) can cross the allowance: that call is made whole, so a leg passes its grant by at most one call. At settle the owner takes an outcome whose calls cost more than the lease was granted — anything, when it asked for nothing — as the attemptfailed, naming the overrun (no Patch Work’s candidate is made from it), so the Work’s allowance of attempts bounds the repeats; the calls are journaled all the same, for what was spent is spent. - The verbs (
hale dna work, GH #946 slice 4;dna/core/legs, vendored asvendor/dna/legs) are a leg as API clients:loginasks the head (GET /.well-known/hale-dna, unauthenticated under OIDC: principal, issuer, client) who signs its people in, and where that is the local stub (a loopback issuer, clientdna-local) mints an ID token for the subject the record’sdna.oidc.membermaps to--as <person>, the client secret read from the vault entryoidc-client-dna-localand never printed, and printsexport HALE_DNA_ID_TOKEN=…; for any other issuer it prints the sign-in URL and exits 1;nextisdna.attempt.claimfor a position (--as position:<name>, the graph’s id, never a free string; a claim matches attempts by performer kind,--kind, and a position the graph states that names no kind of its own claims as anagentand also takes the Works routed to it,ask-position; a refusal names the kinds outstanding and the flag),briefthe hat read — or rendered in the leg,text,promptoragent, recording the hat digest, the digest of what was rendered and the renderer’s version (legs-render/2) —renewisdna.attempt.renew(the lease extended, the token kept: anotherattempt.claimedrow),allowanceisdna.attempt.allowanceread back until the spine answers it (--wait, 60 s: what an external harness asks before its first model call),submitisdna.attempt.outcome,settlethat command read back,releaseisdna.attempt.release(attempt.released; the attempt is another leg’s to claim),frictionisdna.friction.file(friction.filed, a fact nobody admits), andhandruns one of the hat’s hands on this machine (A person’s session, above). Each prints one JSON object and holds nothing afterwards;submitandreleaseare keyed on the lease, so run twice they are one act;nextmints a fresh id per call (a claim by its holder renews);renewis counted by the caller. A leg’s identity is a position the record knows —position:<name>from the graph (agraph.noderow, or a position a ratified graph proposal states: a holehale habitat graftproposed, a positionhale dna position openopened; read through the record the command is admitted over, and ended by a reviewed retirement in a later ratified proposal) or the organization’s own,/allowed in the name as the graph spells a process’s positions (position:api/dev), with#<n>for one worker of several — never free text, at the head as at the verb. While an outcome under a lease awaits the owner the lease is neither renewed nor given back, and the attempt awaits no other leg; without memory the record numbers the leases (the next claim row is the next token). Exit codes: 0 admitted, 1 refused (the receipt printed), unreachable, or arunthat endsunsettledorunresolved; 2 usage, or a catalog with a performer declaring no effect class.runis one cycle through the project’s performers (dna/org/work.hl, generated by the graft): a person’s leg renders the brief and leaves the outcome tosubmit; a deterministic performer wins for the work kinds it takes and settles; the model performer (legs::ModelPerformer) is the catalog’s router behind the performer interface, out of process, one task per run: the brief rendered as a prompt, the answer the result, and every call the router answered handed back as evidence (the adapter that answered, backend, reported model, tokens, cost, wall time under the prompt and context digests), which the owner journals asmodel.calledrows on the attempt through the encoder its in-process calls take; a rate-limited call is backed off inside the attempt (bounded, doubling, the lease held) and each wait is a row of evidence of its own, so the attempt’s cost in time is in the record beside its cost in tokens; the lease is renewed before each wait. The model is sent the render alone (the hat is structure), and the hat’s digest is the context digest on every row.--performernames the performer instead of the catalog’s choice; a performer never answers a kind it does not take, forced or not. Every performer declares an effect class —effect_free(answered, touched nothing),idempotent(may be run again to the same end) oruncertain(an agent’s seat with tools: may have acted once already) — with no default: a catalog with a performer declaring none is refused by the verbs that claim or hand back (exit 2) and read by the rest. The class rides on the claim (effect_class), which the head admits only if the Work admits the class — the class decides what a failed settle becomes, never what a performer may do, so a Work that is answered admits every class and an edit admits noeffect_free; the hat says so ineffects— is recorded onattempt.claimed(a renewal carries it on), must be named again by an outcome under the lease, and is evidence on the outcome (effect_classonattempt.outcome_requested). A settle that fails (the outcome cannot be handed back or read back, was refused, or the owner refused it) on aneffect_freeoridempotentperformer isunsettled: the lease is given back when still the leg’s, and the loop runs the slot again after a rest, performing anew. On anuncertainperformer nothing is retried by a program: the leg marks the attempt unresolved at the head — an outcome of dispositionunresolved, its calls as evidence, admitted past the lease’s end — which landsattempt.unresolvedandeffect.result unknownon the attempt; friction names the attempt, the holder, the lease and the resolution; the leg answersunresolved(exit 1) and the loop stops that slot. A marked attempt awaits no leg (its holder included) until a person resolves it (hale dna effect resolve attempt:<id> --outcome ok|failed), on which the owner settles it. Anuncertainclaim whose lease lapses with no outcome is never asked again either: the owner recordseffect.result unknownitself; such a claim’s lease is an hour by default. The model leg judges a refusal by whether the backend may have acted (ModelResult.made, false only for a refusal before anything was sent or run): behind anuncertainperformer one that may have isunresolved, never retried or failed over; behind the other two afailedoutcome; a rate-limited call is asked again only when it was never made.loop --parallel Nis a worker: N supervised child processes, eachrunas its own holderposition:<name>#<n>(two workers of one position never share a lease), each answer one JSON line as the child ends, started again at once after a task and after a per-slot doubling backoff when idle (nothing claimed, a person’s, given back);--onceruns each once. The leg is its program’s main locus: SIGTERM/SIGINT drains the loop, which ends its children (TERM, then KILL after a grace) and reaps them before it ends;--drain(a marker beside the leg) ends a running loop once its children have. Nothing is held between tasks; a child ended mid-task leaves a lease that expires. A project initialised with no backend configured getsNoModel: its agent Works are a person’s. The catalog and the tape (models.hl,tape.hl) stay in the core while the editor and leader call them in process. A performer is handed the brief and the hands as interfaces — git in scratch, the forge (it decides, a leg never merges; the organism’s forge allocation answers “open a review”,AllocatedForge, and no hand names a forge by kind), the toolchain; deploy and the heart’s API refuse, naming #987 — and answers a performance (a result struct until #732). The verb is the project’s own program, built beside the vendored seed under.hale/dna/legsby the host;hale mcpexposes it ashale_dna_work, and a Work’s hands ashale_dna_hand. The commands go over the head’s api socket (GH #1104 piece 5): each verb acallon a member of the head’s surface —Commands::claim,Commands::renew,Commands::outcome,Commands::release_attempt,Commands::file_friction, requiringposition;Commands::lookupforsettle— with the receipt on the value channel as the record wrote it; the reads (the record head, the hat, the socket’s path in/capabilities) stay HTTP. The principal is the peer’s credentials mapped to a person by the record (dna.unix.member; a record that declaresdna.trust = local, ashale habitat graftdeclares for the one it makes, is that person’s — they hold every position — and one that declares nothing, or another trust, leaves it to the graph’sholdsedges); the lease’s holder is the position the leg works as (--as,#<n>for a worker,--worker <n>), never a free flag; a peer whose person holds no position is told so at attach, and a verb outside the caller’s slice isunknown, exit 1. The leg takes the socket from--socket,HALE_DNA_SOCKET, or the head’s capabilities.
A command is one act per request id: sent again under the same id, a
command that landed is found by it, before anything else is checked, and
answered with its receipt. A head answers snapshot_changed when its
record moved while it read it, and command_busy when the record moved
through every read its admission allows itself. Neither admits the
command, though a pass may have left what the command’s own retry takes
back or finds (a claim is given back; a renewal extends the same lease;
receipts are filed by their digest). So a leg asks the same command
again, 200ms apart, under the same id, until ten seconds have passed,
whatever the command, and reads the hat again the same way. A record
several legs and the owner write moves often. command_outcome_unknown
is not asked again: the leg reads the command back by its id. A hat it
still cannot read gives the lease back.
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 leases (MemoryLeases, and
MemLeases for fixtures) 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.
The solo path: the generated org chart sets solo_without_model on its
substrate. While the organism has no model (has_model: no model.category
row names one, model_mapped, and the editor’s router permits no call, its
hosted backends keyless and its local server, which LocalModel probes on
this host, not running; a canned or recorded backend is a model), a Work the
routing would hand to a leg (an agent’s: a judgment, a Work routed to a
position) or to the editor is a human leaf instead, a case for a person, and
the case is handed unassigned with no word asked of the leader. The
organism’s own steps (an organism Work: applying a ratified change,
raising a concern) need no model and stay the organism’s. Nothing
waits on a model and nothing times out into a leg; mapping a category or
giving the catalog a model gives the next execution its legs back. A
substrate a program wires itself (solo_without_model false, the default)
routes as before. A chart generated
before the flag routes as before, and hale dna upgrade does not add it:
DNA migrates nothing, and such an organism takes the solo path when it is
born again (hale habitat graft).
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 face reaches the same through two rows of the head’s surface, each
in the caller’s name (any person of the record), answering {ok, code, why, task_id, state, assignee}: Commands::take_task ({task_id}: a
handed Task that names no assignee becomes the caller’s, a
task.reassigned row from "") and Commands::complete_task
({task_id, text}: a task.done row whose body is by <who>: <text>,
refused evidence_required where the bound practice wants evidence, and
by memory’s gate for anyone but the assignee).
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 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 the record and the Ledger in memory) 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 memory implementations (RecordJournal, MemoryLeases,
MemoryReceipts, GH #1590) are the ones an assembly wires for an
organism; the in-process 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 boundary the organism’s readers and writers stand on, with a
vocabulary that never names the store: a chain of rows appended at
its head (journal), bodies kept by the digest of their content, the
signer of a row, and the dna.* settings. MemoryRecord is the
implementation over memory, MemRecord the one for fixtures; what the
interface kept for git’s transport (chains built beside a head and
swapped, cells, pointers, families shared with a remote) answers that
the record is memory’s. hale habitat graft and hale dna 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 record only grows, and a reader is held to the same rule: a read that comes back shorter than the rows a reader holds is a read that failed, never a shorter record, and leaves the rows, the head and the text as they were.
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. MemoryRecord reads absent only
for a record memory answers with no rows.
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 habitat graft generates and hale dna run
runs. The application it oversees is not modified by the graft and
contains none of it; it is observed like any Hale binary. The
manifest declares two environments, the application’s and the
organization’s ([claims] no_base = true; each adopts its own law).
- Positions are loci; routing is the bus.
Board(the human authority: intent enters through it, escalations and reports leave through it, it owns every grant),Leader(a model-backed position holding the project’s grant: it decides the Reviews inside the grant by reading the source diff and the semantic diff, and its verdict is a model call with evidence), and the substrateDna(the record, the gateways, verification, the editing position, the Reviews). Its work system hands the agent kind to the legs’ relay (agent: dna::LegRelay, withagent_reconciler: dna::RelayReplaybeside it): a judgment or an analysis is answered pending and a leg claims it throughhale dna work(GH #946); no agent performer runs in process, and the law’spositionsgroup names the relay. A Review is announced as a typedReviewRequestedfact carrying what a deciding position needs. - Authorities are ranked:
board(4,maintaineris its older name),leader(3),supervisor(2),reviewer(1). A claimed authority satisfies a requirement of its rank or below; an unknown name satisfies only itself. - Who decides.
OrgPolicy: a change that touches law, widens effects or crosses ownership, or is of classorganization,constitutional,process-policyortopology, requires the Board; a change outside the grant (dispositionescalate) requires the Board; everything else inside the grant requires the Leader. That required authority is the fallback: it decides only a Review whose route defines no signers. - Signers from the route (GH #1089). A Review is opened by a
definition, and the definition says what it requires: change-deliver’s
Review (a mutation’s, a prepared source’s) requires what
hale dna routecomputes for the paths its candidate changes against its base; practice-ratify’s (every proposal the Board decides — a practice, a hold, a hole, a binding, an edge) requires theboardposition under the purpose. The organization asks memory’s graph when it opens the Review and records the answer once asreview.routed <review>{definition,signers,gates}; that row is what the Review requires from then on, a restart included.signersis every position the route names with the holders it has then,<position>=<holder>,<holder> …— a position an organization holds is held, for signing, by that organization’s members (GH #1123: the route expands the organization into its members, each once), so any one of them signs for it;gatesis the gates guarding the change, andfallbackwhether the route left some path to the fallback. A route that names no held position defines no signers: with no memory named, or before anyone holds a signing position (the Board’s own first holder is ratified this way), the Review keeps its required authority. A change-deliver Review’s fallback is recorded too, and final —review.routedwith no signers andwhy(no memory is named,no position the route names is held), whichhale dna review <id>shows; a practice-ratify Review writes nothing on its fallback and is routed again each time it is rebuilt (a restart), so a Board seated since signs it, and rebuilding a record’s proposals appends nothing. The Review is not opened — and reconciliation asks again — when memory is named and does not answer, when git cannot name the paths the candidate changes (no base, or a failed diff), or when the record does not take its routing; so no Review opens on its claimed authority alone because memory blinked, and no change routes as one that touches nothing. The route is recorded before a post-review candidate is applied. With signers, a verdict is admitted only from a holder of a required position, whatever authority it claims (reviewer <who> holds no position this Review requires (<positions>)) — or, where the route left a path to the fallback, from one claiming the Review’s own required authority; a verdict counts toward every requirement it meets. The author is still refused, except on a governance Review (a practice, a hold, an opening, a graph proposal) requiring the Board, which admits its author’s verdict; a mutation’s Review refuses its author whatever authority it requires; one rejection or revision from a required position settles it; approval settles only once every required position has approved — one holder of two positions signs for both — the required authority where the route left it a path, and every gate has a passing run at the candidate, as the forge reported it. An approval that settles nothing is answered all the same,review.signed <review> {by, awaiting}, andhale dna review <id>shows what it awaits. Holders are routed again: while a routed Review is open the organization reads who holds its positions on each reconciliation, and when that changed — a position filled after it opened, a holder retired — writesreview.routedagain (the latest row is the requirement) and the Review takes the new holders; the positions stay the route’s, and what was signed stays signed. The Leader never decides a routed Review. A gate’s run is the forge’s (GH #1161): a gate is a workflow’s job, and its run is the job’s check run at the candidate, which the host reads from the forge (Forge.checks: GitHub’s check runs for the commit, a file forge’s<n>.checksin fixtures) when it syncs the forge, and records once per run asgate.observed <review> {gate, sha, conclusion, url, forge, run}— completed runs of the gates the Review’s route names, matched by job name (a matrix legjob (…)and a reusable workflow’sjob / …are the job’s). The host’s forge sync writes that row, and it is the only run a Review weighs, and only from the forge itsgithub.prrow names: a run at another commit, one whose conclusion is notsuccess(skippedincluded), another gate’s, or another forge’s counts for nothing, and a verdict cites no run (a receipt anyone files is no gate’s word;--evidenceis refused). The row is trusted as every record row is, by who may write the record. When such a row lands, the organization tells the Review (ReviewRouted, on its reconciliation, once per row), and the approvals it holds are weighed again: with every gate’s passing run in, it settles on the last of them. A Review rebuilt at a restart admits the plain verdicts the record holds for it again, so the approvals given before the runs survive it. A prepared source’s Review (an organization change) takes the route’s positions and no gate. A command verdict (command_id) is decided once by the same rules: redelivered after a lost response, or asked of the Review a restart rebuilds fromreview.command_decided, it answers the decision it made and writes nothing. The typeddna.review.verdictcommand applies the same standing, over the latestreview.routedrow, in every Review profile (a practice, a binding, an edge, a prepared source), naming its reviewer by the principal; its refusal isforbiddenon the wire, and the admission’s result carries awhynaming what the Review requires, as the Review’s own refusal does; the plain path (hale dna review <id> approve --as <who>) names its reviewer as the row’s writer says, so underdna.trust = signedthe position check is as strong as the writer’s signature. - Owners (stage B2, GH #664; the graph’s, GH #1123). A shared
record’s org chart has one owner per organism position: the firm
whose controller admits intents for it. The org chart is the graph’s
(The repository’s graph): a firm is an
organizationnode, it owns a position by holding it (holds(position:org, organization:acme),holds(position:org/collections, organization:north)), and a person is its member by holding the organization (holds(organization:acme, alice)). A position no organization holds takes the owner of its nearest held ancestor by path (org/collections/latetakesorg/collections’s, up toorg), and one under no held ancestor is unowned — admitted by no controller. A graph in which no organization holds anything is one owner, the organization itself, and nothing about a single-owner organism changes. The organism’sOwnershipis built from memory’sgraph_ownership()(JSON[node, holder]pairs of every liveholdsedge an organization is in; memory not answering is no ownership, so a view that may have moved is dropped), read again at rehydrate and on every reconciliation, so the Ledger’s gate (org_members) and every admission read one projection. A body over a shared record says which organization it is (git config dna.owner acme, the bare name of itsorganizationnode, carried asHALE_DNA_OWNER); a body that names its organization admits nothing until memory says what it holds — it fails closed. The host’srunanddevread which organizations exist straight from the record’sgraph.noderows of kindorganization(the record is the membership’s source): over a record that states organizations, a body that names none does not run, nor one that names an organization the record does not state. The host’s admission helpers read memory as a head does (HALE_DNA_MEMORY_DSN_HEAD, else the spine’s DSN). A body admits an intent (Intent.to, the position it is for; “” isorg) only for a position its organization owns, refusing one offered to it for another’s by name (intent.refused: “not this organization’s to admit”). It refuses only on the graph as of the record’s end: it reads the ownership again, and while memory’s projection has record rows still to apply the intent stays unanswered, for the relay to offer it again. A head never offers such an intent to the body beside it:hale dna task create --towrites it to the record for the owner’s controller, the host relays onlyintent.requestedrows for positions its owner holds, andstatuslists the rest as[unadmitted]with the owner. Ownership and membership change like any other hold: proposed (hale dna fill <position>|organization:<name> <holder>|organization:<name>) and ratified by the Board (practice-ratify, which requires theboardposition); no quorum of owners approves anything — every approval is the route’s (Signers from the route), and a position an organization holds is signed by any of its members (a Review’s wire form keeps itsapproversfield, always empty). There is no verb yet to propose a neworganizationnode: a fixture or an operator writes itsgraph.noderow. Ids and effect claims carry their owner (GH #666): a controller mints in its own namespace —acme:t3,acme:m2— and claims an apply asacme:apply:<candidate>, so two controllers never contend for one id, each restores its count from its own births alone, and the ledger’s unique constraint on a claim kind decides a first claim with no coordination between them: atask.bornthe Ledger answersclaimedis minted again under the next id, and nothing is refused. With one owner nothing is prefixed. Work that crosses owners is a transfer inside the one ledger (GH #667), #615’s rule applied between owners: a plan that hands a job to another owner’s member appendstask.transfer_requested <task> {owner, to, assignee, …}in place oftask.handed, and the Task waits; a member of that owner accepts it (hale dna task accept <id> --as <who>,task.transfer_accepted, which memory takes only in a member ofto‘s name) and their controller then appendstask.handedin its own name, namingtransferred_fromand who accepted, so completion is admitted in the assignee’s name as always. Nothing settles on the request alone. Every owner runs a body over a shared record, and every body projects, carries out and erases, none coordinating (The spine). Each owner’s heads write as their owner’s role, which writes only in its owner’s members’ names (The owners’ roles); who the person is remains the head’s to establish — its issuer underdna.principal = oidc. With one owner, the head’s role writes as any person. The organization that ownsorgisownership.host_ownerin the API’s organization read (empty with one owner), which buildsownershipfrom memory under the head’s role; the owners map’shost = <owner>(GH #669) went with the map. An organization source published by a named source owner is published only when that owner ownsorgin the graph. - The foundational law (
dna/org/law.hl, generated, extendable, never weakened): nothing applies except through the substrate (forbid reaches(positions, effects(genome_apply)) avoiding substrate); the editing position reaches neitherrepo_write,worktree_io,genome_applynor the Knowledge; the Leader’s verdict reaches the genome only through the substrate; credentials are sealed. The claim engine follows bus edges, so a position that could reach an effect through a published fact is a build failure with the path as its witness. - Hosts.
hale dna runbuilds and runs the organization with iris attached;hale dna devruns the application under the same host too, rebuilds and restarts it on an apply, recordsexpression.restartedin the host’s name, watches the window and reports on the nerves. The host writesorg.pidandapp.pidunder.hale/dna. Underrunalone a restart request is logged, not answered: expressing an application deployed elsewhere is a deployment gateway’s job. - One body per owner: the body lease, and its epoch (GH #665).
A record admits one body per owner at a time — one body when the
organization is the one owner (bounded attachment; the initial
controller model). Over a shared record (Owners, above) the
lease is a row of memory’s
claimstable per owner,owner/<owner>, taken like the single body’sbodyrow, so two firms’ controllers run side by side over one record and a firm’s second host waits on its own firm’s lease alone. The lease’s token is an epoch: it rises when the lease is taken (a takeover, a forced claim, an expiry) and holds through renewals, and the host hands the body it starts its lease and epoch (HALE_DNA_LEASE,HALE_DNA_LEASE_TOKEN), which the body’sMemoryLedgerchecks before every append: a write under a lease that is not held, is held at another epoch, or has expired lands nothing (fenced: …), so a controller that survived a partition writes nothing after its replacement took over, whatever it still believes. A head’s or a host’s write is a request, which the spine admits. Failover is within an owner; no owner’s positions pass to another on a timeout. The rest of this entry describes the lease as one body’s; it holds per owner. A record admits one body of each. The host takes the body lease in memory (the rowbody,owner/<owner>over a shared record, swapped by its token) before it builds or runs anything, so two hosts on two clones fence each other through memory itself. The lease’s fields are (holder\ntoken\nexpires\npresent), the holderuser@host:<clone>, so the same clone restarting is the same holder and takes its lease back at once; a second clone is refused with who holds it and exits 3. The lease lives 30s and is renewed when a third of that is gone; the host asserts it at the top of every tick, before it relays, restarts or applies, and stops itself (exit 3, the organization with it) when the lease is someone else’s or released. It proves the lease again, renewing it, the moment before it starts a process — the organization and the expression at startup and at every restart — and once more before relaying after the tick’s sync: a build or a sync that outlasts a takeover starts and relays nothing, and the host exits 3. The renewal and the fence do not wait on the host. Beside the host runs the body fence (the host binary’sbody-fence), which renews the lease every 10s with every call bounded to 8s — and to the deadline it last proved (the lease’s expiry less 5s,.hale/dna/body.deadline), so the calls of one renewal together cannot outlast the lease — and writes what it proved to.hale/dna/body.fence.status, each line prefixed with its host’s pid; the host reads its own lines there instead of renewing. The fence’s mandate is.hale/dna/body.fence, naming its host’s pid: a fence whose mandate is gone or names another host (a host restarted on the same clone) stops and kills nothing. When the lease is someone else’s or released, when the fence cannot prove it within 5s of its expiry, or when the host is gone, the fence kills the organization and the expression (by their pid files), each with every process it started — the tree and the session frozen, then killed. Every process the host starts begins a session of its own (dna::in_new_session:setsid(1), or perl’sPOSIX::setsidwhere there is none), and a tool keeps its session through everything a process group does not survive:run_toolgives each tool a group of its own, and a tool whose organism died is reparented, but both stay in the session its organism began, and only a tool that callssetsiditself leaves it (#970). Then every process carrying the body’s mark (HALE_DNA_BODY=<holder>#<token>#<host pid>#, set in the environment of everything the host starts and inherited by every tool they start) — the second net, for thatsetsidtool, where the machine discloses environments — then a repository’s compose services this body brought up (The body runs a repository’s compose services: they live in docker, outside every tree and mark above, so the fence takes them down by the body’s ownership record) — and says why (the host does the same, before it stops the fence, on every exit it takes once it holds the lease: a failed build, a refused start, its organization’s exit), and the host exits 3 when it next looks. The session reaches a reparented tool on every POSIX machine; the mark needs the machine to disclose another process’s environment, and what a machine will give differs (#638).body_scansays which it has:proc— Linux’s/proc/<pid>/environ;ps—ps -E, proved at the time of asking on a child of the call’s, since the answer is what the kernel discloses about another process and not whether the flag parses (macOS shows a process its own environment and, with SIP, no one else’s — GitHub’s runners do show it); or none, where the fence still stops the sessions — every tool the organization and the expression started, orphaned or not — and only a tool that began a session of its own is out of reach, which the host says at startup (#970). A host blocked in a sync, a build or an observation window therefore cannot keep its organism executing past the lease; while memory cannot be reached the lease is kept unrenewed until then: a partitioned body executes nothing past its TTL. Taking the lease is a row (body.claimed <holder> {token, forced, by}), giving it up at exit another (body.released).hale dna bodyreads the lease;hale dna body claim --forcereleases a live lease that belongs to a body that is gone, as abody.claimedrow withforced: truein the forcer’s name (that body stops when it next asserts; the nexthale dna runtakes the lease);hale dna body release [--force]releases it explicitly. - The combination is detected, never stored. An organism is one
point in a space of independent capabilities — where the body
runs, whether a hosted head exists, whether a fleet expresses it,
which records it is connected to — declared by its pieces.
hale dna profileprints one line per axis from the pieces alone: the record’s remote, the body lease and its last tick,[dna] fleet, the knowledge store’s environment,dna.trust, the forge allocation; the status projection carries aprofile:and abody:line.hale habitat graft <name> --profile local|remote-body [--remote <url>] [--body <user@host>]sets pieces (a remote,dna.body); the profile name is not written anywhere. Profiles are examples of combinations, not a closed set; the toolchain refuses only what breaks a commitment (a second body on one record). - The body on a server.
hale dna body provision <user@host> [--dsn <url>] [--dir <path>] [--dry-run]makes a body over ssh, in order: the toolchainhale.lockpins (installed with the site’s installer at that version, verified, or stop), the record’s remote cloned (fetched into a clone that exists),vendor/dnaand the record brought up, memory’s database from the body’s local habitat or a DSN (which goes into the body’s env file asHALE_DNA_MEMORY_DSN_OWNER, never the record), the organism’s memory and nerves migrated once byhale habitat migrate .in the script (GH #1653: the unit’shale dna devmigrates nothing, and hands the host only the spine’s DSN), and a systemd user unithale-dna-<project>-<record>supervisinghale dna dev . --no-iris— on one server the body is the organization and the application under one host — withRestart=always(GH #1026): a failure flows up one more level, and a node that exits to take a moved genome is started again on it (The genome pull). It stops before writing anything when the record has no remote or one local to this machine, when ssh cannot reach the host, or when the host lacks git, curl, systemd, or (without a DSN) docker compose. It recordsdna.bodyanddna.body.dirhere and abody.provisioned <user@host> {dir, toolchain, knowledge, by}row. The default directory is$HOME/dna/<project>on the body, expanded by the body’s shell (the unit’sWorkingDirectory=%h/dna/<project>is the same place). The script can carry the DSN, so its file here is created empty with mode 600 before it is written, and removed once ssh has read it.--dry-runprints the exact script.hale dna body start|stop|logs [--body …]reach the unit over ssh;stopsucceeds when the unit is no longer active, and says so. Postgres, Docker, ssh and systemd are the reference setup; the definition admits other implementations, and names where they plug in (GH #647):Infrastructure— a body’s knowledge database, its supervisor (a unit installed, started, stopped, asked, read) and its credentials (put by name, never read back) — andTransport— how a head reaches a body (probe,execute) — are interfaces in the core.ReferenceInfrastructurein the host is compose, a systemd user unit and the env file, reached throughSshTransport(HALE_DNA_TRANSPORT=ssh, the default) orLocalTransport(local: every script runs in this machine’s shell underHALE_DNA_TRANSPORT_HOME, which is how the fixtures exercise a body with nothing stubbed on PATH for ssh);MemInfrastructureandMemTransportare the core’s fixtures. The provisioning script is composed from the database’s and the supervisor’s own fragments, so a body without systemd gets another supervisor’s unit by implementation, not byif. Nothing in the host spellsssh,systemctl,journalctlordocker composeoutside that one implementation. - Secrets.
hale dna secret set <NAME> [--body <user@host>]reads the value from stdin — never argv (aNAME=valueargument is refused), never the record — and fills that name’s slot of the vault (The organism’s secrets; mode 600, the newest value) on the body over ssh’s stdin, or on this machine. A name is a slot the organism requires —FORGE_TOKEN,OIDC_CLIENT_SECRET, or a credentialdna/org/models.hlordna/org/work.hlnames (HostedCredential { key }; the body’s own credential check readsmodels.hlalone, so a legs key never stands in for the organization’s) — and any other is refused. No process’s environment carries the value: the part that sends it reads it from the vault. Off a terminal the value is the whole of stdin with one trailing newline (\n, or one\r\npair) taken off; at a terminal, one line typed after a prompt; an empty value is refused by the secret’s name and nothing is stored.secret rotate <NAME>is the same for a name already set. The record getssecret.rotated <NAME> {where, by}and nothing else. At start the host checks the credentials its catalog names (HostedCredential { key }indna/org/models.hl): when none is in its slot of the vault (model-<key>, the one source a model key has; no environment variable is read) it says so and appendsbody.credential_missing model {any_of};statusandboardcarry “no credential for the model” until a start finds one and appendsbody.credential_present. - What a body holds on a machine is bound to its record (#635).
<record>in the unit’s and the env file’s names is the first twelve hex digits of the record’s identity (its first row’s digest), so two records whose directories share a name on one machine never share a unit or a credential. Everything else a body reads lives in its clone: thedna.*git configuration (the OIDC member mapping among it) and the knowledge schema, whose ownership is checked against the record (GH #613); the hosted head and the node serve the clone they run in.
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 habitat graft writes in repository mode is the
organization’s environments, not source, and does not make a seed) is
checked and formatted whole. Otherwise (a repository, whose code sits under its
seeds) each edited file’s seed is its nearest ancestor directory under
the root that holds a hale.toml and a .hl file, else the file’s own directory; each
seed is formatted and checked once, in order of the first file edited in
it, the first failure’s diagnostics (named from the seed, not the
worktree) feed the retry. The candidate’s verification follows the same
rule: when the Mutation’s seed is the repository’s root (.) and the
root is not a seed, fmt --check, check, verify, test and the base
topology run in the seed of the first file the candidate changed.
The law in force on a Hale heart (GH #1591). When the practice
standard/logs-are-structured is in force as law (ratified, not retired,
its document saying law: true), the candidate’s check adopts
std::law::Standard from outside, as hale check --adopt std::law::Standard (spec/verification.md, “Adopting from outside the
source”): a heart with a bare println on a served or attached path
fails the check, naming the clause logs_structured, and one that writes
through std::log passes. Nothing is written into the heart’s source,
and with the practice absent, advice or retired the check adopts nothing.
Models
Section titled “Models”The organization’s models are a catalog in source (GH #583 M1):
dna/org/models.hl, generated by the graft and owned by the project.
- The seam.
ModelBackend(identity,adapter,price_micros,model_name,allows,capacity_bytes,complete) is what every adapter implements; every call publishesModelCalled, which the substrate journals asmodel.calledwith the time of the call (at). One vocabulary (GH #1131): a row’sadapteris the adapter that answered the call —openai-chat,anthropic-messages,local,harness,fake,recorded; empty when the router permitted no backend, so none answered — whether the call was made in process or by a leg (the router records it,last_adapter);legsis only a leg’s wait row, a call that was not made (refusedsays why,waited_msfor how long). The prompt is a receipt (GH #611): the evidence carries the prompt and the context as sent, and the substrate files each as a receipt in memory under theprompt_digest/context_digestthe row names — only a body that hashes to its digest, and receipts are content-addressed, so an identical brief is stored once — recordingbodies: stored(ornonefor a call with neither). An organism whose receipts are not the record’s (a file store: a fixture, an embedded use with no record) files nothing and recordsbodies: none (receipts are not the record's: file). Acustomer-class call files no body:bodies: withheld (data class customer), and its digests are all the record keeps.hale dna history <attempt>renders, under eachmodel.calledrow of that attempt,prompt <digest> (<n> bytes), exactly those bytes from the receipt, andend of prompt(the same for the context) — never a re-composition; a withheld body says so, and a body memory lacks says so. A wider history (hale dna history m1) names the calls and leaves their bodies to each attempt’s own, since an editor’s prompt carries whole files. The row saysstoredonly when the store gives the body back. Routers return data, never a backend. Adapters:OpenAiChat(the OpenAI chat shape: OpenAI, OpenRouter, vLLM, Ollama;adapter: openai-chat),AnthropicMessages(the native Messages API:systembesidemessages,max_tokensrequired and defaulted to 4096, the reply’s text blocks joined,anthropic-versionsent;adapter: anthropic-messages),LocalModel(the OpenAI shape to this machine; no credential, noexternal_model, any data class),FakeModel(scripted:answer,answer_file,answers_dir,answer_role;fail_afterrefuses after that many calls;turns_diranswers a request that offers tools from<dir>/<turn>.json, a tool loop’s scripted turns). A request names the model its mapping resolved (ModelRequest.model, else the backend’s own), evidenced asrequested_model; it carriesmetadata(the attempt, the position and the holder: attribution only), whichOpenAiChatsends only withsend_metadata: true, and anidempotency_key, sent asIdempotency-Key(a model leg’s is<attempt>:call:<turn>). A model leg declines a Work whose mapping named a model and refused it (atoolsordoWork on a model that returns no tool calls) before it asks for spend or calls anything. A hosted adapter’scompletecarriesexternal_model; its credential is a sealedHostedCredentialthat names its key (key, whose vault slot ismodel-<key>; GH #989, the vault is the one source a model key has, and no environment variable is read) and itsscheme—bearer(anAuthorization: Bearerheader) orx-api-key— and never returns the bytes: the credential reads its slot and composes the header in the one statement that sends the request, no adapter does. An API error is refused ashttp <status> <the API's message>; a transport failure ashttp 0 <kind> <detail>. - The harness.
HarnessModelruns an installed coding harness (command: claude, orcodexwithoutput: textandexec --skip-git-repo-check --sandbox workspace-write, because a non-interactive codex is read-only by default and would answer without ever editing its export) per request with its own tools on, as a backend thatworks_in_place: for a source-editing request the editor makes an EXPORT of the Mutation’s worktree (a plain directory with the same files, never.git, never.hale), runs the harness with the export as its cwd and the whole objective as its prompt, and imports the export’s diff back under the grant — a file that differs or is new is written through the tools, one that is gone is removed, a change outside the grant is counted (outside_grant) and left behind;files_changedis derived from that diff, never from the harness’s answer. A file the worktree does not have is new whatever it holds, empty included. A written file’s parent directories are made under the grant first, so a harness may add a module in a directory the worktree does not have yet, and an in-grant write or removal that fails is the attempt’s failure (the import was incomplete: …), never a candidate carrying part of the change. Then the same fmt, check, retry (the diagnostics in the next prompt) and assessment as the file-by-file flow. An answer-only request (a review) runs in an empty directory of its own.completecarriesexternal_modelandharness_run; evidence is onemodel.calledrow per call with the harness’s own cost summary (total_cost_usd,usage),tool_grant: harness @export, andconfinement=<kind>inparams. - The boundary is stated exactly. The export is not a sandbox: a
process whose cwd is the export can still reach whatever the
operator’s account can. What DNA guarantees is that the genome is
unreachable from the harness process. A
Confinement(kind,available,wrap(argv, cwd, mask)) supplies it:Bubblewrap(Linux: the filesystem as the operator sees it — the harness’s own home state, the toolchain, the network — with the repository and the worktree replaced by empty tmpfs mounts,--die-with-parent) orNoConfinement. The genome is the model’s own, not the request’s:HarnessModel { genome, genome_env }reads it at birth (the host exportsHALE_DNA_GENOME), every call masks it, and a request’smaskadds what that call knows besides — the worktree an editor exported. Every verb the host runs is givenHALE_DNA_GENOME, not only the organization underrunanddev:hale dna modelsprobes the catalog in a process of its own, and a confined harness with nothing to mask is refused. An answer-only role (a review, a classification, the catalog’s probe) carries no request mask, so a genome on the model itself is what makes the guarantee true for them. A confinement with nothing to mask is refused, not run, so the claim and the fact cannot diverge. A harness whose confinement is unavailable is refused (unconfined harness not allowed) unless the org chart’sallow_unconfinedsays otherwise; the evidence records which it was, andmasked=<n>how many paths the boundary covered. On every platform the assembly also checks that the repository’s head and the worktree’s head did not move during the attempt and fails the Mutation (mutation.failed:the genome moved during the attempt) if they did: not a boundary, but a bypass becomes a loud refusal. Outside the genome the harness has exactly the operator’s account, which is what running it by hand has. TheRepositoryinterface names itsrootfor the mask. - The tape.
RecordedModel { dir, mode, inner }wraps any backend (dir_env/mode_envname environment variables it reads intodir/modeat birth, so a catalog function computes no string: dna/FRICTION.md F.17). The key issha256over the fields that identify a request — role, the inner’s name, the model the request asks for (the one its mapping resolved, else the inner’s), the prompt and context digests, the data class, the grant normalized (… @grant: its path is where it ran, not what it was) and, for a backend that works in place, a digest of the workspace’s starting tree; a tool loop’s request adds digests of its tools and its turns so far, and its entry keeps the tool calls the model asked for, so a replay follows the same turns. An identical request made again in one run — a retried Work planned a second time — is keyed by its occurrence as well (occurrence=nfor the n-th; the first keeps the plain key), so a recording that got two answers replays both in order, and a miss on a later occurrence says it was never recorded.recordmakes the tape’s directory before it writes anything into it, and an entry is counted only once every file of it has landed: a patch or an entry that could not be written is a refusal (cannot write the tape: …), because an entry claiming a patch that is not there replays as a miss on a request the tape appears to hold.recordforwards toinner(whose evidence is the call’s) and writes<key>.jsonwith every keyed field in clear and the answer, and for a workspace<key>.patch: what the backend changed there, before any import, so a replay reproduces the edit and everything after it runs for real.replayanswers from the directory (evidenceadapter: recorded,params: tape=<key> mode=replay), applies the patch to the workspace, and refuses a miss naming the request and, when an entry shares the prompt digest, the fields that differed (tape miss <key> (role …, backend …, …): nearest <key> differs in: context_digest tree). Replay is pure; record reaches whateverinnerreaches. Anything that varies between runs must stay out of the key, and recording finds what does: the editor therefore strips the worktree’s path from the diagnostics it feeds back. The tape is a checked-in fixture, not a store. It proves how the organization handles recorded outcomes; model quality is only ever tested by a fresh run. - The fixture.
dna/acceptance/trio: a gateway (the root seed), an api and a worker under a fleet seed (trio/fleet) of four instances on two hosts, with the routes its bindings derive and claims across them (require publisheson the gateway and the api,require subscribeson the api for the gateway’s orders,forbid reachesgateway → worker avoiding the api). Its organization, with every backend aRecordedModelover the real adapter (dna/acceptance/trio.fixture/catalog.hl; mode fromHALE_DNA_TAPE, the tape fromHALE_DNA_TAPE_DIR), is driven by scripted asks end to end: a change to the gateway approved and deployed to both nodes, the organization grown by a supervisor under pressure from the worker and restarted with it, and a change to one service that still checks but breaks the fleet’s claim denied — every model call answered from the tape, keyless. It is the acceptance for every later change to the organization;HALE_DNA_TAPE=recordwith a key re-records it. - The catalog is source. A backend is a constructor function
(
frontier(),fast(),desk()…); a position’s router is a function composed from them (leader_models(),editor_models(),agent_models(), the model leg’s, behind the performer indna/org/work.hl); the organization’s main takes each position’s router from the catalog and names no adapter inline. A new position is one more function; a provider switch is one file;hale checkvalidates it and the law keeps the concrete types at every boundary. - Discovery. The graft looks at the environment
(
ANTHROPIC_API_KEY,OPENAI_API_KEY) andPATH(ollama, whose first listed model becomes the desk model;claude, elsecodex, becomesharness()), write the catalog for what is there (AnthropicMessageswithclaude-opus-5/claude-haiku-4-5when its key is present, elseOpenAiChatwithgpt-4o/gpt-4o-mini, a placeholder key when none is set; with a harness the editor’s and the agent’squicktier is the harness, and with no key every model-backed slot is), and print what each position was given. Nothing found is fine: a hosted backend without its key is not permitted, and every Review waits for the Board.upgradewrites a catalog for an organization that predates it and says what to point at it; it never edits the organization’s main. - The budget has one owner. The catalog declares
org_budget(), aBudgetPolicy(window:day,weekornone;allowance_micros, 0 = unmetered) as data. The substrate owns the oneBudget: everymodel.calledrow is accounted at its time, the counters are rehydrated from the record at birth, and when a window is exhausted intent is refused (intent.refused:budget exhausted (spent … of … micro-dollars this day in … call(s))),budget.exhaustedis journaled once per window (detail,spent_micros,allowance_micros,window,at), the Board is told, and a Review is not announced to the model-backed positions — it waits for the Board, which needs no model. AModelRoutercounts what its calls cost and has no allowance of its own. - One gate for spend (GH #1131).
Budget.admit(ceiling, now)is the gate every model-backed spend passes, in process and out: the editor’s attempt and the leader’s Review are admitted by it before a call is made, and a leg asks it for its attempt (Legs). It refuses an exhausted window and otherwise admits the smaller of the work’s own cost ceiling and what the window has left (-1 when neither bounds it: an unmetered budget, no ceiling). A refusal is the same row wherever it is asked —attempt.allowance_refused <entity>(why,ceiling_micros,spent_micros,window_micros,window,at) on the editor’s attempt, which is declined and edits nothing, onreview:<id>for the leader’s Review, which waits for the Board, or on a leg’s attempt. Intent admission and the optimize pass ask it too (their answers stayintent.refusedandoptimize.refused). Nothing is reserved, so the window can be overrun, by a bound: legs admitted against one remainder may together pass the window’s allowance by at most the sum of the allowances they were granted plus one call each — a leg makes no call whose known price is past what is left of its grant, nor any once its calls have spent it, and only a call whose cost is learned from its answer can cross it; the owner settles such an attempt failed, naming the overrun, and journals the calls. In process the gate admits the editor’s attempt and the leader’s Review whole: their calls within are not held to an allowance, so their spend is bounded only by the window being open when they are admitted. - Tokens per task (GH #946). The attempt id on a
model.calledrow is the tag its cost is attributed by:<ask>/planto the task born of the ask (thetask.bornrow whose body starts<ask>:);<work>/a<n>to the Work’s task — a wf1 attempt’s admission names the task and the performer kind, a Mutation’s request names the task (the editor’s; an older record says it in the proposal’stask <id>summary);<review>/reviewto the task the Review’s request names, or its Mutation’s, orknowledgefor a knowledge Review (k:<12hex>);optimize/<n>toorg. The probe leaves no row. A call of no known shape is its own and no task’s. The projection (dna/operations/usage.hl,Usage) sums calls, input and output tokens andcost_microsunder an id — the attempt itself, the ask, Work, Mutation or Review it is of, or the task it serves and every task under it (byworkflow.admitted’sparent_task) — by position, keyed by the graph’s id for it,position:<name>(position:leaderfor a plan, a verdict and the optimize pass;position:editorfor a Mutation’s attempts;position:<performer kind>for a wf1 attempt, until an attempt’s claim names the position that took it) and by<backend>/<model>.hale dna history <entity>printsusage of <entity>: <calls> call(s) · <in> in · <out> out · <cost> µ$and the two breakdowns under thehistory ofline, nothing when no call was made, andusage: …for the whole record with no entity; the API carries the same sums asusageon every execution, each of its attempts and every administered Task. The editor’s locate and plan calls carry the Work’s attempt id (the tape key excludes it). hale dna models rule <selector> <size> [--permit <sizes>]appendsmodel.rule <selector>in the setter’s name (--as): a selector isdefault,position:<name>,contract:<Name>ortask:<id>, a sizequick,standardordeep, and--permitthe sizes a more specific selector may move it to.models category <mode>.<size> <model> [--tools] [--price-in N] [--price-out N] [--endpoint <url>] [--key-vault <slot>]appendsmodel.category(prices in micro-dollars per 1k tokens); atoolsordocategory without--toolsis refused.--endpointnames where the model is asked, an OpenAI-compatible chat URL (voice’s on a host, say), and--key-vaultthe vault slot of that endpoint’s key, a slot’s name and never a key: a Work the category resolves for is asked there by itsopenai-chatperformer, with that slot’s key or, with none, no key at all (the performer’s own key never goes to another endpoint). The habitat’s head sets it for a tenant asCommands::set_model_map(spec/habitat.md).models maplists the latest row per selector and per category. A graft (hale habitat graft) seeds a starting mapping when a hosted model’s key is in the vault:model.rule defaultatstandard(permitting every size), and every category<mode>.<size>filled by the hosted pair the catalog writes,quickby the fast model andstandardanddeepby the frontier one, each with tool calls and the catalog’s prices, in the namehale habitat graft; with no hosted key it seeds none.hale dna task create --size <size>appendsmodel.rule task:<intent id>, an override that holds only inside the sizes the rule beneath it permits.hale dna modelsbuilds the catalog beside a one-line main in.hale/dna/probe, runs itsprobe_catalog()from the project root with the organization’s environment, and prints one line per backend: the catalog name, the router-facing slot, the model, and the answer —ok, elapsed, cost and the first line of the reply;refused: …; ornot permitted (no credential present). The organization is not started, the nerves not touched, and nothing is journaled: a probe is nobody’s Attempt.
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).
- The record holds the decided half. A proposal is a document —
canonical JSON with a fixed field order (
kind,text,author,target,provenance) — filed as a receipt under its sha256, so a digest in the record always resolves to recoverable content.Dna.propose_knowledge(idea, target)files the receipt, appendsknowledge.proposed <digest>(body:digest,review_id,kind,author,target,class,at), and births a Review pinned to that digest (review_id=k:+ the digest’s first twelve hex digits;required_authority: board— ratified knowledge is the organization’s; thereview.requestedbody carriesknowledge_digest). The tower rule classifies the binding from who proposes and where it binds — a parent’s idea bound to a child is agoal, a child’s bound to a parent aconcern, one’s own aninitiative— and a lateral proposal (siblings) is refused withknowledge.refusedand no Review. A verdict naming another digest is refused by the Review; a Leader’s does not satisfy the Board’s requirement. The authoritative event isknowledge.ratified <digest>, appended by the assembly onreview.settledwithapprove(body:digest,review_id,outcome,decided_by,kind,target,class,at); any other outcome appendsknowledge.declined. A pending knowledge Review is re-born from the record at birth like a mutation’s. - Binding changes preserve the item. The reviewed
dna.knowledge.binding.bind@1and.unbind@1operations address one(idea, target, author)tuple. Independent explicit grants admit aknowledge.binding.requestedfact. The Body records delivery before creating the canonicaldna.knowledge-binding-change/1candidate and its exact Review (knowledge_binding_digest, notknowledge_digest). The candidate pins the original command, grant and latest Record directive for the tuple. Approval producesknowledge.binding.boundor.unbound; an intervening tuple change or newly inactive bind subject produces a separate apply refusal. Each consequence belongs to the original request identity. Projection changes future package applicability, preserving the item receipt and earlier Work packages. A visible retired item permits exact unbinding. This is the reviewed command profile the API admits (The API, under The surface), not a universal Review requirement on the in-processKnowledge.bindprimitive. - Relationship policy selects direct or reviewed admission. The existing
dna.knowledge.edge.link@1and.unlink@1commands retain their exact directed(from, to, rel)identity and command encoding. Areviewgrant instead admitsknowledge.edge.requested, pinning the latest direct or reviewed directive for that tuple in a canonicaldna.knowledge-edge-change/1candidate. Its Review carriesknowledge_edge_digest; the candidate is not a Knowledge node. The Body derives ownership from both endpoint receipts and requires its owner to admit all four author/target positions in this reference profile. An independent approval producesknowledge.edge.reviewed_linkedor.reviewed_unlinkedonly while the pinned tuple basis and endpoint eligibility still hold. A changed basis or ineligible endpoint produces a separate apply refusal. A retired, ratified endpoint permits exact unlinking. Recovery follows the original admitted variant even if policy later changes mode. Native effects replay in Record order; approval alone does not establish a graph change. This policy does not add a universal Review requirement to the graph primitives. - Memory holds the live half.
dna/core/memory_store.hldeclaresKnowledgeStore:scope/record,open,healthy/reopen,watermark(the next record seq to apply),begin_row(seq, prev, head)/commit_row/abort_row(one record row’s transaction, below),rebuild(watermark, head),upsert(idea, ratified_seq)(idempotent by id, which is the digest),retire,bind,unbind,link,unlink,idea(id),context_ids(target, budget)(accepted ideas bound to the target or any prefix of its path — goals flow down, initiatives stay local — in ratification order;targetis one target or a space-separated set, Targets are paths or nodes, below),ranked_ids,count(what), and the structure and signals of K3, and the repository’s graph (below).Pqis Postgres (nine tables:knowledge_meta,knowledge_ideas,knowledge_bindings,knowledge_edges,knowledge_structure,knowledge_signals,graph_nodes,graph_edges,graph_members, which the migration creates andopenonly checks; node writes are upserts and removals delete exact tuples; rows come back as JSON built by the server, since the driver’s tab- and newline-separated rows cannot carry an ordinary paragraph; a signal counts once per record row, keyed on that row’s sequence). The projection is a consumer of the record:apply_record(store, journal, receipts)indna/operations/memory_tail.hlwalks the record’s rows from the watermark, resolves each knowledge digest to its receipt, upserts (ratifiedaccepted with its binding;proposedanddeclinedkept as such, never over a ratification). Each record row is one transaction (GH #1026):begin_rowmoves the stamp first — the watermark fromnton + 1, thengraph_projection_headfrom rown - 1’s digest to rown’s, each by compare-and-swap on itsknowledge_metarow — then the row’s effects, andcommit_rowlands them together; a failed effect lands nothing (abort_row), and the next pass begins the row again. A projector whose compare finds the stamp moved rolls back having written nothing and ends its pass (yielded): it blocked on the watermark’s row while another projector held it, and that projector committed the row. So any number of projectors may apply one record at once, with no coordinator, and converge on one graph, each row applied once; after a crash a projector resumes at the stamp. Nothing in memory becomes ratified except from aknowledge.ratifiedrow: the record is the sole authority; the projection can lag, never disagree. The observed and inferred tier (K3) is derived and rebuilds from the record. - The record is the scope of its memory. Memory is opened for one
record, named by the record’s identity — the digest of its first row,
record.genesis, which carries a nonce, so two projects created alike are two records (GH #1068, #1590) (scope(record)on the store beforeopen,record()to read it back).store beforeopen,record()to read it back).Pqkeeps one Postgres schema per record,dna_<identity>, selected for the session atopen(withpublicbehind it for the vector type), so two records on one database each see only their own ideas, bindings, structure, signals and watermark; a memory allocation in the local habitat is one record’s anyway. The schema’sknowledge_metacarries arecordrow naming its owner, written by the migration and checked by the migration and by everyopen: a schema that names another record is refused (scope: schema … belongs to record …, not …), never read. A record with no rows yet has no identity: its projection opens underdna_unscoped, where nothing is ever applied, and moves to the record’s own schema once its first row lands. - The genome pins the identity (GH #1590).
hale habitat graftcomposes the record’s first row, writes[dna] record = "<identity>"into the repository’shale.toml, and only then provisions memory and appends the row, so a clone knows which record — which schema, roles and nerves token — it belongs to from its genome. The graft writes it into thednatable wherever the manifest keeps it (a[dna]header in any spelling, dotted keys, an inline table), editing the parsed document so the rest of the file survives.record_identity_at(dna/core/memory_spine.hl) reads it wherever TOML lets it stand, as a basic or a literal string: with no pin there is no record; a pin present but not readable there (not a string, a multi-line string, a basic string with escapes) is refused, never taken for no pin; and a pin that is not 40 or 64 lowercase hex digits is none. A record whose first row names a genome this clone’s history does not hold is not opened (thehale.tomlnaming it was copied from another project). A graft in a clone whose pinned record memory does not hold refuses: it would start a second record under the first one’s name. - The spine projects; readers read.
MemoryProjection(dna/operations/memory_tail.hl) applies the record into the graph on the host’s tick, as the record’s spine role, on every node (The spine, above), so the projection advances without anyone asking. The fence compares the stamp with the record: memory’s stamp is a row of the record (its watermark and that row’s digest), and the record only grows (GH #1590). The stamped row at its place: the rows after it are the delta. A watermark past the record this projector holds: another projector is ahead, and there is nothing to project until it reads again. A stamp that does not match the row at its place is a graph this record cannot vouch for, so it is rebuilt from row 0 —rebuild(watermark, head)empties it only while the stamp is still the one read, so of several projectors one rebuilds, and a projector rebuilds at most once per pass. A question memory cannot answer is a refusal, never an empty answer: a package built from failing reads, or from a projection behind the record, is indistinguishable from “there is no knowledge here”. The projection’s store is opened on the first tick rather than at birth, and the connection is asked on each tick afterwards (healthy), because an open that succeeded once is not a connection that still answers: a session dropped by a restart or a failover is re-established (reopen). A reader —MemoryKnowledgein the organism, the API as a head — never projects. A package (MemoryKnowledge.package_for(target, query): the ids included, their ideas with kind, text and author, the projection’s watermark as its revision, and a digest over the target and the ids — what is handed over, never the revision, which varies between runs and would make a tape unable to answer the same request twice) waits, bounded at 20 s (PACKAGE_WAIT_SECS), for the projection to pass the record’s last row that changes a package (an idea ratified or retired, a binding made or undone), and otherwise refuses:memory's projection is at row <n>, before the record's knowledge row <m>; the spine applies it on its tick. The organization’s knowledge package is read from memory under the spine role. The API reads the graph under the head’s role (The API, below). - The design, as proposals (GH #596 C). The graft seeds the
toolchain’s practices about how a DNA organization works — the
design principles, the evolution pattern, structure follows intent,
standard equipment, the structural signals, signaling, the optimize
pass, software delivery — as
knowledge.proposedrows with receipts bound toorg, one Review per practice (k:<12hex>, groupeddesigninhale dna review’s listing;hale dna review design approve|rejectis a convenience that decides each pending one in turn, never a batch object in the record). A Review pins one digest and settles with one outcome, so the Board decides practice by practice, and nothing is ratified by the toolchain. Each receipt carries a stablename(design/<slug>) and the toolchain version. Supersession is the Board’s decision, not a side effect. Declining a proposal retires nothing (the projection keeps a ratified idea a laterknowledge.declinednames). A proposal whose receipt carriessupersedes: <digest>retires that digest when it is ratified — the assembly appendsknowledge.retired <old>— and leaves it in force when it is declined; aretirementproposal carries only that. A replacement is ratified only while what it replaces is still active: an approval of a proposal whosesupersedesnames a digest already retired is refused (knowledge.refused <digest>, with what retired it) rather than ratified — otherwise two replacements of one version would both be served, the first never retired.hale dna upgradeproposes each practice whose current text is not the latest proposed under its name, superseding the active version under that name (a pending or declined proposal is not a predecessor), one replacement at a time: a practice whose latest proposal is still before the Board waits (upgradesays so), and a refused one is proposed again against what is active now. The projection retires an idea by marking it not accepted: it leaves every package and stays readable by digest. - The operating practices, as proposals (GH #994). Beside the
design, the graft seeds a second family,
operating/<slug>: how the organism runs, which is what a leader plans within and a reviewer cites, where the design says how an organization is shaped — one store per step, row first, readings never act, legs hold nothing, deploy settles on pulse, the forge decides. Each is one paragraph, a rule a person applies. The family is seeded, listed (groupedoperating;hale dna review operating approve|reject), decided and superseded exactly as the design, andupgradeseeds it into an organization whose record never held it. Its receipts carry provenancedesign, which marks a toolchain-seeded practice of either family;HALE_DNA_DESIGN_SUFFIX(fixtures only) appends to every practice of both. A receipt may also carry aschedule, which the practice declares when it is ratified (Schedules):operating/optimize-cadencecarries the optimize pass’s, convened by the Board once a day (The optimize pass). - Targets are paths or nodes (seed/pull). A binding’s target is a
locus path (
org,org/<child>) or a graph node id (language:hale,system:dna,position:leader: The repository’s graph, below). The match rule is one query,ranked_idsandcontext_idstaking a space-separated set of targets: a word holding:is a node id and matches a binding to exactly that id (language:haleis notlanguage:hale-x, and a prefix of an id is nothing); a word without one is a path and matches a binding to it or to any ancestor, as it always has. An accepted idea is in the package once however many words match it, and a set of one path is the tower rule unchanged. A node is bound from above, never beside:classify_bindingcalls a binding to a node agoalwhen its author is a path, so it is neverlateral. A proposal or a binding request whose target is a node the record holds nograph.noderow for (and no latergraph.retiredtook back) is refused naming it:knowledge.refusedfor a proposal,knowledge.binding.refusedwith reasonunknown_nodefor a binding request. The projection’sknowledge_bindingsrows carry the target unchanged — no migration; a node id is just another value of the column — andranked_idskeeps their ordering and budget. - The hat’s target set (seed/pull). The hat’s package is asked for a
set, in this order (
hat_knowledge_targets, dna/core/hat.hl): the Work’s locus path (hat_knowledge_target:orgororg/…, elseorg);language:<l>for everywritten_inedge of the attached application (application.attached’s name, the nodeapplication:<name>), unless the Work is an organization change; thensystem:dnain their place when it is one — the Work’s Task admission carries the leader’s plan with kindorganism(classorganization), the same wordedit_class_ofreads to edit the organization’s own seed; andposition:<name>for the position the hat names (hat_position_of), so a mandate bound to the position arrives with the Work. The hat’s JSON names them astargets(a space-separated string) and its digest covers them, so a hat is a different hat when its set is. The owner’s edit path asks the same set for its package, andPRACTICES (ratified knowledge for <targets>, package <digest>)names it. A reviewer’s brief is the same read:hale dna review <id>prints, underknowledge for <targets>, the names of the ideas ratified for the subject’s targets — the node a knowledge Review is bound to when it is one,system:dnafor a mutation of classorganization— ranked by the Review’s question and bounded by the package’s budget; without memory (HALE_DNA_MEMORY_DSN_HEAD) it says the knowledge was not read, and a subject with no node prints nothing. - The nodes knowledge is about, and the library (seed/pull,
seed/library). The graft
writes
graph.noderows forlanguage:hale(kindlanguage, textHale <toolchain version>…) andsystem:dna(kindsystem) and, for an attached application,application:<account name>(kindapplication, the nameapplication.attachedcarries — the record named the application by that row alone before) with awritten_inedge from it to the language (written_in:application:<name>|language:hale); a repository with no application gets the two nodes and no edge. Beside them it proposes the toolchain’s library: the book per chapter and the spec per section, one idea each.crates/hale-cli/build.rsgenerates the table (LIBRARY) from the tree at build time — no hand list, so a new chapter joins the library by existing: everydocs/src/**/*.mdis an idea namedlibrary/<path without docs/src and .md>, titled by the chapter’s first heading, and every##section ofspec/*.mdis an idea namedlibrary/spec/<file>#<slug>, titled by the section’s heading, its text running to the next##. An idea is bound tolanguage:hale, except the DNA chapters (docs/src/dna/**,docs/src/parts/**,spec/dna.md,spec/model.md), which are bound tosystem:dna. Each is aknowledge.proposedrow of kindidea, provenancetoolchain, whose receipt carries the text, thetitle, thetarget, the toolchain version and the text’s digest (content_digest). The proposal’stargetis the binding: ratifying it binds the idea there, so a library idea has no separateknowledge.binding.proposedrow (a binding request needs a ratified subject). - A family is ratified by one Review (seed/library). The Board does
not review hundreds of ideas one by one. The library is proposed as
two families, one per node —
library/language@<version>andlibrary/design@<version>— and each family is onereview.requested(grouplibrary, kindfamily) whose subject is the family’s own document, a receipt of kindfamilynaming the ideas it ratifies,<name> <digest>a line, in the order they were proposed (the Review’s body carries the same list and acount).hale dna reviewlists a family as one entry;hale dna review <id>renders the ideas it ratifies;hale dna review library approve|rejectdecides each pending family in turn, and its verdict is the verdict of every idea it names. Approving writes, for each idea, the rows it would have had of its own —knowledge.ratified(kind, target and class,supersedeswhen the idea replaces one, the retirement of what it replaces) — and then the family’s own row,library.family.ratified <family digest>when every member was ratified, orlibrary.family.refused <family digest>(withrefused, a<name> <digest>line per refused member, andwhy) when the record refused any — a member whose predecessor another approved revision replaced — so the family is never reported ratified while incomplete; rejecting writesknowledge.declinedfor each andlibrary.family.declined. A family’s rows land as one batch: the journal is read once, every member’s outcome is decided against that read (a member’s ratification is what a later member’s supersession sees), and the rows — the members’ in the family’s order, each retirement after the ratification it follows, then the family’s own row — are written by oneappend_batchat the revision they were decided at. A batch that finds the record moved is decided again against what landed, and a row already there is not written twice; a practice ratified alone is still its own one-row write. The family’s own row is what itspractice-ratifyexecution waits for (itshatstep is done once memory’s projection holds the last idea, ratified); the per-idea rows are the shape the lanes already project, unchanged. Practices (design,operating,using) keep one Review per practice, and the record stays append-only. - Upgrade is supersession (seed/library).
upgradeproposes the library of the new toolchain version, for a record that has one, as new families (library/<node>@<new version>) whose ideas each name the active digest under their name insupersedes; approving the family ratifies the new ideas and retires the old, by the existing supersession rows. Until the Board approves it, nothing is retired: an application pinned to the old toolchain keeps the bindings of the version it was seeded at. A family whose earlier version still awaits the Board waits (upgradesays so), and a family already proposed at this version proposes nothing. A record seeded with--no-librarygets no library from an upgrade.HALE_DNA_LIBRARY_VERSIONandHALE_DNA_LIBRARY_SUFFIX(fixtures only) name another version and append to every text: how a test makes a later toolchain out of the one binary it has.graft --no-libraryseeds none of the nodes, the edge or the ideas (the practices stay).language,systemandapplicationare node kinds andwritten_in(application, language)an edge kind (The repository’s graph). - The using practices (seed/library). A third seeded family,
using/<slug>, besidedesignandoperating: how a person or an agent works with the organism —propose-review-ratify,change-classes,ask-the-leader,cut-structure,read-the-record,evidence-first,bind-knowledge. One Review each, groupedusing, seeded, decided and superseded exactly as the other two (hale dna review using approve|reject), and kept by--no-library: they are practices, not the library. - The charter (GH #596 L). The graft writes
dna/org/charter.hl, a function returning text likepurpose: the leader’s brief, saying that it is the organism’s architect — it proposes, the Board decides — and what it must know before it plans an ask or decides a Review. Project-owned; a change is a reviewed change to the organism.upgradewrites it for an organization from before it. - The leader reads its brief at both moments it thinks. The org
chart hands the
Leaderitscharterandpurpose(the program’s own text) and aMemoryKnowledge; before a review and before a plan it composes its brief —CHARTER,PURPOSE, theLAWas the genome holds it at HEAD, andPRACTICESfrom the package fororgwhen memory has any — and puts it above the diffs. The model call carriesknowledge_bindingsnaming the package, so the evidence of every decision that read it says so. HALE_DNA_DISCOVER=offmakes the graft’s discovery find nothing — no key, no local model, no harness — so a fixture on a developer’s machine makes the organization CI makes: one whose leader has no model that answers, whose plans take the defaults, and which spends nothing. The CLI fixtures set it.- An ask is admitted as a workflow, in the one engine (card 18).
Dna.ask(Intent)— intent through the Board, an optimizer’s — passes the Board’s gate, the owner’s (GH #664) and the budget’s, journalsintent.offered, and admits a workflow for it (workflow.admitted, the one positive discriminator, under the intent’s id as the admission’s identity — offered again it is the same execution) which the engine the substrate owns for its scope runs. Withplanned: trueon the substrate (the generated org chart says so) the leader’s word comes first: the substrate publishesPlanRequestedfor the ask, the leader answersTaskPlanned, and the admission binds that word in its inputs — the objective, the kind (organism,appendage,productorperson), the class applied (anorganismchange is classorganizationwhatever the plan called it: the Board’s), the target, whom and under what obligation, the narrative,planned. A person’s job is admitted under theask-persondefinition (one human leaf, a case the substrate hands to whom the leader named, with no second word asked — cards 16 and 17 complete it); anything else underask-edit(one edit leaf under thePatchcontract, performed by the substrate’s editor as class and target the word names — the target only when it names a file the genome has under the seed the class edits; a model’s prose is not a path — else classapplicationat the ask’s own target; card 15 reports its outcome). An ask for a judgment (hale dna task create --judgment, the head’sTaskCreatewithkind: judgment, or the face’s task form; the intent row carrieskind: judgment, and the receipt echoes it, GH #946, #1144) is the asker’s word: no plan is asked, and it is admitted underask-judge— one leaf requiring judgment under theAssessmentcontract, which capability-first routing hands to an agent on the first attempt (the legs’ relay: a leg claims it throughhale dna work) and to a person on the second. A change routed to a position (hale dna task create --to position:<name>; the intent row carriesposition, andtostays the ask’s owner, the organization) is the asker’s word too: no plan is asked, and it is admitted underask-position— one leaf requiringpositionunder thePatchcontract, the routed position bound into its requirement at admission (position position:<name>). Such a Work is an agent’s, the legs’; a claim takes it only when its lease’s position is that one: a position the graph states, taken as an agent’s, claims withpositionand its own id among its capabilities, which no other claimant has: the head first drops anypositionorposition:<name>word the claimant supplied, and grants them from the verified seat alone. Its hat is the routed position’s (charter, mandate, model rule), and its outcome is a leg’s Patch, applied, verified and reviewed as any. Classorganizationfor a child that is not the organism is a contradiction and is refused before anything is admitted (intent.refused). The execution is asked of the engine with the performer kind of every leaf of the whole bound tree, named for the exact Work — its bound id, never its key, which two Works of different steps or workflows may share — the editor’s for an edit leaf, the human kind for a person’s, else the routing policy’s choice for the Work’s own request under the Task that owns it. The tooling’s answer to an ask (hale dna task create) is the execution whose admission names that ask as its request, never the next birth in the record, which may be another ask’s. An answer that names no kind and no class leaves the defaults standing — classapplication, the ask’s own target — and the admission says it did not parse. On the solo path (below, Workflow execution: human cases), an organism with no model asks no plan: the ask is a person’s job, admitted underask-personand handed unassigned, for a person to take, and never reaches the editor, which is a model too (spec/habitat.md). Without a leader the ask is an application change. An ask whose leader has not answered yet is in planning, and asked again meanwhile is not asked of the leader twice. Notask.planned,task.pendingortask.resumedrow is written: the admission is the plan, and the execution’s facts are the engine’s rows. - Work survives a restart (GH #604, card 18). The record holds an
execution’s admission before anything runs; a birth the record
refuses stops there (
intent.refused,workflow.refused), and nothing has run. On restart the substrate asks its engine again for every admitted root of its own that is not finished — settled AND drained: a cancellation settles the root before its admitted Works have settled, and a root whose tree (its own workflow’s Works and every child’s, by their admissions’parent_task, and every admitted child itself, before its first attempt or after its last Work settled) still owes a settlement is asked for again, so the engine’s own recovery drains it (cards 12a, 13: the responsibilities still owed anywhere in the tree are counted, and a cancelled parent whose step holds only a child member drains the child through that step, born fenced). A child reborn under a cancelled ancestor reads that cancellation from the record at its own state question — a fence published before it existed reached nobody — proposes its own cancellation, and drains like any cancelled execution; a drained record is asked for nothing and written nothing. The engine restores each from its admission and the record’s facts — under the word bound at admission, never replanned; an attempt whose outcome is recorded is answered from it and never runs again; a case handed stays a person’s and waits. A Mutation’s recovery is derived from every row it has, from its first — its request row, or its proposal when no request names it: one whose last word is the request, the proposal, an open worktree or a located file was in flight and fails, once (mutation.failed); one with a candidate, a Review or an outcome after them is left to that. Its id is counted at the restart and never minted again. A Task with no admission — a row of the shape from before card 18 — is left where it is: nothing resumes it, plans it or settles it. Anintent.offeredwith no admission naming it is noted (intent.unrecovered) and never re-offered: work may already have run. - The local habitat holds the organism’s infrastructure (GH #1653).
A machine has one local habitat, under
${XDG_DATA_HOME:-~/.local/share}/hale/habitat/local, run as one docker compose project named for the habitat:hale-habitat-local-<h>,<h>the first eight hex digits of the sha256 of the habitat’s directory. No seed and no checkout appears in a project, container, volume or port name. An organism is a tenant of the habitat: its memory, its nerves and its senses’ store are allocations, services of the habitat’s project named for the organism’s record identity and the role —memory-<id>,nerves-<id>,senses-<id>,<id>the first twelve hex digits of the identity[dna] recordpins. Two organisms are two sets of allocations in the one project; two checkouts of one organism (two worktrees, two clones) are the same organism, with the same allocations. An allocation’s files live beside it, never in a checkout:allocations/<id>/holds itscompose.yaml(pgvector/pgvector:pg16,nats:2, the senses’ Prometheus; each service on docker’s default bridge, so the habitat makes no network per tenant), itsports.yaml(the host ports, memory’s in the 54000s, the nerves’ in the 42000s, the senses’ in the 39000s, a candidate from the service’s name stepped past any port something already listens on, chosen once for the habitat and kept),postgres.secrets,nats.conf,nats.secrets.confandsenses.yml, the directory mode 700 and every secrets file mode 600. Compose runs asdocker compose -p <habitat project> --project-directory allocations/<id> -f compose.yaml -f ports.yaml, withCOMPOSE_IGNORE_ORPHANS=1, since the other tenants are the same project’s. A checkout holds no compose file and no server configuration;upgraderemoves the ones an older toolchain wrote (dna/compose.yaml,dna/nats.conf,dna/nats.secrets.conf,dna/postgres.secrets,dna/senses.yml) and migrates no container an older toolchain started: there is no migration. - The habitat is an organism the toolchain ships (GH #1653). Its
genome is the toolchain’s, as the foundational law and the nursery are
(
hale_dna::HABITAT_FILES,dna/habitat/README.md): a repository organism whose heart slot is empty, its document’s markedlawitems its law with the nursery’sstandardpack, and its fixed positions the nursery’shabitatpack (habitat/placement,habitat/backends,habitat/identity,habitat/admission). It is the one organism born without a parent.hale habitat up [--foreground] [--profile local]births it the first time into<habitat>/genome(written from the toolchain’s files, a repository, then the birth’s seed on it, its record in its own memory allocation, and both packs proposed to its Board), then migrates its own memory and nerves (the habitat’s schema is its own to move, at bootstrap and at a toolchain’s upgrade), brings every tenant’s recorded allocation up through thecomposeprovider — migrating none: an organism’s allocation moves at its own upgrade or withhale habitat migrate <organism>, never under ongoing work — records its placement (the hostlocalthecomposeprovider produces with its offers, thelocalprofile’s policy, and its own head and face placed on that host:spec/habitat.md, Hosts, providers and placement) and starts its body,hale dna dev <genome>: a systemd user unit named for the habitat’s project, or with--foregroundin the process itself (what a fixture runs, to ready).upis idempotent, and thehostedprofile is not built. A habitat’s backends are chosen at its birth (--backends own|operator, kept beside it and as itshabitat.profilerow, which names no DSN):own, the default, provisions memory, the nerves and the senses’ store as allocations of its project;operatoradopts the Postgres and the NATSHALE_DNA_MEMORY_DSN_OWNERandHALE_DNA_NATS_URL_OWNERname, starts no service of its project, and records each allocation with the serviceoperator(what a suite run on an owner’s DSN and NATS is).uprefuses when the environment disagrees with them: anownhabitat with an operator’s server named, anoperatorhabitat with either unnamed, or a--backendsother than the habitat’s. A unit of anoperatorhabitat reads them from<habitat>/backends.env(mode 600), never from its own lines.hale habitat statusreads the habitat, its genome and body, and each allocation: whether each role’s service runs, and the schema version it records against this toolchain’s, naminghale habitat migrate <organism>(orhale dna upgrade) for one behind.hale habitat downstops the body and every service of the project and keeps their data, after the body has drained: it is told to stop and waited for, up to its drain grace (LOTUS_DRAIN_GRACE_MS, the node’s 30 s by default) and a margin, so its drain still reaches its backends; one that outlives that is said and killed, and only then are the services stopped (a unit’sTimeoutStopSecis the same window). A birth that stops part way — memory or docker failing after the identity is pinned — is resumed by the nextup;<habitat>/birth.pendingsays a birth is not whole yet (and which of its profile and packs are done), but whether its record was born is memory’s to say, never the file’s. A pinned identity whoserecord.genesismemory holds is resumed: the rest of the birth, each step of which keeps what is done, against that record — every seed step checks its own completion and runs when it is missing (the seed when the record holds noapplication.attached, the seat when its git config keys are absent, each family for what the record does not hold, the library and the models map when their rows are absent), so a graft that stopped part way is finished the same way. Only when memory answers that it holds no genesis for the pin, or the identity’s memory allocation was never made (docker answers it has no such volume), is the identity drawn again and what was made for it taken down. Memory that cannot be reached is a refusal that destroys nothing. Allocation rows wait for the genesis memory confirms before they are written into the record. - Every other organism is grafted from the habitat (GH #1653).
hale habitat graft <dir> [--attach] [--no-library] [--no-pack] [--with <pack>]births an organism as the habitat’s child: a new application at<dir>, or with--attachthe application or repository already there. An attached application’s organism is its workspace’s: the manifest the birth reads (the nearesthale.tomlabove the seed) is the one its pin and its needs are read from. A graft is complete when the habitat’s record holds itshabitat.graftedrow; one that stopped part way (a pinned genome whose genesis names this habitat, and no such row yet) goes on where it stopped when run again, each step keeping what is done (the birth’s seed, the allocations, the row, thestandardpack, each once). Memory decides whose child a pinned record is, never a guess: its genesis is read from memory (with the clone’s kept head DSN, else one derived from memory that is there, else after the local setup the birth makes), and a genesis that cannot be read is refused, naming why. A genome pinning a record born of another parent, or of none, is refused, and one memory holds no record for is refused by the birth, naming it. The habitat must be born and its record readable, and the environment must agree with its backends. Graft has two halves. Needs by role come first: the genome declares the roles it needs from the habitat in its manifest,[dna] needs = ["memory", "nerves", "senses"]; memory and the nerves are always among them (the record is memory’s, and the organization reads the nerves). A genome that declares none, foreign heart or not (a foreign heart declares nothing), gets exactly the organism’s own roles — memory, the nerves and the senses’ store — and never a heart role (database,broker,cache,object-store): a heart’s infrastructure is its own, and the habitat provides nothing for a heart that did not ask. A Hale heart that needs one declares it. The local profile provisionsdatabase: a tenant database of the organism’s own memory server (its memory allocation’s Postgres, or the operator’s onoperatorbackends),heart_<id>, owned by a role of the same name that it alone reaches (PUBLICrevoked), its password drawn into the vault slotpostgres-heart_<id>; it is recorded as the organism’sdatabaseallocation (habitat.allocated <id>/database), provisioned again byhale habitat migrate, and handed to the heart asHALE_DNA_DATABASE_DSN, a DSN that names its vault slot and carries no password (the attach contract’s name for it;hale dna devderives it, provisioning nothing). The local profile provisionsbrokertoo: two users of the organism’s own nerves server, both confined to its own subject spaceheart.<id>.>, and neither granted_INBOX, so no grant reaches another user’s replies (the heart’s replies come back under its ownheart.<id>._INBOX):heart-<id>, the heart, which publishes and subscribes on all ofheart.<id>.>, andheart-<id>-node, the narrower user its nodes connect as, which subscribes onheart.<id>.node.>and publishes onheart.<id>.reply.>andheart.<id>._INBOX.>; no JetStream; their passwords the vault slotsnats-<org>-heartandnats-<org>-heart-node, drawn with the organism’s other secrets as every drawn secret is — 32 lowercase hex digits, so a password is safe in a JSON document and a URL as it is; recorded as itsbrokerallocation.broker, and any heart role that needs server configuration, requiresownbackends: a habitat onoperatorbackends refuses it at graft, by name (the operator’s NATS is not the habitat’s to configure), so a heart that needs one runs in a habitat onownbackends — a fixture’s own compose stack, taken down by the sweep — whiledatabase, a tenant database and a role, is provisioned on either.cacheandobject-storeare refused by name at graft until a heart needs one, as is a role the habitat does not know. - A heart may hand its migrations to the habitat (GH #1653). It is
the heart’s choice. A Hale heart that owns the schema of a heart role
it declares may say how to migrate it, in its genome’s manifest:
[dna.migrations.<role>]withcommand, the argv of its own migration tool, run in the organism’s directory with that role’s handover (HALE_DNA_DATABASE_DSN, the broker’s) and the rest of the attach handover in its environment. The habitat then runs it at graft, athale habitat migrate <organism>and athale dna upgrade, every time: the tool is idempotent and keeps its own ledger, so it decides whether anything moved, and the habitat keeps no version of the heart’s schema. Each run is a habitat act: the role’s allocation is brought up and waited for first, ashale habitat migratebrings it up, so ahale dna upgradethat restarts the nerves for new passwords runs the migration once they are back. Each run is ahabitat.migrated <id>/<role>row with the command, its exit status and its output (its last 4096 bytes); a run that fails fails the act that ran it, naming its command, and the next act runs it again. A heart that declares none owns its schema however it likes, migrating on boot included; a foreign heart declares none; and the habitat migrates nothing of a heart’s on its own. A declaration for a role the genome does not need, one with nocommand, or one with any other key (schema_versionamong them) is refused by name. A heart role’s allocation carries no schema version: “behind the toolchain” is said only of the organism’s own memory and nerves, whose schemas are the toolchain’s. - The attach handover (GH #1653). What the habitat hands a
habitat-scoped heart, a secret only ever by its vault slot’s name:
HALE_DNA_APPLICATION_ID(the record’s identity),HALE_DNA_HEAD_URL(git config dna.head.url, the local head’shttp://127.0.0.1:8792by default),HALE_DNA_OIDC_ISSUER(dna.oidc.issuer, the local stub’shttp://127.0.0.1:8794by default),HALE_DNA_OIDC_CLIENT(the service client the heart signs in as, its application’s name) andHALE_DNA_OIDC_CLIENT_VAULT(oidc-service-<client>); for an allocateddatabase,HALE_DNA_DATABASE_DSN; for an allocatedbroker,HALE_DNA_BROKER_URL(the nerves server, no credential),HALE_DNA_BROKER_USER,HALE_DNA_BROKER_VAULT,HALE_DNA_BROKER_SUBJECTS, and the nodes’HALE_DNA_BROKER_NODE_USERandHALE_DNA_BROKER_NODE_VAULT, which the heart passes a node at its enrollment (a client names its reply inboxheart.<id>._INBOX: pond’sNatsClient.inbox_prefix). Graft maps the service client in the child’s record (git config --local --add dna.oidc.service <client>=<client>) and draws its secret, so none of it is written by hand.hale dna devhands the set to the host, which the heart inherits, derived (nothing provisioned);hale habitat handover [organism]prints it. The manifest is read once, by graft (a TOML parser:needson one line or several, with comments and a trailing comma); every reader after it (the handover, the organism’s secrets, its nerves server’s users) reads the roles the habitat allocated, its rows as the allocation keeps them (allocation.json), never the manifest again. Where a heart runs and on what port is the deployment’s (#1477), not the handover’s. Then birth satisfies them: the record’s genesis names its parent (parent, the habitat’s record identity, in the first row’s body, and so in the child’s identity), each role it needs is allocated to it and recorded in the habitat’s record (habitat.allocated,habitat.migrated), the graft is ahabitat.grafted <child>row (child,parent,needs,backends), and thestandardpack is proposed to the child’s Board (--no-packleaves it forhale dna pack apply standard, as--no-libraryleaves the library: a fixture that counts the record’s requests says so).--with <pack> [--set <param>=<value>]… [--as <who>]proposes a nursery pack as well, planned on the holes the birth proposed (hale dna pack apply <pack> --on-holes), so onereview all approveratifies the starting organization whole; a pack the holes refuse is the graft’s failing exit, its organism born. A new application takes the profile flags--profile local|remote-body [--remote <url>] [--body <user@host>]; an attached one keeps its own, and graft refuses them with--attach.hale dna newandhale dna initare retired: each exits 2 naming the graft that replaces it (hale habitat graft <name>, `hale habitat graft--attach`, with the flags it was given). A graft of an organism this habitat's record already holds as its child goes on where its birth stopped; one whose genesis names another parent, or none, is refused. - A test’s organisms are grafted too (GH #1653). A DNA fixture
makes an organism with
dna::grafted(dir, args), intodna::test_habitat(): the DNA suite’s slice habitat when the suite runs it — born once per slice whose fixtures graft, with no body (hale habitat up --no-body), under a scratch root the slice sweeps, on the operator’s backends when the slice has an owner’s DSN and NATS (CI’s: no compose) and its own otherwise, its secrets in a vault of its own thattest_habitatlends into the fixture’s (HALE_DNA_TEST_HABITAT,HALE_DNA_TEST_HABITAT_VAULT) — or, run by hand, a habitat of the test file’s own, born the first time. A CLI test does the same throughtests/support/habitat.rs(habitat(),graft(cwd, dir, args)), in the habitat of its process’s vault.HALE_DNA_HABITATnames a habitat’s directory outright. A fixture whose subject is the allocations themselves (their compose project, the nerves’ or the senses’ servers) births the test file’s own habitat under its own vault (hale habitat up --no-body --backends own, oroperatorwhen it only reads the allocations’ files) and grafts into that. - An allocation is a row of the habitat’s record. Every provisioning
act is:
habitat.allocated <id>/<role>the first time an organism’s role is provisioned (organism,role,service,project, theproviderthat placed it and thehostit is on —composeonlocal, or onoperatorbackendsoperatorand no host — and the vault slots it holds by name, never a value) andhabitat.migrated <id>/<role>each time an organism role’s schema version moves (from,schema_version: memory’sMEMORY_SCHEMA_VERSION, the nerves’NERVES_STREAM_VERSION), or each time a heart’s declared migration runs (command,status,output; a heart role carries no version). A row is keyed by the organism and the role alone, never a checkout: two checkouts of one organism are one tenant. An allocation made before the habitat’s record exists (the bootstrap’s own memory and nerves, or an organism migrated before the habitat is born) waits inallocations/<id>/allocation.json, the local profile’s cache of the rows, and is written at birth.statusreads the rows, and the cache only while the habitat’s memory is not up.hale dna upgrademigrates an organism’s allocation whose recorded version is behind this toolchain’s. - The habitat is long-lived, and only its acts provision. Nothing
tears the local habitat down when work ends, and organisms are reused
tenants from run to run:
hale dna devandhale dna work nextrun against whatever is up. Provisioning and migration are the habitat’s acts alone: birth (hale habitat up,hale habitat graft) brings the organism’s memory and nerves allocations up, applies memory’s schema and creates the organization’s stream;hale habitat migrate [organism]brings its allocations up and moves memory’s schema and the nerves’ stream to this toolchain’s, idempotently;upgrademoves the schema of an operator’s own memory (HALE_DNA_MEMORY_DSN_OWNER); and the explicit verbshale dna memory migrate,nerves migrateandsenses upremain until the graft retires them. No other command migrates or provisions as a side effect:dev,run,head,show,workand the host’s verbs derive the roles’ DSNs and URLs from what is up (memory-migrate --derive,nerves-migrate --derive,senses-up --derive: nothing brought up, no schema applied, no stream created, nothing written into the habitat) and, when it is not, say so and namehale habitat migrate <organism>. A schema that is behind is the store’s refusal when it opens (memory_fence), naming the same act. With an operator’sHALE_DNA_MEMORY_DSN_OWNERorHALE_DNA_NATS_URL_OWNER, that server is the organism’s, and the habitat’s allocation for it is not used. The owner’s DSN of the memory allocation names the vault’spostgres-owner-<id>(The organism’s secrets), which a database made with another password adopts first.hale dna runbrings up nothing: beyond one machine memory is a Postgres of the operator’s. Only a test’s habitat is ever taken down: a test’shaleruns under a habitat of its own (hale testgives each file one beside its vault, the CLI tests one per process), which comes down with the vault.hale test, the CLI tests’ reap andscripts/compose-down-under.shshare one teardown,hale habitat sweep <root>: a project is rooted in the root when a container of it ran from under it, or when a directory under it names it whether or not a container is left (a project whose containers went keeps its network and volumes): each<home>/.hale/habitat/<project>ansshhost was handed a render in, and each habitat born there (…/hale/habitat/local, its project named from that path), never a project one of whose containers ran from outside the root. It names every project rooted in the root in the root’s.dna-compose-projectsbefore it takes any down, and drops a name only once the project’s containers, volumes and networks are confirmed gone, so a teardown that did not finish (a volume docker would not remove) is found again by name and retried by the next — a vault whose habitat did not come down is kept for the next sweep. It, anddna::scratch_done, refuse the developer’s habitat’s project whatever a file under a root names. The test vaults of gone processes are swept byhale habitat sweep --test-vaults [<root>](hale test‘s first vault runs it, as do the CLI tests’): each vault’s habitat comes down before the vault goes, and a habitat whose vault went first — every toolchain before this one removes a gone process’s vault without its habitat, and they share<tmp>/hale-test-vaults-<uid>— is found by its compose working directory under a gone process’s vault and taken down by its project’s name. - Knowledge changes later work (K2). The substrate holds a
MemoryKnowledge(budget, 8 by default; memory opened under the spine’s role), the owner’s line to memory; the editor never holds one, which the law states (group knowledge = { dna::Knowledge, dna::MemoryKnowledge },editors_never_learn). When a Mutation opens, the substrate asks memory for the package of the change’s target in the tower —orgfor an organization change,org/<child>for the application,org/<child>/<seed>for a seed inside it — and journalsknowledge.consulted <mutation>(target,hat,digest,revision,included_n,included,error) whenever memory is named (HALE_DNA_MEMORY_DSN_SPINE), answer or not. The package becomes the hat the attempt wears (GH #946): built by the owner, kept in memory by its digest, and named on the request as itscontext_digest; the editor is briefed with the package’s ideas (SourceEditor.brief: what a model is shown is the ask, then aPRACTICES (ratified knowledge for <target>, package <digest>):block, one idea per line) while the record, the commit message and the Mutation keep the ask itself; and the request names the package (knowledge_bindings: package:<digest> <id>…), which every model call of the attempt carries into itsmodel.calledrow. No memory named is an empty package that says so, and nothing waits on it; memory that cannot answer, or whose projection has not reached the record’s head within 20 s, is a package refused with the reason (The spine projects; readers read). - Concerns (K2).
ConcernRaised(source,what,severity) is a child’s signal about the part above it: an application’s ownconcern.raisedevent, which the heart lands and the spine puts into the record (K4), orhale dna concern raise <source> <what…> [--severity N]’s, each aconcern.requestedrow a node relays onto the nerves, and the substrate admits it as an execution ofconcern-escalate(GH #995: The workflow catalog) whose step journalsconcern.raised <source>(an object:what,severity,occurrence,request,parent,task), and when one source has raised itconcern_thresholdtimes (3) it becomes a knowledge proposal by that source bound to its parent path — a concern by the tower rule — throughpropose_knowledge, withconcern.proposed <source>naming the digest, once. A source with no parent (no/) is refused withknowledge.refused concern:<source>. Which occurrence a concern is counted from the record, never from a number the organism keeps, so a restart continues the count where the record left it; a count that starts again is a record read short, not a restart (GH #748). An execution a restart interrupts is resumed with every other; a source over the threshold with noconcern.proposedof its own and no concern-escalate running for it — rows that reached the record another way — is owed a proposal that nothing but a further concern would make. At birth every such source is escalated once, by an execution whoseraisestep finds its raises in the record (rehydrate); a source with no parent was refused where it was raised and is not refused again. - The repository’s graph (GH #1085). An organization and its
product are one recursive hypergraph; the org chart and the deployed
process model are two perspectives over it, neither its trunk. The
record holds it as rows and memory projects it with everything else:
one record, one projection, no second store. The vocabulary is
dna/operations/graph.hl. Node kinds:purpose,axiom,process,seed,contract,noun,deployment,practice,gate,document,witness,position,work,organization(a firm, GH #1123),definition(a workflow catalog definition a schedule convenes, GH #1143), and what knowledge is about (seed/pull):language(language:hale),system(system:dna) andapplication(application:<name>), which the graft seeds and an ingest of a repository never reads; a node’s id is<kind>:<name>. Hyperedge kinds, arity two or more, each a list of members{role, node}whose first is its anchor:unfold(parent, child),meets(contract; server…, consumer…, carrier…),names(contract; noun…),refers(from, to),constrains(axiom; shaped…),runs(deployment; process…),gates(gate; guarded…),binds,witnesses(witness; about…),holds(position, holder),reviews(position, subject)— the subject a contract, a document, a seed or a deployment: what the position signs a change to, a design document being signed too — andconvenes(position, definition)(GH #1143), a position convening a definition on a cadence, written when a schedule is declared (Schedules), its body carrying thecadence(every 1d,every 90s,cron <expr>) and theschedule; itspositionis apositionnode and itsdefinitionadefinitionnode, andwritten_in(application, language)(seed/pull), the codebase an application is written in — anapplicationnode and alanguagenode, keyed by both, the edge a hat reads its language targets from. Aholdsedge is a seat or a membership (GH #1123):holds(position:<p>, <person>)is a person’s seat,holds(position:<p>, organization:<o>)is organizationoowning positionp, andholds(organization:<o>, <person>)is the person’s membership ofo; itspositionrole takes apositionor anorganizationnode, itsholderis a person or, for a position, anorganizationnode, and an organization never holds another organization (Owners). A position has one owning organization and a person one: memory refuses a second of either as ahold.refusedrow, andhale dna fillrefuses to propose it. A pair kind (unfold,refers,holds,reviews,convenes,written_in) is exactly its two members and is keyed by both, so an unfold is one edge per child; any other kind is keyed by its anchor, so there is onemeetsper contract and the latest row says who meets there. An edge’s id is<kind>:<anchor>or<kind>:<anchor>|<second>, and no name or holder holds a|, so an id reads one way. A role may require a node kind (ameetscontract is acontract:node, arunsprocess aprocess:); aholderis a person or anorganizationnode;meetsalso carries its transport (via) and the parties it reaches that are not nodes (outside, e.g. callers). Two kinds are never graph rows: apracticeis a knowledge idea of kindpractice— advice unless the Board ratifies it as law (its document sayslaw: true; one ratified as advice stays advice) — andbindsis its knowledge binding; a member names a practice aspractice:<digest>. The rows aregraph.node(entity the node’s id, body{kind, name, text?, source?}),graph.edge(entity the edge’s id, body{kind, members, via?, outside?}) andgraph.retired(entity the id). The tail checks a graph row whole before memory takes it — the kind is the vocabulary’s, the anchor comes first, every member names a node of the kind its role requires and no node twice, the arity is two or more, and the entity is the id the body implies. A row that fails is refused as a row, as a refused hold is: memory appendsgraph.refused <row id>(kind,why,row,by: memory) once, says it on stderr, and the projection goes on past it, so one bad row never holds back every read waiting on the watermark. A graph row inside a ratified proposal is checked and refused the same way, at the row that ratified it, and the proposal’s other rows land. A store that fails to take a valid row still stops the projection at that row: that is the database’s failure, retried, not the row’s. So does a refusal the record cannot take (an I/O failure; a race with another writer is retried first): the row is never passed without itsgraph.refusedorhold.refused, and the next pass writes it. A node or edge is replaced by a later row with its id (an edge’s members with it) and taken out bygraph.retired; a retired node’s edges stay as the record left them, and the perspectives leave its memberships out (an edge whose anchor is gone is not shown). There is one org chart, and it is the graph: itspositionandorganizationnodes and theirholdsedges, proposed and ratified as holes and holds (below). Who owns a position and who is a firm’s member areholdsedges, never a file: the owners map (dna/org/owners) is retired, not rendered (GH #1123) — a graft writes none, andhale dna upgraderemoves one, noting the lines it named to be stated again asholdsedges, and takes the generatedownership:field out ofdna/org/main.hl(or notes it where the file was edited by hand). The organization’s generateddna/orgfiles and the organism’s program are written beside the graph, never its source, and nothing reads a position from them that the graph states. Perspectives are queries over memory:KnowledgeStore.graph_perspective("org")is the positions in the record’s order, each with the node it unfolds from (under), itsholdersand the contracts and documents itreviews;graph_perspective("processes")is the processes, everymeets(contract,via,servers,consumers,carriers,outside), everyruns(deployment,processes) and everyconvenes(position,definition,cadence,schedule), each as JSON the server builds.graph_nodes_count(kind)andgraph_edges_count(kind)count them;practicecounts the practices proposed or ratified andbindsthe ratified practices that bind something, one hyperedge each.Pq.graph_ownership()is JSON[node, holder]pairs of every liveholdsedge an organization is in (Owners). - Ingest:
hale habitat graft <dir> --attachon a repository (GH #1090). A directory with no Hale source of its own that is no workspace’s seed is a repository, not one application: the graft makes the organization there as it would for an application —vendor/dna,dna/org, the manifest with[claims] no_baseand[environments.org]alone (there is no application entrypoint) — cuts no topology artifact, and seeds the record with the repository’s application attached, then what the repository holds, as the graph, through the host verbgraph-ingest, then proposes the declared purpose (GH #995) (dna/operations/graph_ingest.hl). It never derives one perspective from the other. A directory holding.hlfiles is a seed: with a main locus it is an application, attached as before and read into the graph too (the next bullet), and without one it is refused as before. Ingest reads the tracked files (git ls-files, paths as git holds them withcore.quotePathoff) and recognizes: the purpose — theREADME.md’s title and first paragraph, and none without aREADME.md, so an empty tree is an empty graph; processes — the compose file’s services (compose.yaml,compose.yml,docker-compose.*), then thecomposedeployment that runs them (built) — processes first, since a perspective lists in the record’s order; seeds — each directory holdinghale.toml,package.json,Cargo.toml,go.modorpyproject.toml, the outermost only (a workspace’s members are its own), never the root; a process unfolds into the seed of its name. Aui/at the root holding no manifest is no seed (ruling 11b, 2026-10-12): when the repository references it — a link in a tracked markdown document outside it resolves into it, or the compose file names./ui(a volume or a build) — the standard face serves it as the application’s static assets under/app/(/app/itsindex.html,/app/<path>the fileui/<path>, nothing outside it, for the attached project, which the face asks the head’s read for:ops::application_asset; the head serves none; a file’s bytes are written as they are, with their length and the extension’s content type, so an image or a font arrives whole), and the graft says so on anoteline; when nothing references it the graft says once that it is ignored. Neither writes a row. contracts —spec/*.yaml,spec/*.yml,spec/*.mdand each directory underspec/vendor/; nouns — a YAML contract’scomponents.schemasand a markdown contract’sCREATE TABLEs, named by the contract; a noun is its name, so two contracts that name one merge into one node, which bothnames; thenrefers, from a contract’s local$ref: '#/components/schemas/<X>'(never another key that holds such a path, never a reference that leaves the contract) orREFERENCES <table>, only to a noun the same contract names — every noun of a contract is written before itsrefers; documents — every.md; gates — each CI job (.github/workflows/*), named<workflow>/<job name>, guarding the seeds, contracts and compose deployment its steps name; witnesses — eachFRICTION.mdentry (a##heading) and each issue it links (<owner>/<repo>#<n>), about the seeds, contracts and documents it names. What structure cannot say is written in markdown, in two forms. A marked list item: a code span opening a list item names its kind —axiom,derived,practiceorlaw— and its first bold span is its name; anaxiomis a node under the purpose, and the repository files it links to are what itconstrains. (derived,practiceandlaware practices, proposed at record birth, GH #1091.) A declaring table: a table whose columns includeServed byandConsumed bydeclares onemeetsper row — the row’s first link to a contract is its contract,Overits transport — and one whose first column isDeploymentand hasRunsdeclares a named deployment and what it runs. In a cell a name in code is a process, else a seed, else a repository path; a link is the node its target is (relative to the document); plain words are a party outside the repository; a code span inside a link is the link’s text, not a name; anOverthat names a process makes it the carrier. Links resolve to a contract, else a document, else the compose deployment, else the seed holding the path. Every row comes out once, a node before an edge that names it, and the host checks everygraph.*row against the vocabulary before it appends any (record-seeddoes the same for a seed file), so an ingest the graph refuses leaves the record as it was. A YAML mapping’s children are indented as its first child line is, never by an assumed two. Input the conventions cannot take is refused, with the reason, never dropped: anaxiomitem with no bold name; any name the vocabulary refuses (a|, a control byte, longer than 512 bytes) — a witness’s title, a schema’s, an axiom’s; a meets row that links no contract; a runs row that names no deployment; a name in code in a declaring table that is no process, seed or file of the repository. Nothing is appended then, the purpose’s Review included (the graft seeds it and the graph in onegraph-ingestcall), so the record does not exist and the graft can be run again once the document is fixed. A job’s ownname:names its gate and is not a word its steps say. The graft says what it read:graph <n> node(s), <m> edge(s): <count> <kind>, …; <p> proposal(s). A record that exists is not reseeded. - Ingest:
hale habitat graft <dir> --attachon an application (seed/structure-2). An grafting an application enriches this one path; there is no second proposer and no starter chart. The artifact’s rows are unchanged (application.attached,structure.observedper locus, topic, binding and effect class,responsibility.proposedper locus: facts the Board reads, from which nothing is derived into the graph), and they land in the samegraph-ingestcall as the purpose’s proposal and the graph —graph-ingest --rows <file> --app <name> --description <file>— so a graph the vocabulary refuses leaves no record. The ingest then runs over the application’s workspace root as for a repository (README purpose, compose,spec/*contracts and their description roles, documents, CI gates, FRICTION witnesses, marked lists and declaring tables), with two additions the application’s own structure gives. The application is the root seed (seed:<name>, the project directory’s name, or the application’s directory when it sits below the workspace root, where the manifest walk finds it as before): the root that is never a seed for a repository is one here, a compose process of its name unfolds into it, a CI step naming.guards it, and an application with noREADME.mdtakes the purpose the graft declares (dna/org/purpose.hl) as itspurposenode, so the board has a place. And each API surface it serves is a contract the seedmeetsas server:api:<Surface>, from the toolchain’s own export (hale check --api, handed to the host as a file), one node per surface; a stream served over a hub isstream:<Topic>. A surface whose lower-cased name is the stem of aspec/<stem>.mdor.yamlcontract (the stem rule that readsspec/<Stem>.description.json) is that contract’s node, not a second one; a contract a declaring table already gives ameetskeeps the table’s row. Therequiresof the surface’s members, and of a stream, are the contract’s roles: the equipment of the positions that serve it (Standard equipment, below), never an organization and never authority. The graft says what it read on the samegraph …line, and writesdna/org/structure.hlas for a repository. A fresh application whose export is empty has two nodes (itspurposeand its seed) and no edge. - Holes as proposals (GH #1091). What a repository cannot imply
and a delivery needs, the graft proposes at record birth, each on its own
with one Board Review, in the same checked seed as the graph (so a
repository whose holes cannot be proposed leaves no record either).
The holes are derived from the graph’s edges, never enumerated; a
part is a process, or a seed no process unfolds into (a UI, a
validator); a cell naming a seed a process unfolds into (a link, a path
inside it) names that process. The graft proposes: a
boardunder the purpose; under each part that serves or consumes a contract (meets) — and under an application’s own seed whether or not it serves one — areviewerwhose proposal carries areviewsedge to the part’s seed and to every contract it serves or consumes; under each part whose seed a gate guards, or that serves a contract a gate validates (gates) — and an application’s own seed always — adev, and aworkitem where a gate names it; adevand aworkitem for each validated contract no part of the repository serves; under each deployment anoperatorwith areviewsedge to the deployment; and each item a document marksderived,practiceorlaw, proposed as advice (alawitem’s question asks for law). The one structure policy is the project’s:dna/org/structure.hl, which the graft writes for a repository or an application unless one is there already, and whosefn operational_roles() -> Stringnames, space-separated, the operational roles each deployment also gets (support,accounts,billing,on-call), proposed empty and signing nothing —"", none, by default. It is read once, when the graft seeds the record, so a project opts in by writing the file before grafting; a word that is no operational role refuses the seed, and a role named twice is proposed once.upgraderewrites an existing file to the current shape, keeps the roles it names, and notes every other line the rewrite dropped. A work item is anunfoldof its part (or its contract), named for the part — or for the contract’s file when the gate guards a contract (store.md), its whole path where a part already has that name; two parts delivering one contract give one work item, the first’s — its node’sdone_whenthe first gate guarding it: the question proposes “done when<gate>passes”, and the Board amends that wording at ratification. A hole is a knowledge proposal: its receipt (kind: graph,rows) carries the graph rows it would add, each checked by the vocabulary before it is proposed;knowledge.proposedandreview.requested(required_authority: board,group: holesorpractices) are the rows a practice is proposed with. The existing review path ratifies it (knowledge.ratified); the projection, seeing a ratified document of kindgraph, puts its rows into the graph at the row that proposed it (itsknowledge.proposed), checking each again, so the org chart lists positions in the order the holes were proposed whatever order the Board ratifies them in; the proposal stays in memory as the idea it was, bound to nothing, so it never enters a context package. Ratifying them fills the org chart.hale dna reviewlists the groupsholes,practices,holds,positions,changesandingesttogether, as it doesdesignandoperating, andhale dna review <group> approve|rejectdecides each pending one in turn. A seeded group is listed, and decided by its name, in the order the record requested its Reviews (a position, then its mandate), never by digest. Opening a position at run time (hale dna position open <name> --mandate "<text>" [--text <what it is>] [--under <part>] [--as <who>]). The CLI refuses a position the record already states or proposes, resolves a bare--underto the seed, work part, position or process the graph states by that name, and appendsposition.requestedin the asker’s name, relayed asPositionOpenRequesteduntil it is answered. The organization refuses a name with a space or|, no mandate, no author, a name the graph states, or anunderthe graph does not state (position.refused); otherwise it proposes the position’s node (the body the graft writes for a hole, and anunfoldedge from the part with--under) and its mandate as two members of one family Review in the grouppositions(position.proposed), which ratifying ratifies both. A batch of changes (hale dna apply <file> [--dry-run] [--as <who>],dna/core/batch.hl). Several changes to the structure as ONE Board Review, applied all or nothing. The file is one step a line, in the language the scenario runner reads:open <name> [under <node>] | <mandate>,fill <name> <holder>,fill * <holder>,vacate <name> <holder>,practice <name> [for <position>] | <text>,law <name> [for <position>] | <text>(a practice its document proposes as law,law: true, which the Board ratifies as law or as advice; GH #1591),idea <name> [for <position>] | <text>(knowledge, such as an external source),retire-practice <name>,nursery <source>,assign|unassign <name> reviews <kind>:<node>,close <name>,merge <name> into <name>,split <name> into <name> <name>,expose <kind>:<node> as contract:<name>,depend <kind>:<node> on contract:<name>,externalize seed:<name>andinternalize contract:<name> as seed:<name>;#starts a comment, and a name is a position’s, with or withoutposition:. The host plans the batch against the record (batch_plan): a line that is no step, a name with a space,|or tab, anopenwith no mandate, a position the graph states or the batch opens twice, anunderthat is neither a node the graph states nor a position the batch opens, afillof a position the graph does not state and the batch does not open, afillof a stated position with no ratified mandate, a hold the organization would then hold twice, or avacateof a hold the record does not state, asked twice or filled in the same batch, a practice or an idea named twice, onefora position the graph does not state and the batch does not open, or one already in force with another text or target is refused whole, naming the step, and nothing is written. A practice or an ideafora position is bound to it (its document’s and its member row’s target), and toorgwithout one. One in force with the same text and target is kept: the step leaves the batch, so a pack adopted again proposes only what it adds, and a batch left with no step is refused,nothing to propose: every step is in force already. Opens are taken first, so a step may name a position a later line opens; every other step is taken in the order written, against the organization the steps before it leave.fill * <holder>is taken last: every position the organization would then state with a ratified mandate (or one the batch opens), no holder and novacatein the batch is filled by the holder, in the chart’s order, and the step is written into the batch’s normalized steps as the fills it made, so the Review and the plan at ratification name each seat; a wildcard that finds no vacant seat is refused. Planned on pending proposals (--on-holes,batch_plan_on).hale dna apply <file> --on-holesandhale dna pack apply … --on-holes(whatgraft --withpasses) plan the batch on the holeshale habitat graftproposed that are not decided yet (batch_init_holes: eachreview.requestedin the groupholesauthored byhale habitat graftwhose digest has noknowledge.ratified,knowledge.declinedorknowledge.refused): the record is read with aknowledge.ratifiedrow for each appended (PlannedOn, a read-only journal), so a hole’s position, its edges and its mandate are stated for the plan. The pending proposals the plan stands on — a graph proposal one of whose row ids the batch’s rows or steps name, and a mandate whose target they name — are itsplanned_on, one<review id> <digest>a line, carried in the family document and inbatch.proposed; the question adds; planned on <n> pending Review(s), it is ratified after them, and the printed chart a line naming them. A pending proposal the batch does not touch (a work hole) is no ground of it. With noplanned_onthe family’s documents are byte for byte what they were. Ratifying a batch withplanned_on(resume_family,batch_ground) first reads its ground: while one of them is not decided, the ratification waits (the verdict settles; its execution is performed again on the next verdict or tick), so approving the batch before its holes is not a refusal; when one was declined or refused, the batch is refused whole as stale (the batch is stale: the proposal it was planned on, k:…, was not ratified); when all are ratified, it is planned again against the record and itseffectscompared, as every batch is. A pack that opens a position the graft already proposes is refused when it is proposed (`position:…` is a position the graph already states), not at ratification. The operations, as graph rows.assignadds areviewsedge (reviews:<position>|<node>; the node a contract, document, seed or deployment the graph states) andunassignretires it.closeretires a position: its holds, what it reviews, theunfoldedge it hangs from, then its node; neverposition:board, and never one the batch opens.merge a into bfillsbwith every holder ofait lacks and assigns it whatareviews, then closesa.split a into b copensbandc, each witha’s ratified mandate to start from (the Board amends either later; refused whenahas none), fills both witha‘s holders, assigns both whatareviews, and closesa.expose n as cstatescontract:cif the graph does not and writes itsmeetsedge withnits server (refused when the contract is met already);depend n on crestates that edge withna consumer.externalize seed:s: every contract the part serves is restated without it, the part’s name added to the edge’soutsideparties (refused when nothing in the organism consumes the contract, or when the part serves none), every position unfolding from it closes, and its node retires.internalize c as seed:sstates the part again and restates the edge with it the server and its name out ofoutside. A batch’s rows are each id once, at the last write its steps make (anassignafter anunassignof the same edge stands; a retirement of an id only the batch added is dropped), filed as its nodes, then its edges (each restatedmeetsonce, at its last body), then its holds, then what it retires. The family carrieseffects, a digest of those rows and the mandates, retirements and nurseries it proposes; ratifying plans the batch again and refuses it as stale when the steps would now do anything else (a contract that gained a consumer meanwhile keeps it). Knowledge bound to a closed position stays bound to its node; rebinding it is not built. The org chart a batch is planned against also listsreviews <position> <node>,part seed:<name>,serves|consumes|carries <node> <contract>andoutside <contract> <party>. Planned, the batch prints the org chart before and after (position <id>,mandate <id>,holds <id> <holder>lines,+where the batch adds one,-where it takes one out), and--dry-runstops there. Otherwise the host keeps the receipts and appends, in the asker’s name: one graph proposal (kind: graph, namebatch/<request id>, targetorg) whose rows are the opened positions’ nodes, then theirunfoldedges, then the holds, then agraph.retiredfor each hold vacated; one mandate per opened position (mandate/<name>); one practice perpracticestep (kindpractice, its name, targetorg, the proposer its author); each a member (knowledge.proposed, no Review of its own); the family (kind: family, itsideasthe members in that order, andbatch, the normalized steps), with itsreview.requestedin the groupchangesunder the Board;batch.proposed <request id>(digest,review_id,by,steps); andgraph.requested <request id>, which a running organization answers by opening and routing the Review (graph.proposed), as it does a re-ingest. The same steps while a batch of them is pending are answered with that batch’s Review. The Board’s one verdict decides every member in one append (resume_family); an approval first plans the batch again against the record as it then stands, and a batch whose ground moved (a position it opens was opened since, one it fills was retired) is refused whole: every memberknowledge.refusedwithwhythe batch is stale: …, the familylibrary.family.refused, nothing of it projected. Memory still admits each ratified hold as it admits any (hold.refusedfor a holder it does not know), so the batch checks structure, and the store stays the gate on who may hold what.hale dna vacate <position> <holder> [--dry-run] [--as <who>]is a one-step batch: the holder leaves, the position stays. Every reader of holds takes a retirement carried by a ratified proposal as it takes agraph.retiredrow: the graph, memory’s projection, the head’s seats (HoldsReader) and the CLI’s and the head’s structure commands’ (held_state_of,dna/operations/structure_command.hl). The org chart a batch is planned against also listspractice <name>for each named practice in force. Packs and the nursery (hale dna pack list | show <name> | apply <name> [--set <param>=<value>]… [--from <dir|git-url[#rev]>] [--dry-run] [--as <who>] [project]). A pack is a batch with{param}placeholders; a nursery is a directory or a git repository whosenursery.tomllists its packs ([[pack]]:name,version,path,summary,paramswith each one’s default,""for none). With no--fromthe nursery is the toolchain’s (dna/nursery, embedded:dev-team,small-startup,content-team,founder,fill * {founder}, one person in every vacant seat, whatgraft --with founder --set founder=<who>proposes, andstandard, the standard practices every organism is to adopt at birth, each alawstep that extends the foundational law once ratified: its first clause,standard/logs-are-structured, is that every line a running part logs is a record, held by the Board in review until the compiler classes the stdio streams, the law can name aprogramgroup andstd::logis the logger (GH #1591); beside the clauses it holds a template, two?lines kept only whenintegration,position,source,kindandeveryare all set, that propose one watched upstream: the practicestandard/upstream-watched/<integration>and the ideastandard/upstream-source/<integration>, bothforthe position (Upstream watches)); a git repository is cloned into$XDG_CACHE_HOME/hale/nursery/and read at the revision after#, its default branch’s head without one. The CLI renders the pack: every parameter replaced by its--setvalue or its default, a line beginning?kept only when every parameter it names has a value, and a parameter with neither refused by name.applyproposes the steps as a batch whose family and graph proposal are namedpack/<name>@<version>, its questionratify adopting the pack <name>@<version> …, andbatch.proposedcarriespack: the name and version,sha256:of the pack’s text, and its source (toolchain <version>, the directory, or<url>@<commit>).hale dna pack retire <name> [--dry-run] [--as <who>]undoes the latest adoption of the pack the Board ratified, as one batch (pack/<name>/retired): every position it opened that is still stated closes (its holds and reviews with it), every hold it filled and assignment it made on a position that stays is taken back, and every practice it proposed that is in force retires (retire-practice <name>, a member of kindretirementsuperseding the practice, which ratifying retires); whatever has gone since is passed over. A pack supersedes nothing, so there is nothing to restore. A git nursery is trusted by the Board before its packs are adopted:hale dna nursery add <source>proposes the batch stepnursery <source>(a knowledge idea of kindnursery, named by the source, targetorg), and once it is ratified the org chart listsnursery <source>;hale dna nursery listprints the ones in force.pack apply --from <git URL>is refused, before anything is fetched, while the URL is not one of them, unless the habitat’s Board trusts it (hale habitat nursery add, below); the toolchain’s nursery and a directory need no trust, and reading a nursery (list,show) needs none either. Saplings (phase 4) are a second kind of nursery entry beside packs, and packs stay what they are:[[sapling]]withname(one word, no/),summary(its purpose),repo(a git URL or path),revision,digest(sha256:overgit ls-tree -r --full-tree <revision>, the tree’s listing, so one tree is one digest at any commit),packs(the packs it adopts at graft, by name, from its own nursery or else the toolchain’s) andset(their parameters). A sapling is grafted by name into the habitat (spec/habitat.md, Nurseries and saplings); the toolchain’s nursery offershale, and a repository may be its own nursery (repo = "."). Every mutation is a head command; the UI is a client. A change to the structure asked of the head is not a second code path: the head’s surfaceHeadCommandscarriesCommands::open_position(PositionOpening { request_id, record_head, position, text, mandate, under }),Commands::fill(HoldFill { request_id, record_head, position, holder }) andCommands::apply_batch(BatchApplication { request_id, record_head, steps, pack }), eachrequires: [position], and the readCommands::preview_batch(BatchDraft { steps }, ungated). The commands are operations of the composed head’s governance service (dna.position.open@1,dna.hold.fill@1,dna.batch.apply@1,dna/operations/structure_command.hl), admitted and recovered by request id as every command is, and their handlers write the rows the CLI verbs write, in the caller’s person’s name:position.requestedandhold.requestedwith the CLI’s keys in the CLI’s order, and a batch’s receipts kept and its rows (members,review.requested,batch.proposed,graph.requested) landed together by one ref move,batch_filing’s bytes; each request row’s id is the one the CLI mints for the same ask by the same person at the same head (p,horband twelve hex digits of the digest of the head and the ask), and the command’s fields (command_format: dna.structure-command/1,command_id,command_payload,command_fingerprint,command_authority,command_authority_basis,command_record_head) follow the CLI’s keys in the request row (batch.proposedfor a batch), so a relay and the organization read it as the CLI’s. Before a row the head checks what the CLI checks, over the same state: a position the record states or proposes is not opened again, a fill takes the CLI’sfillrefusals word for word, and a batch is planned whole; a refusal is a receipt (state: refused,structure.why) and no row, and the same steps still pending are answeredalready_proposedwith the Review they await. A command is conditioned onrecord_head(stale_subjectonce the Record moved). The receipt’sstructurebranch (state,request_id,review_id,event_id,why,warning) readsrequesteduntil the organization answers the request row, thenproposedwith its Review orrefusedwith why; the Board decides throughCommands::verdict.vacateis a one-step batch throughapply_batch.Commands::preview_batchplans a batch against the Record’s head and writes nothing:BatchPreview { ok, code, why, application_id, head, revision, steps, before, after, added, removed, unchanged, pending }, the org chart’s lines before and after and those the batch adds, takes out and keeps, one a line, andpendingthe Review the same steps await. The habitat UI is a client of the generated surface (hale api client --surface HeadCommands), and wires its affordances to these members. Mandates (seed/structure). Each position the graft proposes carries a mandate, proposed in the same seed right after its hole: a knowledge idea of kindmandate(familymandate,author: hale habitat graft, namemandate/<position name>,targetthe position’s node id,position:<name>), whose text states what the position decides, what it may not, what it cites, how it writes and what it escalates.reviewlists it underholesdirectly after its position (question:ratify the mandate of <p>: what it decides, what it may not, what it cites, how it writes, what it escalates), sohale dna review holes approveratifies the position and then its mandate; the ordinary ratification binds the idea to the position’s node (a mandate is not agraphdocument, so it is bound, unlike a hole). A mandate ratified before its position lands is a binding whose node does not exist yet: nothing refuses it, and the hat finds it once the position does (hat_knowledge_targetsnames the performer’sposition:<name>, so a mandate bound to it arrives in the hat’s package and brief). The texts are the toolchain’s (dna/operations/graph_mandates.hl), one per position kind the proposer knows (board; a part’sdevandreviewer; a deployment’soperator; each operational role), the part’s or deployment’s name substituted. The authority defaults are text in the mandate: which change classes and magnitudes the position may ratify alone, which need a second signer, which are the Board’s, in the law’s names (OrgPolicy: the classesorganization,constitutional,process-policyandtopology, and any change that touches law, widens effects or crosses ownership, are the Board’s; the rest, inside the grant, the Leader’s), cited and not re-implemented; the charter refines them. The organism’s own positions the hat already names (position:leader,position:editor,position:agent) get theirs at the graft andupgradeunderdesignbeside the design practices (design/mandate-<name>, a seeded practice with atarget, proposed as amandatebound to it and superseded onupgradethe same way). Standard equipment. A proposed position’s node body carriesequipment, a JSON object withroles(the API roles the position is fitted with on fill) and nothing else for now. It is derived from the graph the ingest read and the toolchain’s own export: for a contract whose stem matches aspec/<Stem>.description.json(whathale api describe/hale check --apiwrites) case-insensitively, the ingest keeps therequiresroles of its members (GraphIngest.roles,<contract>\t<role>lines; the description is the contract’s roles, not a node of its own; an application’s own surfaces and streams are contracts whose roles are read from the same export, § Ingest on an application). A role named like a position’s last segment (reviewer,dev,operator, an operational role) is that position’s equipment for the part that serves the contract (foroperatorand the operational roles, the deployment that runs the part); every other role (editor,reader, …) is the equipment of the part’sdev. A repository with no description proposes the same holes with empty equipment. The ruling that governs this: an API surface’srequiresinforms contract relationships and equipment; it never determines the organization and never grants authority. Thereviewsedge over the surface’s contract is what it was; no position exists because a role does. The seeded practicedesign/standard-equipmentstates the list: API roles now; the broker account and the vault names of the application a repository attaches are not proposed as equipment yet.hale dna fill <position>|organization:<name> <holder>|organization:<name> [project] [--as <who>], once it has proposed the hold, printsequipment: <roles>when the position’s node names API roles (read from the node the record states or proposes); granting them, a token that carries those roles, is deferred, andfillgrants nothing. It asks the running organization for a holder — a person’s seat, a firm owning a position (fill org/collections organization:north) or a person’s membership of a firm (fill organization:north bob), each aholdsedge (GH #1123) — ashale dna practice proposeasks for a practice (the graft alone writes proposals directly, since no organism exists yet): ahold.requested <id> {request_id, position, holder, by}row in the asker’s name, relayed asHoldRequesteduntil the organization answershold.proposed(digest,review_id) orhold.refused(why). The organization proposes it like any hole (group: holds), in the asker’s name. Whoever holds the Board may ratify it, the asker included: a governance proposal requiring the Board’s authority (the organization marks its knowledge Reviewsgovernance) is the one exception to a Review’s independence from its author (a solo organization seats itself), which every change to the work keeps, a mutation requiring the Board included. The CLI checks first that the position or organization is one the record states or proposes and has not retired (graph.retired); a firm holding a position needs no person checks — only that the firm is an organization the record states or proposes, and that it holds a position, never another organization; a person must not be a name the organism writes as, must not have retired (person.retired), and, for a seat, must be a person the record knows — who wrote a row in their own name, or an organization’s member — under the same-holder rule. A membership’s holder need not have written a row before: joining an organization is how a person becomes known to a shared record. The store is the gate: a hold is checked again where memory projects it, however it reached the record — a seat whose holder the record never knew before the row (no row in their name, and no organization’s member) is not projected, nor is a retired holder’s, nor a membership of a second organization (a person belongs to at most one), and a part’sreviewerheld by itsdev’s holder is a warning on stderr underdna.trust = local(one person holds every role) and not projected under any other trust. A hold the store refuses is a row, once:hold.refused <hold id>(why,row,by: memory). A person retired after their hold was projected gives every seat and membership back where the retirement is projected.fillandpractice proposename a request by its digest at the record’s head (h…,p…), so two requests in one millisecond never share an id. - A repository record builds its organization alone (GH #1091). When the
record’s seed holds no Hale source of its own,
hale dna devandhale dna runcheck and build the organization only: there is no application to cut, build or restart on a Mutation’s request (the body says so). Its expression is the repository’s compose services (below). - A repository’s application is its compose services (ruling 11,
2026-10-12). The compose file is a repository’s expression, and the
record attaches its application all the same: the graft lands
application.attached(entity.) with the project’s name asname,repository: trueand nomain,artifactor shape (nothing was cut), before the purpose and the graph. Its broker account is drawn as any attached application’s (The application’s account):app-<name>in the vault and the nerves allocation’snats.conf, andhale dna nerves migrateprintsHALE_DNA_NATS_URL_APP,HALE_DNA_NATS_USER_APPandHALE_DNA_NATS_VAULT_APPfor it, so what the repository’s processes publish under<org>.app.<name>.reaches the heart.hale dna statussaysexpression: the repository's compose services, attached as <name>. Such a row is not an application’s artifact: a repository is re-ingested (hale dna ingest) with it. - The body runs a repository’s compose services (ruling 11). Under
devandrun, a repository body whose root holds a compose file (compose.yaml,compose.yml,docker-compose.yamlordocker-compose.yml, compose’s own order) brings its services up once the organization reads its nerves, as the organism’s own compose project,<record schema>-<application>(dna_<id>-todo): two organisms on one machine, or a person’s owndocker compose upof the same file, never share a container.up -d --wait(300 s, scaled byHALE_DNA_WAIT_SCALE) waits for every service to be running, and healthy where it declares a healthcheck; the body saysthe repository's compose services are up (compose project <p>), or that they did not all come up and why, and the organization stays up either way. Every 10 s the body’s tick looks at them again: a service with no container, or whose container stopped other than by finishing (exit 0), is brought up again (compose: <service> <state> <code>; brought up again); one compose is restarting under its ownrestart:policy is left to it. Every exit of the body once its lease is held takes them down (the repository's compose services are down). Beforeupthe body labels every service with its mark (hale.dna.body=<holder>#<token>#<host pid>#, an override.hale/dna/compose.body.<token>_<host pid>.yamlbeside the compose file) and records who owns them in.hale/dna/compose.owner.<token>_<host pid>(the host’s pid, the mark, the project, the compose file, the project directory and the override, a line each); without the record nothing is brought up. Both files are the body’s own: a body creates, reads and removes only its own, so a replacement host on the same clone never loses its files to the cleanup of the body before it. Nothing ever takes the project down whole:docker compose downacts on whatever the project holds when it runs, and the project is the organism’s, so another clone of the record on the same docker daemon — a replacement body, once this one’s lease is lost — brings its services up in it under a mark of its own. The body’s services go by the daemon’s own label filter, evaluated container by container: the project’s containers that carry this body’s mark are stopped and removed (docker rm -f), then the project’s networks are removed, which the daemon refuses while any container is attached, so a network a replacement uses stays; volumes are never removed. The host does this on every exit, stopping them within 10s first — one of its exits (stop_body) is taken after its lease is lost, when a replacement may already be running, so its own exit is no safer for a project-wide down than the fence’s. A host that dies cannot: when the body fence stops a lost body (its host gone, its lease lost or expired) it reads the record, and one that names its host and its mark has that body’s containers removed at once, the same way; a record another body wrote (a replacement host on this clone) is left, with its services. The fence’s status line sayscompose removed this body's containers of <project>orcompose not this body's: <project>, and the body’s own record and override go once its containers are gone, by the host or by the fence. A host starting its services first sweeps the records of other bodies of its clone whose host is gone (a fence that died with it): their containers go by their mark, then their files (compose: removed the containers of earlier bodies whose host is gone (<marks>)); a record whose host still runs is left to that body and its fence. Compose runs without the organism’s credentials in its environment (the ones an application’s start removes: the spine’s, owner’s, head’s and reflexes’ URLs and DSNs), so a compose file cannot interpolate them, and with nerves here it is handed the attached application’s:HALE_DNA_NATS_URL_APP(the broker as the body reaches it, its ownHALE_DNA_NATS_URL_APPif set, else the spine’s server),HALE_DNA_NATS_USER_APP,HALE_DNA_NATS_VAULT_APP,HALE_DNA_NATS_ORG, andHALE_DNA_NATS_SECRET_APP, the path of the local vault’s entry holding the application’s password. The compose file mounts that file read-only as a secret (secrets: <name>: file: ${HALE_DNA_NATS_SECRET_APP:-/dev/null}) and the service reads it as a vault of one entry (HALE_VAULT_DIR: /run/secrets,HALE_DNA_NATS_VAULT_APP: <name>): the vault stays the one ingress, and the password is never copied, never in an environment and never in the repository. A vault served over HTTP (HALE_VAULT_ADDR) has no file to hand; the body says so and hands none. The broker’s address is handed as the body sees it, and a service on the host’s network (network_mode: host) reaches it there; on Linux a broker on the host’s loopback is out of a bridge network’s reach (host.docker.internalthroughhost-gatewayreaches only what listens beyond the loopback), so the address is not rewritten. A mounted entry is the file the vault held when the container was created: a password drawn anew reaches the service when the body next brings it up.HALE_DNA_COMPOSE=offleaves the services to the operator, as before ruling 11, and the body says so; a repository with no compose file runs no application under the body, which says that too. Not yet: a Mutation’s restart request does not bring a changed compose file up (the body says the repository has no application to restart), and the services’ logs stay compose’s (docker compose -p <project> logs). - Review routing (GH #1087). A change set goes to the positions that
must sign it, read from the graph, never guessed:
hale dna route [--json] (<path>… | --diff <range>)(the host verbroute, reading memory as the head, overgraph_perspective("routing"); the routing isdna/core/route.hl). A path is the repository’s: one given from a subdirectory is taken from there, and the organization’s root is the nearest directory holdingdna/org(a seed’s ownhale.tomlis not it). A path names a node — a contract (its file, or a file under its directory), else a document, else a deployment (the file it was read from, when that is no document:compose.yaml), else the longest seed holding it — and thereviewsedges say who signs, whatever a position is called. Any node: every position thatreviewsit — a part’s reviewer reviews its seed and the contracts it serves or consumes, a deployment’s operator the deployment (compose.yamlgoes tocompose/operator, against the gates guardingcompose). A contract, beside those: the positions reviewing a consumer that is no part (a contract, a document, a deployment), and theboardwhere the contract is law: a practice the Board ratified as law is bound to it (its receipt sayslaw: true); one ratified as advice is not law, and the board does not sign for it. A position that reviews nothing — a deriveddev, an operational role — signs nothing; one areviewsedge names signs, whatever its name. The evidence a verdict is given against is the run of every gate guarding a node the change touches. A consumer no position reviews is listed as such (unsigned), never dropped: a consuming process or seed with no position under it reviewing the contract; a path that names no node, or whose node no position signs (or operates), is left to the fallback — the Review’s own required authority, the task routers of GH #697 when they exist, which the route names. Text listssigned by,against(no gate guards itwhen no gate does),reviewed by no positionandleft to the fallback, <what it falls back to>, each signer with its holders and why it signs;--jsonis one object:signers(position,because),evidence(gate ids),unsigned(node,because),fallback(path,because),fallback_to. A Review takes its signers and gates from the same computation when it opens (Signers from the route, above). - Perspectives:
hale dna show(GH #1086).hale dna show org | processes [--json] [project]is the host verbshow. It takes--jsonand at most one project directory, and refuses any other flag, a second project or one that is not a directory. It reads memory under the head’s role alone (HALE_DNA_MEMORY_DSN_HEAD), never the spine’s, even in a process that holds both, scoped to the record, and prints whatgraph_perspectiveanswers. Nothing is edited through it: a change moves through the record’s rows, and the spine projects it into both perspectives. A record with no rows is the empty perspective, with no memory asked, and stderr says a graft births a record; with rows and no memory named it refuses, and says the graph is memory’s. The projection’s stamp is read before the graph, so the rows it reports never overstate what the graph holds. Text (dna/operations/graph_show.hl) shows a node by the last segment of its name. The org chart is nothing when there are no positions; otherwisepurpose, then a line per node positions sit under, in the record’s order: a position under the purpose is a line of its own (board), and any other node is its name, padded in code points to the widest, then its positions’ roles. A role is the position’s name less its node’s (api/reviewerunderapiisreviewer), followed by(<the contracts and documents it reviews>)and, when it has holders,[<holders>]; a position under no node is under(unplaced), after the rest. Each role ends with{mandate: ratified|pending|none[; roles: <API roles>]}:ratifiedwhen a ratified idea of kindmandateis bound to the position’s node (memory),pendingwhen one is proposed and undecided (read from the record),noneotherwise;rolesare the node’sequipment.roles, when it has any.--jsoncarries each position’srolesandmandate(ratified, or empty) beside its holders. The process model is a line permeets— its outside parties and consumers, the arrow--<via>: <contract>-->, its servers or(nobody here), and(carried by <carriers>)after — then one<deployment> runs { <processes> }perruns, then one<position> convenes <definition> <cadence>perconvenes(board convenes optimize-walk every 1d).--jsonprints one object:perspective,record_rows(the record’s length),projected_rows(memory’s watermark),stamp_validandgraph, the query’s answer as memory built it. How memory stands to the record is said on stderr, never in the perspective: behind it (memory has projected <m> of the record's <n> rows), past it (a read that raced a writer: read again), or with a stamp that vouches for nothing (hale habitat migrate <organism>). On voice’s record with its holes filled the two texts are GRAPH.md’s renderings, byte for byte (dna/tests/graph_show_test.hl). - Projections and ranking (K3). The tail also projects the
record’s
structure.observedrows (the graft’s loci, topics, bindings, effect classes and claims) into memory by kind and name, the latest row winning, and itspressure.raisedandconcern.raisedrows into signals counted per (kind, source, what) with the last row’s seq (knowledge_structure,knowledge_signals), so the graph has what ideas bind to and what the fleet is saying. Retrieval is by binding and provenance first: the bounded set is the accepted ideas bound to the target or above it, and nothing outside it is retrieved. Inside it, a query — the objective, which the substrate passes from the ask, its first 2000 characters — ranks by similarity so the budget keeps the most relevant (KnowledgeStore.ranked_ids(target, budget, query_vec); no query is ratification order). The embedding is lexical —embed_text: a hashed bag of words in 64 dimensions, normalized, deterministic, rendered as a pgvector literal — soPqranks with<=>over avector(64)column (the migration creates thevectorextension and says so when the Postgres has none). - The application side (K4). A concern is one of the application’s
own events (GH #986, #987). It declares it as its own topic,
concern.raisedunder its own subjectapp.<app>., with a codec that writes one JSON object: itsid, and the concern’ssource,whatandseverity. It publishes it through pond’s NATS library with the application credential its node handed it, and imports nothing of the DNA. Itsidis that concern’s alone: a repeat is a replay, refused. Itssourceis a/-path of letters, digits and-_., at most 128 bytes, one of whose parts is the application the subject names: an application speaks for its own parts, never another’s, and a concern that does not is refused (acknowledged, recorded nowhere).NatsPublisher, the publish-only adapter, dispatches nothing into the program: its artifact stays exact, so a fleet claim over it can still be certified (aNatsConnhands whatever arrives to__local_dispatch, which no checker can bound). The heart lands it as a reading first,reading.recorded <app>/concern.raised/<id>. The spine then appendsconcern.requested <source>(requestthe reading’s entity,source,what,severity,app) before the event is acknowledged, once for that request, whichever delivery reads it; a concern the record refuses leaves the event unacknowledged, to come again. A node relaysconcern.requestedrows onto the nerves likeintent.requestedandreview.verdictuntil the record holds the answer, and the organization journalsconcern.raised, admitted once by its request.hale dna concern raisewrites the sameconcern.requestedrow by hand. - The learning scenario is the acceptance (K4), in the fixture:
the worker on the second node observes its mail backlog and raises
the concern three times; each travels as the worker’s own event →
the heart’s reading →
concern.requested→ the relay → the nerves →concern.raised; the third makes a proposal byorg/trio/workerbound toorg/trio(a concern by the tower rule); the Board ratifies the exact digest (hale dna review k:… approve --authority board); the spine projects it into memory on its tick; and the next change to the trio consults memory (knowledge.consulted m1with the digest and the hat’s digest), briefs the editor with the concern under the objective, and everymodel.calledrow of the attempt names the package and the digest. Something observed and ratified today informs the work done tomorrow, and the receipt says so.
Backends by role
Section titled “Backends by role”The assembly names what fills each role; hale check sees the wiring.
- Record —
Journal:RoutedJournaloverRecordJournal(memory) andMemoryLedger,MemJournal(tests). - Board (the
boardfield, aMembrane) — where humans see and decide:Board/LocalHumanMembrane, reached over the nerves on one machine; the record itself across clones (intent.requested,review.verdictrows, relayed by the host); the organism’s forge, mirrored by the host: the forge is a gate, not a store, and which forge stands in it is the organism’s forge allocation (spec/habitat.md, Forge and identity) — the habitat’s own Forgejo by default in the local profile, GitHub when a Board names it (hale habitat forge <organism> github --repo owner/repo). Against whichever it is, every pending mutation Review becomes a pull request (github.pr, naming the forge; the candidate pushed todna/<id>, the three views as the body), every GitHub review on it becomes areview.verdictrow in the reviewer’s login (authorityboardwhendna.github.boardlists the login, elsereviewer; each review once, keyed by login, commit and state), every settlement goes back as a comment (github.commented) and an approval pushes the genome — both once the Mutation is applied (or has failed), since a Review settles before its Mutation is applied. GitHub is a projection of the record and the record wins: a review whose head moved is refused here and shows as refused there. The forge is an implementation (GH #648):Forgein the core is the vocabulary of a code-review host and nothing of GitHub’s —open_review,verdicts(one per line: who, outcome, when, the forge’s own key so each is admitted once),comment,close_review,present,name.GitHubForge(gh) is one implementation; theFileForge(.hale/dna/forge/<n>.review, its verdicts the lines of<n>.verdicts) is the fixtures’;ForgejoForge(Forgejo’s API, a token the vault slot of the organism’s own Forgejo user holds) is the local profile’s;NoForgeis a bare remote’s honest behaviour, the no-infrastructure fallback, andhale dna profile‘sgithub:line names which the host found: the organism’s forge allocation (orHALE_DNA_FORGE, the fixtures’ override).hale dna forge syncsyncs with the allocated forge; withnoneallocated it answersforge: no forge allocated. (hale dna github syncis gone.) The forge’s pass is written against the interface; the rows it appends (github.pr,github.commented,review.verdictkeyed by the forge’s key) do not change with the forge. Only thegithub.prrows of the forge in the gate now are read: after the allocation (and the remote) moves to another forge, a Review still open is opened again there and nothing migrates. Nothing in the core or the host spellsghoutside that one implementation, and no practice, workflow or standard names a forge by kind: “open a review” is the verb, and the allocation answers it. - Deployment —
Deployment:NoDeployment(a host expresses:hale dna dev),ShellDeployment { command, seed }(the command owns expressing and judging:express <candidate> <seed>returning 0 means up and healthy,rollback <base> <seed>restores; the exit code is the observation,expression.deployedrecords it), andLocalApplyDeployment(tests). With a gateway wired, an approval expresses and judges in the organization’s own handler and no host is asked; without one,expression.restart_requestedasks the host. - Observation — for a shell gateway, the command’s exit; for a host, the window it watches; for a fleet, what its nodes report (below).
The 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 a graft and hale dna 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-<id>, when the organism has a memory allocation |
drawn | the memory allocation’s superuser in the local habitat |
nats-<org>-<role>, for owner spine head reflexes |
drawn | the nerves’ accounts (The nerves) |
nats-<org>-app-<name>, while the record attaches the application |
drawn | the application’s broker account (The application’s account) |
oidc-client-<dna.oidc.client> (dna-local when unset) |
drawn for an issuer on the loopback, else a slot | the head’s OIDC client |
oidc-service-<service>, per dna.oidc.service |
drawn | a service client (The principal source) |
forge-token |
slot | the forge’s token: with none, nothing is done at the forge, never as gh’s own login |
model-<NAME>, per credential dna/org/models.hl or dna/org/work.hl names |
slot | a model’s key (Models); hale dna secrets lists a work.hl key as the legs’ model key |
- Drawn or a slot. A secret the organism owns the value of is drawn
from urandom (32 hex characters) into the vault when the vault lacks
it; the nerves’ are drawn anew on every graft and
upgrade, since the server’s include is written from the same draw. The habitat’s allocations read theirs from files written from the same draw as the vault’s entries, beside the allocation and never in a checkout (GH #1653):nats.secrets.conffor the nerves andpostgres.secretsfor memory (itsPOSTGRES_PASSWORD_FILE), each mode 600 inallocations/<id>/. No committed file holds a secret. A secret a person supplies is a named empty entry thathale dna secret set <NAME>fills (FORGE_TOKEN,OIDC_CLIENT_SECRET, or a model key’s name); an entry that is empty is not held. - The vault. The local vault is
std::secret::vault_local_dir()(HALE_VAULT_DIR, else the toolchain’s cache), each entry a file of the secret’s exact bytes, mode 600. A real vault (HALE_VAULT_ADDR) is provisioned out of band: there the bootstrap only checks, and says what the vault lacks. The local vault is one directory per user, not a boundary between the parts one user runs: any of them can read any entry by name. What keeps a part to its own credential is what it is handed (an application gets its own account’s vault name and none of the others’), not what it could read. - Presented, never held. A credential is presented by the adapter
that puts it on the wire, and never exists as a String elsewhere: a
part carries a
std::secret::Credential { vault: … }(a sealed locus), andreveal_text()is called on the line that writes the secret out — pond’s NATS client onCONNECT, the head’s token request, the model’s request header. No field, URL, argument or environment variable carries one; memory’s DSNs name their role’s entry and pq presents it in its handshake. Two are not sealed yet: the migration sets each role’s password in the SQL it runs as the owner (CREATE/ALTER ROLE … PASSWORD, an SQL literal on pq’s ordinary query path), and the forge’s token reachesgh, which takes it no other way, asGH_TOKENin that one child’s environment (through a file unlinked at once). - Fail closed.
hale dna run(anddev, which starts it) refuses when the vault lacks a drawn secret, naming each;nerves migraterefuses a role with no entry; nothing falls back to a default. hale dna secretslists every secret the organism requires, whether the vault holds it, and the remedy for each missing one; never a value. The board says so for each missing one (a model key is reported by the body, since the vault that matters is the one on the machine that calls the model).
The fleet
Section titled “The fleet”The genome’s topology is a fleet seed Hale already checks — a seed
declaring fleet locus (spec/verification.md, “Fleet composition”)
— and the DNA expresses one arrangement of it:
- The fleet seed describes the workspace’s own services. Its params
are the workspace’s seeds’ mains, and composition cuts each one’s
artifact from its seed, so a candidate is checked as the fleet it
would deploy. An instance’s placement host (
host("edge-1")) is the node that expresses it.[dna] fleet = "<name>"inhale.tomlnames which entry of[fleets](a fleet seed) the DNA expresses. The DNA reads the fleet as its rows:hale fleet export <seed>(hale dna fleet-ofresolves the name to the seed in a checkout’s manifest); a plan JSON is read nowhere. - The fleet is evidence. Verification runs
hale fleet check --in <worktree> --if-declaredon every candidate (evidence.fleet;fleet_clean, true when the workspace declares no fleet). A change to one service that breaks a claim the fleet makes over all of them ismutation.deny(“candidate breaks the fleet”) with the witness in the receipt. A candidate whose diff names a fleet seed’s source (a.hlfile in a directory one of whose own files declaresfleet locus, so an import-only change counts) or the manifest (hale.toml) changes the fleet’s shape: it is re-classedtopology(mutation.topology) whatever it was asked as, andOrgPolicysends it to the Board. - A deploy is a row.
fleet.deploy(entity: the Mutation, or the short revision for an operator’s deploy) carries the fleet’s name (plan), the revision, the seed the change edits (""for the whole genome), the instances touched — every instance with a host whose seed is that directory, read from the fleet’s export at that revision (checked out into a scratch worktree for it) — and the reason (apply,rollback, or the operator’s). The revision is published first on the genome’s forge (the record’s remote) as the branchdna/revision/<rev>, and every node fetches it by its sha, ashale fetchfetches a pinned dependency (GH #1590: a revision is the genome’s, not the record’s). Underhale dna runwith[dna] fleetset, the host answers an application’sexpression.restart_requestedwith a deploy row;hale dna deploy <revision>andhale dna rollback <mutation>write the same row by hand. A rollback’s row asks for the base; nothing is watched. - A node expresses.
hale node <name> [--repo <clone>] [--fleet <name>] [--tick <ms>]runs in a clone of the governed repository. Every tick it syncs the record and reads the latestfleet.deploy; when that names a revision it does not express, it fetches the revision by its sha from the forge (git fetch <remote> <sha>; a fetch that fails is said and tried next tick), checks it out, reads the fleet’s export there, and for each instance the fleet places on the node’s host that the deploy touches (or that is not up) cuts the artifact, builds the seed, restarts the process (cwd the clone,LOTUS_OBS=1,HALE_DNA_NODE,HALE_DNA_INSTANCE,HALE_DNA_EXPRESSION) and appendsinstance.upasnode/<name>: node, instance, revision, model hash, build digest, pid. An instance that exits appendsinstance.exitedwith its code. The expression identity per instance is therefore reported by the node that expresses it and joined to the fleet by instance id. The node decides nothing..hale/node/<name>/<instance>.pidis the only local state. - The window is over every touched instance. After an apply’s
deploy row the host waits for every touched instance’s
instance.upat the revision (180 s to settle), then for the observation window with noinstance.exitedat the revision among them. All up and none exited:Observation healthywith the model hash the instances reported. One exited:expression.crashednaming the instance and its node,Observation crashedwith the same detail, and the organization rolls back — a rollback deploy row touching the same instances, which every node answers. Never up:expression.crashed“never expressed by …” andObservation build_failed. hale dna fleetrenders what the fleet expresses from the record: every instance of the fleet, its node, whether it is up, the revision and model hash it last came up at, and the last deploy.
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.
- Readings. Every long-running part serves its readings as a
Prometheus scrape target (
std::metrics::Endpoint), namespacedna. It listens on the port its environment names inHALE_DNA_SENSES_PORT, else the one the seed names for it: the spine (the node’s host) on 9464, a node on 9465, the head on 9466 (dna/core/senses.hl). It binds every interface, orHALE_DNA_SENSES_HOST, from adna::Sensesits program’smain locusholds on a cooperative pool of its own (senses), whose shutdown ends the accept, so the part drains at once. A port another process holds is the server’slisten_failed, whichSensesabsorbs, as the only handler it has: the part runs on, unread, and says so.- Labels. Every series carries
part=<name>, and a node’snodeas well. No label is per event, per task or per revision: a series cannot be retired, so the set a part serves stays bounded by its parts and instances. A task joins senses, spend and receipts through its rows (amodel.calledrow names its attempt, and the attempt its task), not through a label. - Series.
dna_pulse_seconds{part}is each part’s tick.dna_instance_up{part, node, instance}is 1 while the instance runs and 0 once it exited.dna_instance_since_seconds{part, node, instance}says since when (a down transition writes its since first, an up its state first, so no scrape pairs a new state with an old since).dna_model_calls_total{part}counts the model calls whose rows the spine announces landed (with its nerves up). - A part never learns where its readings go.
- Labels. Every series carries
hale noderuns as the host program’smain locus(Host, verbnode), the one place itsSensescan be placed on a pool of its own. It is no spine: its nerves connection has no server and stops at once, and it takes no application’s events. It drains on SIGTERM.- The store is the organism’s
senses-<id>allocation in the local habitat (GH #1653): Prometheus, scraping what the allocation’ssenses.ymlnames over the host gateway (honor_labels, so a part’s own labels are the readings’), and keeping what it scraped for its retention (--storage.tsdb.retention.time, a week). Its port is in the 39000s, on 127.0.0.1, kept in the allocation’sports.yamlas memory’s and the nerves’ are.- The allocation’s
compose.yamlandsenses.ymlare written the first time a habitat act brings an allocation of the organism up. hale habitat migrate [organism]andhale dna senses up [dir]bring the store up;senses upprintsHALE_DNA_SENSES_URL, its read URL.hale dna senses url [dir]prints the same line for a store that is up, bringing nothing up, and nameshale habitat migrate <organism>when it is not.hale dna devnever brings the store up: it says the store’s URL when it is up, or that it is not, unless the environment names its own memory or nerves (an owner’s DSN or URL), and nothing stops for it.- A reading never acts.
- The allocation’s
- The reflexes (
dna/reflexes) are a program on the private-services tier. They hold the store’s read URL and a publish-only credential, thereflexesuser of the nerves (HALE_DNA_NATS_URL_REFLEXES, whichhale dna nerves migrateprints). That user may publish on<org>.app.reflexes.>alone, and an application’s user may publish only under its own name, so no application can speak as the reflexes. They hold nothing else: never the record, memory or the vault. To the organism they are one more application.- The rule. One is built in,
instance.down: an instance its node readsdna_instance_up0, asked of the store each tick. It fires once per outage,reflex.fired { id, rule, node, target, action: "restart", since, fired_at }on<org>.app.reflexes.reflex.fired. The id names the outage (instance.down:<node>:<instance>:<since>, each segment kept to the heart’s id token). - The row. The heart lands the firing as a reading row first,
reading.recordedreflexes/reflex.fired/<id>(The heart’s events), so a second publish of the same outage is refused as a duplicate. They remember what they published only while it is down. - Launch. The face’s launcher (
hale dna head,start.sh) builds and starts them beside the stub OpenID provider when a project is attached, with that project’s store and nerves (hale dna senses url,hale dna nerves urls), and stops them with the launcher. The launcher brings nothing up: a store or nerves that is not up leaves the face running without them, and the launcher nameshale habitat migrate <project>.
- The rule. One is built in,
- The node acts on the row. On its tick a node reads every firing
that names it and has no
reflex.actedrow yet. It restarts that one instance at the latest deploy’s revision, through the path afleet.deploytakes, touching no other instance and not marking the deploy expressed. It then appendsreflex.acted(entity: the firing’s id;rule,target,action,outcome,node), whatever the outcome was:restarted,already up, or why not. A restart that did not take becomes a concern as well (concern.requested, requestreflex-failed:<id>), not a quiet retry. A node restarts; any other action is refused on the row. - The spine’s read. The host that lands a firing, or finds it landed
already, reads the firings for its node and target. A second firing
within the window (
HALE_DNA_REFLEX_WINDOW_SECS, 600 s) becomes a concern:concern.requested(sourcereflexes/<target>, requestreflex:<id>). There is one such concern a window, however many firings fall in it, and it lands before the firing is acknowledged; a concern the record refuses leaves the firing unacknowledged, to come again. The organization raises it,concern.raised, and that is what a workflow answers to. Only the rule fires, only the node acts, and both leave rows.
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):
- the body,
dna/hostitself:runanddev(the supervision, in which the host is its program’smain locusand binds the nerves),node(the node agent,hale node) andbody-fence(the renewer a body starts beside itself, from its own binary). Each runs until it is stopped. - the verb program,
dna/host/verb: every other verb, one-shot — the projections (status,history,review,board,report,pressure,fleet), the writers (task create, a verdict,pressure raise,sync,deploy,rollback,forge sync) and the rest. It imports the host’s seed and starts no locus that runs or handles the bus, so it runs to exit, and the memory-bound survey judges it as it judges a script (spec/verification.md).
hale dna <verb> resolves the project from the manifest (the root, the
application’s seed, the fleet and its fleet seed 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> <fleet seed> …, 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 RecordJournal, appends in a person’s or a node’s name
through memory’s append function, 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 the graft and upgrade (they
embed the sources), hale fleet check and export, and the
exec shims. The host decides nothing.
A seed is built once per content fingerprint
Section titled “A seed is built once per content fingerprint”The host builds the organization’s seed before it starts it, and the
application’s seed too under dev; it rebuilds both on a restart. A
build is a pure function of what it reads, so the host does it once
per content fingerprint and starts the stored binary thereafter. The
cache is $XDG_CACHE_HOME/hale/dna-build (or ~/.cache/hale/dna-build),
a sibling of the toolchain cache the host and the surface are themselves
built into; it holds the 32 most recently used entries and prunes the
rest. An entry is a directory, <fingerprint>/<seed name>, so the
process keeps the seed’s own name.
A build never writes into the seed’s directory. The host passes hale build -o: with a cache the binary is built into a private directory
beside the entries and renamed whole to its entry, and the entry’s path
is what the host starts; without one (HALE_DNA_NO_BUILD_CACHE, no
cache home, a fingerprint that could not be taken) it is built into
.hale/dna/build/<seed>/<seed name> under the project root. Either way
a checkout has no binary in it afterwards.
The fingerprint is a SHA-256 over:
- every file the build reads, each as its path relative to the
project and the SHA-256 of its bytes. The list is the compiler’s
own —
hale inputs <seed>, which resolves imports exactly as a build does — so a seed that grows an import, or a vendored core that is upgraded, is a different fingerprint without anything here having to be kept in step; - the project’s
hale.tomlandhale.lock; - the toolchain version, and the
halebinary’s own size and mtime, so a compiler rebuilt in place without a version bump is a different fingerprint; - every environment variable that changes codegen;
- the seed’s own path, because a binary carries the paths of the sources it was built from.
A hit is the entry used where it lies, and nothing else is done. A
store is one rename of a whole directory into the cache, so a reader
sees a whole entry or none, and a binary that is still running keeps
its inode when its entry is replaced or pruned. Two hosts building one
thing at once need no lock: the second rename finds the entry there
and drops its own copy, and both are right. Anything that stops the fingerprint being taken
whole — no cache home, hale inputs failing — is a miss, and a miss
builds exactly as it did before. HALE_DNA_NO_BUILD_CACHE=1 makes
every build a miss.
The embedded source has a name
Section titled “The embedded source has a name”A graft and hale dna upgrade materialize vendor/dna from the
DNA source embedded in the hale binary, so an organism runs the
core that binary carries and not the one in any checkout. A version
does not identify that source — two builds of one version can embed
different dna/ source — so the toolchain names it (GH #726):
EMBEDDED_DIGEST is a length-framed SHA-256 over the sorted (path, content) pairs of every embedded file (dna/core, dna/host,
dna/host/verb, dna/operations, dna/organization_runtime,
dna/organization_source, dna/ui, and pond’s driver and NATS client
pinned under dna/core/pond), computed when the toolchain is built. It is:
- the second line of
hale --version(embedded dna: <first 16 hex>; the first line stays the version alone, since a provisioning script reads field 2 of it); - all 64 digits on stdout, alone, from
hale dna --embedded-digest, and — with--from-tree <dir>, a checkout holdingdna/core— the digest that checkout would embed, by the same algorithm. Unequal digests mean the binary predates the tree: what it materializes is the older core, and a test of an edit to the tree measures the wrong source. A missing directory is refused, never digested as a smaller set; - the first line of
hale dna status, with the toolchain version, and a refusal to stay quiet whenvendor/dnawas materialized by another build (hale dna status --jsonis unchanged: the projection is the host’s); - recorded where a materialized tree can be asked about it:
vendor/dna/README.mdand.hale/dna/embedded.digest(hale <version>thenembedded dna: <digest>), both toolchain-owned and git-ignored, both refreshed byupgrade. It is deliberately not written into the generated organization’s source:dna/org/main.hlis project-owned source that is reviewed, diffed and read by models, and a line that changes with every DNA edit belongs in neither a review nor a recorded baseline.
The build enforces its own snapshot: the digest is computed over the on-disk tree by the build script, the crate digests what the compiler actually embedded, and the two must agree — source edited while a build runs, or a file added without being embedded, fails the build rather than shipping a digest that names nothing.
The surface
Section titled “The surface”hale dna ui [project] [--port N] serves the DNA surface in a browser
from the record in memory (GH #1590: a head reads no record without
memory): 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 the face fronts, is the
surface’s counterpart for the operator’s machine. hale dna head [project] [--port P] [--head-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/face/standard, 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 five 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_FACE_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 alone,
onto the launcher’s environment and never its output, when memory is
up and migrated: an owner’s server (HALE_DNA_MEMORY_DSN_OWNER), or
the organism’s memory allocation in the local habitat already running.
Otherwise it says memory is not up, names hale habitat migrate <project>, and starts the head with Knowledge unsupported; it brings
nothing up and migrates nothing (GH #1653). The launcher reads the
reflexes’ store and nerves with hale dna senses url and hale dna nerves urls, which bring nothing up either. 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 surfaces only, 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.
Faces (Phase 3). A face is a role an application fills: the browser’s
door to the organism. This slice has one, the standard face
(dna/face/standard), which the toolchain ships and grafts as it grafts
the habitat genome and the packs: its sources and the browser’s assets are
part of the head’s files, built into the toolchain cache beside the head’s
seeds. A genome names others as below.
Named faces. An organism names a face by a param of its main locus
of type dna::Face (dna/core/face.hl, in the embedded core), recognised by the type it resolves to (whatever alias imports the core; a look-alike Face is no face) as
std::observe::Probes is and built by its literal, never a manifest table:
admin: dna::Face = dna::Face { app: "faces/admin", roles: "operator", consumes: "Orders", port: 8801 };The param’s name is the face’s; app is the seed of the application
that fills it (relative to the application’s seed), roles the roles it acts
under and consumes the surfaces it calls (space-separated names), port
the loopback port it is served on, a literal 1..65535. The compiler reads the
literal into hale check --api: the inventory carries a faces array
(name, app, roles, consumes, port), present only when the program
declares one, so an inventory without faces is as before. The check refuses a
face with no app, no port or a port another face holds, a role the program
does not declare, or a surface it does not declare.
The record states a face from that description, composing the graph’s
vocabulary and adding none: an application:face/<name> node (the application
and port on its row), and the face as a consumer on the meets edge of the
contract of each surface it consumes, the one edge the contract has (beside the
seed that serves it). A face is a consumer, never a part: no position is
proposed for it. The head’s Commands::index row answers them as a faces
section (name, app, port, roles, consumes), and the standard face
lists them in /index, each a link to http://<host>:<port>/ on the host the
browser reached the face by. Mounting is navigation to the face’s URL, not a
reverse proxy.
hale dna dev and hale dna run build each declared face’s seed and start it
as <binary> <port> with HALE_DNA_FACE=<name> and
HALE_DNA_FACE_PORT=<port>, beside the application and holding none of the
organism’s credentials (as the application holds none); the pids are
.hale/dna/face-<name>.pid, listed in .hale/dna/faces, and the body’s stop
and fence stop them with it. A face that does not build is said, and the
organism does not wait on it. An organism that names none is served by the
standard face alone, unchanged. In both kinds of face the contract is the
surfaces: headers carry only identity and the digest. A face is not restarted
by an apply in this slice.
The head serves surfaces only. The standard face stands in front of it,
on the port the browser opens (8792; --port), the head on the loopback
behind it (--head-port, 8796; it learns the face’s as
HALE_DNA_PUBLIC_PORT, its origin for sign-in, host checks and
commands). The face serves GET / and the shell’s assets from
dna/face/web and GET /app/… from the attached project’s ui/, and
passes every other request to the head as the browser sent it: method,
path and query, headers (cookie, origin, host) and body, the answer
written back as it came, an event stream held open until either end closes.
/ is the head’s gate: the head answers it 204 when the person may
open the shell (a session under OIDC, the launch-token cookie under a
fixture’s trusted-local session), naming them on X-Hale-Person (the
session’s person, or the head’s own principal), and the face then serves
the shell; any other answer (the sign-in redirect, the launch token’s
cookie and redirect to /, a refusal) is passed on unchanged. A face
given HALE_DNA_GRAFANA_PORT and HALE_DNA_GRAFANA_HEADER serves the
habitat’s Grafana under /grafana/ behind the same gate, in the person’s
name (spec/habitat.md, Readings); without them /grafana/ is a 404. /app/… asks the
head’s read (GET /api/hale/v1/head) as the same session which project is
attached, and serves its ui/ (ops::application_asset), or the
head’s refusal. The head has no static file and no /app/: those paths
are no route of it. The browser’s traffic keeps relaying as the browser
sent it (the head’s browser routes are not rows of a surface); the face’s
own reads go through a client.
The index. GET /index on the face is the record’s index: a static
page, plain HTML in the shell’s stylesheet and no script, generated on
each request. Behind the head’s read as the person’s session (the same
gate as /app/: the head’s refusal is passed on; a trusted-local session
reads bare, as every head read does), the face reads the record through a
client of the head’s surface (api::client, spec/api.md § Calling a
surface; the face declares the one row it calls, Commands::index, whose
member digest is the row’s request and response shapes, so the two sides
agree by shape). The row is Commands::index of HeadCommands
(dna/api/record_index.hl), a read any authenticated caller the record
knows may ask: it takes {limit} (at most that many entries of each
section, 200 by default, 1000 at most) and answers the record’s revision
and six sections, each a JSON array the face renders and the purpose a
line: the declared purpose (the purpose node of the graph the
record states); the parts and positions (each position node with
the holders of its holds edges); the attached codebases (name,
repository, branch, build, test, toolchain); the watched upstreams
(each ratified, unretired standard/upstream-watched/<name> practice with
its position, source, kind and codebase, its terms refused if they cannot
stand, the result of its last occurrence — the snapshot’s digest, short,
or unreached — and the instant of that occurrence in UTC; the
occurrences are the Ledger’s rows, read beside the record); the
contracts (each contract node with its source and text); and every
outside link the record holds as knowledge, each http:// or
https:// URL in the text or source of a graph node or in the text of a
ratified, unretired practice, with what holds it. The row writes nothing
and adds no manifest table, record family or row kind: the sections are
read from the rows that exist. The face’s client is http::Client on the head’s /api/hale/v1/head/surface
(POST …/call/Commands::index), its bearer the requesting visitor’s own
Cookie header, which the face received and presents on the visitor’s
behalf. The head reads that bearer as the visitor’s cookies, runs the
request through its own gates (a session under OIDC, the loopback host
check, the trusted-local session), and relays the call to the API child’s
commands port under the visitor’s credential: the session’s ID token under
OIDC, the child’s launch token in a trusted-local session. So the index is
the visitor’s read: a visitor who is not signed in, or whose session is
forged, is refused as the head refuses every read (401), the face passing
the refusal on.
A page the face serves (the habitat’s operator face) calls the same row from
the browser: the face relays that one POST with the page’s own Cookie
header as the Authorization bearer, which is where the head reads the
visitor’s session, so the generated client of the head’s rows needs no token
of its own.
The habitat’s face. The habitat is an organism, so its face is this
one: hale habitat up starts face (hale dna head on the habitat’s
genome, the standard face over the habitat’s own head) beside body
(hale dna dev <genome> --no-iris) as services of the habitat’s compose
project, its head’s state in face-state under the habitat’s directory;
hale habitat status prints its address on a face line, and down
takes the compose project down. The habitat’s index is the same page
and the same Commands::index row, with one more section in the answer: a
record that grafted organisms lists them under Organisms (the
organisms array, empty and then no section in any other record). Each
entry is read from a habitat.grafted row and says the organism’s name (the
directory it was grafted into), its state (up while its face accepts
connections on the loopback, grafted otherwise), the address of its face
and the address of its head, its declared purpose (the PURPOSE of its
dna/org/purpose.hl, taken at its graft) and its pulse (the worst state
the providers of its allocations observe now, each where it was last
applied: down over degraded over up, "" when none is observed; its
forge and identity, the habitat’s services it is given, are observed with
the allocations and are not its pulse). No manifest table, record family or row kind
is added: the addresses are fields of the existing habitat.grafted row
(name, face, head), taken at the graft. An organism’s address is a
block of HEAD_BLOCK (8) consecutive loopback ports, found free from a
starting point of the habitat’s own and distinct from every block already
handed out, and kept in the organism’s dna.head.url (its face is the
block’s base, its head the next port, then its API child, provider and
commands). hale dna head <organism> serves on that block unless a port
flag says otherwise, so the recorded address is the one the head answers
on. The habitat’s own address is assigned the same way, in its genome’s
dna.head.url, the first time up needs it.
The habitat’s placement. Three more reads of HeadCommands
(dna/api/habitat_rows.hl), each taking {limit} as the index does and
answering the record’s revision and one JSON array, are what the operator
face’s hardware and infrastructure screens read: Commands::hosts (each
habitat.host row: the host’s name, the provider that produced it and
what it offers), Commands::policy (each habitat.policy row: the
role, its provider and the profile it came from) and
Commands::allocations (each habitat.allocated row: the allocation
(<instance>/<role>), its role, instance, organism, provider,
host, service and address, and the state its provider observes
now with the detail it saw: up, degraded or down from the
compose provider; "" for the operator’s, which the habitat does not
observe). The latest row of an entity is the one listed. A host is a
machine the habitat places on (spec/habitat.md, Hosts, providers and
placement), never the program dna/host. They are reads any authenticated
caller the record knows may ask, they write nothing, and the head relays
them from the face as it relays the index: POST /api/hale/v1/head/surface/call/<member> for the surface’s reads
(Commands::index, ::hosts, ::allocations, ::policy) and no other
member. habitat.host and habitat.policy are row kinds of the habitat’s
record, written by hale habitat up; an allocation’s provider and host
are fields of the existing habitat.allocated row. Commands::hosts
leaves out a removed host. Four Board rows (requires: [owner]) manage
the hosts: Commands::add_host ({name, user, port}) and
Commands::probe_host ({name}), each answering {ok, code, why, host, public_key, authorize}: the host as its row now states it, and for
add_host the habitat’s public key and the line the host’s
~/.ssh/authorized_keys takes; Commands::plan_remove_host ({name}), a
read, answering {ok, code, why, plan, confirm}: the removal’s plan, a
JSON array of items (spec/habitat.md, Destroying: plan, then confirm),
and the name to type back to confirm it ("" when the plan stops and
deletes nothing on the host); and Commands::remove_host ({name, confirm}), answering {ok, code, why, host, plan, report}: it plans
again with the same auditor and, when the plan stops or deletes anything
on the host, executes it only when confirm is the host’s name, item by
item, report saying of each item done, failed or left. A refusal
is code with why (invalid_host, host_exists, unreachable,
key_unavailable, not_found, host_key_changed, not_removable,
confirm_required, confirm_mismatch, plan_changed, partly_removed,
record_refused). The plan’s reply carries its digest, and
remove_host takes it ({name, confirm, digest}), refusing plan_changed
when the plan is another now. Two more Board rows remove an organism the
habitat grafted, the same way: Commands::plan_remove_organism ({name},
a read answering {ok, code, why, plan, confirm, digest}) and
Commands::remove_organism ({name, confirm, digest}, answering {ok, code, why, plan, report}; refusals not_found, ambiguous,
not_removable, confirm_required, confirm_mismatch, plan_changed,
partly_removed); an organism removed is its habitat.grafted and
habitat.allocated rows written again with "state": "removed".
Each row they write is in the name of the person the caller maps to. They are what hale habitat host add|probe|remove
calls, over the head’s socket (spec/habitat.md, Hosts, providers and
placement). Two more Board rows place and apply the allocations:
Commands::place_allocation ({allocation, host}, answering {ok, code, why, allocation}, the row it wrote) and Commands::apply_allocations
({host, fresh}, answering {ok, code, why, restart, applied}, applied
a JSON array of each allocation’s allocation, host, provider, ok,
why, state and detail): hale habitat place and hale habitat apply
call them (spec/habitat.md, Placing and applying).
Two more reads list the habitat’s nurseries and saplings for the operator
face’s organisms screen: Commands::nurseries (source, kind: the
toolchain’s, bundled, then each habitat.nursery row the Board trusts)
and Commands::saplings (each habitat.sapling row: name, purpose,
repo, revision, digest, nursery, packs, and proposed, the
revision a newer habitat.sapling.proposed row files, "" when none
waits); the head relays both from the face. Two Board rows act on them:
Commands::add_nursery ({source}: a habitat.nursery <source> row,
state trusted) and Commands::ratify_sapling ({name}: the sapling
pinned at the revision its proposal filed), each answering {ok, code, why}, each row written in the name of the person the caller maps to.
The services an organism’s host command placed on a host
(spec/habitat.md, Host commands) are three rows: Commands::record_placed
({organism, command, host, id, service, platform, readings, toolchain},
the Board’s, answering {ok, code, why, readings}: a habitat.placed row
keyed <organism>/<command>/<host>/<id>, and readings the endpoint it
registered with the habitat’s senses, "" for none),
Commands::record_unplaced ({organism, command, host, id}, the Board’s:
a habitat.unplaced row keeping what the placement said), each refusing
invalid_placement, not_found or record_refused, and
Commands::placements, a read the head relays from the face, answering
{ok, why, revision, placements, history}: the placements that hold (each
key’s latest row) and every one the record names with its state. hale habitat organism place and unplace write the first two once the command
ran.
Commands::brief is the habitat’s brief for an agent pairing with a
person (spec/habitat.md, Pairing), a read. The head serves the surface
over mcp::Rpc too, under the commands port’s bearer and roles, on a port
the system gives it and reports as mcp=<port> (HALE_DNA_API_PORTS).
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 organism’s nerves allocation in the local habitat while it is 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 the graft’s discovery (harnesses on PATH, local model servers) find nothing; for fixtures. The api head passes it on to the graft 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_EXCHANGE_PER_SENDER=<n> |
10000 | How many envelopes of a kind one sender’s subject of this record’s exchange stream holds (max_msgs_per_subject), set when hale dna nerves migrate creates or updates the stream; a fixture lowers it to test a full inbox (GH #1590). |
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 memory allocation | A Postgres of your own instead of the organism’s memory allocation in the local habitat: the owner role that migrates memory and creates the record’s roles. Migrated by hale habitat migrate, memory migrate and upgrade; read, never migrated, by dev, head and show; and by 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_APPLICATION_ID=<identity> |
set by hale dna dev |
The attach handover (GH #1653): the record’s identity, the application id a habitat-scoped heart presents to its head. |
HALE_DNA_HEAD_URL=<url> |
set by hale dna dev |
The attach handover: the head the heart reaches (git config dna.head.url, http://127.0.0.1:8792 by default). Also read by hale habitat and hale dna verbs as the head URL when --head <url> is not given (precedence: --head, HALE_DNA_HEAD_URL, <habitat>/head.url, habitat.profile). |
HALE_DNA_OIDC_ISSUER=<url> |
set by hale dna dev |
The attach handover: the OIDC issuer the heart’s service client asks (dna.oidc.issuer, http://127.0.0.1:8794 by default). |
HALE_DNA_OIDC_CLIENT=<id> |
set by hale dna dev |
The attach handover: the heart’s service client id, mapped at graft as dna.oidc.service <id>=<id>. |
HALE_DNA_OIDC_CLIENT_VAULT=<slot> |
set by hale dna dev |
The attach handover: the vault slot holding the service client’s secret (oidc-service-<id>), never the secret. |
HALE_DNA_BROKER_URL=<url> |
set for a heart the habitat allocated broker |
The attach handover: the organism’s nerves server, with no credential. |
HALE_DNA_BROKER_USER=<user> |
set for a heart the habitat allocated broker |
The attach handover: the heart’s broker user, heart-<id>. |
HALE_DNA_BROKER_VAULT=<slot> |
set for a heart the habitat allocated broker |
The attach handover: the vault slot of the broker user’s password, nats-<org>-heart. |
HALE_DNA_BROKER_SUBJECTS=<subjects> |
set for a heart the habitat allocated broker |
The attach handover: the heart’s own subject space, heart.<id>.>. |
HALE_DNA_BROKER_NODE_USER=<user> |
set for a heart the habitat allocated broker |
The attach handover: the user the heart’s nodes connect as, heart-<id>-node. |
HALE_DNA_BROKER_NODE_VAULT=<slot> |
set for a heart the habitat allocated broker |
The attach handover: the vault slot of the nodes’ broker password, nats-<org>-heart-node, which the heart passes a node at enrollment. |
HALE_DNA_HABITAT=<dir> |
${XDG_DATA_HOME:-~/.local/share}/hale/habitat/local |
The local habitat’s directory, named outright (GH #1653): a test’s or a DNA suite slice’s habitat; the developer’s stays the default. A test never inherits it: hale test sets each file’s to the habitat beside its vault, and the CLI tests’ hale() its process’s, whatever the caller exported. |
HALE_HABITAT_IMAGE=<tag> |
built from Dockerfile and hale.pin |
An existing local docker image for the habitat’s body and face compose services, skipping the Dockerfile build (--image <tag> on hale habitat up takes precedence). hale habitat image build [<checkout>] builds it once from a local checkout and hale test and the DNA suite pass its tag to fixture children. |
HALE_DNA_TEST_HABITAT=<dir> |
unset | Set by the DNA suite for a slice whose fixtures graft (GH #1653): the slice’s habitat, born once with no body, which dna::test_habitat hands a fixture; unset, a test file births a habitat of its own. |
HALE_DNA_TEST_HABITAT_VAULT=<dir> |
unset | Set beside HALE_DNA_TEST_HABITAT: the vault the slice’s habitat was born with, whose entries dna::test_habitat lends into the fixture’s own vault. |
HALE_DNA_DATABASE_DSN=<dsn> |
set for a heart whose genome declares database |
The heart’s own database, a tenant of the organism’s memory server provisioned at graft (GH #1653): a DSN naming the role heart_<id> and its vault slot postgres-heart_<id>, never a password. hale dna dev hands it to the host, and the heart inherits it. |
HALE_DNA_MEMORY_FROM=owner|habitat <project>/memory-<id> |
written by every migration | Not set by hand: the line beside the head’s DSN in a clone’s .hale/dna/memory.env that says where the DSN was derived, an operator’s own Postgres (owner) or the organism’s memory allocation in the local habitat. A kept DSN whose origin is anything else (another habitat’s allocation, an older toolchain’s database, or no origin) is removed and refused, naming hale habitat migrate (GH #1653). |
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 nerves allocation | The owner user’s URL: it creates the organization’s stream (hale habitat migrate, nerves migrate); dev and nerves urls derive the roles’ URLs on it. |
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_SECRET_APP=<path> |
unset | Handed by a repository body to its compose services alone: the path of the local vault’s entry holding the application’s nerves password, for the compose file to mount as a secret (ruling 11). |
HALE_DNA_COMPOSE=off |
on | A repository body leaves its compose services to the operator: it neither brings them up nor down, and says a repository: its compose services are left to the operator (HALE_DNA_COMPOSE=off) (ruling 11). The DNA suite sets it for every fixture; a fixture about compose opts in with on. |
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, pinned by dna/face/start.sh (8795 by default) for the head and by the head for its API child; unset, the child’s transport listens where the system puts it and the child reports the port it got (HALE_DNA_API_PORTS). |
HALE_DNA_API_PORTS=<file> |
unset | Where an API child writes the ports it serves on once they are bound (api=<port>, commands=<port> and mcp=<port> lines, renamed into place; the MCP port is always the system’s): set by the head for its child, which it starts with port 0 for whatever it does not pin, and reads the ports back from there. |
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 habitat graft 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_HOST_NAME=<name> |
unset | The name of the ssh host a compose project is applied on, handed to that project’s environment by the habitat (spec/habitat.md, The handover); a project maps it in its compose file. Unset on this machine. |
HALE_DNA_HOST_LISTEN=<addr> |
unset | That host’s listen address (the first IPv4 address its name resolves to on the habitat’s machine), handed beside HALE_DNA_HOST_NAME, through the toolchain’s own resolver (hale habitat resolve); an apply on a host whose name resolves to none is refused. Unset on this machine. |
HALE_DNA_HABITAT_LISTEN=<addr> |
127.0.0.1 |
The address a habitat declares its face, head and OIDC stub on: what the face service binds (HALE_DNA_LISTEN_HOST) and where up probes them. Not set by hand: hale habitat up --host <name> sets it on every container it runs on that machine to the address <name> resolves to on the admin machine. |
HALE_DNA_HABITAT_EXPECT=<record> |
unset | The record a hale habitat down run on a habitat’s machine through a pointer must find there: when it is set and the habitat’s genome pins another record, or none, nothing is stopped or removed. Not set by hand: hale habitat down on the admin machine sets it to the pointer’s identity, after checking the same over SSH. |
HALE_DNA_HABITAT_ADDRESS=<name> |
127.0.0.1 |
The host a habitat’s face and head are recorded at (http://<name>:<port>, on its habitat.profile row and in head.url). Not set by hand: hale habitat up --host <name> sets it to <name> on the containers it runs there. |
HALE_HABITAT_SSH_CONFIG=<file> |
unset | An SSH config file hale habitat up --host and the verbs of a habitat on another machine hand ssh -F; unset, ssh reads the person’s own. |
HALE_DNA_IDENTITY=publish |
unset | The face’s launch whose sign-in provider is the habitat’s, shared with every organism it grafts (dna/face/start.sh): its key is kept in the face’s state (HALE_DNA_HEAD_STATE/oidc.key, made once) and the provider is published at <habitat>/identity.env (issuer, the key’s public half, the client, where it answers). A launch under a habitat that published one starts no provider and signs in through it. Not set by hand: the habitat’s compose render sets it on its face service. |
HALE_DNA_GRAFANA=off |
on | hale habitat up runs no Grafana behind the habitat’s face (and removes one an earlier up started); status names none. The DNA suite sets it for every fixture; habitat_operator_face_test opts in on its own up. |
HALE_DNA_GRAFANA_PORT=<port> |
unset | The loopback port of the habitat’s Grafana, which the standard face serves under /grafana/ behind the head’s gate. Not set by hand: the habitat’s compose render sets it on its face service (the head block’s sixth port). Unset, /grafana/ is a 404. |
HALE_DNA_GRAFANA_HEADER=<name> |
unset | The header the habitat’s Grafana trusts for the signed-in person’s name, drawn once per habitat (X-Hale-Grafana-<32 hex>, <habitat>/grafana.secrets, 0600): the face sets it on what it relays to Grafana, and nothing else on the machine knows it. Not set by hand. |
HALE_DNA_FACE_HOME=<path> |
unset | The path the standard face lands a signed-in visitor on in place of the organization shell at / (dna::face_home); the shell is then at /organization. Not set by hand: the habitat’s compose render sets it on its face service to /app/, the operator face. A value that is not a path of the face is ignored. |
HALE_DNA_LISTEN_HOST=<addr> |
127.0.0.1 |
The address the face, the head and the OIDC stub bind (dna::listen_host). Not set by hand: the habitat’s compose render sets it on its face service from the address the habitat declares (127.0.0.1 for a local habitat); on macOS, where face runs on the project’s network, it is 0.0.0.0 inside the container and compose publishes the ports on the declared address alone. |
HALE_DNA_LOOPBACK_HOST=<host> |
127.0.0.1 |
The host a process dials a port published on this machine’s loopback at (dna::loopback_host, dna::reach_url): an allocation’s recorded 127.0.0.1:<port> is dialled there. Not set by hand: the habitat’s compose render sets host.docker.internal on its body and face on macOS, where they run on the project’s network; on Linux they share the host’s network and it is unset. A listener in the process’s own container is dialled at 127.0.0.1 regardless. |
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, or on exactly the habitat’s declared address (HALE_DNA_HABITAT_ADDRESS), 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_NODE_EXPORTER=off |
on | No machine’s node exporter or container exporter is installed or removed by this run: not by hale habitat up on the habitat’s machine, host add on a host, host remove, or a forget. The DNA suite sets it for every fixture (habitat_operator_face_test opts in on Linux); the run up --host makes inside the habitat’s image always has it, since the person’s machine installs the habitat machine’s exporter over SSH instead. |
HALE_DNA_OIDC_DIAL=<url> |
unset | Where the habitat’s sign-in provider answers on its own machine (http://<listen>:<port>), as its face published it in <habitat>/identity.env: an organism’s face launch probes it there before it signs in through the provider. Not set by hand. |
HALE_DNA_OIDC_SERVICES_FILE=<path> |
unset | A file of more service clients’ ids the stub issuer accepts, read at each request (dna/oidc). Not set by hand: the habitat’s face hands its provider <habitat>/identity.services, which a graft appends each organism’s heart’s service client to. |
HALE_DNA_FACE=<name> |
set by the host | The name of the named face (dna::Face param of the organism’s main) an application is started as. |
HALE_DNA_FACE_PORT=<port> |
set by the host | The loopback port a named face serves on, the port of its literal (also its one argument). |
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, and the compose projects run from under them. Without it, dna::scratch_done and the sweep take every compose project whose working directory is under a root they remove down with it (docker compose -p <project> down -v, found by the com.docker.compose.project.working_dir label on containers, or by name in the root’s .dna-compose-projects), and confirm it by the project’s label on containers and volumes. Neither ever takes down the developer’s local habitat, nor the habitat XDG_DATA_HOME names outside the root (GH #1653). A teardown that fails or leaves anything fails the fixture (or the slice), and keeps the root, its registry entry, the slice’s ledger and the projects it names for another attempt. |
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_FACE_BIN=<path> |
built from the checkout | dna/face/start.sh: a prebuilt standard face (dna/face/standard) to start in front of the head, as --face does. hale dna head hands in the one it built. |
HALE_DNA_PUBLIC_PORT=<port> |
unset (the head’s own) | The port the browser reaches the head on, when the standard face stands in front of it (dna/face/start.sh sets it to the face’s): the origin the head signs in under, answers as and checks a command against. |
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. |