DNA
DNA is the part of a Hale application that governs how the application changes. It ships as a library inside the toolchain (vendor/dna, the dna/core seed of…
Reference material, synced from the compiler repo’s
spec/. The guide is the gentler path in.
DNA is the part of a Hale application that governs how the application
changes. It ships as a library inside the toolchain (vendor/dna, the
dna/core seed of the hale repository) and as the hale dna commands.
This file specifies what the library and the commands promise; the
design is GH #521 and its successor #566; docs/src/dna/ is the guide.
Three memories
Section titled “Three memories”An organism has three memories, and every row kind belongs to exactly one (GH #646).
| Memory | Holds | Lives in | Written by |
|---|---|---|---|
| Structure | what the organism is: positions, routes, workflow definitions and grants as authored, the law, the app, the models catalog | the codebase | the editor, under review |
| Record | how the organism changed and was allowed to: mutations and their reviews, practices proposed and ratified, authority contracted or revoked, connections, provisioning, people, the adoption of the ledger | git, refs/dna/*, one signed commit per row, cloned and synced |
the Board, the organism’s pipeline, a head from its clone |
| Ledger | what the organism did today: intents, tasks, assignments, decisions, bills and receipts, money reserved and settled, schedules declared and their occurrences, concerns, handoffs, effect claims, liveness, the application’s readings | memory: Postgres, in the record’s own schema dna_<identity> |
every writer under its own role — the nodes as the spine’s, each head as its own — through memory’s insert function, the gate (GH #1026) |
The routing table is memory_of in the core (routing.hl): the
case, intent, task, decision, completion, exception,
schedule, receipt, handoff, pressure, instance, effect,
report, lease, claim, reading (an application’s events,
#987) and reflex (what a reflex made the body do, #988) families are
the Ledger’s, as are
concern.requested / .raised / .refused, grant.reserved /
.released / .fenced / .reservation_refused (money;
grant.refused — a grant born wider than its ceiling — is authority
and the record’s), spend.reserved / .settled / .compensated,
body.claimed / .released / .credential_*, model.called,
knowledge.consulted, optimize.refused, budget.exhausted, and a
workflow execution’s own facts (workflow.admitted / .refused /
.ask_refused / .settled, step.registered / .activated /
.completed / .failed, work.settled, attempt.admitted /
.outcome), and a leg’s rows on an attempt (attempt.claimed,
.outcome_requested / .outcome_refused, .allowance_requested /
.allowance_granted / .allowance_refused, .unresolved,
.released). Every other kind, and any
kind a build does not know, is the record’s. Routing is versioned and
is a fact of the record: version 0 is every kind in the record;
version 1 is the table; an organism is on the version its last
ledger.adopted row names, and on 0 again after ledger.abandoned.
There is no dual-write: a kind has one home, and a writer that cannot
reach it fails rather than writing elsewhere.
RoutedJournal is the Journal an organism and the host stand on:
the record’s journal and the Ledger’s read as one merged sequence (a
row’s seq as read is its merged position; rows name each other by
id), appended to by kind under the record’s routing. The Ledger is
MemoryLedger (dna/core/memory_spine.hl) over PqLedger (Postgres,
the record’s dna_<identity> schema, one insert conditioned on the
tail, the claim kinds effect.requested, spend.reserved and
task.born unique by (kind, entity)), opened on first use as the
role its process’s environment names (Memory, below). There is no
service in front of memory (GH #985): a part that reads or writes it
opens Postgres itself, and the role’s grants are the trust boundary.
The organization’s Ledger carries the body lease and the epoch the
host handed it (HALE_DNA_LEASE, HALE_DNA_LEASE_TOKEN): an append
under a lease that is not held, is held at another token, or has
expired lands nothing (fenced: …).
Adoption is explicit, per organism and one-way. hale dna ledger adopt, with the record synced, appends ledger.adopting {routing, by} to the record: a request, which the spine carries out on its
tick (The spine, below). It copies every operational row of the
record up to that row into the Ledger keyed by its commit (so a copy
interrupted anywhere is rerun, never repaired), records the cutover,
carries the running body’s lease into memory at its token (unless
memory already holds a lease under that key), and appends
ledger.adopted {routing, checkpoint, rows, by}: the checkpoint is
the ledger.adopting row’s commit, the record head the copy was taken
at, and from that commit on operational kinds never go into the
record. Historical operational rows stay in git, read-only; readers of
history (history, the projections, the export) read both memories as
one. hale dna ledger abandon --why appends ledger.abandoning {by, why}; the spine empties the Ledger and appends ledger.abandoned {by, why}, and the organism is on routing 0 again with nothing lost.
Until a body with memory runs, an adoption or abandonment waits in the
record. hale dna run on an adopted record refuses to start without
HALE_DNA_MEMORY_DSN_SPINE (#646 decision 3: a body without its
memory admits nothing); dev applies memory’s schema and hands the
host the spine’s DSN. hale dna ledger [status] says the routing,
whether memory is named here, and the Ledger’s row count and cutover;
hale dna ledger rows prints the Ledger’s rows as JSON lines (seq,
kind, entity, body, author, writer — the role that wrote it, so a head writing as host is told apart from the body — prev, digest), read under the
head’s role or the spine’s.
Writers write directly; the store is the gate (GH #1026). A head —
the CLI, hale dna ui, the API — writes a Ledger row itself, straight
into memory under its own role, and a node writes the organism’s rows
under the spine’s; nothing sits in front of memory and no process
admits anything. Every Ledger row lands through memory’s insert
function, ledger_append(expected, kind, entity, body, author, lease, epoch, request), a definer function run as the schema’s owner, and the
calling role is who wrote it (session_user, kept in the row’s
writer column beside lease and epoch). It refuses, returning the
reason the verb prints:
- a kind that is not the Ledger’s (
ledger_kind, the routing table above as data): 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).
On routing 0 an operational row goes into the record, as it always did.
An adoption and an abandonment are the record’s ledger.adopting and
ledger.abandoning rows above, carried out by a node under a claim on
the asking row (Claims by id, below).
The org chart in memory (GH #1026, #1123). The checks above read
what the spine projects, and the org chart is the graph’s (The
repository’s graph, below): org_members(person, owner) is a view
over the graph’s live holds edges whose position side is an
organization node — a person holding organization:<o> is o’s
member, and owner is the organization’s name — so it moves with the
graph’s own projection and nothing rewrites it; org_retired(person, seq) is the record’s person.retired rows, projected in the same
per-row transaction as the graph (and emptied with it on a rebuild).
The head reads both, so a verb that checks before writing — a retired
person, a transfer’s owner — says so in its own words; the store’s
check is the gate, the head’s the message.
The owners’ roles (GH #1026). Over a shared record each owner’s
heads write the Ledger as that owner’s role, <schema>_head_<owner>,
which the migration creates for every owner the owners’ keys name
(HALE_DNA_OWNER_KEYS, <owner>=<key> …, in the owner’s environment
at migration) and prints as HALE_DNA_MEMORY_DSN_HEAD_<OWNER>=<dsn>;
the plain head role of a shared record reads and writes as no one. A
role is who a head is: memory holds no signature bytes, and the keys
stay the record’s until the vault holds them (#989). The host is a
principal: it writes its own rows as host, under the spine’s role
or an owner’s, and its body.* rows carry owner in their body when
the record names one, so a row by host still says whose body it was.
Who a person is remains the head’s to establish — its issuer under
dna.principal = oidc.
Admission, as a reader checks it (GH #1026). A row of the record
counts only when dna::row_admissible(record, row, trust) says so:
under dna.trust = signed its commit carries a signature the record’s
trust verifies (git’s keyring or allowed-signers file), and an unsigned
row, or one signed with a key the record does not know, is refused;
under local trust every writer is trusted. The projection runs it on
every row it applies and moves its stamp past a refused row without
projecting anything of it. A Ledger row needs no reader’s check: the
insert function refused it before it landed.
Evidence and the export across two memories (stage 4, #653).
Receipts are placed by what they evidence: evidence.* (a
verification step’s output, a diff document — evidence of a mutation)
and review.reasoned are the record’s; receipt.* — a bill filed, its
class, its disclosure, its reads, its holds, its redaction — are the
Ledger’s, whatever the class, and the body itself is where it always
was (a git blob for an internal body, memory’s sealed store for a
protected one; only the digest is ever a row). A redaction of a git
receipt after adoption is therefore a Ledger row, and the body it names
may be a blob filed before adoption (a head’s redaction of a protected
body is a row of the record, which the spine reads to erase it): sync applies redactions and candidate drops
over both memories read as one, so the treatment holds across the
split. The books export is a supported ledger query: it reads hale dna ledger rows beside the record’s rows and never SQL of its own.
Recovery between the two memories needs no transaction spanning them:
every step that touches both writes its intent in the memory that owns
the decision, acts, and writes its outcome, and the restart scans the
merged journal — a task settles on its mutation’s outcome, a settled
review births its task once, an effect claim is resolved by evidence
or gated as unknown, a retirement’s reassignments are reapplied, a
filed body without its row is filed again — exactly as they did over
one journal, because the routed journal is that journal. The
continuity gate is a pre-split record built with the real git
journal — unfinished work, a reservation, a practice bound to a handed
task, a retired participant, redacted evidence, a pending handoff, a
schedule — adopted through the same steps a live organism takes, its
projections read before and after, and a redaction after adoption
removing a body filed before it (dna_ledger.rs).
Handoffs between records (stage 5, #662). A handoff crosses
between records as an envelope through an Exchange, the core’s
contract for one record’s mailbox in another: peer_identity,
deliver (idempotent by the envelope’s kind and entity — its entity
is handoff:<id> and the id is the handoff’s origin, peer, kind and
subject, so a delivery repeated after an interruption is one
envelope), delivered (whether the peer already holds it), received
(every mailbox of this record). The exchange is git’s alone: mailbox
refs in a private peer cache of the other record’s remote,
refs/dna/exchange/<sender>, beside the identity each record
publishes under refs/dna/identity. hale dna connect refuses an
http:// or https:// url: a connection exchanges through the other
record’s git remote. The exchange keeps the contract #646 set:
durable delivery — a handoff published here whose envelope the
peer does not hold is delivered again, once, by hale dna handoff sync
from the envelope kept with its handoff.published row; a delivery
that cannot reach the peer at all records nothing and says to deliver
again; the disclosure scope is the connection’s classes, checked
before anything is delivered and again before anything is admitted;
settlement only on the receiver’s acceptance — the origin’s task
moves to transfer_accepted when the receiver’s handoff.accepted
envelope reaches it through the exchange, never when a delivery
returned.
Coordination in memory (stage 2, #651). On routing 1 every lease
is a row of memory’s claims table, swapped by its token:
MemoryLeases is Coordination over current(key) and put(key, lease, expected), and a put lands only when the stored token is the
one expected (0 for an absent lease). GitLeases — the record’s
coordination — takes a mutation lease from the record’s cells on
routing 0 and from memory on routing 1, reading the routing from the
record whenever its head moved: an adopted organism’s leases move with
it, and nothing in its wiring changes. The body lease moves the same
way: on routing 1 it is the row body (owner/<owner> over a shared
record), the fence renews the row every third of its life with every
call bounded, a stale token — a host that lost the lease to another —
lands nothing, hale dna body reads it there, and refs/dna/lease/*
is not written. A head takes and fences a lease under its own role.
Effect claims are unique rows of the Ledger ((kind, entity)), so the
contended-claim row the git rule needed does not arise. Memory lost
after activation is the case the git lease already handles: the
renewal fails, the fence keeps the lease it last proved until the
margin before its expiry and then stops the organism, and status
says THE LEDGER IS UNREACHABLE and that nothing is admitted until it
answers.
Memory
Section titled “Memory”Memory is Postgres, and only Postgres (GH #985). Its stores are the
core’s — dna/core/memory_store.hl (the graph, Pq),
memory_ledger.hl (PqLedger, PqLeaseStore), memory_protected.hl
(PqProtected), memory_embed.hl, memory_schema.hl (the schema,
the roles, the grants and the functions) and memory_spine.hl
(Memory, MemoryLedger, MemoryLeases, MemoryKnowledge,
MemoryVault, LedgerAdoption) — and they open Postgres through
pond’s driver, pinned under dna/core/pond and vendored beside the
core at vendor/dna/pond. The projection tail, MemoryProjection, is
not in the core: it lives in dna/operations/memory_tail.hl, beside
the command codecs it decodes admitted facts with, and runs on the
host’s tick (The spine, below).
- 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 seed compose database’s owner DSN namespostgres-owner-<seed>the same way. 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 to migrate: byhale dna memory migrate [dir], byhale dna devbefore it starts the host, byhale dna upgradewhen it is set, and by a provisioned body, whose env file carries it for thehale dna devits unit runs. Without it,memory migrateanddevuse the databasedna/compose.yamlbrings up.hale dna memory migrateprints 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:devsets it from the migration, andhale dna runtakes it from its own environment; both takeHALE_DNA_MEMORY_DSN_OWNERandHALE_DNA_MEMORY_DSN_HEADout of the host’s. With no database to migrate,devsays why and starts the host without the owner’s DSN. 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 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 routing table 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 dna memory migratewith the owner’s DSN; it also refuses a schema whose claim names another record (it is not read). The host checks the version on the spine’s DSN before it starts anything, and refuses to start (exit 2) on a mismatch. - Grants. A head’s role — the head’s, or an owner’s — has
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 but an adoption’s copy 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), carries out an adoption or abandonment
the record asks for (LedgerAdoption, under a claim on the asking row),
and erases the protected bodies the record redacted
(MemoryVault.complete_redactions). Nothing coordinates the nodes; the
stores serialize them — the projection a row at a time by
compare-and-swap, an adoption by its claim, an erasure by digest (it
is idempotent). There is no spine lease and nothing is admitted: a
writer writes, and memory’s insert function is the gate (Writers
write directly, above). Sync before project: when the record has
a remote, a node projects only as far as the head the remote holds
after that tick’s sync, and not at all on a tick whose sync did not
complete, so a stamped row is always a shared one — a node that
projected a row it never shared and then died would leave every other
node waiting on a commit none of them can receive. With no remote there
is one clone, and nothing to wait on. A host with no
HALE_DNA_MEMORY_DSN_SPINE says so and runs with no memory: nothing is
projected. What failed on a tick is said once, when it changes, and
tried again on the next.
Claims by id (GH #1026). Every non-idempotent act a node takes is
claimed by id, with an expiry, in memory before it is taken, so any
number of nodes may run one organism over one record with no
coordinator. The table is claims(key, holder, token, until, present); the body lease is one of its rows. MemoryLeases.claim(key, holder, ttl) takes a key that is free, expired or already this
holder’s, with one conditional write — of two holders racing for a key
one lands and the other finds it taken — and answers with the claim as
it stands. Expiry is the database’s clock, never a node’s: the write
computes until from Postgres’s now() and compares against it, so
nodes whose clocks disagree agree on when a claim ends. A renewal by
its holder keeps the token; any other take raises it.
release_claim(key, holder) gives it back on completion, its token
kept so a late renewal is refused. A holder that dies leaves
its claim to expire and another finishes the act. Without memory (no
DSN) there is no one to race and a claim is granted. The substrate
names its node by HALE_DNA_NODE (the host sets it to the body’s
holder, user@host:<clone>; otherwise this machine and process) and
claims before it spends: an ask is claimed as plan/<intent>, and a
human case the leader is asked to plan as plan/case:<case>, for
claim_ttl (300 s) before the leader is asked, and, once
claimed, the record is read again, so an ask the claim’s previous
holder admitted meanwhile is answered with that admission and the
claim given back; a node that finds the claim taken answers planning elsewhere: <holder> holds plan/<intent> and writes nothing, and the
ask comes back to it with the host’s relay until it is answered or the
claim expires (a case is held and driven again by redrive; memory
that cannot be reached is said as such, not planned: memory could not be reached to claim …). The claim is given back once the plan’s
admission is recorded (the case handed). The optimize pass takes no
claim: it is a scheduled occurrence, one execution under its key
(Schedules). A claim a node acts on is a
claim.taken <key> {holder, token, until} row and its return a
claim.released <key> {holder} row (the Ledger’s), so the record
shows who took what, and a second node’s take after the first one’s
expiry; a claim taken without memory is not recorded.
The genome pull (GH #1026). Nodes converge on the genome by
pulling; there is no push deploy for the organism’s own nodes (the
heart’s deploy is another thing). At start a host fetches the forge’s
default branch (the record’s remote, its HEAD), moves its genome
forward to it when it descends from what is checked out, and checks and
builds it — the application’s artifact, the organization and, under
dev, the application, each through the build cache. A genome that does
not check or build is a node.build_failed <holder> {sha, why, by, owner} row; the node returns its checkout to the last genome that built
(.hale/dna/genome.built), builds that, and serves it. A node that is up
records node.started <holder> {sha, by, owner}. On its tick, every
HALE_DNA_GENOME_POLL seconds (300 by default; 0 never), it fetches
again, into refs/dna/remote/genome (never FETCH_HEAD, which any
other fetch in the clone rewrites); when the forge’s head is neither what it runs nor a sha that did
not build here, it stops cleanly — the organization and the expression
drain on SIGTERM, the body lease is given back (body.released {why: "genome"}) — and exits with NODE_RESTART (75); its unit
(Restart=always) starts it again, and it builds the new genome at
start. It never replaces itself in place. A forge head that did not build
here is said once and waited out. Each poll writes
.hale/dna/genome.json: sha, failed, polls. hale dna status
carries a genome: line: the sha each node runs, from its last
node.started, and a newer one that did not build since.
The record
Section titled “The record”The Journal is a git branch, refs/dna/journal, in the governed
repository:
- One commit per event. The commit’s tree holds
journal.jsonl, every event so far, one JSON object per line:seq,kind,entity,body,author. The commit’s subject is<kind> <entity>. - The commit DAG is the chain. An event’s digest is its commit;
its
previs the parent; the first event has no parent. Integrity is git’s: the ref resolves, and its commit count is the event count. - Append is compare-and-swap on the ref. A writer builds the next
commit on the head it read and updates the ref with that head as the
expected old value. A writer that lost the race reloads and, when it
was appending at the tail, re-appends at the new tail;
seqis the position in the record, never a promise made before the append. A lost race is not a failure: the append goes round with the new tail until it lands, within a bound only a wedged repository reaches. - Authorship is git’s. The organism’s own events carry its configured author; a human’s facts (a verdict, an intent through the CLI, a host’s crash accounting) carry the git identity of whoever ran the command. Commit signing, where the repository requires it, applies unchanged.
- Receipts are blobs under
refs/dna/receipts/<sha256>, filed by the digest of their content: a verification step’s output, a diff document. Events name receipts by digest. A receipt has a data class (GH #606): apublicorinternalbody is such a blob; acustomerorconfidentialbody (protected_class) never is. 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, sosyncnever carries a protected body into a clone. Reading one is an act in the reader’s name for a purpose (hale dna receipt show <digest> --purpose <p> [--as <who>]): it is refused unless 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: a git receipt’s ref is deleted (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 a git receipt’s redaction only at the head it read the hold at and, once the organism has adopted the ledger, only at the ledger revision it read the hold at (the request’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. Everysyncdeletes redacted receipts’ refs from the clone and the remote, so a clone that fetched one drops it at its next sync; the blob stays in each object store until git collects it (git gc --prune=now), and a copy disclosed or cloned outside the record cannot be recalled. The projection retires an idea whose receipt was redacted instead of stopping. - Leases are blobs under
refs/dna/lease/<key>(:in a key becomes/),holder,token,expires,presenton four lines, compare-and-swapped on the ref. Tokens are monotonic per key. - Effects are requested before they run, and the claim is exclusive
(GH #604 rule 3).
effect.requested <key>is appended before a dispatch 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. - Sync.
hale dna sync(and the host, every tick) fetches the remote’s record intorefs/dna/remote/journal, reconciles, and pushes. Local ahead: push. Remote ahead: fast-forward. Diverged: the local-only events are re-appended on top of the remote’s head, bodies and authors unchanged,seqtheir new position, then pushed; a push the remote refuses is fetched and reconciled again. A fetch is a read (GH #961, #1072): two at once in one clone — the host’s tick and a verb beside it — race on git’s lock of the ref they update, and the one that loses is tried again, up to five times, waiting 100 ms more before each (the other already moved the ref at least as far). A fetch that still fails is a read that did not happen and refuses the sync, the connect or the handoff with git’s reason; only git’s own “couldn’t find remote ref” is a remote that holds nothing under the family. The reconciled chain is built beside the ref and swapped in with one compare-and-swap: the ref never points at a record missing a row it held a moment before, so a reader’s position only grows, a writer that reloads after a lost race finds its own rows, and a clone that appended while the chain was built is not overwritten — the swap fails and the reconcile goes round. A local-only event with no row refuses the reconcile rather than being skipped. A fact’s identity in the record is its content and its place among rows of the same content, never itsseq(a reconcile renumbers, and a node raising one concern three times writes three identical rows, each a fact): a host relays and handles by that identity, so a renumbered fact is not relayed twice and a repeated one is not relayed once. A reconcile rebuilds only what this clone may sign (GH #639). Underdna.trust = signedevery local-only row is read before any chain moves: a row this clone signed is rebuilt and re-signed, and its commit names the original (Rebuilt-From: <commit>in the message’s body); a row never signed is rebuilt unsigned, so a reconcile upgrades nobody’s provenance; a row signed with another key refuses the whole reconcile — the row and the key it was signed with are named, every local row and commit stays where it is, the remote is untouched, and the refusal says what keeps the work (that row’s writer syncs first, since a writer rebuilds its own rows; or the remote is fast-forwarded once it holds the row). A fast-forward is never refused. Underlocaltrust nothing is read and every row is rebuilt as before. A remote with no record yet is the local one’s push, not “up to date”: the first sync after an ordinarygit push origin maincarries the record, so no one has to pushrefs/dna/*by hand for a second clone to have the organization’s history. Receipts and candidates travel by refspec both ways; mailboxes are received. The remote isdna.remotein git config, ororigin. A plain clone has no record until it syncs. - Candidates are kept. The gateway points
refs/dna/candidates/<mutation>at a candidate when it commits it, and sync carries the pointers with the record, so a refused or revised change stays readable as a diff after its worktree is gone:hale dna candidateslists them 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 stops keeping it — here, at the remote, and at every clone on its next sync, the way a redaction removes a receipt. The commits stay in each object store until git collects them. - An apply expresses the candidate, tree and all. An approval
applies exactly the reviewed candidate, or nothing: the candidate’s
worktree head is the pinned digest, the genome’s head is the base the
review was against, and the genome has nothing uncommitted — tracked
changes anywhere, and every input of the candidate’s build that
the tree does not hold. The graph is the candidate’s, read by the
compiler in the candidate’s worktree (
hale inputs <seed>: the seed’s.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. 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). A replica of one record is inside
the horizon: sync carries all of it, to equally trusted readers. A
handoff crosses a horizon into a separate record, and a connection is
its only path; nothing selective leaves a record by sync.
hale dna connect <record-url> --name <name> --as <position> --purpose <purpose> --classes <internal,customer,confidential> [--by <who>](the url is the other record’s git remote; anhttp://orhttps://url is refused as a service url) reads the other record’s identity — its genesis, the root commit of its journal, which every record publishes as a blob underrefs/dna/identitywhen it syncs (until a push of it to the remote as it is now has succeeded —dna.identitypublishednames that remote and blob — each sync asks the remote and pushes it, never forced, andsyncsays when it could not) — and nothing else of it; it refuses this record’s own, and appendsconnection.proposed(name,url,peer,position,purpose,classes,by,review_id) 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.hale dna handoff <name> task <id> | receipt <digest> [--note …] [--as <who>]writes onehandoff.receivedenvelope into the other record’s mailbox for this record,refs/dna/exchange/<this record's identity>— a chain of rows only this record writes, pushed there under compare-and-swap. Neither record reads the other’s journal: this clone’s cache for a connection (.hale/dna/peers/<name>.git) holds the other record’s identity and this record’s own mailbox there, nothing else. The envelope carrieshandoff(hand twelve hex digits of the origin genesis, kind and subject, so a fact crosses once),origin_record,origin_url,origin_author,origin_row(the source row’s digest),lineage(the subject’s rows here, askind#seq),purpose,position,via,kind,subject,class,fact,note. This record then appendshandoff.published(withpeer_row, the commit it landed as) and, for a Task,task.transfer_requested. A handoff retried after a crash completes what the first try left (#637): an envelope already in the mailbox is reused, never written twice, and a published Task missing 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 written across. The receiving record reads its own mailboxes (syncfetchesrefs/dna/exchange/*from its remote; a row claiming another origin than the mailbox it is in is ignored) and admits what arrives under its own policy:hale dna handofflists a received handoff as admitted only under a connection in force back to its origin record that carries its class, andhale dna handoff accept <id> [--as <who>], only for an admitted one, appendshandoff.acceptedto its own journal (with the envelope’s origin, lineage, purpose and fact) and sends ahandoff.acceptedenvelope into the origin’s mailbox for it.hale dna handoff syncreads, for every connection in force, the envelopes the other record sent to this record’s mailbox: each acceptance of a handoff published through it is admitted once, 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. - 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 underrefs/dna/receipts/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) declares it in itsratifystep; 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 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 — the URL’s host is parsed, userinfo refused, never matched by prefix): 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;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; none given, none served, and 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 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 git identity, 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 row only when its commit carries a signature git verifies (git verify-commit; git’s keyring or allowed-signers file is the list of who may act), and a row that does not is refused in the record in the host’s name (intent.refused,review.refused,concern.refused: “unverified writer”) and never relayed. Undersigned, rows this clone writes are signed commits. Signing is one mechanism for this edge, not the boundary itself: a hosted head is admitted through a reviewed mapping of principals to authority and acts as the user, never as itself. The organism’s answers (intent.offered,task.born,review.settled,review.refused) return the same way. The body 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: beside a live organism once the row is appended, from a clone with no organism once the row is appended and synced. “Beside a live organism” is the record’s body lease: live, and held by this clone’s body.
The nerves (GH #986) are NATS JetStream, through pond’s client and
std::bus adapter, vendored under dna/core/pond/realtime/nats.
- Bindings where the publishes are. The host binds every DNA fact
topic it publishes (
IntentOffered,ReviewVerdict,ExpressionObserved,PressureRaised,ConcernRaised,PracticeRequested,HoldRequested,PositionOpenRequested,GraphReviewRequested,ScheduleRequested,KnowledgeNodeRequested,KnowledgeBindingRequested,KnowledgeEdgeRequested,WorkSubmit,WorkAllowanceAsk,RowLanded) 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 (whichinitwrites 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.hale dna nerves migrate [dir]creates or updates the stream with the owner’s URL (HALE_DNA_NATS_URL_OWNER, or thenervesservice ofdna/compose.yaml, brought up) and printsHALE_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;hale dna devruns it before it starts the host and hands the host the spine’s URL and the token, never the owner’s nor the head’s;hale dna 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>anddna/nats.conf‘s$NATS_APP_<NAME>_PASSWORD, and it may publish on<org>.app.<name>.>and nowhere else; no other application’s subjects, nor the reflexes’. It is provisioned whenapplication.attachedlands (hale dna initlands it, 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 leavesdna/nats.conf, its password the include and its entry the vault, and compose’snervesis restarted, 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.
dna/nats.confis a tracked seed file with no secret in it: itincludes./nats.secrets.confand names each user’s password as its variable ($NATS_<ROLE>_PASSWORD, and the attached application’s$NATS_APP_<NAME>_PASSWORD).dna/nats.secrets.conf, written by the bootstrap from the same draw as the vault’s entries, is the one file that holds a password: mode 600, ignored by the project’s.gitignore(whichinitandupgradegive the line), mounted into compose’snervesservice. - The guard. The writer refuses a secrets file git tracks or would track.
- Rotation.
initandupgradedraw every role’s password anew (a regenerated seed rotates them); a server compose is running is restarted to read them, and a restart that fails is said.upgraderewritesdna/compose.yaml, which mounts the secrets file. A server of the operator’s (HALE_DNA_NATS_URL_OWNER) takes the conf and the secrets file again after each rotation.
- The server’s files.
- 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.
- Shared records. Over a shared record that keeps its rows in git (routing 0), each owner’s clone appends on its own, and only the ledger can decide an id across owners. There readings wait in the stream until the ledger is adopted, and the host says so once. They wait at most as long as the stream keeps them (a week).
- Not a reading. A message that names no application and event, or whose body is not such an object, is not a reading: the host says why and records nothing.
- Acknowledgement. The host acknowledges the durable’s message once
its row has landed, or once it has been refused as a duplicate or as
not a reading. One whose row could not be written stays
unacknowledged and comes again, so a host that stops between the
hand-off and the row loses nothing — within the stream’s retention:
an event not landed within a week (
NERVES_MAX_AGE_SECS) ages out of the stream unrecorded. One whose write keeps failing comes again every ack wait (30 s) and holds one of the durable’s 64 places.
- Rows landing. For every row its view of the organism gains, a
node publishes
head.row.landed(RowLanded: the row’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.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. - 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;devkeeps them fromnerves migrate, a node is started with them), and none of the organism’s other credentials: the spine’s, the owner’s, the head’s and the reflexes’ NATS URLs and memory DSNs are taken out of what it inherits. A node materializesvendor/dnain its clone for its toolchain, so an instance can import pond’s NATS client from it. Local mode is the seed compose’s nerves.
.hale/dna/ holds only what is not the record: the status projection,
worktrees, scratch inputs to the toolchain. Deleting it loses nothing
the record holds.
Event kinds
Section titled “Event kinds”Every kind names its memory. The memory column is the routing
table under version 1 (memory_of in routing.hl); on routing 0
every kind is the record’s, and a kind no build knows is the
record’s.
| kind | memory | what it is |
|---|---|---|
application.attached |
record | the application the organism oversees: its entrypoint, its artifact, the toolchain, and its name, which its broker account carries |
application.detached |
record | the attached application removed (hale dna application remove): its name and who; its broker account is revoked |
structure.observed |
record | the compiler’s model of one of its parts, at init |
graph.node |
record | a node of the repository’s graph, entity <kind>:<name> (The repository’s graph) |
graph.edge |
record | a hyperedge of it, entity its id: kind, members {role, node} in order, via, outside |
graph.retired |
record | the node or edge the entity names leaves the graph; a ratified graph proposal’s receipt may carry one too (a re-ingest’s retirement), with the same effect |
graph.requested |
record | hale dna ingest asks the running organization to open the Review it filed (request_id, review_id, by), relayed as GraphReviewRequested (dna.graph.requested) |
graph.proposed / graph.refused |
record | the organization opened that Review (routed, its signers told), or could not; graph.refused <row id> is also memory’s refusal of a graph row it would not project, in the record or in a ratified proposal (kind, why, row, by: memory) |
graph.ingested |
record | hale dna ingest read the repository at a commit (entity the commit; added, changed, retired, the review_id of the one proposal it filed, "" when nothing differed) |
hold.requested |
record | someone asks the organization to propose a holder for a position — a person or an organization — or a member for an organization (hale dna fill, or Commands::fill, whose row carries the command’s fields after the CLI’s) |
batch.proposed |
record | a batch of changes hale dna apply (or vacate, or pack apply) proposed as one family Review in the group changes (digest, review_id, by, steps, pack for a pack: <name>@<version> sha256:<digest> <source>, and planned_on for one planned on pending proposals, <review id> <digest> a line; through Commands::apply_batch, the command’s fields after those); its graph.requested, under the same id, asks the organization to open the Review |
position.requested |
record | someone asks the organization to open a position at run time, with its mandate (hale dna position open: position, text, mandate, under, by; Commands::open_position adds the command’s fields after them), relayed as PositionOpenRequested (dna.position.requested) |
position.proposed / position.refused |
record | the organization proposed the position’s node and its mandate as one family Review, group positions (digest, review_id), or why not (a name the graph already states, no mandate, an under that names no node the graph states: the CLI resolves a bare --under <name> to the seed, work part, position or process the graph states by that name) |
hold.proposed / hold.refused |
record | the organization proposed it to the Board (digest, review_id), or why not; hold.refused <hold id> is also memory’s refusal of a hold it would not project (why, row, by: memory) |
responsibility.proposed |
record | a one-line responsibility inferred for a part, not yet ratified |
law.deferred |
record | a clause init could not certify |
intent.requested |
ledger | an ask from a clone with no organization running |
intent.offered / intent.refused |
ledger | the outcome an ask was admitted for, or the refusal |
intent.unrecovered |
ledger | an intent offered before a restart that no admission names; never re-offered |
review.verdict |
record | a verdict appended in the reviewer’s name, from a clone or a forge |
task.born |
ledger | the work an intent or a settled review made |
task.handed |
ledger | handed to a person: assignee, obligation, acceptance |
case.admitted |
ledger | a human Work’s case, its own handed Task (card 16): parent Task, Work and attempt, objective, assignee, obligation, acceptance, required evidence, its origin (owner), disposition (to) and performer (version 2, card 17) — before its task.born / task.handed, which say what it recorded; its completion (task.done, decision.reported) is the attempt’s outcome (card 17) |
task.reassigned |
ledger | the assignment moved to someone else |
task.<state> |
ledger | a case’s states, to done or failed; an admitted execution’s root is settled by workflow.settled, its workflow’s row (card 18) |
mutation.requested |
record | which exact Work and attempt asked for the Mutation, with the request as asked; written before mutation.proposed (card 14); for a leg’s candidate also the leg (leg), its commit (leg_commit) and the receipt that is its patch (patch, GH #1156) |
mutation.proposed |
record | a change proposed for a Task: class, objective, target, base |
mutation.worktree |
record | the sandbox opened for it, and removed |
mutation.located |
record | the files found, and the grant they were found under |
mutation.candidate |
record | the commit the editor produced |
mutation.<disposition> |
record | what the autonomy boundary decided: review, stage, escalate, release, deny |
mutation.topology |
record | the diff names a plan or the manifest: re-classed for the Board |
mutation.applied |
record | the candidate applied to the genome |
mutation.apply_retried |
record | the apply ran again on a settled review |
mutation.retained |
record | kept after its observation window |
mutation.rolled_back / mutation.rejected / mutation.revise |
record | the genome back at the base, refused after review, or sent back for another pass |
mutation.refused / mutation.failed |
record | not applied (the candidate moved, the gate said no), or the change did not survive its own verification |
effect.requested / effect.result |
ledger | the exclusive claim on an effect key, and its outcome |
effect.relayed |
ledger | an attempt a leg relay answered pending: a leg’s to claim at the head, once per attempt |
evidence.<step> |
record | a verification step’s output, kept by the digest the row names |
evidence.magnitude |
record | the measured magnitude of the change |
review.requested |
record | the Review: question, authority, candidate, disposition, evidence, diffs |
review.routed |
record | what a routed Review requires: definition, signers (<position>=<holder>,…), gates, fallback; written when it opens and again when its holders change (the latest is the requirement) |
review.signed |
record | an approval a routed Review admitted that settled nothing: by, awaiting |
review.settled / review.refused |
record | the verdict that decided it, or why one was not admitted |
review.reasoned |
record | the deciding verdict’s comment — a person’s note or the Leader’s reasoning — right after review.settled (hale dna review <id> renders it as why:) |
candidate.dropped |
record | a kept candidate is no longer kept, here and at every clone’s sync |
knowledge.proposed / .ratified / .declined / .refused |
record | a practice through its review |
knowledge.retired |
record | a practice superseded by a later version |
library.family.refused |
record | a library family approved while a member was refused (its predecessor was replaced); names the refused members |
knowledge.consulted |
ledger | what a piece of work looked up today |
grant.contracted |
record | authority narrowed, and what it leaves |
grant.revoked |
record | the parent took the authority back |
grant.refused |
record | a grant born wider than its ceiling: authority |
grant.reservation_refused |
ledger | a spend the window would not admit, naming the field: money |
grant.reserved / grant.released |
ledger | a spend admitted against the window, and the reservation settled at what was spent |
grant.fenced |
ledger | an admission refused because the grant’s epoch moved since |
budget.exhausted |
ledger | the window’s model allowance is spent |
model.called |
ledger | a model call and its evidence |
model.rule |
record | the model mapping’s rule for a selector (default, position:<name>, contract:<Name>, task:<id>): the size it needs and the sizes an override may move it to (size, permit, by); the latest row per selector is the rule (dna/core/model_map.hl) |
model.category |
record | which model fills a category <mode>.<size> today, whether it returns tool calls and its price (model, tool_calls, input_micros_per_1k, output_micros_per_1k, by); the latest row per category stands |
optimize.refused |
ledger | the organization’s pass over itself did not run |
node.started / node.build_failed |
record | a node runs a genome, by its sha; a genome did not check or build, and the node stayed on the last that did (sha, why) |
violation.recorded |
record | a closure the organism absorbed (GH #989), recorded by the part that owns it, which goes on: {kind, subject, owner, detail}. adapter_undeliverable: a node’s connection to the nerves collapsed, a publish the stream did not acknowledge in its window (subject nerves, owner the node’s holder); the node stops and exits 75 for its unit to start it again. lease_unsettled: a leg’s lease expired with no outcome (subject the attempt, owner spine), once per lease, before the attempt is asked again. pulse_stopped: the heart’s pull on its durable refused while the connection held (subject heart, owner the spine’s holder), once per outage; the durable is made again. The board shows the latest five |
observation.requested / observation.refused |
record | the host’s observation report as a row (mutation_id, outcome, model_hash, detail), relayed until expression.observed answers it; refused, unrelayed, when the row is not verified under signed trust (GH #986) |
claim.taken / claim.released |
ledger | a node took a claim by id before acting — plan/<intent>, plan/case:<case> — and gave it back: holder, and for a take its token and until |
attempt.claimed |
ledger | a leg’s claim on an admitted, outstanding attempt, taken at the head (GH #946): the lease (holder, token, until), the principal that took it (principal_mode, principal_name: whose outcome the lease admits), the attempt’s task, work and performer kind, and the command that took it |
attempt.outcome_requested |
ledger | the outcome a leg handed back under its lease: the disposition, the result (result, result_ref), the receipts it filed (evidence_ref), the calls it made (calls), the hat it wore (hat_digest, hat_head, hat_watermark, prompt_digest, renderer) and the command (request); relayed until attempt.outcome or attempt.outcome_refused answers it |
attempt.outcome_refused |
ledger | why the owner would not settle a leg’s outcome (why, holder, token, request) |
attempt.allowance_requested |
ledger | a leg’s ask for its attempt’s spend, under its lease (holder, token, request, at, and the command); relayed until attempt.allowance_granted or attempt.allowance_refused answers it (GH #1131) |
attempt.allowance_granted |
ledger | the one gate admitted a leg’s spend: what the attempt may cost (allowance_micros, -1 when nothing bounds it), the ceiling, the window’s spend, the lease and the request |
attempt.allowance_refused |
ledger | the one gate admitted no spend (why): on a leg’s attempt (naming the request), the editor’s attempt, or review:<id> for the leader’s Review |
attempt.released |
ledger | a leg gave its lease back without an outcome (holder, token, why, and the command); the attempt is another leg’s to claim (GH #946) |
friction.filed |
record | what got in a position’s way (position, attempt_id, text, and the command), filed on the attempt it was met on, else on the position; nobody admits it, it is a fact (GH #946) |
org.reviewed |
record | that pass’s own answer |
person.retired |
record | someone left, and who took their work |
body.claimed / body.released |
ledger | who is running this record, by the lease’s token |
body.provisioned |
record | a machine made able to run it |
body.credential_missing / body.credential_present |
ledger | whether the model’s key is in the vault where the body runs |
secret.rotated |
record | a credential set or rotated — the name and the place, never the value |
schedule.requested / schedule.answered |
ledger | a schedule asked of the organization (hale dna schedule declare), relayed by the host; its answer under the request’s id: declared, why |
schedule.declared / schedule.refused |
ledger | a schedule a ratified practice or a request declares (every_ms, cron, definition, args, convener, from); or one that would not be declared (why, and hole for a convener nobody holds), or an occurrence whose admission was refused (occurrence, why) |
schedule.skipped / schedule.missed |
ledger | an occurrence not admitted while the last execution is open (occurrence, task); occurrences whose period passed with no tick (first, last, count, why) |
schedule.paused / schedule.resumed |
ledger | paused and resumed by hand, in your name |
receipt.classified / receipt.withheld |
ledger | a protected body memory keeps sealed, or one withheld because no memory could keep it |
receipt.disclosed |
ledger | a reader authorized, for a purpose |
receipt.read / receipt.read_refused |
ledger | a read in the reader’s name, or its refusal |
receipt.held / receipt.hold_released |
ledger | a hold that refuses redaction, and its release |
receipt.redacted |
ledger | the body removed, the digest kept |
concern.requested / concern.raised |
ledger | a concern from a part about the part above it; the request carries its own request id; the answer is one object (what, severity, occurrence, request, and since GH #995 the parent it is routed to and the concern-escalate execution that raised it, task), so a concern’s words are never read as metadata, and one request is one concern, however often it is delivered |
concern.refused |
ledger | one the organization would not admit |
practice.read |
record | what the hat read once a proposal was ratified, at which head (digest, target, ratified_at, hat, head, included, task): practice-ratify’s hat step (GH #995) |
concern.proposed |
record | three raises became a proposal — at concern-escalate’s raise step once the source crossed the threshold, including the execution a birth admits for a source over it, unproposed |
pressure.requested / pressure.raised |
ledger | a signal from a source; the request carries its own request id and a node relays it until answered (GH #986); the answer is one object (what, count, request) and is written once per request — a row from before is <what> x<n> |
pressure.remeasured |
ledger | the declared fitness signals, measured again after the change |
appendage.proposed |
record | an organ the organization proposes for itself |
expression.restart_requested |
record | the organization asks for a change to be expressed |
expression.restarted / expression.deployed |
record | the shape and build the new expression reports, or what a deployment gateway expressed and judged |
expression.observed / expression.crashed |
record | the observation window’s outcome; crashed names the instance and node on a fleet |
fleet.deploy |
record | a genome revision expressed through the fleet’s nodes |
instance.up / instance.exited |
ledger | a node’s report on one instance of the plan |
github.pr / github.commented |
record | the pull request a Review opened, and the settlement commented back |
gate.observed |
record | a gate’s check run at the candidate as the forge reported it: gate, sha, conclusion, url, forge, run; written only by the host’s forge sync, once per run (GH #1161) |
ledger.adopting / ledger.adopted / ledger.abandoned |
record | the move of the day’s work into the ledger, its checkpoint, and its undoing |
Their bodies are documented in the guide’s reference chapter; the set grows by ordinary change, and a reader that meets an unknown kind must keep walking.
Workflow definitions
Section titled “Workflow definitions”A recursive workflow can be written as data (dna/core/workflow_definition.hl;
the execution contract it serves is dna/WORKFLOW-CONTRACT.md). A
WorkflowCatalog holds definitions: define(id, revision, stores, title)
declares a workflow whose ordered steps each write the one store its word
names — stores holds one word per step, record, forge, genome,
heart, graph, nerves, memory, vault or host, so the step
count is the word count (GH #995). A step is where a fact is written: reading, or waiting on
another writer, is part of the step whose fact it serves, never a step of
its own; leaf(workflow, revision, step, key, work, attempts) adds a required member carrying
the content of one Work request and its retry allowance; child(workflow, revision, step, key, child, child_revision) adds a required member that
invokes another definition. Each returns "" or why it was refused (a
revision defined twice, a member for an undefined workflow).
Definitions refer to each other by id and revision, in two flat
collections.
Before anything is built, expand checks every definition reachable
from the root once, counting each one’s Works and nesting height from
its children’s and saturating at the limits: a catalog whose expansion
would be enormous is refused from those counts, in one pass over the
definitions, without materializing a node of it. The check also holds
for the root, which is depth 1, and for the limits themselves (a limit
below 1 admits nothing and is refused).
expand(root_task, id, revision, limits) binds one execution: the whole
finite tree as BoundNodes (tasks, steps, works) with their ids — a
child Task is <parent>.s<i>.<key>, a Workflow <task>/wf<revision>,
a Step <workflow>/s<i>, a Work <step>/<key> — each Work carrying its
authored request content and allowance (bound_request makes its
WorkRequest), and each Task its definition, parent, spawning step,
depth and definition path. It refuses the whole expansion, binding
nothing and naming what broke, when a definition is missing (by id and
revision), a workflow has no steps, a step has no members, a member
names a step outside its workflow’s steps, a step declares no store
(-), two (record+forge: declares two stores (record, forge)) or a
word that is no store, a step writes or reads a part that is not built
yet (named with its issue: the heart, GH #987; the vault, GH #989; a
host the organization provisions, which only hale dna body provision
reaches today), a member key is not
[a-z0-9-]+ or repeats in its step, a member is neither a leaf nor a
child, a workflow invokes itself directly or through its children, or
an AdmissionLimits value is exceeded: max_depth (8),
max_steps (32), max_members (32), max_attempts (8, the largest
allowance a leaf may bind) and max_works (256), the defaults in
parentheses, supplied by the caller and returned with the Expansion.
Two invocations of one definition are two executions with their own ids;
a later revision changes nothing an earlier revision expands to.
A definition id is [a-z0-9][a-z0-9-]*, checked when it is defined and
when it is read, so an id can never carry the delimiters the definition
path is written with. encode() writes every definition and member as
one JSON document (format: dna.workflow-definitions/3: each workflow’s
steps, its stores, and how it occurs on a schedule — concurrency
and takes) and decode(text) reads one back, refusing a
document that does not close, another format, an id outside the grammar,
an already defined revision, a document that defines one revision twice,
a workflow whose stores do not name one per step, or a concurrency
that is not skip or overlap (an absent one is skip), and adding
nothing then. latest(id) is the newest revision the catalog holds: what
a new Task binds.
How a definition occurs when a schedule points at it (GH #1143,
Schedules) is the definition’s:
occurs(id, revision, concurrency, takes) sets
WorkflowDef.concurrency — skip (the default: an occurrence while
an execution of it is open is skipped) or overlap (admitted beside
it) — and takes, the input names, space-separated, that a schedule’s
args must carry; it refuses another concurrency, or a definition the
catalog does not hold. The baseline’s ask-edit@1 and ask-person@1
take objective. hale dna definitions prints occurs: <concurrency> while open[, takes <inputs>] under a definition only when it says more
than the default.
encode_bound() writes one bound execution as a recipe (format: dna.workflow-recipe/1; an admission nests it as an object of its own,
never escaped into a string) carrying the node count it was written with, and
decode_bound(text) reads one back. It refuses another format, a missing
node array, a count that disagrees with what the document carries, a node
without an identity or of no known kind, a node whose number was written
as text or whose text was written as a number, and a document that does not close: every object,
array and string closed by its own delimiter, with nothing after the
value. That is a closure scan rather than a JSON grammar validator — it
does not check escape sequences or number syntax — which is what reading
back one encoder’s own output needs; a document from outside the system
wants a real parser in front of it. A whole final node also ends in }, so the last
character says nothing about the document. Every refusal binds nothing:
half a recipe is not a smaller execution. Nothing yet executes a bound
expansion.
The workflow catalog (GH #995)
Section titled “The workflow catalog (GH #995)”DNA ships a catalog of definitions, each a chain in which every step
writes one store (dna::baseline_definitions(), in
dna/core/workflow_definition.hl). A project’s dna/org/workflows.hl is
generated — written by hale dna new and rewritten to the current shape
by every hale dna upgrade, whatever an earlier toolchain wrote — and
returns it plus the project’s own (fn workflows() -> dna::WorkflowCatalog),
which live in dna/org/own_workflows.hl (fn own_workflows(catalog):
project-owned, written only where there is none, never rewritten;
what it answers when a definition is refused is kept on the catalog,
refused, which hale dna definitions leads with and fails on, never a
smaller catalog; an upgrade that rewrites a workflows.hl holding
definitions of its own names each line it dropped, for the project to
move into own_workflows.hl),
and the generated organization admits from it (catalog: workflows() on
its dna::Dna; a body given no catalog takes the baseline at birth). The
baseline is the vendored toolchain’s, so an upgrade supersedes it by
revision: a Task born under a revision finishes under it — its recipe was
bound at admission — and a new Task binds the newest (latest).
| id | steps (the store each writes) |
|---|---|
ask-triage |
offered · classify · plan (record each) |
change-deliver |
candidate (record) · review (forge) · verdict (record) · apply (genome) · deploy (heart) · settle (record) |
practice-ratify |
ratify · hat (record each) |
deploy-observe |
deploy (heart) · settle (record) |
concern-escalate |
raise (record) |
optimize-walk |
walk (record) |
secret-rotate |
rotate (vault) · recorded (record) |
body-provision |
provision · start (host) · lease (memory) · observed (record) |
ask-edit, ask-person, ask-judge, ask-position |
the ask’s one leaf: an edit prepared for review, a person’s job, a judgment handed back as an Assessment, or a change made by the position it is routed to (record) |
Every step writes a fact of its own. What #995 listed as steps that write
none is folded into the step whose fact it serves: practice-ratify’s
verdict is the Review’s row, so ratify waits for it and writes the
ratification or the decline; memory’s projection writes the idea and its
binding, so hat waits for it and records the row it was fenced to; the
request and the Review are written by whoever proposes (a person’s
request, a concern, the record’s birth); a concern’s routing is a field of
its concern.raised row and its proposal is written in the same step at
the threshold; the optimize pass’s walk reads what its org.reviewed row
records; deploy-observe’s pulse is what settle waits for, and its
rollback is the failure edge, not a step: the happy path writes nothing
to the genome.
A step the organization performs itself is one leaf that requires: "organism". The engine relays its attempt to the assembly serving the
scope (OrganismRelay, kind organism: StepRequested, answered
pending), which performs the step and reports the attempt’s outcome
(Dna.on_step_requested). A step ensures its rows — performed again
after a redelivery it writes none twice — and one that waits (a verdict,
the leader’s word, memory’s projection) is kept and performed again when
what it waits on may have arrived: a verdict, the leader’s answer, the
tick. The organization performs the steps this toolchain knows, by
definition and key: practice-ratify/ratify, practice-ratify/hat,
concern-escalate/raise, optimize-walk/walk. A definition with an
organism step it does not know — a path not moved onto the catalog yet
(ask-triage, whose ask is still admitted as ask-edit or ask-person
after the leader plans it), or a revision from a newer toolchain — is
refused at admission naming the step, never failed when it runs, as a
definition on a part not built is refused naming the part; both are
workflow.refused rows and nothing else. The organization performs a
step only of a definition admitted directly, so one that invokes a
definition with an organism step anywhere below it is refused at
admission too. A ratification the catalog refuses is answered once per
incarnation — one workflow.refused row — and asked again after a
restart, so a catalog fixed in between ratifies it, and nothing retries
at every reconciliation. hale dna definitions lists
what an admission would refuse each definition for.
- 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).
The record is the local-mode floor; the ledger is the runtime. Every
step is a few rows of the day’s work (step.*, attempt.*,
work.settled, effect.*). Until hale dna ledger adopt, each is a
commit on refs/dna/journal, so an execution costs tens of commits: a
Board deciding a repository’s 35 seeded proposals waits some 15 to 18
seconds for their ratifications on a git-backed record (measured), where
memory takes the same rows in about a second. A git-backed record is what one person working
alone starts on; an organization runs with its ledger adopted.
hale dna review <group> approve|reject writes every verdict of the
group first and then waits for the answers once, bounded by the group’s
size. hale dna review all approve|reject decides every group with a
pending Review in the order a new organization is ratified (purpose,
design, operating, using, library, holes, practices,
positions, changes, holds, ingest), each group waited for before
the next, since a hold waits on the position it sits on. hale dna init --with <pack> [--set <param>=<value>]… [--as <who>] runs init, then
hale dna pack apply --on-holes on the toolchain’s nursery: the pack
proposed as one batch beside init’s own proposals and planned on the
holes init just proposed, so it may fill them (the toolchain’s
founder pack is fill * <founder>, every seat left vacant), and one
review all approve ratifies the starting organization, its seats
included: the holes in holes, then the pack in changes. Planned on
the record alone, a fill of a hole is refused (the position is a
proposal until the Board ratifies it), which is why onboarding took a
second batch and a second verdict. The holes stay one Review each, and
the pack one family: init’s proposals are not folded into the pack,
since a hole’s graph rows (its unfold from the purpose or the part,
the reviews edges, its equipment) are not steps a batch writes, and
plain init stays as it is. What the batch stands on is named in it
instead (planned_on, under the batch of changes below), and its
ratification waits for that ground.
The declared purpose is a proposal like any other. hale dna init
proposes it in the record’s one seed call (host verb graph-ingest, a repository’s
or an application’s: kind purpose, author org, for
org, provenance: declared) with the Board’s Review under the
group purpose, which hale dna review lists first and hale dna review purpose approve decides; the generated organization holds no
Review of its own for it.
The graph read again (hale dna ingest [project] [--at <rev>]).
init reads the repository once; a later commit is read again with the
same ingest, the commit (HEAD by default) checked out beside the
record under .hale/dna/ingest/ with the repository’s own directory
name and removed after. The ingest also states a tool node per
operation of every surface the repository serves (tool:<Surface>::<op>:
its contract, the roles it requires, and its effect class, uncertain
while a description says nothing of an operation’s effects). What
differs from the graph the record states (its rows and its ratified
graph proposals, latest body per id), over the kinds an ingest owns
(purpose, axiom, process, seed, contract, noun, deployment, gate,
document, witness, tool; meets, names, refers, constrains, runs, gates,
witnesses, and an unfold between two of them), is filed as ONE graph
proposal in the group ingest: added and changed nodes and edges with
their bodies, then what is gone as graph.retired rows, edges before
nodes. A graph.ingested row names the commit either way; the same
difference at the same commit is proposed once, and nothing differing
proposes nothing. The host files the proposal and its Review, and a
graph.requested row, relayed like any request, has the running
organization route and open that Review (graph.proposed), so the
Board’s verdict settles it there; an organization started later finds
it in the record. The proposal’s author is hale dna ingest, and a verdict the record holds before the organization
opens the Review is weighed when it opens, in record order: the first
decisive verdict decides, as it would have live. An attached
application’s own re-ingest is not built yet.
hale dna definitions [project] [--json] lists the catalog
dna/org/workflows.hl returns, built beside a one-line main as the model
catalog’s probe is: every definition with its revision and title, each
step with its key and store, what a newer revision
supersedes, and what a definition is refused at admission for; --json
is the catalog’s own document. The head serves the same catalog to the
face’s definitions views (ops::ProjectWorkflowCatalog: what
hale dna definitions --json reads, re-read when either file or the
vendored catalog changes; the baseline alone for a project with no
workflows.hl; unavailable, with the reason, when it does not build).
Workflow facts
Section titled “Workflow facts”What a workflow execution leaves behind is written as a fact of the day’s
work (dna/core/workflow_events.hl). Seven kinds carry an execution:
workflow.admitted names the Task, the definition and revision, the
inputs and their receipt, the bound recipe, the limits that were applied
and what the expansion came to; step.registered names a step’s whole
required set before any of it is dispatched; attempt.admitted carries
one attempt’s id and the WorkRequest it was admitted with;
attempt.outcome its disposition, result and evidence; and
work.settled, step.completed / step.failed and workflow.settled
settle each level to the one above it, a child workflow naming the parent
Task and spawning step it answers. An admission also names the engine (wf1), and every fact a version.
A decoder refuses a version it does not know, another engine’s
admission or refusal, a fact that does not name the entity it is about,
a fact that says nothing where a number belongs (an absent revision is
not revision 0), and a fact whose own identities contradict each other:
an attempt id that is not its Work and number, an attempt carrying a
request for another Work or another attempt, a Work settling under a
Step it is not part of or on another Work’s attempt, a step failing on
a member it never had, a child naming its parent but not the step that
spawned it, an admission deeper or wider than the limits it says were
applied. A fact is read whole or not at all. The identities an attempt
admission carries are written once and derived from the fact, so its id,
its number and its embedded request cannot disagree.
step.activated and workflow.refused have codecs of their own, like
the rest; a routing row is not a durable shape. A fact a transition
committed carries the scope, key and proposal id it was committed
under, appended with it, so the committed proposal can be reconstructed
from the journal after a restart and compared against a later reuse of
that id.
These kinds are the ledger’s under split routing and the record’s on routing 0, like every other operational kind.
An attempt’s id is its Work and its number (<work>/a<n>, numbered from
0 as §4 has it: the first attempt is a0 and a retry is a1), so a
retry is a new id and a re-sent admission is the same one. A transition’s
proposal id (§9 of dna/WORKFLOW-CONTRACT.md) is what the transition
does, not a counter or a clock: the same id carrying the same scope,
key, kind, entity and body is that transition proposed again, which the
committer answers from its journal; the same id carrying anything else
is a conflict, and transition_conflict names which part differs.
Workflow state
Section titled “Workflow state”The state an execution is in is read from those facts and nothing else
(dna/core/workflow_projection.hl). replay(projection, kind, entity, body) applies one row and answers one of three things: the fact was
applied, it is a replay of what the projection already holds, or
it is refused. Applying a fact asks nothing of the world: the entry
point carries @no_syscall, which the compiler checks through everything
it reaches, and the projection holds no journal, gateway or model to
call. The contract guards syscall-class effects; having nothing to call
is what rules out the rest.
The admitted recipe is the authority. An admission carries the whole bound tree under its Task, and every later fact is checked against it: a Step is one the recipe binds for that Task at that index; a registration names exactly the members the recipe bound under that Step, each by key, kind and entity, none missing and none invented; a Work’s retry allowance is the one bound for it; a child is admitted only as the Task its parent bound under that key, from a step that is registered and active, and answers only as that Task. A child’s admission carries the subtree its parent bound under it, exactly: the same nodes, each the same in every field, none added, none left out, none changed. It activates a binding; it never redefines the execution its parent admitted. An attempt asks for what the Work is: its request’s content — objective, context, bindings, output contract, data class, requires, target, cost ceiling — is the bound Work’s, on the first attempt and on every retry; who performs it is the application’s choice. A fact the recipe does not know is refused, however well-formed.
Every transition has its basis. A Work settles done only on a
current attempt that recorded done; it settles failed only on a failed
attempt with no allowance left, or once its Step has failed or its Task is
cancelled, under which a member records its outcome and stops (the drain
policy); it settles cancelled only under a cancelled Task (the fence).
A retry is the next attempt, admitted only after the last one recorded a
failure and only within the allowance. A Step activates only after the
step before it completed, completes only when every member it registered
has settled done, and fails only on a member that failed. A Task
settles done only when every Step the recipe binds for it completed,
and failed only after a Step failed and every responsibility it admitted
has settled: the root settles last. Cancellation stops further admission
in the cancelled Task and in everything under it — no attempt, no step,
no child is admitted below a cancelled ancestor — while what was already
admitted still records its outcome and settles under the fence. The walk
to the root goes as deep as the caller’s max_depth admitted — nothing
here has a ceiling of its own — and an ancestry that does not resolve
refuses admission rather than permitting it. A failed ancestor is not a
cancelled one: under the drain policy a member keeps its responsibility
until its own outcome.
A fact is one row. The row’s entity is the fact’s own identity, and a
step.completed row carries a completion. A fact whose identity is
already recorded is a replay when it is the same bytes and a conflict
otherwise, checked before the state, so a row from history is recognized
as such after the state has moved past it. The join is by identity, never
by counting: two answers from one member are one member. A Work’s result
is the output of the attempt it settled on, readable only once it has
settled; what an outstanding or superseded attempt said is never the
Work’s result.
Workflow admission
Section titled “Workflow admission”An application admits a defined workflow through the assembly
(Dna.admit_workflow(WorkflowAsk)): which definition and revision, with
what inputs, under an identity of the caller’s. The durable fact precedes
everything. The id is minted, the definition expanded under the
assembly’s admission_limits (its catalog holds what the application
defined), and the admission — engine, bound recipe, limits, counts, the
ask’s identity — appended with exact compare-and-append before anything
could run; only then the legacy task.born summary, so the tooling of
the day lists the Task. A refusal, by the catalog or by the record, is a
workflow.refused row under the id it minted and nothing else: no
summary, no child, no work request. Dna.run_workflow(WorkflowAsk) is
the admission followed by the execution’s start (card 18, below).
The ask’s id is the admission’s identity. Asked again — relayed after a lost answer, retried — it is one execution, found in the record by that id. One decision, one revision: the record is read once, at a revision the body captures; whether the ask already landed and whether a candidate id is already an execution’s or a refusal’s are decided against that reading; and the append is exact at that same revision. An exact append protects the revision it was given, not a decision taken at an older one, so when the record has moved — stale, or the store’s unique claim on the id — the whole decision is taken again from a fresh reading. A taken id is skipped before any append. Two bodies contending for one Task id cannot both take it, and one ask admitted by another body between this body’s reading and its append is found on the next reading, not admitted twice.
Only the owner of the position admits (GH #664): the same rule ask
applies, applied before an id is minted, so a body over a shared record
that names no owner admits no workflow either — it answers the asker and
writes nothing, the ask being the owner’s to admit. A refusal, whether
the catalog’s or the store’s, is written as the versioned
workflow.refused and the write is checked: when the record will not
take even that, the answer says the refusal went unrecorded rather than
presenting it as recorded. A refusal bound to a Task id carries the
admission’s own guarantee — it is appended exactly at the revision that
found the id free, and when the record has moved it is read again: the
ask may have landed from another body, in which case that execution is
the answer, and the id may be another execution’s now, in which case the
refusal is not a Task’s at all. A refusal that reaches no Task — the id
taken meanwhile, every id it would mint already taken, the record moved
under it too often — is a fact of its own kind, workflow.ask_refused,
under the ask’s own identity and a decision ordinal (<ask>#<n>), which
can share no identity with any Task under any owner’s name; the
projection holds it beside the executions, never as one. The same ask
refused again for the same reason is the same decision, answered without
another row; refused for another reason, it is the next ordinal. That
decision, too, is one reading of the record: the decisions already taken
on the ask are counted at a captured revision, the ordinal chosen against
that count, and the append is exact at that revision, so another body
deciding on the same ask meanwhile moves the record and the decision is
taken again — its decision for the same reason becomes this body’s
answer, and for another reason takes the ordinal ahead of this body’s.
Nothing is appended as a changed body under a recorded identity, and a
refusal never attaches to an execution another body admitted. A restart counts admitted and refused
ids among those minted, so an id is never minted twice, and an admitted
Task is never resumed as legacy edit work, with or without its summary
row: the admission is the one positive discriminator, and its recovery
belongs to the engine’s own cards.
Workflow execution: one attempt
Section titled “Workflow execution: one attempt”The durable Work owner admits an attempt (attempt.admitted) and then
asks for it to run through AttemptExecutor (dna/core/ workflow_execution.hl), which runs one admitted attempt once. Nothing
runs before the admission is in the record as admitted: the attempt’s
identity, its performer kind and every field of its request are checked
against the recorded admission. An attempt whose outcome is recorded is
not run again; the recorded outcome is the answer, claim or no claim.
The decision to run is one reading of the record: refreshed, read at a
captured revision — the admission, whether the attempt is settled,
whether its execution is claimed — and the claim appended exactly at that
revision, with the effect claim of the existing effect records, keyed by
the attempt. When the record has moved the decision is taken again: an
outcome recorded meanwhile is the answer and the performer is not
called; a claim taken meanwhile is attached to; only a claim that lands
is followed by a performer call. A request that arrives while the
attempt is running attaches to its pending outcome instead of starting a
second performer, and a claim the record refuses runs nothing. The performer the admission names runs
once. Every reply, the one that comes back from the call and the one
that comes later, is judged first for whose it is: it must be about this
attempt (a reply that echoes an attempt_id must echo this one) and
from that performer (the identity the kind selects), and a reply that
is neither is not this attempt’s pending answer either — it is refused
before anything is made of it, and the attempt stays outstanding under
its claim. A reply that is this attempt’s and not terminal leaves it
outstanding and records nothing; a terminal one must say something the
outcome contract carries — done, failed, declined or timeout —
or it is refused without a row and without closing the claim. The
later reply is settled through the same path, under the same rules. An accepted outcome is persisted as attempt.outcome with an
exact append before anything is answered, and a reply for an attempt
whose outcome is already recorded changes nothing; an outcome the
record will not take is reported unrecorded, never as done, and nothing
advances on it.
The executor retries nothing (one retry owner: the Work’s lifecycle) and
advances nothing (the Work settles on the outcome in a later card). It
makes no claim that external effects happen once: a performer that
acted and died before its outcome was saved is a later card’s. The
WorkSystem retries nothing of its own (card 18): it routes each
admitted attempt to the performer of the kind the admission names.
Legs: the hat, the claim and the outcome
Section titled “Legs: the hat, the claim and the outcome”The spine’s whole API to a leg is three things (GH #946): the hat as a
read, dna.attempt.claim, and dna.attempt.outcome. A leg holds
nothing between tasks and has no database role; the owner still admits
and settles. Once the organism has adopted the ledger the attempt kinds
live there: the head reads them and writes a leg’s rows there under its
role (HALE_DNA_MEMORY_DSN_HEAD; memory’s insert function is the gate,
keyed by the request), and refuses the commands and the hat only when no
memory is named to it.
- 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 dna initproposed, 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 at init): 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 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 dna newdeclares 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 cell leases (MemLeases,
GitLeases) stay for what they hold — a body, a mutation — where the
operation they fence is not an append to this record. A runtime with no
lease key is unfenced: a standalone runtime over a memory record.
Workflow execution: edit outcomes
Section titled “Workflow execution: edit outcomes”An edit leaf of a workflow execution is performed by the assembly —
its editor, its gateway, its record — never by a router performer.
The engine reaches it through a fifth performer kind, edit: the
EditRelay publishes the attempt (EditRequested, keyed by the
runtime’s scope, which is the organism’s id) and answers pending; the
assembly performs or waits, and reports the outcome for that exact
attempt (AttemptReported), which the runtime settles as it settles a
late reply — judged, recorded, answered to its Work and only its Work;
a report whose outcome names another attempt than the report does, or
none, is nobody’s and settles nothing.
The Work settles as any Work does, its step counts it, and the
workflow settles the root: no Work of an admitted execution settles
its Task, and a Mutation’s review outcome never settles it either —
one root terminal writer.
Two output contracts, never reinterpreted as each other. Patch — the
default for an edit leaf — is a candidate prepared for review: the
attempt is performed once, the Mutation it produces is bound to it
(card 14), and the Work is done when the candidate is in review,
escalated or released, failed when preparation failed before a
candidate; a later verdict changes nothing about it. Applied (or
Approved) is the candidate approved and applied: the Work performs
nothing and waits on one exact candidate: the Work its request names
in target, as <step>/<key> within the workflow (s0/e) or a
whole Work id, resolved to the Mutation bound (card 14) to the attempt
that Work’s settlement accepted — a first attempt that failed to
prepare and was retried is not it. A Work naming none fails at once,
with why, since the dependency is explicit and never a Task-wide or
Work-wide guess; one whose producer settled other than done fails,
there being no candidate. The producer’s settlement and its
candidate’s review and application are prerequisites that land in
either order, and each landing is an occasion to look again: a
waiting attempt is evaluated afresh — by the same resolution — when
any Work of the engine ends (RunLives), when a review settles, when
the record resumes, on redrive, and whenever the assembly is asked;
it is answered once, when its state is an outcome. The same resolution
serves an ask again after a restart, which waits once. It is answered from that candidate’s durable state
alone: rejected or sent back is failed; approved and applied (or
retained) is done; approved and rolled back is failed; approved but
not applied — the apply gate refused, an application pending or
unresolved — is no outcome yet, and the Work keeps waiting, to be
answered when the application lands, on a first approval or on an
approval again. An attempt asked again — a redelivery after a restart
— is answered from the Mutation bound to it and edits nothing twice,
so the edit kind’s reconciliation is a replay. A request row the
record refuses starts nothing: the attempt is held, and redrive puts
it again. A published report is not a durable one: the assembly keeps
every report until the record is seen to hold the attempt’s outcome,
and puts a kept one again when the record resumes or on redrive —
the validated outcome re-persisted, never the edit performed again.
Workflow execution: human cases
Section titled “Workflow execution: human cases”A human leaf of a workflow execution is a case for a person. The
engine reaches the assembly through the human performer kind: the
HumanRelay publishes the attempt (CaseRequested, keyed by the
runtime’s scope, the organism’s id) and answers pending, and the
attempt stays out until the person reports the case done. The
assembly admits the case as its own handed Task, <task>.s<i>.<key>
— the Work’s step index and member key under its Task, the child-Task
form of §4 — stable across redeliveries and naming its exact parent
Work and attempt by construction. The admission (case.admitted,
versioned and self-sufficient: parent Task, Work and attempt,
objective, assignee, obligation, acceptance practice, required
evidence, who said so) lands first, exact at the revision read, and
only then the rows the tooling of the day reads — task.born <case>,
then task.handed <case> (or task.transfer_requested, for another
owner’s person) — so hale dna task done <case> --as <who> addresses
that case as it addresses a job today, and the same gate applies.
Assignee, obligation, acceptance practice and required evidence bind
as a job’s do: in an organism the leader plans for, the leader says
who and under what obligation; without one the case is unassigned. A
retired assignee’s successor, a transfer to another owner, and
reassignment keep their semantics.
What the leader proposed — whom, under what obligation — is one
thing; what binds is decided from the reading the admission is exact
at: who is admissible now (a retired person’s successor), which
acceptance practice is in force now, whose the assignee is. A reading
gone stale under the append repeats the decision, not only the
write, so a retirement that landed between sends the case to the
successor, never to the person retired. The admission names its
origin (owner) and its disposition (to: the owner whose person
takes it, empty for the origin’s own), and the compatibility rows say
what the admission recorded, never what the body writing them is.
The hand-off settles nothing: the parent Work stays pending, and the
root is neither closed nor marked by it. Two human Works in one step
are two cases, each completable on its own. An attempt asked again —
a redelivery after a restart, a later attempt of the same Work —
finds the case by its stable identity and admits no second, and asks
the leader nothing: the terms bound at admission stand, and a case
already awaiting the leader’s word is not asked for twice. A restart
that fell between the admission and either compatibility row
completes them from the admission, in that order, and never plans,
resumes or pends a case as edit work: a case is a person’s, never an
executable root, never legacy edit work. Only the recorded owner
completes them: a transfer to another owner is theirs to hand once a
member of theirs accepts it (task.transfer_accepted, as a job’s
today), and another owner’s restart neither hands it nor completes
the offer. An admission the record refuses holds the attempt for
redrive; a compatibility row the record refuses holds the case,
completed when the record resumes or on redrive without a restart,
and the hand-off is announced only once it is durable.
Workflow execution: human completion
Section titled “Workflow execution: human completion”A case is completed by its person through the tooling of the day —
hale dna task done <case> --as <who>, with the evidence its bound
acceptance condition wants (completion.linked) or an exception
someone else authorized (completion.excepted), or a decision
someone else made, reported by the assignee and accepted under the
bound practice (decision.reported, GH #616) — and that accepted
fact is the outcome of the attempt the case was admitted for (card
17). The assembly reads the completion from the record
(case_completion, completion.hl) and reports it to the runtime of
its scope as it reports an edit leaf’s outcome: judged, recorded as
attempt.outcome and answered to its Work by the runtime’s own
barrier, so the Work settles done, the step counts it, and a
completion the record already holds an outcome for changes nothing.
The report is kept until the record is seen to hold the outcome, as
an edit’s is.
The completion is judged against what the admission bound — the
assignee (moved by task.reassigned), whether evidence is required,
which practice — never against a practice in force later: a practice
relaxed after the hand-off relaxes nothing, while a case handed after
the relaxation closes under the new one; evidence linked under
another practice is none, and a decision accepted under another
policy than the bound one, or under none, or naming another case or
none as its scope, is none. An exception
applies to the person on the completion row: recorded in their name
and authorized by someone other than them — a case reassigned to its
exception’s authorizer does not close under their own authorization.
A completion the bound condition refuses — not the assignee’s, no
evidence linked under the bound practice where it is required, an
exception that is not the closer’s or that the closer authorized, a
decision reported by someone other than the assignee — connects
nothing and is noted once; a later valid one connects. The first
valid completion is the outcome; a second is a duplicate and settles
nothing twice. One case’s completion advances nothing of another’s.
It is observed wherever the assembly reads the record: live, at the
organism’s tick, which examines the cases whenever the record has
rows past the revision they were last examined at — by that
revision, never by whether the tick’s own refresh moved, since
another handler (a redelivery’s) reads the record without examining
every case; when the record resumes; on redrive; at a restart of
the assembly (which reads what the record holds); and at a restart
of the execution, whose redelivery asks for the attempt again and is
answered with the outcome the record holds.
A case keeps its parent step waiting through a restart of the
execution: nothing settles it but its completion. Only the recorded
owner connects a completion — the Work runs in the origin’s
execution. The tooling’s own gate (hale dna task done, evidence,
exceptions, reported decisions) is unchanged.
Workflow execution: the baseline
Section titled “Workflow execution: the baseline”What the sections above promise is held, as one oracle, by
dna/tests/workflow_conformance_test.hl over the canonical three-level
example (dna/WORKFLOW-CONTRACT.md §6), in every supported mode, with
a unix listen binding declared so every publish is queued as in a
bound organism. A review traces each promise to it; what it does not
cover is not promised.
Delivery. A leaf’s attempt is admitted, claimed and delivered to its performer once per claim. A performer answers at once or later; a later reply names the attempt and the performer, and reaches the runtime whenever it comes — after the grandchild’s step, before the root’s own leaf, in any order. A reply for an attempt whose outcome is recorded is answered from the record and writes nothing, whether it repeats the recorded reply or arrives for an attempt the Work has retried past. A reply from another identity than the admitted performer’s is nobody’s and is refused. A reply for an attempt the record never admitted or never claimed is nobody’s.
Failure. A leaf that fails its allowance fails its step; the step fails its execution; a child’s failure fails the step that invoked it, up to the root. Nothing later activates and nothing further is requested. A leaf still out under a failed step keeps its responsibility: its outcome is recorded when it comes, it reopens nothing, and the execution above it settles failed only after it settled (the drain policy, contract §6).
Cancellation. A root’s cancellation fences everything below it: what was admitted records its outcome; nothing further is admitted or claimed at any depth; the tree drains and reclaims from the leaves up; a late reply for a fenced attempt is still recorded, as the attempt’s outcome, and reopens nothing. A root’s own residents learn of a cancellation committed by another hand through a restart or through the fence on their next transition, not by being told.
Restart. The record is the execution. An incarnation booted over
it — in process, a new runtime over the same git record, a new routed
record over the same two memories, or a new process — rebuilds only
what is unfinished, under the original ids: no execution is admitted
again, no step registers or activates twice, no completed member runs
again. An attempt claimed by the incarnation that died is reconciled
before anything is delivered: a durable adapter that recorded the
effect answers from its store and the attempt is not performed again;
a performer that truthfully has not acted is delivered the attempt
again, once, under the same id, and the record says so
(effect.redelivered). The completed record rebooted gains no row and
asks no adapter. Every fact of an execution is routed to the ledger
(one memory), so a reconstruction of the routed record keeps their
order.
Fencing. Under a lease, a holder whose token is not live commits nothing: its replies are refused and written nowhere; with the lease back, the same replies go through and the run completes as every other.
Reclamation. A settled execution dissolves every resident of its tree, leaves first; repeated completed runs leave nothing alive. What grows is the record.
Logical exactly-once, apart from external-effect reconciliation.
One effect.requested per attempt, ever; one attempt.outcome; one
work.settled on that attempt; one settlement per execution. Whether
the external effect happened once is the adapter’s to say: an adapter
whose store holds the invocation is not asked again; one that cannot
say leaves the claim resulted unknown and the attempt waits for a
person (card 12c); one documented idempotent is replayed. The record
distinguishes the cases: a redelivered attempt has its
effect.redelivered row and, in the adapter’s store, two invocations
under one id.
The process boundary. dna/tests/conformance/runner.hl is the
public assembly (Dna.run_workflow over a GitJournal) with a durable
service adapter whose store is a file; the fixture builds it with the
hale under test, runs it as a separate process, kills it with
SIGKILL the moment C1’s attempt is admitted, the moment it is claimed,
and the moment its adapter has recorded the effect but before the
outcome landed, and runs it again over the same record each time: the
assembly resumes the admitted root, the attempt keeps its one
admission and one claim, the redelivery is recorded once, the store is
the outcome, D runs once, the root settles last. Run once more over
the completed record, it runs nothing.
Storage interfaces
Section titled “Storage interfaces”Journal (ordered append with an expected revision, read by index,
chain verification), Coordination (leases with fencing tokens) and
Receipts (content-addressed store and read) are interfaces in the
core. The git-backed implementations are the ones an assembly wires
for an organism; the in-memory ones exist for tests. Protected bodies
go through MemoryVault to memory’s sealed store instead
(ProtectedBodies, PqProtected, which files, reads and erases only
through receipt_file, receipt_read and receipt_erase; GH #606,
#985).
The record has an API of its own (GH #646 stage 0). Record is
the one boundary the git-backed GitJournal, GitReceipts and
GitLeases and the host’s sync, reconcile, body lease, identity and
exchange mailboxes all stand on, with a vocabulary that never names
the tool: chains of rows appended under compare-and-swap at a head
(journal; exchange/<identity> for a mailbox; remote/journal for
what the remote held at the last receive), a row built beside every
chain and swapped in later, bodies kept by the digest of their
content, cells swapped by version (a lease, the published identity),
pointers naming an object the repository already holds
(candidates/<mutation>, revisions/<rev>), families received from
and shared with a remote, the signer of a row, and the dna.*
settings. GitRecord is the one implementation over git plumbing —
the only file in the core and the host that spells git for the
record; the genome’s own git in workspace.hl, verification.hl,
org.hl and the host’s genome.hl is the Structure, not the record.
MemRecord is the one for fixtures. hale dna init and upgrade seed
the record through the host (graph-ingest, design-upgrade): the
driver keeps no record code of its own.
A reload never shortens what a reader holds (GH #748). The chain
only grows — a reconcile builds beside the ref and swaps in one whole
chain, so the ref never points at a record missing a row it held a
moment before — and a reader is held to the same rule. A chain that
reads back shorter than the rows a GitJournal already holds, or
empty, or with no head at all, is a read of the record that failed,
never a shorter record: git unable to run under load, the repository
unreachable for a moment, a tool that could not be forked. Every read
is made before a row is dropped, and one that comes back short
leaves the rows, the head and the text as they were, counts itself
(reads_refused, with last_error naming the read), and answers
refresh() with “nothing moved” — so the next refresh tries again,
and an append whose reload could not read the chain ends as the i/o
failure it is rather than re-offering a stale head two hundred times.
The view a reader holds is therefore stale at worst. It used to be
emptied first and refilled from those reads, so one failed read left
an organism holding no record at all, in silence, and everything it
derives from a scan of its own record began again from nothing: how
often a source has raised a concern, which Review is open, which Task
is in flight.
A head read tells an absent chain from a read that failed
(GH #961). Record.head(chain) answers a Head: ok when the read
happened, then absent for a chain that does not exist or id for
its head; a read that did not happen is ok: false with why, and no
caller takes it for an absent chain. GitRecord reads absent only
from rev-parse -q --verify exiting 1 with nothing on either stream.
Every caller that compares a head it read earlier — an admission
fence, an API response’s final check, a writer’s compare-and-swap —
refuses on a failed read with the reason (record_unavailable, or an
io: append error), never treating it as a Record that did not move.
A CLI verb says the record could not be read rather than that there
is none, record-head answers none only for a record known absent
(the driver seeds on it), and a receipt is not written again on a
read that did not happen. Both used to be the empty string. hale dna sync refuses, with the chain untouched, when either head cannot be
read, or when a round of its reconcile reads either head behind where
the previous round left it; it pulls into an empty clone only while
the ref is still absent.
Every external dependency of the DNA is declared the same way, one
interface in the core with one implementation over the real thing and
one in memory: Infrastructure (a body’s database, supervisor and
credentials; GH #647) and Transport (how a head reaches a body) in
infrastructure.hl, Forge (a code-review host; GH #648) in
forge.hl. Their memory implementations exist; the host still drives
compose, systemd, ssh and gh directly until each is moved behind its
interface.
The organization
Section titled “The organization”The organism is an organization written in Hale (GH #566 F2): a
program at dna/org that hale dna init generates and hale dna run
runs. The application it oversees is not modified by init and
contains none of it; it is observed like any Hale binary. The
manifest declares two environments, the application’s and the
organization’s ([claims] no_base = true; each adopts its own law).
- 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), so a shared record is known as one without memory: over a record that states organizations, a body that names none does not run, nor one that names an organization the record does not state; andrunof a body that names its organization needs the ledger adopted. The host’s admission helpers read memory as a head does (HALE_DNA_MEMORY_DSN_HEAD, else the spine’s DSN). A body admits an intent (Intent.to, the position it is for; “” 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”). 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; a shared record runs no body until its ledger is adopted. The lease’s token is an epoch: it rises when the lease is taken (a takeover, a forced claim, an expiry) and holds through renewals, and the host hands the body it starts its lease and epoch (HALE_DNA_LEASE,HALE_DNA_LEASE_TOKEN), which the body’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 takesrefs/dna/lease/bodybefore it builds or runs anything — at the record’s remote when it has one, so two hosts on two clones fence each other through the remote itself (the lease blob is pushed with--force-with-lease, git’s compare-and-swap), in the clone otherwise. The blob is aGitLeaseslease (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 git call bounded to 8s — and to the deadline it last proved (the lease’s expiry less 5s,.hale/dna/body.deadline), so the calls of one renewal together cannot outlast the lease — and writes what it proved to.hale/dna/body.fence.status, each line prefixed with its host’s pid; the host reads its own lines there instead of renewing. The fence’s mandate is.hale/dna/body.fence, naming its host’s pid: a fence whose mandate is gone or names another host (a host restarted on the same clone) stops and kills nothing. When the lease is someone else’s or released, when the fence cannot prove it within 5s of its expiry, or when the host is gone, the fence kills the organization and the expression (by their pid files), each with every process it started — the tree and the session frozen, then killed. Every process the host starts begins a session of its own (dna::in_new_session:setsid(1), or perl’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 — 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 the remote cannot be reached the lease is kept unrenewed until then: a partitioned body executes nothing past its TTL. Taking the lease is a row (body.claimed <holder> {token, forced, by}), giving it up at exit another (body.released).hale dna 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,dna.github; the status projection carries aprofile:and abody:line.hale dna new --profile local|remote-body [--remote <url>] [--body <user@host>]sets pieces (a remote,dna.body); the profile name is not written anywhere. Profiles are examples of combinations, not a closed set; the toolchain refuses only what breaks a commitment (a second body on one record). - The body on a server.
hale dna body provision <user@host> [--dsn <url>] [--dir <path>] [--dry-run]makes a body over ssh, in order: the 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 fromdna/compose.yamlor a DSN (which goes into the body’s env file asHALE_DNA_MEMORY_DSN_OWNER, never the record: the unit’shale dna devmigrates with it 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.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 journal’s genesis), so two records whose directories share a name on one machine never share a unit or a credential. Everything else a body reads lives in its clone: 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 dna init writes in repository mode is the
organization’s environments, not source, and does not make a seed) is
checked and formatted whole. Otherwise (a repository, whose code sits under its
seeds) each edited file’s seed is its nearest ancestor directory under
the root that holds a hale.toml and a .hl file, else the file’s own directory; each
seed is formatted and checked once, in order of the first file edited in
it, the first failure’s diagnostics (named from the seed, not the
worktree) feed the retry. The candidate’s verification follows the same
rule: when the Mutation’s seed is the repository’s root (.) and the
root is not a seed, fmt --check, check, verify, test and the base
topology run in the seed of the first file the candidate changed.
Models
Section titled “Models”The organization’s models are a catalog in source (GH #583 M1):
dna/org/models.hl, generated by init and owned by the project.
- 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 the record (refs/dna/receipts) 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 this clone lacks sayshale dna syncfetches it. A wider history (hale dna history m1) names the calls and leaves their bodies to each attempt’s own, since an editor’s prompt carries whole files. The row 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 plan of four instances on two nodes with routes between them and claims across them (require_publisheson the gateway and the api,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.
initandnewlook 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]appendsmodel.category(prices in micro-dollars per 1k tokens); atoolsordocategory without--toolsis refused.models maplists the latest row per selector and per category.hale dna initseeds 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 dna init; 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: git is the sole authority; memory can lag, never disagree. The observed and inferred tier (K3) lives only in Postgres and is backed up like any Postgres; the ratified tier rebuilds from git. - The record is the scope of its memory. Memory is opened for one
record, named by the record’s identity — the sha of the journal’s
first commit, the same in every clone of the record and different
for every record: that first commit carries a
Record-Nonce:of 16 random bytes, so two projects created alike in the same second (same author, same scaffold, same row) are two records, not one organization to memory and the nerves (GH #1068); a record that cannot draw them writes no first row. A record made before keeps its identity (GitJournal.genesis();scope(record)on the store 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;hale dna dev’s compose database 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 spine projects; readers read.
MemoryProjection(dna/operations/memory_tail.hl) applies the record into the graph on the host’s tick, as the record’s spine role, on every node (The spine, above), so the projection advances without anyone asking. The fence compares heads by ancestry: memory’s stamp is a commit of the record, so every clone can place it. Nothing stamped, or the stamped head is this clone’s row at the stamp: the rows after it are the delta. This clone’s head an ancestor of the stamped head, or the stamped head a commit this clone has not received: another projector is ahead, and there is nothing to project here until the record is received (a head a clone cannot place is never taken for a replaced chain, or two nodes that append before they share would rebuild each other’s projection until they synced). Neither, of a head this clone holds: the chain was rebuilt beneath the projection (a reconcile re-appends local rows with new digests, GH #603), so the graph is rebuilt from row 0 —rebuild(watermark, head)empties it only while the stamp is still the one read, so of several projectors that see one replaced chain one rebuilds, and a projector rebuilds at most once per pass. A question memory cannot answer is a refusal, never an empty answer: a package built from failing reads, or from a projection behind the record, is indistinguishable from “there is no knowledge here”. The projection’s store is opened on the first tick rather than at birth, and the connection is asked on each tick afterwards (healthy), because an open that succeeded once is not a connection that still answers: a session dropped by a restart or a failover is re-established (reopen). A reader —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).
initseeds 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 — asknowledge.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,
initseeds 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).
initwritesgraph.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.init --no-library(andnew --no-library) seeds 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).
initwritesdna/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=offmakesinit’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. 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. - Dev relies on docker compose.
initwritesdna/compose.yaml(its own compose project,name: hale-dna-<project>, so two seeds on one machine never share a container; theknowledge-dbservice,pgvector/pgvector:pg16, a named volumehale-dna-<project>-knowledge, a host port in 54xx). Each service’s host port (memory’s in 54xx, the nerves’ in 42xx, the senses’ in 93xx) is taken at seed time: a candidate from the project’s name, stepped past any port something on the machine already listens on, asdna::free_porttakes a fixture’s, so a seed never publishes on one another server holds (a CI runner’s NATS on 4222). The file is part of the genome, andupgraderewrites it to the template, keeping the ports the file already publishes, so a clone’s upgrade regenerates the same file. With noHALE_DNA_MEMORY_DSN_OWNER,hale dna devrunsdocker compose -f dna/compose.yaml up -d --wait knowledge-dband derives the owner’s DSN from the published port and the vault’spostgres-owner-<project>(The organism’s secrets), which a database made with another password adopts first; an operator’sHALE_DNA_MEMORY_DSN_OWNERwins. It applies memory’s schema with the owner’s DSN and starts the host with the record’s spine DSN alone (Memory). With compose not on PATH, or no compose file, it says exactly which it needs and the host runs with no memory.hale dna runbrings up no database: beyond one machine memory is a Postgres of the operator’s, migrated withhale dna memory migrate. - Knowledge changes later work (K2). The substrate holds a
MemoryKnowledge(budget, 8 by default; memory opened under the spine’s role), the owner’s line to memory; the editor never holds one, which the law states (group knowledge = { dna::Knowledge, dna::MemoryKnowledge },editors_never_learn). When a Mutation opens, the substrate asks memory for the package of the change’s target in the tower —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>), whichinitseeds 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) —hale dna initandnewwrite 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 dna initon 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:initmakes 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 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; 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 (init seeds it and the graph in onegraph-ingestcall), so the record does not exist and init can be run again once the document is fixed. A job’s ownname:names its gate and is not a word its steps say.initsays what it read:graph <n> node(s), <m> edge(s): <count> <kind>, …; <p> proposal(s). A record that exists is not reseeded. - Ingest:
hale dna initon an application (seed/structure-2). An application’sinitenriches 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 purposeinitdeclares (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.initsays 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,
initproposes 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.initproposes: aboardunder 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, whichinitwrites 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, wheninitseeds the record, so a project opts in by writing the file beforeinit; 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 bodyinitwrites 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> | <text>,retire-practice <name>,nursery <source>,assign|unassign <name> reviews <kind>:<node>,close <name>,merge <name> into <name>,split <name> into <name> <name>,expose <kind>:<node> as contract:<name>,depend <kind>:<node> on contract:<name>,externalize seed:<name>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, or a practice named twice or already in force is refused whole, naming the step, and nothing is written. Opens are taken first, so a step may name a position a later line opens; every other step is taken in the order written, against the organization the steps before it leave.fill * <holder>is taken last: every position the organization would then state with a ratified mandate (or one the batch opens), no holder and 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(whatinit --withpasses) plan the batch on the holeshale dna initproposed that are not decided yet (batch_init_holes: eachreview.requestedin the groupholesauthored byhale dna initwhose 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 init already proposes is refused when it is proposed (`position:…` is a position the graph already states), not at ratification. The operations, as graph rows.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, andfounder,fill * {founder}, one person in every vacant seat, whatinit --with founder --set founder=<who>proposes); a git repository is cloned into$XDG_CACHE_HOME/hale/nursery/and read at the revision after#, its default branch’s head without one. The CLI renders the pack: every parameter replaced by its--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; the toolchain’s nursery and a directory need no trust, and reading a nursery (list,show) needs none either. Every mutation is a head command; the UI is a client. A change to the structure asked of the head is not a second code path: the head’s 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) and iscommands_unsupportedonce the Record’s ledger is adopted. 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 positioninitproposes carries a mandate, proposed in the same seed right after its hole: a knowledge idea of kindmandate(familymandate,author: hale dna init, 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 atinitandupgradeunderdesignbeside 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 when an application attaches in repository mode.hale dna fill <position>|organization:<name> <holder>|organization:<name> [project] [--as <who>], once it has proposed the hold, 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 (initalone 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 runs 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, express or restart, anddevsays so. - Review routing (GH #1087). A change set goes to the positions that
must sign it, read from the graph, never guessed:
hale dna route [--json] (<path>… | --diff <range>)(the host 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 sayshale dna initseeds a record andhale dna syncfetches one; with rows and no memory named it refuses, and says the graph is memory’s. The projection’s stamp is read before the graph, so the rows it reports never overstate what the graph holds. Text (dna/operations/graph_show.hl) shows a node by the last segment of its name. The org chart is nothing when there are no positions; 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 (this clone is behind its remote:hale dna sync), or with a stamp that vouches for nothing (hale dna memory migrate). On voice’s record with its holes filled the two texts are GRAPH.md’s renderings, byte for byte (dna/tests/graph_show_test.hl). - Projections and ranking (K3). The tail also projects the
record’s
structure.observedrows (init’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:GitJournal(the branch),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); GitHub, mirrored by the host whengit config dna.githubnamesowner/repo: every pending mutation Review becomes a pull request (github.pr; 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’;NoForgeis a bare remote’s honest behaviour, andhale dna profile’sgithub:line names which the host found (HALE_DNA_FORGE, ordna.github). 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. Nothing in the core or the host spellsghoutside that one implementation. - Deployment —
Deployment:NoDeployment(a host expresses:hale dna dev),ShellDeployment { command, seed }(the command owns expressing and judging:express <candidate> <seed>returning 0 means up and healthy,rollback <base> <seed>restores; the exit code is the observation,expression.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 hale dna init and upgrade, whether or not the part
that uses one runs; per-member secrets are provisioned at that member’s
admission, and are not on the organism’s list: an owner’s head role on
a shared record (postgres-<schema>_head_<owner>) where the owner
is declared to memory (memory migrate under HALE_DNA_OWNER_KEYS);
a node’s own account, not yet built. The application the record
attaches is the exception on the list: its broker account is drawn
once application.attached lands, and revoked by application.detached
(The application’s account). One skin-owned function
(dna/host/secrets.hl, organism_secrets) holds the list, and one
(provision_secrets) provisions it; every other part only consumes a
secret, by its vault name, and never creates one.
| vault name | kind | whose |
|---|---|---|
postgres-dna_<identity>_spine, _head |
drawn | memory’s roles (Memory) |
postgres-owner-<seed>, when the seed has dna/compose.yaml |
drawn | the compose database’s superuser |
nats-<org>-<role>, for owner spine head reflexes |
drawn | the nerves’ accounts (The nerves) |
nats-<org>-app-<name>, while the record attaches the application |
drawn | the application’s broker account (The application’s account) |
oidc-client-<dna.oidc.client> (dna-local when unset) |
drawn for an issuer on the loopback, else a slot | the head’s OIDC client |
oidc-service-<service>, per dna.oidc.service |
drawn | a service client (The principal source) |
forge-token |
slot | the forge’s token: with none, nothing is done at the forge, never as gh’s own login |
model-<NAME>, per credential dna/org/models.hl or dna/org/work.hl names |
slot | a model’s key (Models); hale dna secrets lists a work.hl key as the legs’ model key |
- Drawn or a slot. A secret the organism owns the value of is drawn
from urandom (32 hex characters) into the vault when the vault lacks
it; the nerves’ are drawn anew on every
initandupgrade, since the server’s include is written from the same draw. The servers compose runs read theirs from files written from the same draw as the vault’s entries:dna/nats.secrets.conffor the nerves anddna/postgres.secretsfor the database (itsPOSTGRES_PASSWORD_FILE), each mode 600, ignored, and refused when git tracks it or would. No committed file holds a secret. A secret a person supplies is a named empty entry 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 the fleet plan Hale already checks
(spec/verification.md, “Fleet composition”), and the DNA expresses
one arrangement of it:
- The plan describes the workspace’s own services. Plan schema
1.2 lets an instance name a
seed(relative to the plan) instead of anartifact; composition cuts the artifact from the seed first, so a candidate is checked as the fleet it would deploy. An instance may name thenodethat expresses it.[dna] fleet = "<name>"inhale.tomlnames which entry of[fleets]the DNA expresses. - 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 plan (*.plan.json) 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 plan name, the revision, the seed the change edits (""for the whole genome), the instances touched — every instance with a node whose seed is that directory — and the reason (apply,rollback, or the operator’s). The revision is pushed torefs/dna/revisions/<rev>on the record’s remote first, so every node can fetch it. 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, checks it out, and for each instance the plan assigns to the node that the deploy touches (or that is not up) cuts the artifact, builds the seed, restarts the process (cwd the clone,LOTUS_OBS=1,HALE_DNA_NODE,HALE_DNA_INSTANCE,HALE_DNA_EXPRESSION) and 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 plan 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 plan, 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. The seed’s
dna/compose.yamlhas asensesservice: Prometheus, scraping whatdna/senses.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 the seed’s, in 93xx, on 127.0.0.1, taken as the compose file’s other ports are.initwrites both files.upgradewritesdna/senses.ymlwhen it is missing, and says so when the compose file has nosensesservice ordna/nats.confnoreflexesuser.hale dna senses up [dir]brings the service up and printsHALE_DNA_SENSES_URL, its read URL.hale dna devruns it with the rest of compose, unless the environment names its own memory or nerves (an owner’s DSN or URL).- A reading never acts.
- The reflexes (
dna/reflexes) are a program on the private-services tier. They hold the store’s read URL and a publish-only credential, 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, and stops them with the launcher. A store or nerves that does not come up leaves the face running without them.
- 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,github sync) and the rest. It imports the host’s seed and starts no locus that runs or handles the bus, so it runs to exit, and the memory-bound survey judges it as it judges a script (spec/verification.md).
hale dna <verb> resolves the project from the manifest (the root, the
application’s seed, the fleet and its plan under [dna]), builds both
programs once into the toolchain cache (side by side, the first time
any host verb needs either), and execs the one the verb belongs to in
place — the pid that ran hale dna <verb> is the host:
<program> <verb> <root> <seed> <fleet> <plan> …, with HALE_BIN (the
toolchain) and HALE_DNA_TOOLCHAIN in the environment. Either program
refuses a verb that is the other’s. The host reads the record through
the core’s GitJournal, appends in a person’s or a node’s name with
the ref compare-and-swapped, and starts every child — the
organization, the application, iris, an instance — through sh,
detached, with its pid, its exit code and its log as files under
.hale/dna/ or .hale/node/<name>/, echoing the log to the terminal
a tick at a time. The compiler keeps init / new / upgrade (they
embed the sources), hale fleet check and the plan schema, and the
exec shims. The host decides nothing.
A seed is built once per content fingerprint
Section titled “A seed is built once per content fingerprint”The host builds the organization’s seed before it starts it, and the
application’s seed too under dev; it rebuilds both on a restart. A
build is a pure function of what it reads, so the host does it once
per content fingerprint and starts the stored binary thereafter. The
cache is $XDG_CACHE_HOME/hale/dna-build (or ~/.cache/hale/dna-build),
a sibling of the toolchain cache the host and the surface are themselves
built into; it holds the 32 most recently used entries and prunes the
rest. An entry is a directory, <fingerprint>/<seed name>, so the
process keeps the seed’s own name.
A build never writes into the seed’s directory. The host passes hale build -o: with a cache the binary is built into a private directory
beside the entries and renamed whole to its entry, and the entry’s path
is what the host starts; without one (HALE_DNA_NO_BUILD_CACHE, no
cache home, a fingerprint that could not be taken) it is built into
.hale/dna/build/<seed>/<seed name> under the project root. Either way
a checkout has no binary in it afterwards.
The fingerprint is a SHA-256 over:
- every file the build reads, each as its path relative to the
project and the SHA-256 of its bytes. The list is the compiler’s
own —
hale inputs <seed>, which resolves imports exactly as a build does — so a seed that grows an import, or a vendored core that is upgraded, is a different fingerprint without anything here having to be kept in step; - the project’s
hale.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”hale dna init / new / upgrade materialize vendor/dna from the
DNA source embedded in the hale binary, so an organism runs the
core that binary carries and not the one in any checkout. A version
does not identify that source — two builds of one version can embed
different dna/ source — so the toolchain names it (GH #726):
EMBEDDED_DIGEST is a length-framed SHA-256 over the sorted (path, content) pairs of every embedded file (dna/core, dna/host,
dna/host/verb, dna/operations, dna/organization_runtime,
dna/organization_source, dna/ui, and pond’s driver and NATS client
pinned under dna/core/pond), computed when the toolchain is built. It is:
- the second line of
hale --version(embedded dna: <first 16 hex>; the first line stays the version alone, since a provisioning script reads field 2 of it); - all 64 digits on stdout, alone, from
hale dna --embedded-digest, and — with--from-tree <dir>, a checkout 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 alone: a Hale program (dna/ui, embedded in the
toolchain like the core and the host, built once
into the toolchain cache) that answers every request by running one
offline verb of hale dna in the project root and returning what it printed
— the status projection (/api/status), the Board’s queue
(/api/board), the pending Reviews and one Review’s three views
(/api/reviews, /api/review/<id>), the fleet (/api/fleet), the
history (/api/history[/<entity>]), pressure (/api/pressure). A
verdict (POST /api/verdict), a task (POST /api/task/create) and a
pressure signal (POST /api/pressure) are the CLI’s own verbs sent
and not waited for: rows in the record, in the name the form gives,
which a node relays. A path segment
reaching the CLI is cleaned of separators and leading dashes, so a
request cannot name a file or a flag. The surface reads nothing
itself and decides nothing; with or without an organization up, it
shows what the CLI shows. hale dna review <id> <verdict> --no-wait
is the same non-blocking verdict from the terminal. Iris stays the
observer: attached to the organization’s process it renders the org
as the live topology it is.
The head (dna/api/project_service), which serves the face, is the
surface’s counterpart for the operator’s machine. hale dna head [project] [--port P] [--api-port P] [--oidc-port P] [--commands-port P] [--source-drafts] starts it from an installed toolchain: the toolchain
embeds the head’s seeds (dna/api, dna/api/practice_review,
dna/api/project_service, dna/oidc, dna/oidc/serve, dna/reflexes),
the face’s assets (dna/face/web) and the launcher (dna/face/start.sh)
beside the host’s sources — not in the embedded DNA digest, which names
what vendor/dna is materialized from — materializes them into the
toolchain cache, whose key covers them, builds the four seeds there
side by side under the cache’s build lock once per key, and execs the
materialized launcher with every binary handed in (HALE_API_BIN,
HALE_HEAD_BIN, HALE_OIDC_BIN, HALE_REFLEXES_BIN) and HALE_BIN
the running toolchain. With a project and no
HALE_DNA_MEMORY_DSN_HEAD, it derives the head’s memory DSN as
hale dna memory migrate does, onto the launcher’s environment and
never its output, when memory is up: an owner’s server
(HALE_DNA_MEMORY_DSN_OWNER), or the project’s compose knowledge-db
already running. Otherwise it says memory is not up, names hale dna dev, and starts the head with Knowledge unsupported; it brings no
database up. From a checkout, dna/face/start.sh [project] is the same
launcher, building each seed it is not handed from the checkout into
temporary storage on every start. The head is
one loopback process that serves the browser shell, keeps a registry one loopback process that serves the browser shell, keeps a registry
of projects and a journal of receipts under a state directory, and
proxies the Record’s read and command routes to a per-project API
child it starts and replaces. Its own operations — create, init,
attach, detach, forget, sync, publish, forge, the local body, the
remote body, secrets by source name, the model probe, connections,
handoffs — are the CLI’s verbs run detached with a
pid, an exit code and a log as files, and each answers a receipt
(recorded → admitted|refused → running → succeeded|failed|outcome_unknown)
under a command-<digest> identity that replays on an identical
retry and conflicts on a changed one. A run that passes its deadline
is outcome_unknown when the verb reaches beyond this machine, never
failed; the head appends nothing to any record itself. It is
trusted-local and refuses an oidc project.
The head tells the browser when to read again, and the browser does
not poll it (GH #986). GET /api/hale/v1/head/events is a
text/event-stream the head writes event: changed to — data: rows
when a row landed in the active project’s organism, data: head when
the head’s own state moved: a receipt journaled, a run or a child
started or ended, the API child answering. An event carries nothing to
render; the face reads the API, the one source of truth, again, and
once more on every reconnect. A comment line every 15 s keeps a quiet
stream open and finds one whose browser left; a write that does not
complete within a second closes its stream. At most 16 streams are
held; one more is refused 503 busy. The rows come from the nerves
(Rows landing): the head subscribes to the active project’s
organization and every owner’s, as the head’s user, on
HALE_DNA_NATS_URL_HEAD, else on the server of
HALE_DNA_NATS_URL_OWNER (which it hands a body it starts), else on
the project’s compose nerves service while hale dna dev has it up
(asked for, never brought up; asked again every 5 s). With none, only
the head’s own state is pushed. The application view still reads on a
timer: the application’s readings (The heart’s events) do not reach
it yet.
The API (dna/api, source-built; the head starts one per project)
is a head of memory. It reads the Knowledge graph in its own process
under the head’s role (HALE_DNA_MEMORY_DSN_HEAD), through one handle
opened on the first read and held for the process’s life; a projection
that has not reached the record’s head answers
knowledge_projection_unavailable until the spine’s tick applies it,
and without the DSN Knowledge reads are unsupported
(knowledge_unsupported). It admits Knowledge commands — the
*_knowledge members of its surface, requiring position, reached over
its HTTP exposure like its other commands — into the
record in the same process, under the explicit authority policy in the
file HALE_DNA_KNOWLEDGE_COMMAND_POLICY names (dna.knowledge-authority/1,
at most 64 KiB, decoded by KnowledgePolicyCodec in
dna/operations/knowledge_policy.hl against the record’s identity and
dna.trust). The policy is read once, when the API starts: an edit to
the file does not change a running API’s basis, and a policy that does
not decode stops the API (exit 2). dna/api/README.md describes the
reads and the topics.
What is deliberately not here yet: a fleet-level semantic diff (a
Review’s diff is the edited seed’s; the deploy row names the instances
it reaches), pressure raised from services’ typed metrics (hale dna pressure raise is the spelling; nothing raises it for a node), and
the organization as an instance of its own plan.
Environment
Section titled “Environment”Every HALE_DNA_* variable the DNA reads, and the few others its
programs read. The host, the CLI and the
programs they start pass most of these to each other: a variable the CLI
sets for a child is listed under the process that reads it. Variables
that name a role’s memory or nerves are never inherited by a process
that is not that role; the harness strips them from everything it starts
(see Memory and The senses and the reflexes).
crates/hale-cli/tests/env_knobs_are_documented.rs fails if a variable
the tree names is missing here or in the runtime’s tables
(runtime.md), or a row names one the tree no longer does.
A boolean is on for any value but empty or 0 unless its row says
otherwise.
| Variable | Default | Effect |
|---|---|---|
HALE_DNA_DISCOVER=off |
discover | off makes init’s discovery (harnesses on PATH, local model servers) find nothing; for fixtures. The api head passes it on to the init it runs. |
HALE_DNA_TAPE=record|replay |
the catalog’s mode |
Whether a RecordedModel whose mode_env names it records answers or replays them; the trio fixture’s catalog and scripts/dna-cycle name it. |
HALE_DNA_TAPE_DIR=<dir> |
the catalog’s dir |
The tape directory of a RecordedModel whose dir_env names it. |
HALE_DNA_TOOLCHAIN=<path> |
the running hale |
The toolchain binary the host and the processes it starts use; hale dna sets it to the binary it is. It is part of a seed’s build fingerprint. |
HALE_DNA_GENOME=<dir> |
unset | The record root the organization takes its genome from; hale dna and the host set it for the organization they start, and the model adapters read it at birth like a credential names its source. |
HALE_DNA_GENOME_POLL=<s> |
300 | How often the host polls the record’s remote for a changed genome, to restart the organization on it. |
HALE_DNA_NO_BUILD_CACHE |
off | Turns the content-fingerprint seed build cache off, so every seed is built. |
HALE_DNA_FORGE=<kind> |
inferred | Names the forge (github, the file forge) instead of inferring it from github: in the record’s profile or the remote. |
HALE_DNA_TRANSPORT=<kind> |
ssh |
local runs every infrastructure script on this machine instead of over ssh; for fixtures. |
HALE_DNA_TRANSPORT_HOME=<dir> |
unset | With local, the body’s home directory. |
HALE_DNA_GIT_TIMEOUT=<s> |
8 | Seconds a record fetch, push or ls-remote may take before it is killed. |
HALE_DNA_NODE=<name> |
<hostname>:<pid> |
This node’s name in its claims (GH #1026). |
HALE_DNA_OWNER=<org> |
unnamed | The organization this body is, for the ownership graph. |
HALE_DNA_LEASE=<name> |
unset | The lease a body holds; set by the body for the processes it starts. |
HALE_DNA_LEASE_TOKEN=<n> |
unset | The lease’s fencing token; set with HALE_DNA_LEASE. |
HALE_DNA_MEMORY_DSN_OWNER=<dsn> |
the compose Postgres | A Postgres of your own instead of dna/compose.yaml’s: the owner role that migrates memory and creates the record’s roles. Read by memory migrate, dev, show and the fixtures that migrate or drop a record. |
HALE_DNA_MEMORY_DSN_SPINE=<dsn> |
unset | The spine role’s DSN, printed by memory migrate and handed to the process that is the spine. |
HALE_DNA_MEMORY_DSN_HEAD=<dsn> |
unset | The head role’s DSN, handed to the head and to nothing else. |
HALE_DNA_OWNER_KEYS=<keys> |
one owner | <owner>=<key> …, space-separated: the owners memory migrate writes for a shared record. The keys stay the record’s; memory never holds signature bytes. |
HALE_DNA_RECEIPT_KEY=<key> |
unset | Sixteen characters or more: the key evidence bodies are sealed under. The first key given is written; a different one later is refused, because bodies sealed under the first would no longer open. |
HALE_DNA_NATS_URL_OWNER=<url> |
the compose NATS | The owner user’s URL: it creates the organization’s stream (nerves migrate, dev). |
HALE_DNA_NATS_URL_SPINE=<url> |
unset | The spine user’s URL, handed to the process that is the spine. |
HALE_DNA_NATS_URL_HEAD=<url> |
unset | The head user’s URL: subscribes, publishes nothing. |
HALE_DNA_NATS_URL_APP=<url> |
unset | The application’s URL: publishes on its own subjects only. |
HALE_DNA_NATS_URL_REFLEXES=<url> |
unset | The reflexes’ URL: publishes on <org>.app.reflexes.> alone. |
HALE_DNA_NATS_USER_APP=<user> |
unset | The attached application’s own nerves user, app-<name> (hale dna nerves migrate prints it; GH #989). |
HALE_DNA_NATS_VAULT_APP=<name> |
unset | The vault entry holding the application user’s nerves password (hale dna nerves migrate prints it): presented on CONNECT, never carried in a URL (GH #989). |
HALE_DNA_NATS_VAULT_REFLEXES=<name> |
unset | The same for the reflexes user; dna/reflexes refuses to start without it. |
HALE_DNA_COMMANDS_PORT=<port> |
unset | The loopback port of the API child’s HTTP commands transport, given by dna/face/start.sh to the head and by the head to its API child; without it the head answers commands_unavailable (503). |
HALE_DNA_NATS_ORG=<token> |
unset | The organization’s token, the prefix of its subjects; dev hands it to the host and the host to the parts. |
HALE_DNA_SENSES_URL=<url> |
unset | The senses store’s read URL, for the reflexes. |
HALE_DNA_SENSES_PORT=<n> |
the seed’s per-part port | The port a part serves its scrape endpoint on; 0 serves nothing. |
HALE_DNA_SENSES_HOST=<addr> |
0.0.0.0 |
The address a part’s scrape endpoint listens on. |
HALE_DNA_REFLEX_TICK_MS=<ms> |
5000 | How often the reflexes read the store. |
HALE_DNA_REFLEX_WINDOW_SECS=<s> |
600 | How close two firings for one target are a pattern, not an accident. |
HALE_DNA_ORGANIZATION_LAUNCH=<attempt> |
unset | Set by the host on the organization process it starts for a workflow attempt: which attempt launched it. |
HALE_DNA_RESTART_FOR=<mutation> |
unset | Set by the host when it restarts an expression on a Mutation’s request; the expression journals expression.restarted for it (GH #529 D6). |
HALE_DNA_EXPRESSION=<name> |
unset | The expression that was restarted; set with HALE_DNA_RESTART_FOR. |
HALE_DNA_DESIGN_SUFFIX=<text> |
unset | Appended to the design practice hale dna new seeds, so a fixture can exercise upgrade’s supersession against real record history. |
HALE_DNA_LIBRARY_VERSION=<version> |
the toolchain’s | The version the library is proposed under (library/<node>@<version>, and on each idea): a fixture’s way to make a later toolchain out of the one binary it has, so upgrade’s supersession is exercised against real record history. |
HALE_DNA_LIBRARY_SUFFIX=<text> |
unset | Appended to every library idea’s text, so a later version’s ideas differ from the earlier’s (fixtures only). |
HALE_DNA_LIBRARY_EARLIER_NAMES=1 |
unset | The two moved chapters are proposed under their earlier names (library/api, library/shaping): a fixture’s way to make a record the earlier toolchain would have seeded, so an upgrade is seen finding them (fixtures only). |
HALE_DNA_INSTANCE=<id> |
unset | Set by a node on each instance it starts: which instance of its shape the process is. |
HALE_DNA_BODY=<mark> |
unset | The body’s mark: the host puts it in the environment of the processes it starts as a body (body_mark: holder, lease token, pid), and the body fence finds them by it. |
HALE_DNA_SCAN_NEEDLE=<mark> |
unset | Set by the body fence’s scan on a probe process: the mark it looks for in every other process’s environment (ps -E) to find the body’s processes. |
HALE_DNA_SCAN_PROBE=yes |
unset | Set on a probe process by the fence’s environment scan, which checks that this operating system shows one process another’s environment before relying on HALE_DNA_BODY. |
HALE_VERSION=v<pin> |
unset | Not read by the DNA: the host sets it for hale-lang.org’s install.sh when it installs the toolchain version a body is pinned to. |
HALE_DNA_ONESHOT |
off | A hale dna run application returns after the ping instead of staying up. |
HALE_DNA_API=<url> |
http://127.0.0.1:8793 |
The head API a leg’s verbs (hale dna work …) talk to when --api is not given. |
HALE_DNA_ID_TOKEN=<token> |
unset | The bearer token a leg presents to the head; sent only over https:// or loopback, never plain http off this machine. |
HALE_DNA_HEAD_STATE=<dir> |
${XDG_STATE_HOME:-$HOME/.local/state}/hale/dna/head |
The project head’s state directory (its launch token and key). |
HALE_DNA_COMMAND_POLICY=<path> |
unset | The explicit authority policy the native command lanes and the governance verbs require; with none, they refuse. |
HALE_DNA_KNOWLEDGE_COMMAND_POLICY=<path> |
empty policy | The policy file for Knowledge commands. |
HALE_DNA_ORGANIZATION_POLICY=<path> |
unset | An optional, independent policy for organization-source proposals and Review writes; configuring evidence reads does not enable them. |
HALE_DNA_TASK_POLICY=<path> |
unset | The policy for task administration commands. |
HALE_DNA_RPC_SOCKET=<path> |
the record’s socket under XDG_RUNTIME_DIR/hale/dna (else .hale/dna) |
Where the head serves its surface (HeadCommands, GH #1417) over unix::Rpc; the capabilities name it to the legs. |
HALE_DNA_ORG_DRAFTS=1 |
off | Lets the api accept organization-source drafts (1 only). |
HALE_DNA_OIDC_ISSUER=<url> |
unset | The project head’s OIDC issuer; plain http is accepted on this machine only. A project head needs the four HALE_DNA_OIDC_* below or it refuses to start. |
HALE_DNA_OIDC_CLIENT=<id> |
unset | The head’s client id at the issuer. |
HALE_DNA_OIDC_MEMBER=<subject>=<member> |
unset | Which member a subject is. |
HALE_DNA_OIDC_KEY=<pin> |
unset | The pin of the issuer’s signing key. |
HALE_DNA_OIDC_KEY_FILE=<path> |
unset | The stub issuer’s private signing key. |
HALE_DNA_OIDC_KEY_X=<x> |
unset | The x coordinate of the stub issuer’s public key. |
HALE_DNA_OIDC_KEY_Y=<y> |
unset | The y coordinate of the stub issuer’s public key. |
HALE_DNA_OIDC_SERVICES=<clients> |
unset | The service clients the stub issuer accepts. |
HALE_DNA_CHILD_PROOF=<proof> |
unset | The proof a face gave its child; the child answers an unauthenticated request with X-Hale-Child: <digest of the proof>, so the face knows its own child answered before it forwards a token. |
HALE_DNA_TRUSTED_LOCAL=1 |
off | The trusted-local session, a fixture’s mode alone (1 only). A project serves its heads under OIDC. |
HALE_DNA_KEEP_SCRATCH |
off | Fixtures and the suite’s sweep keep their scratch roots, for looking at what a fixture built. |
HALE_DNA_SUITE_TAG=<tag> |
unset | The stamp a DNA suite slice puts on the processes and scratch roots it starts, so its sweep collects only its own. |
HALE_DNA_WAIT_SCALE=<n> |
1 | A whole number of at least 1 that widens every fixture wait. |
HALE_API_BIN=<path> |
built from the checkout | dna/face/start.sh: a prebuilt api binary to launch, as --api does. A binary handed in is checked before anything is built. |
HALE_HEAD_BIN=<path> |
built from the checkout | dna/face/start.sh: a prebuilt head (project service) binary to launch, as --head does. |
HALE_OIDC_BIN=<path> |
built from the checkout | dna/face/start.sh: a prebuilt stub OpenID provider (dna/oidc/serve) to start. hale dna head hands in the one it built. |
HALE_REFLEXES_BIN=<path> |
built from the checkout | dna/face/start.sh: a prebuilt reflexes binary (dna/reflexes) to start beside an attached project. hale dna head hands in the one it built. |