# tik — full documentation > The complete tik documentation (https://tik.projects.metio.wtf/) concatenated for > LLMs. For a concise link index see https://tik.projects.metio.wtf/llms.txt. # tik **tik is a process system, not a ticket system.** A ticket is an append-only log of signed, content-addressed facts and artifacts. Where that ticket stands in its process is a **derived value** — a pure function of the log, the process definition, and the current time. Nobody moves a ticket. People and agents contribute evidence, guards check it, stages derive, and `tik explain` says exactly what is missing and who can supply it. ```console $ tik set 3184 severity=:high resolution.ref=abc123def ok $ tik explain 3184 To reach :resolved: ✓ [:fact [:resolution :ref]] ✗ attach an artifact whose path starts with "repro/" blocks: :closed (see: kb/runbooks/support-request-resolved.md) ``` ## The one law **Derived beats declared.** If something follows from the log, storing it as authoritative state is a bug. Stage, readiness, "who is blocked", the board, the inbox, the roadmap — every one of them is computed on read, so every one of them is correct by construction. A cache that goes stale is a cache; the answer lives in the events. The law has a companion: **coordination-free by construction**. Every operation is correct on arbitrarily many replicas that share nothing and reconcile by unioning their event sets. No leader, no lock, no consensus — contention resolves by derivation. [Derived beats declared →](/concepts/derived-beats-declared/) ## What you get - **An answer to "what now?"** — `tik explain` names the missing evidence, its schema, and who is allowed to sign it. `tik next` turns that into a per-person inbox. - **Offline forever.** A store is files in a directory. Signatures are detached sidecars from `ssh-keygen -Y`; the filename of an event is its own SHA-256, so `sha256sum` alone audits the store. - **Replication by `git pull`.** Two clones append independently and merge by set union, with no file-level conflict to resolve by hand. - **Time travel that costs nothing.** Derivation takes `now` as an argument, so `tik status --at 2026-03-01` answers what was true in March by evaluating March, reproducibly. - **Processes as data.** A definition is plain EDN, hash-pinned at ticket creation, linted for a closed guard vocabulary and stratified negation, and testable with scripted cases. ## Start here - [Install](/get-started/install/) — one binary, or babashka from source. - [Your first ticket](/get-started/first-ticket/) — a working store in five minutes, no server and no database. - [Concepts](/concepts/) — the model behind the CLI. - [Authoring processes](/authoring/) — designing one worth deriving. Licensed 0BSD and [REUSE](https://reuse.software)-compliant. --- # Get started Source: https://tik.projects.metio.wtf/get-started/ A tik store is a directory. Make one, create a ticket, record a fact, and the stage derives itself — there is no server to run and no schema to migrate. - [Install](/get-started/install/) — the single binary, babashka from source, or the nix devshell. - [Five-minute guide](/get-started/guide/) — from an empty directory to a bug tracker with signed events and a personal inbox. Every command on that page is executed by the test suite, so it stays true. Once a store exists, [Concepts](/concepts/) explains the model the CLI is a surface over, and [Authoring processes](/authoring/) covers designing one of your own. --- # Getting started Source: https://tik.projects.metio.wtf/get-started/guide/ A working ticket system in under five minutes: no server, no database, no account, no YAML. Tickets are plain files in a directory you own; everything else — stages, boards, inboxes — is computed from them when you ask. Every command below is executed verbatim by the test suite (`test/tik/guide_test.clj`), so if this page and the software ever disagree, the build breaks before you can notice. ## Install Either run the single binary (Linux x86-64, no dependencies): ```sh ./tik --help ``` or run from source with [babashka](https://babashka.org/): ```sh bb tik --help ``` The pages below write `tik`; substitute `bb tik` if you run from source. ## Minute one: track a thing Make an empty directory and create a ticket. That is the entire setup: ```sh mkdir my-tickets && cd my-tickets tik new track --title "replace the office router" ``` The command prints the ticket's id. Two commands tell you everything about it (a unique prefix of the id is enough, like git): ```sh tik status # where it stands, and what would move it tik ls # every open ticket, with its derived stage ``` The built-in `track` process has two stages: the ticket exists (`open`), and it ended with an outcome on record (`done`). Record the outcome and the ticket settles itself: ```sh tik set outcome=ordered the UniFi one, arrives Tuesday ``` Notice what you did NOT do: no status dropdown, no "move to Done". You recorded a fact; the stage followed from it. That is the whole model — **stages are never set, they are derived from facts**, and that stays true from this two-stage starter to the largest process you will ever define. ## Minute three: a real process from a template `track` is deliberately minimal. For a bug tracker, start from the built-in template: ```sh tik author --template bug ``` This writes three things you own and can edit freely: - `processes/bug.edn` — the process definition: four stages (`reported`, `confirmed`, `fixed`, `verified`), each defined by WHAT MUST BE TRUE to reach it, never by who dragged a card where. - `kb/runbooks/bug-*.md` — one short runbook per stage. - `processes/bug.tests.edn` — scripted tests for the process itself. Open `processes/bug.edn` and put real usernames in the two roles (they ship as `change-me`), then file the first bug: ```sh tik new bug --title "login fails on Firefox" tik set severity=:high repro.steps=open the login page, click sign in, watch it 500 ``` Now ask the question every ticket system exists to answer: ```sh tik explain ``` ```text To reach :confirmed: ✗ fact [:approval :triager] = :approved — a member of role :triager must sign ``` explain never speculates: every line is derived from the definition and the recorded facts, ordered by what you can act on right now. When several people share the board, each gets a personal answer: ```sh tik next --actor alice # what can alice do that unlocks the most? tik next --role :triager # what is the triager role being waited on for? ``` ## Minute five: your own process Describe your workflow in plain words and tik writes the definition — no EDN knowledge needed: ```sh tik author ``` The interview asks for stages ("what does reaching this stage mean?") and, per stage, what must be true: a piece of information, a choice from fixed options, a signature by a role, a file, or waiting time. It compiles your answers to a linted definition plus runbooks and a test skeleton. Prefer to draft with an LLM? `tik author prompt` prints a prompt that makes any model emit the answers file, and `tik author --from answers.edn` builds from it. Try a definition live before using it — a scratch ticket, reloading on every save: ```sh tik sim processes/bug.edn ``` And pin its behavior with scripted tests (steps in, expected stages out; failures print explain so the process tells you why): ```sh tik test processes/bug.tests.edn ``` ## What you now have that a tracker does not give you **An audit trail nobody can quietly edit.** A ticket is an append-only log of content-addressed events: the filename of every event IS the sha256 of its bytes, and each event names its parents. `tik verify` re-checks the whole store with nothing but hashes — and with a signing key (`tik actor add`, `TIK_KEY`), every write is signed and verification extends to WHO said everything: ```sh tik verify ``` **Time travel and what-ifs, for free.** Because stages are computed from facts, any question about any moment is answerable: ```sh tik status --at 2026-07-01T00:00:00Z # the state back then tik whatif severity=:low # what would change? (nothing is written) tik causal # which events made each stage true ``` **A board you can mail.** `tik board` renders the whole store into one dependency-free HTML file; `tik serve` serves it live; `tik bundle ` packs one ticket into a tarball a third party can verify with nothing but coreutils and ssh-keygen. **Email in, alerts out.** `tik bridge email` turns mail into tickets and comments (replies with `tik> key=value` lines become facts); `tik effects run` pushes derived transitions to Slack, Discord, Matrix, Teams, ntfy, PagerDuty, plain webhooks, email, or any program via the command sink — see `tik --help` for the full list. ## Where your data lives In the directory you made: `tickets/` holds the events, `processes/` your definitions, `actors` the signer registry. Plain files — put them in git and you have replication, history, and backup; two machines that both changed a ticket merge by file union, and the derived stage converges because derivation is a pure function of the event set. Nothing is ever stored about a ticket's stage. If that sentence bothers you, you now understand tik; if it delights you, read [the design plan](https://github.com/metio/tik/blob/main/docs/PLAN.md) — the design in full. --- # Install Source: https://tik.projects.metio.wtf/get-started/install/ Each release publishes two downloads, a `SHA256SUMS` file covering both, and a cosign keyless signature over those checksums. ## Download a release From the [releases page](https://github.com/metio/tik/releases): - **`tik-linux-amd64-glibc`** — the native binary, no runtime needed. It is labelled for exactly what it is: linux/amd64 against glibc. - **`tik.jar`** — the uberjar, for macOS, Windows, and arm. Runs on any JDK 21: `java -jar tik.jar --help`. The container image is the universal deployment path. ```sh chmod +x tik-linux-amd64-glibc ./tik-linux-amd64-glibc --help ``` Verify before trusting. The checksums cover the downloads, and the cosign bundle covers the checksums: ```sh sha256sum --check SHA256SUMS cosign verify-blob SHA256SUMS \ --bundle SHA256SUMS.bundle \ --certificate-identity-regexp '^https://github.com/metio/tik/' \ --certificate-oidc-issuer https://token.actions.githubusercontent.com ``` ## From source with babashka The CLI runs on [babashka](https://babashka.org/), so a checkout is enough: ```sh git clone https://github.com/metio/tik.git cd tik bb tik --help ``` Every command in this documentation is written as `tik`; substitute `bb tik` when running this way. ## The nix devshell Contributors get the whole toolchain — JVM, Clojure, babashka, clj-kondo, TLC, GraalVM, ssh-keygen — from the flake: ```sh nix develop --command bb test ``` [Contributing](/contributing/) describes the gate a change is expected to pass. ## Signing your writes A store works unsigned, and signing turns authorship into evidence anyone can check offline. Register yourself as an actor and point `TIK_KEY` at an ed25519 private key: ```sh ssh-keygen -t ed25519 -f ~/.config/tik/id_ed25519 tik actor add alice ~/.config/tik/id_ed25519.pub export TIK_KEY=~/.config/tik/id_ed25519 export TIK_ACTOR=alice ``` Signatures are detached sidecars produced by `ssh-keygen -Y`, so `tik verify` checks them with stock OpenSSH and nothing else. The public registry (`actors`) belongs in version control; the private key stays outside the store. ## Where a store lives Commands find a store the way git does: `TIK_ROOT` wins, otherwise the nearest ancestor directory holding `tickets/`, `tik.db`, or `.tik/`, otherwise the current directory. `tik init` marks one explicitly — `--sqlite` for the single-file backend, `--hidden` to keep everything inside `.tik/` when the store sits above several repositories. --- # Concepts Source: https://tik.projects.metio.wtf/concepts/ tik has a small model and holds to it strictly. Everything below follows from one law and one companion law, and the whole vocabulary is closed and enumerable: seven event types, twelve guard operators, five fact statuses. - [Derived beats declared](/concepts/derived-beats-declared/) — the law, and what it costs to keep. - [Events and facts](/concepts/events-and-facts/) — what a ticket actually is on disk. - [Stages](/concepts/stages/) — how position in a process is computed. - [Guards](/concepts/guards/) — the closed operator vocabulary. - [Explain](/concepts/explain/) — the product surface: what is missing and who can act. - [Replication](/concepts/replication/) — merging by set union, with no leader and no lock. - [The event log](/concepts/event-log/) — the storage contract in detail. - [Witnessing](/concepts/witness/) — countersignatures and anchoring. The [decision log](/decisions/) records why each of these is the way it is, including the alternatives that were rejected and the reason. --- # Derived beats declared Source: https://tik.projects.metio.wtf/concepts/derived-beats-declared/ **If something can be derived, storing it as authoritative state is a bug.** That is the whole law, and every design question in tik resolves against it. A ticket's stage is the obvious case. It is never written down anywhere. It is `f(events, now)` — a pure function of the ticket's own event log, the process definition that ticket pinned, and the instant you are asking about. Ask twice with the same inputs and you get the same answer, forever. ## What the law buys **Correctness by construction.** A stored status can disagree with the facts; a derived one cannot. When somebody retracts the approval that a ticket's `:approved` stage depended on, the stage regresses on the next read, with no rollback step, no cleanup job, and no window during which the board is lying. **Every question about every moment.** Because `now` is an argument rather than an ambient clock, "what was true on March 1" is answered by evaluating March 1: ```sh tik status 3184 --at 2026-03-01T00:00:00Z ``` **Replication without coordination.** There is no authoritative mutable cell to serialize behind a lock, so replicas merge by unioning their event sets and each derives the same answer independently. See [Replication](/concepts/replication/). **Auditability that survives the tool.** The conclusion is reproducible from the signed bytes by anyone, including someone who does not run tik. `tik bundle` packs one ticket into a tarball that verifies with coreutils and `ssh-keygen` alone. ## What the law costs Derivation is work done on every read, and the law forbids the obvious shortcut of remembering the answer. tik pays that cost deliberately and keeps it bounded: the fold is linear in events and polynomial in the size of the process definition, which is authored and small. Fact lookups are served from indexes the fold maintains, so history length does not turn into a quadratic. Performance problems get indexes, caches, or a different storage backend. They never get new authoritative state. A cache that some lens keeps is fine as long as nothing treats it as the truth — the truth is recomputed from the events. ## The companion law **Coordination-free by construction: no leader, no lock, no consensus.** Every operation is correct on arbitrarily many replicas that share nothing and reconcile only by eventually unioning their grow-only, content-addressed event sets. Reads consult one ticket's own log, so they shard without limit. Writes are either content-addressed events that are a pure function of their intent — two replicas forming the same intent emit byte-identical events, and the union keeps one — or appends whose contention resolves by *derivation*: two competing claims about a fact reduce to `:conflicted`, a derived state, rather than a lock to be won. A design that needs a replica to win an election, hold a lock, or agree with a quorum before it can act breaks this law as surely as caching a derived value breaks the first one. ## What the kernel refuses to answer **The kernel answers "what follows from these signed facts?" It never answers "what should happen next?"** There are no workflow transitions, no scheduler, no policy engine, and no external queries in the core. A guard consults one ticket's log and nothing else — never a service, never another ticket, never a clock of its own. Notifications, inboxes, webhooks, boards, and agent surfaces are all porcelain over derivations. That boundary is what keeps evaluation offline, reproducible, and true years from now. --- # Events and facts Source: https://tik.projects.metio.wtf/concepts/events-and-facts/ A ticket is an append-only set of events. Everything else about it is computed. ## Seven event types The vocabulary is closed and versioned, because the semantics of a verifiable kernel have to be enumerable: | Event | Meaning | |---|---| | `:ticket/create` | the ticket exists, pinned to a process definition by hash | | `:fact/assert` | a claim about a path: `[:severity] = :high` | | `:fact/retract` | withdraw a claim, with no replacement | | `:fact/dispute` | reject a claim, with a reason | | `:artifact/attach` | a file, stored and addressed by its hash | | `:attestation/add` | a signed claim the kernel does not interpret | | `:process/migrate` | re-pin this ticket to a newer definition | Things that look like they need their own type turn out not to. Comments are artifacts — text blobs attached by hash. Links are facts under a `[:link …]` path. Work records are `:work` attestation claims. Witness countersignatures are detached sidecars over a head rather than events, because an event would move the very head it witnesses. ## Content addressing An event's id is the SHA-256 of its canonical bytes, and the stored file is named for that id. The bytes on disk are exactly the hashed region, so a store audits with nothing but coreutils: ```sh sha256sum tickets/*/events/*.edn # filename must equal the digest ``` Signatures never live inside the hashed region. They are detached sidecars alongside the event, which is what lets a second person add their signature to an event without changing its identity. Every event names its parents, forming a Merkle DAG. Parents carry integrity and causality — they are how a replica knows whether two writes saw each other. They never carry ordering: the fold orders by `(at, id)` over the event *set*, which is what makes the reducer total, commutative, and idempotent. ## Fact status One function decides why a fact does or does not satisfy a guard, and guards consult nothing else: | Status | Meaning | |---|---| | `:present` | a value stands | | `:absent` | nothing has been claimed | | `:retracted` | withdrawn, no replacement offered | | `:disputed` | rejected with a reason, and unusable until corrected | | `:conflicted` | causally concurrent claims disagree | ## What a dispute means A dispute rejects **the value that stood when it was raised**, so the assertion that answers it has to claim something else. Re-asserting the rejected value verbatim leaves the path `:disputed` — otherwise the party a dispute holds accountable could clear it in one command by retyping the same fact. A retraction clears disputes too, because the rejected claim is gone. A dispute raised on a path holding nothing rejects no particular value, so the first assertion answers it, which is what keeps a dispute from making a path permanently unusable. Disputes accumulate: several people may reject the same claim, and each withdraws only their own. ```sh tik dispute 3184 category --reason "this is billing, not technical" tik dispute 3184 category --withdraw # takes back your own objection ``` ## What a conflict means When two replicas write the same path without having seen each other, and they disagree, the fact reads `:conflicted` and every guard that depends on it fails with a reason saying so. Nobody has to win. A later write that observed both supersedes them, and the conflict resolves by derivation. Concurrent writes that happen to *agree* are not a conflict — there is no disagreement to surface. --- # Explain Source: https://tik.projects.metio.wtf/concepts/explain/ A traditional tracker exposes state. tik exposes **justification**: what is true, why it is true, and what evidence is missing next. `tik explain` is where that surfaces, and every other view is a rendering of it. ```console $ tik explain 3184 To reach :resolved: ✓ [:fact [:resolution :ref]] ✗ attach an artifact whose path starts with "repro/" blocks: :closed (see: kb/runbooks/support-request-resolved.md) ``` Nothing in that block is speculation. The checkmarks are guards that already hold, the crosses are structured reasons produced by guard evaluation, `blocks` is the downstream closure, and the hint is the [runbook](/runbooks/) the definition declares for that stage. ## Reasons are data The prose lives in this lens only. Underneath, each missing step is a structured reason carrying the path, the schema, the role, the actor whose signature already counted — whatever the guard knows: ```sh tik explain 3184 --edn ``` That data contract is stable plumbing; the English is not. It renders as CLI text, as web forms built from the schemas in the reasons, and as agent task specifications whose acceptance criteria *are* the guards. Reasons are sorted by **who can act on them right now**: values anyone can supply first, then corrections, artifacts, specific people, attestations, other stages, and finally time, which is nobody's to act on. ## Who can act `--actor` filters a block to what one person can do, and counts the rest rather than hiding it: ```console $ tik explain 3184 --actor alice To reach :resolved: ✗ attach an artifact whose path starts with "repro/" … 1 step(s) waiting on others or time ``` `tik next` rotates the same derivation into an inbox — for a person, or for a whole role: ```sh tik next --actor alice tik next --role :triager ``` The inbox ranks by unlock impact, so the step that frees the most downstream work comes first, and it holds back tickets whose dependency links point at unsettled upstream work. ## Waiting versus impossible Some blocked tickets are waiting for a colleague. Others can never move at all — a role with no members, a negation over a sticky stage already reached, a prerequisite that is itself dead. Those look identical until you say so, and telling somebody to wait for something that will never arrive is the failure that matters most for a tool whose whole claim is answering what is blocking. ```console $ tik explain 9c21 To reach :approved (unreachable): ⊘ fact [:sign-off] must be asserted by a member of role :auditor (currently by "seb") — nobody can ever do this ``` The derivation stays conservative: a step is only called impossible when it is provably undischargeable from the definition and the log. A choice dies only when every one of its branches does. Anything merely waiting keeps its `✗`. ## Proving it `tik causal` answers the auditor's question — which signed events made each reached stage true, with negations and time saying so honestly: ```sh tik causal 3184 ``` And `tik whatif` asks the counterfactual without writing anything: ```sh tik whatif 3184 severity=:low +P2D retract:category ``` --- # Guards Source: https://tik.projects.metio.wtf/concepts/guards/ # Guards Deterministic pure functions of `(events, now)` from a closed, versioned vocabulary (`tik.guard`). Never effectful: `verify` must re-evaluate them identically, offline, years later. Effectful validation lives only in edge admission checks (CLI porcelain, server ingest), before events are minted. Design law: **facts over flags.** A bare boolean guard is a checkbox with extra steps; `tik lint` warns. Prefer facts useful downstream — categories that route branches, artifacts, signed approvals. --- # Replication Source: https://tik.projects.metio.wtf/concepts/replication/ A tik store replicates by copying files. Two clones append independently, merge by set union, and derive the same answer — because derivation is a pure function of the event set and events are addressed by their content. ```sh git pull && git push # this is the replication protocol ``` ## Why the merge is trivial Every event lives in its own file whose name is the SHA-256 of its bytes. Two people adding evidence to the same ticket create different files, so the merge is a union of directories and git never sees a conflicting hunk. Two people making the *identical* claim produce byte-identical files with the same name, and the union keeps one — deduplication for free. That is why merging never requires a human to resolve a text conflict inside a ticket. What it can produce is a *derived* disagreement, and that is the point. ## Disagreement resolves by derivation When two replicas wrote the same fact path without having seen each other, and their claims differ, the fact reads `:conflicted`. Every guard reading it fails with a reason naming the competing claims, and `tik explain` asks for a value that supersedes them: ```console $ tik explain 3184 To reach :triaged: ✗ fact [:category] has conflicting concurrent assertions — one must supersede (ADR 0003) ``` Nobody wins an election. A later write that observed both competitors settles it, and the conflict disappears from the derivation. Parents are what make "observed" a checkable claim rather than a guess. ## No leader, no lock, no consensus No operation needs a leader, a distributed lock, or a quorum to be correct: - **Reads shard without limit.** A derivation reads one ticket's own log and nothing else. Guards never query across tickets, so N stateless replicas across M shards scale reads with zero coordination. - **Writes never resolve by lock.** A write is either a content-addressed event that is a pure function of its intent, or an append whose contention resolves by derivation. - **Self-minted events are deterministic.** Anything a replica mints on its own — a recurring ticket, a scheduled probe — derives its id and every byte, `:at` included, from its inputs. Two replicas firing the same schedule concurrently mint the same event, and the union keeps one. A tik replica is stateless by construction, so horizontal scaling is the preferred deployment. ## Partial logs say so Between syncs, a replica's copy of a ticket is incomplete by definition, and a derivation over a partial log is not merely incomplete — it is confidently wrong in a specific way. Ancestry the replica cannot see reads as concurrency, so writes that supersede each other in a linear history surface as `:conflicted`, and explain asks somebody to resolve a conflict that does not exist. Every lens therefore checks whether referenced ancestors are missing and says so, rather than presenting a mid-sync view with the confidence of a complete one. ## Backends The file store is the signed interchange format: a `tickets/` tree that `sha256sum` audits and git replicates. The SQLite backend keeps the same events in a single file when that suits operations better. Convert in place with `tik store migrate --to sqlite|file` — events and their detached signatures both travel. --- # Stages Source: https://tik.projects.metio.wtf/concepts/stages/ A process defines stages. Each stage names its prerequisites (`:after`) and the conditions that must hold to reach it (`:guards`). Reaching a stage is never an action somebody takes; it is a conclusion. ```clojure {:stage/id :resolved :after [:triaged] :guards [[:fact [:resolution :ref]] [:or [:not [:fact= [:category] :technical]] [:stage-reached :reproducible]]]} ``` That second guard reads "technical implies reproduced" — material implication written out, because the vocabulary has `:or` and `:not` and needs no separate conditional. ## The reached set is a fixpoint Derivation computes the set of reached stages by iterating to closure: every stage whose prerequisites are reached and whose guards hold joins the set, which may in turn enable further stages, until nothing changes. Each iteration is a **synchronous sweep**: every stage is evaluated against the snapshot taken at the start of the sweep, and all newly enabled stages are added at once. Firing one stage at a time gives order-dependent answers even on definitions the linter accepts, because a later stratum negating an earlier one can jump the queue. The synchronous sweep is normative — a TLA+ model exhibits the counterexample, and a conformance corpus case pins the correct answer. Because `[:not [:stage-reached …]]` is negation inside a fixpoint, the linter enforces **stratified negation**: a definition may only negate stages in a strictly earlier stratum. That is what makes determinism provable rather than incidental. ## Regression is by derivation Nothing rolls a ticket back. Withdraw the evidence and the conclusion stops following: ```console $ tik retract 3184 resolution.ref --reason "wrong commit" $ tik status 3184 stage: triaged (reached: received, triaged) ``` The same holds for a dispute, and for a fact that a schema no longer accepts. The board cannot show a stage the evidence does not support, because there is no stored stage to go stale. ## Sticky milestones Some stages are milestones: reaching them once is a historical fact that later evidence does not undo. `:stage/sticky? true` says so, and the fold carries such a stage forward once any prefix of the log reached it. ```clojure {:stage/id :closed :after [:resolved] :stage/sticky? true :guards [[:fact [:customer :ack]]]} ``` A customer who withdraws their acknowledgement does not un-close the ticket. The log shows both truths — it was closed, and the ack was withdrawn — and the derived present misrepresents neither. Sticky is monotone **in fold position**, not in the event set. The reached set is a function of the whole trajectory, so an event that arrives by merge carrying an earlier timestamp splices a new prefix into that trajectory and every later prefix is re-derived. A replica can therefore hold a sticky reach, sync, and no longer derive it. Convergence is unaffected — the same event set always derives the same answer — but a reach observed before a sync is not promised to survive it, which is why an effect pipeline records that it fired as its own fact. ## Time Time enters derivation as an explicit `now` argument. The kernel reads no clock of its own, which is what makes a re-derivation years from now reproducible. Guards read the claimed clock — the `:at` an actor asserted — by default, and each fold step evaluates at its event's `:at` **clamped to the read's `now`**. No step acts as though more time has passed than actually has, so a postdated event cannot buy the 48 hours an `:elapsed-since` guard is waiting for. The clamp lifts by itself once that time really passes. ## Several stages at once A process is a graph, not a line, so a ticket can sit at several current stages at the same time — two branch tips, both maximal. `tik status` reports the reached set and the current tips; `tik debug` shows every sweep and every guard verdict that produced them. --- # The event log Source: https://tik.projects.metio.wtf/concepts/event-log/ # The event log A ticket is an append-only set of immutable, content-addressed events (canonical-EDN SHA-256). Replica merge is set union: replicas converge without coordination — *merge* is conflict-free by construction, *truth* is deliberately not. Conflicting claims are never hidden or resolved by merge; they remain in the log and surface as derived fact states ([decisions/0003](../decisions/0003-conflicts-block.md)). Ticket state and stage are pure functions of the log; any materialized view is a rebuildable cache ([decisions/0001](../decisions/0001-event-log-acceptance-test.md), [decisions/0013](../decisions/0013-derived-state-never-authoritative.md)). Fact semantics: - **assert** establishes the current effective value for a fact path while preserving every prior claim in history — replacement by a new claim, never "later timestamp wins". - **retract** withdraws a fact from satisfying guards without asserting a replacement ("wrong, no replacement"). - **dispute** records a signed rejection with a reason; a disputed fact stops satisfying guards until superseded by a corrected assertion. Stage regression is therefore never a mutation or workflow transition: when the evidence no longer entails a stage, the stage simply ceases to be derivable. The invariant underneath all of it: **the log records claims; derivation decides which claims currently participate in truth.** Append-only does not mean every claim stays true; immutable does not mean authoritative; signed does not mean correct; merged does not mean agreed. --- # The witness Source: https://tik.projects.metio.wtf/concepts/witness/ # The witness Servers countersign events at ingest: "I saw hash H at time T." Trusted signing time is the load-bearing input for "actor held role R at signing time" and the moment a stage's social meaning solidifies. **witnessed.dev** productizes this as a neutral third-party witness: countersigning event *hashes only* (no content leaves the organization), giving self-hosted instances independent timestamping evidence for audits. --- # CLI Source: https://tik.projects.metio.wtf/cli/ `tik` alone prints the full usage text, which is the authoritative reference. These pages group the verbs by what you are trying to do. - [Recording work](/cli/recording-work/) — creating tickets, asserting facts, attaching evidence, correcting mistakes. - [Reading a store](/cli/reading-a-store/) — the inbox, the board, explain, history, and counterfactuals. - [Store administration](/cli/administration/) — identity, roles, verification, migration, and packaging. Two conventions run through all of them. Any lens takes `--edn` or `--format json` for machine output, and any ticket id can be abbreviated to a unique prefix, the way git accepts short SHAs. --- # Reading a store Source: https://tik.projects.metio.wtf/cli/reading-a-store/ Every view here is computed when you ask, so none of them can be stale. ## What should I do? ```sh tik next --actor alice tik next --role :triager tik explain 3184 --actor alice ``` `next` ranks by how much downstream work each step unlocks, with quiet tickets rising. `explain` is the per-ticket answer: what is missing, whose signature it needs, and which stages it blocks. See [Explain](/concepts/explain/) for the model behind them. ## The board ```sh tik ls tik ls --all --long tik ls --where 'stage=:blocked and fact:severity=:high and not disputed' tik search firefox login ``` A selector is space-separated terms, all ANDed, each optionally negated: `stage=:blocked`, `fact:severity`, `fact:severity=:high`, `actor=seb`, `disputed`, `conflicted`, `unsigned`, `derived-from=`, `~text`. The same grammar drives `search`, and `tik dupes` reports near-title lookalikes. ## One ticket ```sh tik status 3184 tik status 3184 --at 2026-03-01T00:00:00Z tik log 3184 tik diff 3184 5 ``` `status` reports the derived stage, the facts behind it, links, and what is next. `--at` answers the same question about any past moment by evaluating that moment. `diff` shows the evidence gained over the last few events. ## Why, and what if ```sh tik causal 3184 tik whatif 3184 severity=:low +P2D retract:category tik debug 3184 ``` `causal` names the signed events that made each reached stage true — including negations and time saying so honestly. `whatif` shows the stage diff a change would produce and writes nothing. `debug` shows the fixpoint with its working: every sweep, every guard verdict. ## The roadmap and the wider picture ```sh tik plan tik plan roadmap.html tik roles tik work week --actor alice ``` `plan` derives the dependency-link roadmap — ready, blocked, done, cyclic, the critical path, and each item's unlock impact. `roles` shows who gates what: every role on the open board, its effective members, and the stages waiting on its signature. ## Sharing what you see ```sh tik board board.html tik serve --port 8080 tik bundle 3184 --out ticket-3184.tgz ``` `board` renders the whole store into one dependency-free HTML file you can mail or archive. `serve` publishes it live, read-only, with `/tickets.edn` and `/explain/.edn` for tools. `bundle` packs one ticket — events, signatures, witness marks, the pinned ruleset, and a `verify.sh` — into a tarball a third party checks with coreutils and `ssh-keygen`, no tik required. --- # Recording work Source: https://tik.projects.metio.wtf/cli/recording-work/ You record evidence. Stages follow. ## Create a ticket ```sh tik new support-request --title "login fails on Firefox" ``` The ticket pins the process definition's content hash at creation, so it is judged by the rules it was minted under until somebody deliberately moves it with `tik reprocess`. Created beneath a store, a ticket inherits its context as signed facts: `repo=` from the enclosing git repository, plus any `.tik-facts.edn` maps on the way down, nearest winning, with anything explicit beating both. ## Assert facts ```sh tik set 3184 severity=:high resolution.ref=abc123def456 ``` Dotted keys nest, so `parked.reason="waiting on legal"` writes the path `[:parked :reason]`. Values parse as EDN, and a bare word becomes a keyword — `severity=high` and `severity=:high` mean the same thing, which keeps facts out of the swamp of free-form strings. Links are facts too: ```sh tik set 3184 link.depends-on=9c21f0a4 ``` `tik next` then holds 3184 back while 9c21 is unsettled, and `tik status` names the blocker. ## Attach evidence ```sh tik attach 3184 ./crash-repro.sh tik comment 3184 reproduced on a clean profile, video attached ``` Artifacts are stored by hash. A comment is an artifact too — a text blob attached by its digest — which is why comments need no event type of their own. ## Correct the record Nothing is edited or deleted. Corrections are new events: ```sh tik set 3184 severity=:critical # supersedes; history retained tik retract 3184 resolution.ref --reason "wrong commit" tik dispute 3184 category --reason "this is billing" tik dispute 3184 category --withdraw # take back your own objection ``` A later assertion supersedes an earlier one. A retraction says the claim should not exist and offers no replacement. A dispute rejects the value that stood when it was raised, so only a *different* value answers it. In every case the stage regresses by derivation — there is no rollback step. ## Signed claims and attestations ```sh tik attest 3184 {:ci :green} ``` An attestation is a signed claim whose meaning the kernel does not interpret. Lenses read it, and the `:attested-within` guard checks that a fresh-enough one exists — a replayed "CI green" from last month is cryptographically valid and fails that guard honestly. ## Derive facts from the world ```sh tik probe 3184 ``` A probe is any executable that prints `key=value` lines. It runs with its working directory in the ticket's `[:repo]` repository, and changed values land as ordinary signed facts, so a ticket regresses on its own when reality does. The environment carries `TIK_TICKET`, `TIK_REPO`, and every present fact as `TIK_FACT_`, which is what lets one repository hold many subjects — a package, tenant, or workload per ticket. ## Recurring and bulk work ```sh tik recur weekly-review --period 2026-W32 tik rollout dependency-audit --parent-title "Q3 audit" ``` `recur` mints this period's ticket exactly once: it creates only when no ticket already carries that period label. The schedule lives outside the log — run it from cron or a timer — and tik derives whether the period exists yet. `rollout` creates one ticket per git repository under the store, wired to a parent by link facts, so the parent is a checklist whose checkmarks derive from each child's evidence. --- # Store administration Source: https://tik.projects.metio.wtf/cli/administration/ ## Identity ```sh tik actor add alice ~/.config/tik/id_ed25519.pub tik sign 3184 tik witness 3184 ``` `actor add` registers a signer in the store's allowed-signers registry. Exporting `TIK_KEY` signs every write as it happens; `sign` catches up on your own earlier events, and only your own — signing somebody else's would assert something false. `witness` countersigns a ticket's head, and one signature timestamps the entire ancestry beneath it. ## Roles ```sh tik roles tik roles add triager alice tik roles remove triager bob ``` A process definition declares which roles exist and who starts in them; the store's register decides who is in one today, for every ticket at once. Use it for a hire or a departure — it takes effect on in-flight tickets immediately, with no definition version bump and no per-ticket migration. See [Roles and authority](/authoring/roles-and-authority/). ## Verification ```sh tik verify tik verify --changed tik root --witness ``` `verify` runs the ladder: content addressing, schema, signatures, and re-derivation. `--changed` skips unchanged heads for a fast drift check rather than the full audit. `root` prints one hash committing to the entire store, optionally countersigned, optionally anchored to a third-party timestamp. ## Moving tickets to newer rules ```sh tik reprocess 3184 processes/support-request.edn tik reprocess 3184 processes/support-request.edn --apply ``` A ticket is judged by the rules it was minted under, so a definition that grows leaves existing tickets deriving under the old one until you move them — deliberately, because an implicit upgrade would re-judge a whole store on an edit. The dry run prints the pinned-versus-proposed hashes, which stages would be gained or would regress, and the new blockers. `--apply` records the re-pin as a signed event, so the log keeps which rules judged the ticket when. ## Storage ```sh tik init --sqlite tik store migrate --to file tik export ./audit-copy tik pack tik gc --apply ``` The file store is the signed interchange format; SQLite keeps the same events in one file. Migration is lossless in both directions — events and their detached signatures travel together. `export` materializes any store as the file format. `pack` consolidates settled tickets into one content-addressed pack each, and `gc` removes archived definitions no ticket pins any more. ## Alerts out, mail in ```sh tik effects run --config effects.edn --dry-run tik bridge email < message.eml ``` `effects run` pushes derived stage transitions to Slack, Discord, Matrix, Teams, ntfy, PagerDuty, plain webhooks, email, or any program through the command sink. Any sink field may be a secret resolved at send time — from an environment variable, a file, a command, or a systemd credential — so no secret sits in the config file. The email bridge turns messages into tickets and comments, and replies carrying `tik> key=value` lines become facts, which closes the loop for people who never leave their inbox. ## The gated agent surface ```sh tik agent actions 3184 --actor bot tik agent set 3184 severity=:high --actor bot ``` An agent sees only what the frontier admits for its role, and anything else is refused with the derived reason. The boundary is the same derivation everyone else reads, so it cannot be talked around. --- # Authoring processes Source: https://tik.projects.metio.wtf/authoring/ A process definition is plain EDN describing stages and the evidence each one requires. Tickets pin it by content hash, so a definition is a document with an identity rather than a mutable setting. - [Writing a definition](/authoring/writing-a-definition/) — the shape, the guard vocabulary, and the design law that separates a process from a task list. - [Lint, simulate, test](/authoring/lint-simulate-test/) — proving the definition does what you meant. - [Roles and authority](/authoring/roles-and-authority/) — who may sign what, and keeping membership current. Start from an interview rather than a blank file: ```sh tik author # answer questions; tik writes the definition tik author --template bug # start from a finished interview tik author prompt # an LLM recipe that yields the answers file ``` --- # Lint, simulate, test Source: https://tik.projects.metio.wtf/authoring/lint-simulate-test/ Three checks, in the order you reach for them. ## Lint ```sh tik lint processes/support-request.edn ``` The linter enforces what the kernel cannot: - **The closed guard basis** — only operators the declared `:process/guard-vocab` admits. - **Graph sanity** — every `:after` names a stage that exists, and no cycles. - **Stratified negation** — `[:not [:stage-reached …]]` may only name a stage in a strictly earlier stratum, which is what makes the fixpoint provably deterministic. - **Facts over flags** — a warning where a bare boolean stands in for information worth recording. - **Prefix boundaries** — an `:artifact` prefix that does not end at a path boundary matches more than its author expects. With no argument it lints the *store* instead: open tickets missing descriptions, titles, or signatures. ## Simulate ```sh tik sim processes/support-request.edn ``` A scratch ticket against a definition that reloads on every save. Assert facts, watch stages derive, and edit the definition in another window — the fastest loop for finding out that a guard means something other than what you read into it. ## Test ```sh tik test processes/support-request.tests.edn ``` Scripted cases: evidence in, expected stages out. Deterministic — a fixed epoch, pure derivation, no store — and a failing case prints `explain`, so the process itself says why a stage did not derive. ```clojure {:test/process "support-request.edn" :test/cases [{:case/name "facts alone do not triage — the triager role must sign" :case/steps [[:actor "rando"] [:set [:category] :technical] [:set [:severity] :high]] :case/expect {:excludes #{:triaged}}} {:case/name "uncategorized tickets escalate after 48h" :case/steps [[:now "+PT49H"]] :case/expect {:includes #{:escalated}}}]} ``` `[:now "+PT49H"]` moves the evaluation clock, so time-gated stages are testable without waiting and without a mock. `[:actor "rando"]` switches who is signing, which is how a `:signed-by` guard gets tested from both sides. Write the negative cases. "A triager categorizing reaches `:triaged`" is the easy half; "facts alone do not triage" is the half that catches a `:signed-by` you dropped. ## Publish ```sh tik process sign support-request ``` Signing archives the definition by content hash and signs those canonical bytes. The hash stays the identity; the signature is the authority behind it. --- # Roles and authority Source: https://tik.projects.metio.wtf/authoring/roles-and-authority/ A role is a name a definition uses in its guards. Membership is store state that decides who is in that role today. ## Declaring a role ```clojure :process/roles {:triager {:members ["seb"]} :approver {:members ["alice" "bob"]}} ``` Guards refer to the name: ```clojure :guards [[:signed-by :triager [:category]]] ``` That guard holds when the fact at `[:category]` was asserted by a member of `:triager`. It is a statement about evidence — who put this claim on the record — rather than a permission check performed somewhere else. ## Membership lives in the register A definition declares which roles exist and who starts in them. The store's register decides who is in one now: ```sh tik roles # who gates what, with effective members tik roles add approver carol tik roles remove approver bob ``` The register overrides a definition role by role, and takes effect on in-flight tickets immediately — no version bump, no `reprocess`. That is what makes a departure actually remove authority and a hire actually confer it, rather than leaving a departed member able to sign every ticket minted before they left. Overriding is whole-role: the register's entry replaces the definition's members for that role rather than merging with them, because a departure has to be expressible. The first `tik roles add` on a role says so. A store with no register derives exactly as before, so a definition's declared members are a working default rather than something to restate. Resolution takes no `now`. Time-aware validity — "was this actor a member last March" — is a separate concern that wants signed bindings rather than a mutable file, so a re-derivation at a past instant reads today's membership. ## Separation of duties Two guards express the common controls without a policy engine. **Four eyes.** `:different-person` holds when two facts are present and were asserted by distinct actors: ```clojure [:different-person [:proposal] [:approval]] ``` Nobody approves their own proposal, and `explain` says so by name when they try — the reason carries the actor whose signature already counted, so anyone else re-asserting one path breaks the tie. **Fresh evidence.** `:attested-within` holds when an attestation of a claim exists and is recent enough: ```clojure [:attested-within {:claim :ci-green} "P7D"] ``` A replayed attestation from last month is cryptographically valid and fails this guard honestly, which is exactly the distinction a stale-evidence control needs. ## An empty role is a dead end A role with no members can never sign, so every stage behind it is unreachable. `explain` says that rather than leaving somebody waiting: ```console To reach :approved (unreachable): ⊘ fact [:sign-off] must be asserted by a member of role :auditor — nobody can ever do this ``` The fix is `tik roles add`, or a definition that does not demand a signature nobody can give. ## Signing Authority only means something once writes are signed. Register a signer once, then let every write carry authorship: ```sh tik actor add alice ~/.config/tik/id_ed25519.pub export TIK_KEY=~/.config/tik/id_ed25519 ``` Signatures are detached sidecars from `ssh-keygen -Y`, checked by `tik verify` with stock OpenSSH. A signature is an authorship claim, which is why `tik sign` refuses to sign somebody else's events. --- # Writing a definition Source: https://tik.projects.metio.wtf/authoring/writing-a-definition/ ## The design law **A stage is defined by what must be TRUE to reach it, never by who moved what.** Ask of every stage: what evidence, on record, would convince a sceptical auditor a year from now? Write that as guards. If the honest answer is "somebody said so", then make *that* the fact — a named person's signature on a named claim — rather than a status somebody sets. A definition whose stages are really a checklist of activities is a task list wearing a process costume. It derives nothing, because nothing about it follows from evidence. ## The shape ```clojure {:process/id :support-request :process/version 1 :process/guard-vocab 2 :process/roles {:triager {:members ["seb"]} :billing {:members ["billing"]}} :process/facts {[:category] [:enum :billing :technical :account :abuse] [:severity] [:enum :low :normal :high :critical] [:resolution :ref] [:string {:min 8}]} :process/stages [{:stage/id :received :hint "kb/runbooks/support-request-received.md" :guards []} {:stage/id :triaged :after [:received] :hint "kb/runbooks/support-request-triaged.md" :guards [[:fact [:category]] [:fact [:severity]] [:signed-by :triager [:category]]]} {:stage/id :closed :after [:resolved] :stage/sticky? true :guards [[:fact [:customer :ack]]]}]} ``` `:process/facts` declares the schema for each path, which is what lets `explain` tell somebody not merely that a fact is missing but what shape it must take — and what lets a web form be generated from the reason. `:hint` names a runbook for the stage. Those files are published here under [Runbooks](/runbooks/), so the hint a person sees in `explain` resolves to a page. ## The guard vocabulary Twelve operators, closed and versioned. New keywords need a version bump, because the semantics of a verifiable kernel have to be enumerable: | Operator | Holds when | |---|---| | `[:fact path]` | a value stands at `path` | | `[:fact= path v]` | that value equals `v` | | `[:artifact "prefix"]` | an attached artifact's path starts with `prefix` | | `[:signed-by :role path]` | the fact at `path` was asserted by a member of `:role` | | `[:stage-reached :id]` | that stage is in the reached set | | `[:elapsed-since :ticket/create "PT48H"]` | that much time has passed | | `[:attested-within claim "P7D"]` | a fresh-enough attestation of `claim` exists | | `[:different-person path-a path-b]` | two facts came from distinct actors | | `[:malli schema]` | the fact map satisfies a schema | | `[:and …]` `[:or …]` `[:not …]` | the connectives | `:different-person` is the four-eyes principle as a derivable condition. `:attested-within` closes the stale-evidence gap: a replayed "CI green" from last month is cryptographically valid and fails the guard honestly. There is no conditional operator, because material implication already spells one — "technical implies reproduced" is: ```clojure [:or [:not [:fact= [:category] :technical]] [:stage-reached :reproducible]] ``` ## Guards never query A guard reads one ticket's own log and nothing else. No service call, no database lookup, no other ticket, no clock of its own. Anything from the outside world enters as a signed attestation event first, which is what keeps evaluation offline, reproducible, and true years from now. ## Facts over flags Prefer a fact that carries information to a boolean that carries a decision. `[:customer :ack] = true` records that somebody clicked; a richer fact records what they actually agreed to. The linter warns about bare booleans for this reason, and the warning is opt-out per definition when a flag genuinely is the whole truth. ## Versions and pinning A ticket pins the definition's content hash at creation. Editing a definition therefore changes its identity and leaves existing tickets deriving under the version that judged them, until somebody moves each one with `tik reprocess`. The version number is a human label; the hash is the identity. --- # Integrations Source: https://tik.projects.metio.wtf/integrations/ A tik store is meant to be read by tools as readily as by people. Every lens takes `--edn` or `--format json`, `tik serve` publishes `/tickets.edn` and `/explain/.edn`, and the frontier doubles as an agent's authorization boundary. - [Claude Code skill](/integrations/claude-code/) — install the plugin and Claude drives a store correctly, recording evidence rather than setting statuses. - [MCP server](/integrations/mcp-server/) — the frontier as a gated tool surface over stdio. --- # Claude Code skill Source: https://tik.projects.metio.wtf/integrations/claude-code/ Install the tik skill into [Claude Code](https://claude.com/claude-code) and Claude gains working knowledge of tik: it recognises a store, records facts with `tik set` rather than inventing a status field, reads `tik explain` before asking a person anything, and authors process definitions whose stages derive from evidence instead of listing activities. That last part is the reason the skill exists. The failure mode for any model writing a tik process is a task list wearing a process costume — stages named after activities, with nothing following from anything. The skill carries the design law that rules it out, along with the closed guard vocabulary and the authoring loop that checks the result. The skill lives in this repository under `skills/tik/`, packaged as a Claude Code plugin by the manifests in `.claude-plugin/`. ## Install Add this repository as a plugin marketplace, then install the plugin: ```text /plugin marketplace add metio/tik /plugin install tik@tik ``` Claude activates the skill whenever a repository holds a `tickets/` or `.tik/` directory, an `actors` registry, or `processes/*.edn` — or when you mention tik, tickets, processes, or stages. ## What it grants Claude The skill teaches the CLI surface and the model behind it: - **The daily loop** — `tik ls`, `tik next`, `tik new`, `tik set`, `tik explain`, `tik status`, and the selector grammar for filtering a board. - **Corrections** — retract, dispute, and the fact that a dispute is answered only by a *different* value, so Claude does not try to clear one by retyping the same fact. - **Authoring** — `tik author`, the guard vocabulary, and the lint, simulate, and test loop that proves a definition before a real ticket depends on it. - **The one design law** — a stage is defined by what must be TRUE to reach it, never by who moved what. It also knows when *not* to act: an unreachable step in `explain` means waiting is pointless and the definition or the role register needs fixing, not patience. ## For agents generally The skill is one surface over a derivation every tool can read. The gated agent commands enforce the same boundary without any prompt-side cooperation: ```sh tik agent actions 3184 --actor bot tik agent set 3184 severity=:high --actor bot ``` An agent sees only what the frontier admits for its role, and anything else is refused with the derived reason. Because the boundary is the derivation rather than an instruction, it cannot be talked around. This site also publishes `/llms.txt` and `/llms-full.txt`, so a model can read the documentation directly. --- # MCP server Source: https://tik.projects.metio.wtf/integrations/mcp-server/ ```sh TIK_ACTOR=bot TIK_KEY=~/.config/tik/bot_ed25519 tik mcp ``` `tik mcp` speaks the Model Context Protocol over stdio and exposes one store as a tool surface. The tools an agent is offered are exactly the steps the frontier admits for its role at that moment, so the tool list *is* the authorization boundary. ## Why the boundary holds An agent's permission to act is derived from the same guards a person's is. A step whose `:signed-by` names a role the agent is not in never appears as an available tool, and calling it anyway is refused with the derived reason rather than a generic denial. That matters because the alternative — telling a model in its prompt what it may not do — is enforcement by cooperation. Here the check happens where the answer is computed, and it produces the same reason string a person would see. ## Accountability Every write the agent makes is an ordinary signed event with the agent's own actor identity. The log therefore distinguishes what a person claimed from what an agent claimed, permanently and without a separate audit trail. `tik causal` names the events behind each reached stage, so a conclusion an agent contributed to can be traced to the evidence it supplied and the key that signed it. ## Task specifications from guards An agent asking "what should I do?" gets the same structured answer every other lens renders: ```sh tik explain 3184 --actor bot --edn ``` Each missing step carries its path, its schema, and who may satisfy it — which makes an acceptance criterion out of a guard, rather than out of a sentence somebody wrote in a ticket description. --- # Runbooks Source: https://tik.projects.metio.wtf/runbooks/ A process definition names a runbook per stage through its `:hint` field, and `tik explain` prints that link beside the missing evidence: ```text To reach :triaged: ✗ set fact [:category] ([:enum :billing :technical :account :abuse]) (see: kb/runbooks/support-request-triaged.md) ``` So these pages are read at the moment somebody is stuck, and each answers a single question: what has to become true here, and who can make it so. They live in the repository's knowledge bundle under `kb/runbooks/`, so a checkout, an agent, and this site read the same document. Each process below is listed in stage order rather than alphabetically — a runbook makes most sense beside the stages it sits between. ## support-request The sample process the conformance corpus pins: a customer report from arrival to acknowledgement. | Stage | What it means | | --- | --- | | [`:received`](/runbooks/support-request-received/) | A new report exists and nothing is known yet. | | [`:triaged`](/runbooks/support-request-triaged/) | A triager has signed off a category and a severity. | | [`:reproducible`](/runbooks/support-request-reproducible/) | A reproduction is attached under `repro/`. | | [`:resolved`](/runbooks/support-request-resolved/) | A resolution reference points at the fix. | | [`:escalated`](/runbooks/support-request-escalated/) | Derives on its own: 48 hours with no category. | | [`:closed`](/runbooks/support-request-closed/) | Sticky milestone — only the customer's acknowledgement closes it. | ## release A version's supply chain as evidence: one ticket per release, each stage saying what must be true of the artifacts rather than which job ran. | Stage | What it means | | --- | --- | | [`:built`](/runbooks/release-built/) | Artifacts exist and CI has signed which commit produced them. | | [`:scanned`](/runbooks/release-scanned/) | A vulnerability scan came back clean, attested within the last day. | | [`:attested`](/runbooks/release-attested/) | An SBOM, build provenance, and a signature are on record. | | [`:published`](/runbooks/release-published/) | Sticky — a maintainer shipped it, and not the one who built it. | | [`:withheld`](/runbooks/release-withheld/) | A maintainer decided it does not ship, with the reason. | ## tik-dev The process tik's own development runs in; this repository is a live store. | Stage | What it means | | --- | --- | | [`:captured`](/runbooks/tik-dev-captured/) | A thought exists and the ticket preserves it. | | [`:triaged`](/runbooks/tik-dev-triaged/) | A summary and a kind are on record. | | [`:implemented`](/runbooks/tik-dev-implemented/) | A commit is named. | | [`:landed`](/runbooks/tik-dev-landed/) | The full local gate came back green. | | [`:parked`](/runbooks/tik-dev-parked/) | Deliberately not now — with the reason, which is the deliverable. | ## hypothesis Falsifiable claims carrying a kill criterion, so a plan can be wrong on purpose rather than by accident. | Stage | What it means | | --- | --- | | [`:captured`](/runbooks/hypothesis-captured/) | A belief worth testing exists, even half-formed. | | [`:stated`](/runbooks/hypothesis-stated/) | The claim and what would kill it are both written down. | | [`:running`](/runbooks/hypothesis-running/) | The experiment is named: what is run, on what, measured how. | | [`:validated`](/runbooks/hypothesis-validated/) | Evidence a stranger could check. | | [`:killed`](/runbooks/hypothesis-killed/) | Which criterion fired, and the evidence it fired on. | ## track Two stages, for something that needs recording rather than a workflow. | Stage | What it means | | --- | --- | | [`:open`](/runbooks/track-open/) | The ticket exists; record what is true as it happens. | | [`:done`](/runbooks/track-done/) | The thing ended — say how. | ## identity-registry | Stage | What it means | | --- | --- | | [`:registry`](/runbooks/identity-registry-registry/) | Always this stage: key bindings are evidence, not workflow. | --- # hypothesis / :captured Source: https://tik.projects.metio.wtf/runbooks/hypothesis-captured/ # Runbook: hypothesis / :captured A belief worth testing exists. Capture it even half-formed — but do not state it until you can also state what would kill it. A hypothesis you cannot lose is marketing. --- # hypothesis / :killed Source: https://tik.projects.metio.wtf/runbooks/hypothesis-killed/ # Runbook: hypothesis / :killed Assert `verdict=:killed` and `evidence` of the kill: which criterion fired, observed how. A kill is a RESULT — it retires a belief and usually implies design work (the roadmap says what dies with it: kill H1 and the core loop is wrong, not under-featured). Write the evidence so the next design argument can cite it. Judgment stage, same rules as :validated: sign reasoning a stranger could dispute. --- # hypothesis / :running Source: https://tik.projects.metio.wtf/runbooks/hypothesis-running/ # Runbook: hypothesis / :running Assert `experiment`: what is actually being run, on what, measured how. If the full experiment cannot start yet, name the leg that can and the leg that waits (H3 is the template). While running, record datapoints as comments — negative ones ESPECIALLY; an experiment that only logs good news is not running, it is performing. --- # hypothesis / :stated Source: https://tik.projects.metio.wtf/runbooks/hypothesis-stated/ # Runbook: hypothesis / :stated Assert `statement` (the falsifiable claim) and `kill` (what counts as losing, decided NOW, before any work). Write the kill criterion as an observable event, not a feeling: 'abandoned within a month', 'requires prompt-side enforcement'. The process will not let a verdict through without this — that is the point. --- # hypothesis / :validated Source: https://tik.projects.metio.wtf/runbooks/hypothesis-validated/ # Runbook: hypothesis / :validated Assert `verdict=:validated` and `evidence` that a stranger could check: what ran, what was observed, why the kill criterion never fired, and any scoping decisions that bound the claim. Sticky — later doubts are a NEW hypothesis, not an edit. This stage is judgment: the process cannot verify your evidence is sufficient, only that you signed it. Leave enough that someone can dispute you specifically. --- # identity-registry / :registry Source: https://tik.projects.metio.wtf/runbooks/identity-registry-registry/ # identity-registry: registry The registry ticket accumulates signed `:identity` attestations, each binding an IdP subject to an actor's public key. The ticket is always in this stage — bindings are evidence, not workflow. ## Recording a binding - `tik bridge oidc --registry --actor ` runs the device-flow login and appends the binding, signed by the bridge's `TIK_KEY`. - Rotation and re-attestation are newer attestations by the same bridge; readers take the latest binding per (issuer, subject). - Revocation is a dispute of nothing — attestations are claims by the bridge; a binding the bridge no longer stands behind simply stops being re-attested, and lenses can require freshness via `:attested-within`. ## Reading bindings `tik log ` shows every binding with its signature; `tik verify ` checks them offline — no IdP call, ever. --- # release / :attested Source: https://tik.projects.metio.wtf/runbooks/release-attested/ # release: attested The container image exists, an SBOM and build provenance were produced for it, and the checksums carry a signature. ```sh tik set image=ghcr.io/metio/tik@sha256:... --actor ci tik set signature.bundle= --actor ci tik attest :sbom --body '{:format "spdx-json" :digest "sha256:..."}' --actor ci tik attest :provenance --body '{:format "slsa-v1" :digest "sha256:..."}' --actor ci ``` The guard checks that a trusted attester said an SBOM exists. It does not read the SBOM — guards never query anything, which is what keeps evaluation offline and reproducible forever. The derivation is therefore exactly as trustworthy as `:ci`, and that is a property to state plainly rather than bury. Name the digest in the attestation body so a reader can fetch the document and check it themselves. --- # release / :built Source: https://tik.projects.metio.wtf/runbooks/release-built/ # release: built The artifacts for this version exist and the pipeline has said which commit produced them. ```sh tik set version=2026.8.5204821 commit= --actor ci tik attach checksums/SHA256SUMS ``` `commit` must be signed by a member of `:ci`: the pipeline is the only party that can honestly say which source produced these bytes, and a human asserting it is a claim about something they did not observe. The checksums file is attached rather than pasted so it is addressed by its hash. The hash is in the trust domain; the blob is not (ADR 0014). Anyone can re-fetch the artifacts and check them against it without trusting this store. --- # release / :published Source: https://tik.projects.metio.wtf/runbooks/release-published/ # release: published A maintainer decided to ship, and that maintainer is not the party that built it. ```sh tik set approval=:ship --actor seb ``` The `:different-person` guard compares who asserted `commit` with who asserted `approval`. The pipeline cannot approve its own release — not because a rule forbids it, but because the two facts would carry the same signature and the guard would not hold. Sticky milestone: shipping is a historical fact. A vulnerability disclosed next week does not un-publish this version. It starts a new ticket, and this one keeps saying what was true on the day. --- # release / :scanned Source: https://tik.projects.metio.wtf/runbooks/release-scanned/ # release: scanned A vulnerability scan ran against this version's dependencies, found nothing, and said so recently enough to be about today's advisories. ```sh tik attest :vulnerability-scan --body '{:tool "clj-watson" :db "github-advisory"}' --actor ci tik set scan.result=:clean --actor ci ``` Both halves are needed and they say different things. The fact records the verdict; the attestation records that a scan happened and when. The one-day freshness window is what makes replay useless — a scan from last month is cryptographically valid and fails this guard honestly, because the advisory database it consulted no longer exists. A scan that finds something is `scan.result=:findings`, which is not a failure to record but a reason this version does not reach `:scanned`. Fix the finding and cut a new version; do not re-assert the same value on this one. --- # release / :withheld Source: https://tik.projects.metio.wtf/runbooks/release-withheld/ # release: withheld A maintainer decided this version does not ship. ```sh tik set approval=:hold --actor seb tik comment "held: the scan is clean but the advisory for the transitive dep is unresolved upstream" ``` Recorded with the same weight as shipping, because the reason a version never shipped is worth as much a year later as the reason one did — and it is the question nobody can answer from a green pipeline. Withholding is not terminal. A later `approval=:ship` supersedes it, and the log keeps both decisions with their timestamps and signatures. --- # support-request / :closed Source: https://tik.projects.metio.wtf/runbooks/support-request-closed/ # Runbook: support-request / :closed Sticky milestone: only the customer's acknowledgement closes, and a later retraction will NOT reopen (the log shows both truths). Ask for the ack explicitly — the customer information-request pattern. If the customer goes silent, the ticket honestly stays at :resolved; do not assert the ack on their behalf, ever (authorship is the whole point, ADR 0010). --- # support-request / :escalated Source: https://tik.projects.metio.wtf/runbooks/support-request-escalated/ # Runbook: support-request / :escalated This stage derives on its own: 48 hours without a category. There is no action that produces it — the runbook is for reacting to it. An escalated ticket means triage is the bottleneck: find why (nobody on rotation? report unintelligible?) and either triage it now or record what blocks triage. Escalation clears itself the moment a category lands (fact-level negation). --- # support-request / :received Source: https://tik.projects.metio.wtf/runbooks/support-request-received/ # Runbook: support-request / :received A new report exists; nothing is known yet. Read the title and any attached artifacts. Your job is to move it toward triage: get a category and a severity asserted by a triager. If the report is unintelligible, ask the reporter (a comment or an information-request effect) rather than guessing a category — a wrong category asserted confidently is worse than an honest gap. --- # support-request / :reproducible Source: https://tik.projects.metio.wtf/runbooks/support-request-reproducible/ # Runbook: support-request / :reproducible Attach a reproduction under `repro/` — a script, a request trace, a recording; the smallest artifact that makes the failure happen again. If you cannot reproduce it, say so in a comment with what you tried; do not attach a placeholder to satisfy the guard (evidence theater, PLAN §18). A technical ticket without a repro simply is not resolvable yet — that is the process being honest, not slow. --- # support-request / :resolved Source: https://tik.projects.metio.wtf/runbooks/support-request-resolved/ # Runbook: support-request / :resolved Assert `resolution.ref` pointing at the fix (commit, config change, knowledge-base answer). For technical tickets the reproduction must exist first — 'fixed it but can't show the bug' does not resolve. Resolution is a claim the customer has not yet accepted; do not consider the work done here. --- # support-request / :triaged Source: https://tik.projects.metio.wtf/runbooks/support-request-triaged/ # Runbook: support-request / :triaged Assert `category` and `severity` — both must come from a triager (the `:signed-by` guard checks the role, not the values). Category is a claim about what KIND of problem this is; when genuinely unsure, pick the closest and say why in a comment — a later dispute is cheap and leaves better history than stalling. Severity reflects impact on the customer, not effort to fix. --- # tik-dev / :captured Source: https://tik.projects.metio.wtf/runbooks/tik-dev-captured/ # Runbook: tik-dev / :captured A thought exists; the ticket preserves it. Give it a title that will mean something in six months. Nothing else is required — capture must stay cheap or ideas stop being written down. Move to triage when you can honestly write one summary sentence and name the kind. --- # tik-dev / :implemented Source: https://tik.projects.metio.wtf/runbooks/tik-dev-implemented/ # Runbook: tik-dev / :implemented Assert `commit` naming the commit that does the work. The commit must exist before the fact does — the fact is a claim about evidence, and the evidence is the git history. One feature, one commit, stacked on main (this repo's workflow). --- # tik-dev / :landed Source: https://tik.projects.metio.wtf/runbooks/tik-dev-landed/ # Runbook: tik-dev / :landed Assert `gate=:green` only after the FULL local gate: kaocha, kondo (0/0), eastwood, splint, cljfmt, tla, process tests, reuse, typos, markdown. Not the one check your change touched — all of them. Sticky: a later gate dispute does not un-land; file a new bug ticket instead. If the gate is red, assert `gate=:red` honestly and fix forward. --- # tik-dev / :parked Source: https://tik.projects.metio.wtf/runbooks/tik-dev-parked/ # Runbook: tik-dev / :parked Assert `parked.reason` — the reason is the deliverable. Cite the verdict that parks it (a PLAN §19 entry, a trigger condition not yet met). Parked is not closed: the ticket stays visible on the board and can resume by simply asserting the missing facts. Parking without a real reason is deferral theater. --- # tik-dev / :triaged Source: https://tik.projects.metio.wtf/runbooks/tik-dev-triaged/ # Runbook: tik-dev / :triaged Assert `summary` (what and why, one breath) and `kind` (feature/bug/docs/spike) as a maintainer. If it duplicates existing work, link it (`link.duplicate-of=`) instead of triaging twice. If it should wait, this is also where you park it — with the reason, not silently. --- # track / :done Source: https://tik.projects.metio.wtf/runbooks/track-done/ # track: done The thing ended — say how: ```sh tik set outcome="shipped in v2; see order #1234" ``` A few honest words beat a closed flag: the outcome is the only thing anyone will want from this ticket in a year. --- # track / :open Source: https://tik.projects.metio.wtf/runbooks/track-open/ # track: open The ticket exists. There is no workflow to satisfy — record what is true as it happens: - `tik set key=value` for facts worth keeping. - `tik comment ` for anything else. - `tik attach ` for evidence. When several track tickets start following the same informal shape, run `tik author` and give that shape a real definition. --- # Decisions Source: https://tik.projects.metio.wtf/decisions/ Every load-bearing choice in tik has a record: the decision, the context that forced it, and the consequences that follow. Several exist to name something tik will *not* do, which is the part that keeps the model small. New here, read these four: [derived state is never authoritative](/decisions/0013-derived-state-never-authoritative/) is the one law written as a constraint on every future feature; [coordination-free](/decisions/0021-coordination-free-horizontal-scaling/) is its companion; [stored bytes are the hashed region](/decisions/0007-stored-bytes-are-the-hashed-region/) is why `sha256sum` alone audits a store; and [three clocks](/decisions/0012-time-semantics/) is why a guard's meaning includes which clock it reads. These files live in the repository's knowledge bundle under `kb/decisions/`, so a checkout and this site carry the same record. ## The laws | Decision | What it settles | | --- | --- | | [0013](/decisions/0013-derived-state-never-authoritative/) | Derived material may be cached only if disposable and untrusted. | | [0021](/decisions/0021-coordination-free-horizontal-scaling/) | No leader, no lock, no consensus — on any correctness path. | | [0001](/decisions/0001-event-log-acceptance-test/) | The event-log acceptance test every feature must pass. | ## Derivation | Decision | What it settles | | --- | --- | | [0005](/decisions/0005-stratified-negation/) | Negation inside the fixpoint must be stratified to stay deterministic. | | [0003](/decisions/0003-conflicts-block/) | Concurrent conflicting assertions block guards; people resolve them. | | [0016](/decisions/0016-explain-stability-contract/) | Explain's structured reasons are the stable API; the prose is not. | | [0018](/decisions/0018-conformance/) | Conformance is the corpus, the laws, and the normative sweep semantics. | | [0019](/decisions/0019-effects-observe-derivation/) | Effects observe derivation; transport is not a domain concept. | ## Time | Decision | What it settles | | --- | --- | | [0012](/decisions/0012-time-semantics/) | Three clocks, never conflated; the clock is part of the guard. | | [0022](/decisions/0022-no-step-reads-the-future/) | The claimed clock is clamped to the evaluated clock. | ## The log and its bytes | Decision | What it settles | | --- | --- | | [0004](/decisions/0004-mandatory-parents/) | Parents are mandatory; the log is a Merkle DAG. | | [0006](/decisions/0006-hash-policy/) | SHA-256, self-describing ids, one algorithm per store. | | [0007](/decisions/0007-stored-bytes-are-the-hashed-region/) | Stored bytes are exactly the hashed region; signatures are detached. | | [0008](/decisions/0008-canonical-serialization-protocol/) | Canonical serialization is a versioned wire protocol. | | [0017](/decisions/0017-deletion-and-compaction/) | Events are never deleted; blobs may be; nothing compacts into authority. | | [0020](/decisions/0020-eventstore-contract/) | The contract every storage backend must honour. | ## Trust and authority | Decision | What it settles | | --- | --- | | [0010](/decisions/0010-authority-model/) | Signatures establish authorship; authorization is derived. | | [0011](/decisions/0011-log-admission-vs-trust/) | The log admits all well-formed claims; trust is evaluated, not filtered. | | [0009](/decisions/0009-unknown-data-policy/) | Unknown data is handled differently per layer, on purpose. | | [0014](/decisions/0014-artifact-semantics/) | The artifact hash is in the trust domain; the blob is not. | | [0015](/decisions/0015-process-definition-trust/) | Definition hash is identity; publication signatures are authority. | | [0002](/decisions/0002-pinned-process-versions/) | Tickets pin their definition hash; migration is an event. | --- # No fold step reads the future — the claimed clock is clamped to the evaluated clock Source: https://tik.projects.metio.wtf/decisions/0022-no-step-reads-the-future/ # ADR 0022: No fold step reads the future ## Decision **Every step of `evolve` evaluates guards at the event's own claimed `:event/at`, clamped to the read's evaluated `now`: `min(at, now)`.** No step may act as though more time has passed than actually has at the moment of the read. Derivation stays exactly what it was — a pure function of `(events, now)` — and reduction order is untouched: events still fold in `(at, id)` order, and only the instant each step's guards are *evaluated at* is clamped. Logs whose events are all dated at or before the read derive byte-for-byte as they did before, so the clamp changes the answer only for the case it exists to fix. ## Context `:elapsed-since` reads the claimed clock (ADR 0012). Combined with a per-event evaluation instant, a single postdated event was enough to satisfy a time guard at its claimed instant — and on a **sticky** stage the fold carried that reach forward permanently. The result was a milestone reachable by writing a date, which no later evidence could retract: retraction cannot help, because sticky is exactly the promise that retraction does not. Refusing postdated events at our own write boundary does not close this. An event can arrive from another replica already postdated, and under ADR 0021 a replica may not be trusted to have applied any such check — so the property must hold in the derivation, which every replica runs, rather than in one write path. ADR 0012 remains intact: claimed time is still the default clock and backdating is still detectable rather than preventable. The clamp is about the *evaluated* clock, the third of that ADR's three: a read at `now` may not consult a claimed instant later than `now`. Reading a claimed future early was the one place the evaluated clock was not actually governing evaluation. ## Consequences - `tik.stage/evolve` and `stage-timeline` take `now`. Every caller had one already; only the two lens call sites that had been folding without one changed. - The clamp lifts by itself. As `now` advances past a postdated event's claimed instant, that event is evaluated at the instant it claims, and the derivation converges on the unclamped answer. Nothing is discarded, censored, or rewritten — the event is simply not read early. - `:at` in the timeline stays the event's own unclamped claim: the timeline records what the log says, not what a particular read believed. - The reference kernel (`test/tik/reference.clj`) clamps identically, so the differential property test still compares two independent implementations of the same semantics rather than one of each. - A postdated event is still evidence of something. Surfacing future-dated events in the lenses remains worth doing and is now a reporting nicety rather than the only defence. --- # Coordination-free horizontal scaling — no leader, no lock, no consensus Source: https://tik.projects.metio.wtf/decisions/0021-coordination-free-horizontal-scaling/ # ADR 0021: Coordination-free horizontal scaling ## Decision **tik's second law, standing beside derived-beats-declared: every operation must be correct on arbitrarily many replicas that share nothing and reconcile only by eventually unioning their grow-only, content-addressed event sets. No feature may require leader election, a distributed lock, a quorum/consensus round, or any synchronous cross-replica coordination to be correct.** This is not an aspiration to bolt on later; it is a constraint every feature is checked against now, exactly like the first law. Three consequences are load-bearing and must stay true: 1. **Reads shard without limit.** A derivation reads one ticket's own log and nothing else — guards never query across tickets (ADR 0004 scope). So N stateless replicas × M `hash(ticket-id)` shards scale reads with zero coordination; cross-ticket views are scatter-gather or a disposable index (ADR 0013), never a shared authority. 2. **Writes never resolve by lock.** Every write is either (a) a content-addressed event that is a **pure function of its intent** — two replicas forming the same intent emit byte-identical events that union-merge to one (append is idempotent by id, ADR 0020) — or (b) a CRDT-safe append whose contention resolves by **derivation**: two competing facts reduce to `:conflicted` (reduce.cljc fact-status), a derived state, not a lock to be won. 3. **Generated events are deterministic.** Any event a replica MINTS on its own — `recur`, `probe`, any scheduled or automated create — must derive its id and every byte-bearing field, `:at` included, as a pure function of its inputs. `recur` is the worked example: ticket id = `nameUUIDFromBytes(process, period)` and `:at` = the period-start the porcelain parses from the label (or `--at`), so two backends firing the same schedule concurrently mint the same event, and the union keeps one. ## Context The first law (derived-beats-declared, ADR 0013) is what makes the second law reachable: with no authoritative mutable state, there is nothing that must be serialized behind a lock or agreed by a quorum. A "current stage" column, a uniqueness table, a first-come assignment claim would each reintroduce a linearization point — and a linearization point is what forces leader election or consensus and caps horizontal scale. tik has none, and this ADR forbids adding one. The pressure point is automation. The moment a replica acts on its own (the delegated-agent backend in IDEAS: scheduled `recur`, standing-ticket `probe`, outbound `effects`), the naive design reaches for "elect one replica to fire the timer" — a leader. The second law rejects that: make the minted event a pure function of its intent instead, and every replica may fire the same timer because the duplicates collapse by content address. Coordination is designed out, not coordinated away. "No sync" is precise: no *synchronous coordination on the correctness path*. Replicas still exchange events, but asynchronously — git-style have/want set reconciliation over event ids (ADR 0020), which is lock-free and can only ever leave a replica temporarily *incomplete* (self-healing on the next sync), never *wrong*. The sole admissible exception is outbound side-effect **delivery** that genuinely cannot be made idempotent; a narrow per-pipeline lease may serialize *delivery* there, never the log, and never a read or a derivation. ## Consequences - **A tik replica is stateless by construction.** No StatefulSet ordering is needed for correctness, no leader sidecar, no quorum PodDisruptionBudget. Scale a Deployment to N, evict any pod at any moment, and every replica still answers correctly — which is why horizontal scaling (Kubernetes) is the preferred deployment. - **The violation test is a first-class smell.** A design that needs a replica to win an election, hold a lock, or agree with a quorum before it can act violates this law as surely as caching a derived value violates the first. The fix is always the same shape: turn the operation into a pure-function content-addressed event (dedup by hash) or a derivation of conflict (`:conflicted`), never a lock. - **Determinism is now a review gate for any generated event.** A new automated mint must be shown to be a pure function of its inputs — a property test that two independent stores produce byte-identical events is the standard evidence (`cli_test.clj` `recur_mints_byte_identical_events_on_independent_stores`). - **The TLA+ Merge model is the formal backstop.** Convergence under arbitrary replica interleavings is already the property it checks; a feature that needs coordination would be a feature the merge model cannot express as pure set union. --- # Artifacts — the hash is in the trust domain, the blob is not Source: https://tik.projects.metio.wtf/decisions/0014-artifact-semantics/ # ADR 0014: Artifact semantics ## Decision An `:artifact/attach` event binds a path label to a content hash. The permanent answers: - **The hash is inside the trust domain; the blob is not.** The event (signed, hashed, parented) proves *that* an actor attached *exactly these bytes* at a claimed time. The bytes themselves are payload: stored by hash, transferred lazily, verifiable on arrival. - **Blobs are immutable** — a "changed" artifact is a new hash attached by a new event; the old attachment remains history. - **Deletion removes availability, never history** (GDPR, leaked secrets). The event stands, the hash stands, and `verify` L3 reports **verifiable absence** — "blob absent" is a truthful verification outcome, not a failure to be papered over. This is a designed property; transcripts from real environments contain things that must be deletable (PLAN §13). - **Metadata is descriptive, never authoritative.** The path label, media type, and any annotations are the attaching actor's *claims* about the bytes — disputable like any claim — not properties the kernel vouches for. A guard trusting `path = "repro/…"` trusts the attacher's labeling; when it matters, processes require an attestation about the content, not a filename shape (PLAN §18, evidence flooding). - **Two hashes are two artifacts.** Logical identity across encodings ("the same report as PDF and HTML") is not a kernel concept; if a process needs it, it is asserted as a fact and carries its asserter's accountability. ## Context Artifacts are where the trust domain touches arbitrary external bytes, and every mistake here has the same shape: attributing to the blob a guarantee that only the *event about the blob* carries. Keeping the boundary explicit — evidence about bytes vs. the bytes — is what lets blobs be deleted, lazily transferred, and quota-limited without any of it touching the integrity story. ## Consequences - Verify: L0/L1 cover the attach event; L3 covers blob presence and hash match; the two never blur. - Store quotas and retention policies apply to blobs freely (PLAN §18 storage exhaustion) — policy about payload, not about history. - The `[:artifact prefix]` guard checks that a matching *claim* exists; richer content requirements compose with attestations, per the closed guard basis. --- # Canonical serialization is a versioned wire protocol Source: https://tik.projects.metio.wtf/decisions/0008-canonical-serialization-protocol/ # ADR 0008: Canonical serialization is a protocol boundary ## Decision Canonical EDN is a **versioned wire protocol**, not an implementation detail. A format version defines the *complete* byte representation; implementations MUST NOT serialize equivalent structures differently. The permanent answers, per format-version 1: - **Numbers**: integers only, printed as longs. Floats, ratios, and bigdecimals are *rejected at emit* — no numeric normalization exists because no ambiguous numerics are admitted. - **Keywords and symbols**: printed textually (`:ns/name`, `name`); namespaces are part of identity forever. - **UUIDs**: lowercase textual form, tagged (`#uuid "..."`). - **Timestamps**: UTC, truncated to millisecond precision, tagged (`#inst "..."` in `java.time.Instant` ISO-8601 form). - **Maps**: entries sorted by the canonical encoding of the key (never by a host-language print function — `pr-str` was tried and is identity-hash-unstable for types without a print-method). - **Sets**: members sorted by their canonical encoding. - **Vectors and seqs**: preserve order (`[..]`, `(..)`). - **Unknown types**: rejected at emit, never "best-effort" printed. - **Whitespace**: single space between elements; no other whitespace. Any change to canonical output — however "equivalent" — is a format-version bump. ## Context A future developer will be tempted by "this is semantically equivalent, let's make the serializer nicer." **That is a hash fork**: every event id and signature in every store is derived from these exact bytes. This is the most immutable layer after the hash function itself (ADR 0006), and it is where the one real encoding bug so far lived (the `pr-str` map-ordering instability, found by property test). ## Consequences - Golden byte tests pin the encoding; breaking one means the format changed, whether or not that was intended. - The rejection list is a feature: admitting floats "for convenience" would smuggle cross-runtime print instability into the hash domain. - A second implementation targets this ADR plus the corpus, not the Clojure serializer's incidental behavior (ADR 0018). --- # Conformance is defined by the corpus, the laws, and normative sweep semantics Source: https://tik.projects.metio.wtf/decisions/0018-conformance/ # ADR 0018: Second-implementation conformance ## Decision The corpus, not the Clojure, is the definition of tik. A conforming implementation must agree on all five layers: 1. **Canonical bytes** (ADR 0008): identical serialization for every supported value, byte for byte. 2. **Event validity** (ADR 0004, 0009): minting rules (mandatory parents, root uniqueness), totality over unknown types. 3. **Reduction**: identical ticket state from identical event sets — ordered by `(at, id)`, deduplicated by id, handler semantics per the closed vocabulary. 4. **Fixpoint semantics**: the **synchronous sweep is normative** (ADR 0005) — all enabled stages added per sweep against the sweep-start snapshot. Fire-one-stage-at-a-time iteration is nonconformant even on stratification-clean processes; `spec/ChaoticFixpoint.tla` exhibits the divergence and the corpus case `sweep-order-negation` pins the correct result. 5. **Explain laws**: soundness (every block re-derivable, nothing speculative) and completeness (every unreached-with-prereqs-reached stage appears) — the data contract of ADR 0016. Conformance is demonstrated by passing the corpus and the property laws, not by code review of the implementation. ## Context The offline-verification story only survives multiple implementations (tik-rust, tik-go, a browser verifier) if "agrees with tik" is testable without reading Clojure. The corpus provides exact expectations; the reference kernel (`test/tik/reference.clj`) provides an executable oracle; the generators provide adversarial inputs; the TLA+ models document the semantics that are easy to get subtly wrong. The sweep requirement exists because it is the one place where a plausible independent implementation of "iterate to closure" silently diverges. ## Consequences - Growing the corpus is a conformance act: new semantics land with corpus cases or they are not fully specified. - Format-version bumps (ADR 0006/0008) version the conformance target with them. - A federation partner claiming "we verified this" is claiming corpus conformance — which is checkable, which is the point. --- # Definition hash is identity; publication signatures are authority Source: https://tik.projects.metio.wtf/decisions/0015-process-definition-trust/ # ADR 0015: Process definition trust ## Decision A process definition's **hash is its identity** (ADR 0002/0006); a **detached signature sidecar over its canonical bytes is publication authority** — who vouches that this definition is an approved rule set. The two are independent: an unsigned definition is still a definite, pinnable identity; a signature adds "and Compliance published it". The permanent answers: - **Signatures are over canonical definition bytes** (the ADR 0007 pattern), never over the hash string alone — signing a name instead of content is how substitution bugs are born. - **Multiple signatures accumulate** as sidecars (author, security review, compliance) without touching identity. - **Who may publish is deployment policy**, expressed as which publisher keys a deployment accepts — checked at ticket creation and by lint/CI, *not* by the kernel at derivation time: a ticket pinned to a definition derives under it regardless, because reproducibility of past conclusions must not depend on today's trust list. - **Revocation is prospective**: revoking a definition (an attestation by its publisher) means "create no new tickets under this; migrate existing ones". Existing tickets keep deriving under their pin — ADR 0002's reproducibility — while lint, `next`, and migration sweeps surface them as work. Retroactive invalidation would rewrite what past conclusions meant, which is the exact audit hole pinning closes. ## Context A process definition is executable governance: publishing a bad one is the definition-poisoning attack (PLAN §18), and the kernel is correctly neutral about it — the verifier proves correct evaluation, not good policy. What the kernel *can* do is make authorship and endorsement of definitions first-class evidence, so "who allowed these rules" is a log question with the same answerability as "who asserted this fact". ## Consequences - The compliance-library product (IDEAS) and starter templates ship as signed definitions; the signature is the product's warranty label. - "Pinned to a revoked definition" is a governance lens finding and a `next` item, never a derivation change. - Definition provenance rides the identity ladder (SSH keys → OIDC attestations → Sigstore), same as actors — no parallel trust system. --- # Derived material may be cached only if disposable and untrusted Source: https://tik.projects.metio.wtf/decisions/0013-derived-state-never-authoritative/ # ADR 0013: Derived state is never persisted as authoritative ## Decision Derived material — stages, frontiers, explain output, totals, indexes, reduction summaries — may be cached, indexed, or materialized **only if it is disposable and verification never trusts it**. Deleting any cache must be a no-op for correctness. `verify` re-derives from the log, always; it never reads a cache, an index, or a checkpoint as input. Checkpoints (a verified reduction summary pinned to a head) are **untrusted accelerators**: an implementation may use one to skip work, but replay always outranks it, and any disagreement is resolved in favor of replay. ## Context This is the central law (PLAN §1) given ADR status because implementation pressure will attack it specifically, with reasonable requests: "cache stages", "store current status", "index the frontier", "persist explain output". All are fine as *porcelain* — and each becomes a system-corrupting bug the moment anything treats the stored copy as truth, because a stored aggregate can drift from its log and a derived one cannot (PLAN §13). `database.stage = "approved"` is how workflow engines are born. ## Consequences - The question for any persistence PR is mechanical: *if this data were deleted right now, would anything be wrong?* If yes, it is authoritative state and rejected. - Performance work targets indexes, caches, and alternative stores behind the EventStore seam — never new authoritative state. - Lenses may serve stale caches for speed (an inbox a few seconds old is fine); anything making a *claim* — verify, witness attestations, evidence bundles — derives fresh. --- # Effects observe derivation; transport is not a domain concept Source: https://tik.projects.metio.wtf/decisions/0019-effects-observe-derivation/ # ADR 0019: Effects observe derivation ## Decision Derivation is pure; **effects observe derivation**. An effect planner watches derived frontier transitions and fires outbound integrations — webhooks, mail, chat — under these rules: - **Delivery never touches truth.** Success or failure of an outbound call changes nothing in any ticket's log. - **No transport event types, ever.** There is no `:webhook/sent`, no `:email/delivered`, no `:kafka/published` — transport is not a domain concept. When the *business outcome* matters ("customer was notified"), it re-enters as a fact or attestation asserted by the notifying actor, accountable like any claim. - **Idempotency is structural, not stateful**: the effect key is the content hash of `(ticket, stage, sink identity)`, so replays and re-derivations dedupe without a delivery-state machine — and any delivery ledger an effect runner keeps is disposable porcelain (ADR 0013). The key deliberately excludes the head: a head moves with every appended event, so keying on it would re-notify the same stage on every subsequent write — the opposite of dedup. - **Structural dedup is scoped to one runner's ledger, not to the estate.** The key is stable across replicas, but the ledger recording which keys were sent is local, so N runners observing the same transition deliver N times. Under the horizontal scaling ADR 0021 calls preferred, that is the narrow per-pipeline delivery lease ADR 0021 admits as its one exception — the only place coordination is allowed, and it serializes DELIVERY, never the log. Until a runner takes such a lease, run exactly one effect runner per estate. - **Inbound is symmetric and already covered**: an external system's webhook is just another actor whose bridge validates, authenticates, and appends signed events (ADR 0001, 0011). ## Context The first integration author under deadline pressure will want to record "the webhook succeeded" in the ticket — and each such record is a transport detail promoted to domain truth, the exact accretion path by which event vocabularies grow to `CommentEdited`/`EmailSent` size. The rule that prevents it is cheap and total: effects are a lens with side effects, downstream of truth, never upstream. ## Consequences - Retry policy, dead-letter queues, and delivery dashboards are effect- runner concerns with no kernel surface. - Notifications phrase themselves from the timeline ("resolution added by Alice — now eligible for QA"), because that is the only truth there is. - An effect runner crashing and replaying produces the same effect keys — at-least-once delivery, deduped against its own ledger, no coordination. Across runners the guarantee is at-least-once per runner; exactly-once across an estate is what the ADR 0021 delivery lease buys, and nothing more. --- # Events are never deleted; blobs may be; nothing is compacted into authority Source: https://tik.projects.metio.wtf/decisions/0017-deletion-and-compaction/ # ADR 0017: Deletion, retention, and compaction ## Decision - **Events are never deleted.** History is the truth substrate; an event store that forgets events is corrupt (missing parents, ADR 0004), not compact. - **Blobs may be deleted.** Deletion removes *availability*, never *history*: the attach event and hash remain, and verify L3 reports verifiable absence (ADR 0014). This is the designed answer to GDPR erasure, leaked secrets, and retention policy — personal data and sensitive payloads belong in blobs, precisely so they are deletable. - **No cryptographic compaction of event history.** Any "verified reduction summary" is derived state: usable as an untrusted accelerator, never as a replacement for the events it summarizes (ADR 0013). A store that can no longer replay is a store that can no longer verify. ## Context Long-lived append-only systems always eventually ask "can we delete old events?", usually citing storage cost or privacy law. The two motives have different correct answers, and blurring them is the danger: storage cost is a payload problem (blobs, quotas, lazy transfer — events themselves are tiny), while privacy law is about *content*, which is why deletable content lives behind hashes rather than inside events. The design keeps a sharp line: **verification reports absence; it never rewrites truth.** ## Consequences - Process design guidance: anything that may need erasure goes into an artifact, not a fact value. Fact values are forever. - Retention policy is blob policy. Event retention policy does not exist. - If event volume itself ever becomes a real cost (PLAN §18 fact spam), the levers are write authorization and quotas at the store seam — admission control, not amnesia. --- # Explain's structured reasons are the stable API; renderings are not Source: https://tik.projects.metio.wtf/decisions/0016-explain-stability-contract/ # ADR 0016: Explain stability contract ## Decision **Explain output is structured data with stable semantics; rendering is not stable.** Clients — web UIs, MCP agents, chat surfaces, dashboards — bind to the reason data (`:reason` keywords like `:fact/missing`, `:role/unsatisfied`, plus their payload keys), never to English strings. The compatibility rules: - Reason keywords and their payload keys are **versioned with the guard vocabulary**: a guard-vocab version enumerates exactly which reasons can occur (a closed vocabulary implies a closed reason set). - **New reasons appear only additively under a version bump**; existing reason keywords never change meaning or payload shape within a version. - The block structure (`:stage`, `:satisfied`, `:missing`, `:blocks`, `:hint`) is part of the same contract, backed by the property-tested soundness/completeness laws (PLAN §8). - **Renderings may change freely** — wording, ordering, localization, ranking, capability-based redaction (IDEAS) are all lens behavior. A client that greps CLI text has no compatibility claim. ## Context Explain is the product surface, which makes it the API everyone will integrate against. Without this contract, MCP and UI clients would inevitably couple to English strings, and improving a message would become a breaking change — freezing exactly the layer that must stay free to improve. The kernel-speaks-data rule (PLAN §5) already provides the mechanism; this ADR adds the promise. ## Consequences - Localization and the explain-as-chatbot surface are renderings — automatically within contract. - Corpus expectations and property tests target the data layer, so the contract is enforced by the same suites that enforce derivation. - A new guard operator (version bump) documents its reasons as part of its admission (ADR 0001, PLAN §19 gate). --- # Signatures establish authorship; authorization is derived Source: https://tik.projects.metio.wtf/decisions/0010-authority-model/ # ADR 0010: The authority model ## Decision **Signatures establish authorship. Authorization is derived** — from identity attestations, role facts, and process guards. The kernel never interprets a signature as permission by itself; there is no code path where "validly signed" alone unlocks anything. The permanent answers: - **Revocation is prospective, not retroactive.** A key revocation is an attestation event. Signatures made before revocation remain valid *authorship claims* forever — the log is immutable and history does not get rewritten (ADR 0002's reproducibility applies to trust too). Whether the author was *authorized* is evaluated against role validity at the relevant time. - **Role membership is time-dependent.** "Alice was a triager *when she triaged*" is the derivable question; the three clocks (PLAN §5) decide which "when" — claimed by default, witnessed where the process demands it. - **Delegation is an attestation with scope and expiry** (`:valid-until`, capability), including human→agent delegation. An authority chain (Alice ← Bob ← Compliance) is derivation over delegation attestations. - **Key compromise does not invalidate history**; it changes interpretation. The response is evidence: revoke (attestation), dispute facts the compromised key asserted, and let derivation regress what depended on them. A lens may flag "signed by a later-revoked key"; the kernel keeps the record. ## Context Someone will eventually read `[:signed-by :manager]` and conclude the signature system *is* the authorization system. It is not: that guard expands to authorship (the signature) **plus** role derivation (is the author in the role, at the right time, per the identity attestations). Losing this distinction is how "it's signed" quietly becomes "it's approved" — the sidecar-discovery footgun (ADR 0007) and role-decay attack (PLAN §18) are both instances. ## Consequences - Verify L1 checks authorship only. Authorization questions are L2 questions — re-derivable, explainable, offline. - Roles are security boundaries and must get process-grade discipline: attestation-backed grants with provenance, temporal validity, explicit migration (PLAN §19 identity concretions). - "Was this allowed?" is always answerable from the log, never from a key server's current opinion. --- # The EventStore contract every backend must honor Source: https://tik.projects.metio.wtf/decisions/0020-eventstore-contract/ # ADR 0020: The EventStore contract ## Decision Any storage backend (file/git today; SQLite next; anything later) is valid iff it honors this contract: - **Append-only**: no operation deletes or mutates an event, ever (ADR 0017; blobs are separate and deletable, ADR 0014). - **Append is idempotent by id**: appending an already-present event id is a no-op — union semantics are the store's job to preserve, which is what makes replica merge trivial. - **Events are returned unordered**: the reducer orders by `(at, id)`; a store that returns "helpful" ordering invites callers to depend on it (ADR 0004: parents are not ordering either). - **`has-event?` is the reconciliation primitive**: sync between any two stores is git-style have/want set reconciliation over event ids; a server is just a well-connected replica. - **The store holds bytes, not interpretations**: no store may index its way into authority — any derived index it keeps is disposable (ADR 0013). For the file store this is literal (`sha256sum(file) = filename = id`, ADR 0007); other backends must preserve the exact canonical bytes so the same guarantee is testable (`events(id TEXT PRIMARY KEY, bytes BLOB)` — never a parsed-columns schema that re-serializes on read). ## Context The storage seam is where "just this once" optimizations concentrate: a parsed-column schema that re-serializes (hash fork risk, ADR 0008), a store that orders by insertion (hidden coupling), a cleanup job that prunes "obsolete" events (corruption, ADR 0004). Writing the contract before the second backend exists means SQLite gets built against rules, not against the file store's incidental behavior. ## Consequences - A backend is validated by the same corpus and property tests as the kernel: store round-trip must preserve bytes exactly. - Quotas, retention (blobs), and access control live at this seam — policy about payloads and actors, never about which events exist. - Nothing in the kernel knows which backend is underneath; nothing in a backend knows what events mean. --- # The log admits all well-formed claims; trust is evaluated, not filtered Source: https://tik.projects.metio.wtf/decisions/0011-log-admission-vs-trust/ # ADR 0011: Event existence vs. participation in truth ## Decision **The log contains all received well-formed claims. Derivation consumes the whole set.** Trust is never a silent reducer filter — it is expressed where it is visible: in guards (`:signed-by`, and any future trust-conditions) and in the verify ladder (L1 authenticity). A fact asserted by an actor lacking the required role *exists*, *is derived over*, and *fails the guard with an explainable reason* (`:role/unsatisfied`, naming the actor) — it is never treated as nonexistent. ## Context "Event exists" and "event participates in a given conclusion" are different statements, and conflating them creates ambiguity in both directions. If the reducer silently dropped untrusted events, then two verifiers with different key knowledge would derive different states from the same log — derivation would no longer be a pure function of the event set, and explain could not say *why* a claim didn't count (it would have vanished). Keeping admission total and trust explicit means every exclusion has a structured reason a human can read. Signature-invalid sidecars are an L1 finding about an *endorsement*, not grounds to un-exist the claim: the `.edn` file is the claim, and a claim whose endorsement fails is a claim with a visible problem (ADR 0007). ## Consequences - Reducer totality (ADR 0009, property-tested) extends to trust-questionable events: reduction never asks "do I trust this?". - explain can render "fact exists but its author lacks the role" — strictly more useful than "fact missing", and only possible because the fact was admitted. - Write-side gatekeeping (who may append at all) is store/transport policy — quotas, authenticated endpoints — and lives outside derivation entirely (PLAN §18, fact spam). --- # Three clocks; claimed is the default; the clock is part of the guard Source: https://tik.projects.metio.wtf/decisions/0012-time-semantics/ # ADR 0012: Time semantics are part of the process contract ## Decision Three clocks, never conflated: 1. **Claimed** — `:event/at`, asserted by the actor, inside the hashed region. 2. **Observed** — witness countersignature over a head: "this history existed no later than T". 3. **Evaluated** — the explicit `now` argument to derivation; never an implicit system clock. **Claimed is the default clock for guards.** A guard opts into observed time with `{:clock :witnessed}` where backdating matters. Evaluation time is always explicit — the kernel has no ambient "now" (CLAUDE.md: no kernel I/O includes no clock reads). The permanent answers: - **Backdating is detectable, not preventable**: a claimed time earlier than a countersigned observation of the event's absence is visible to any verifier. Pretending clocks can be trusted would be dishonest; every clock is just another evidence source, and stronger time means moving up the ladder (claimed → witnessed → externally anchored). - **A witness arriving after a stage derived** changes nothing retroactively: derivation at `(events, now)` is a pure function, and a conclusion is indexed by its inputs. Under `{:clock :witnessed}` a stage may only become derivable once the witness exists — that is the guard working, not history changing. - **Re-evaluation at a later `now` never changes history** because history is not a single state: it is `f(events, now)` for every now. "What was derivable on March 1" is answered by evaluating at March 1, reproducibly, forever. ## Context Someone will eventually write a time-dependent guard without saying which clock it reads, and every implicit answer is wrong for someone: claimed time is gameable, witnessed time may not exist yet, system time is unreproducible. Making the clock part of the guard's meaning keeps audit questions answerable — and makes `verify` L2 possible at all, since re-derivation years later must not depend on the wall clock of the machine running it. ## Consequences - `:elapsed-since` and any future temporal guard name their reference point and read the claimed clock unless the process opts into witnessed. - The deferred recency guard (PLAN §19) defaults to the witnessed clock because staleness is exactly where claimed time cannot be trusted. - Timestamp games (PLAN §18) are answered by this ladder, not by NTP. --- # Unknown data handling differs by layer, on purpose Source: https://tik.projects.metio.wtf/decisions/0009-unknown-data-policy/ # ADR 0009: Unknown data policy ## Decision Three layers, three different answers — because the cost of being wrong differs: | Layer | Unknown data | | ------------------ | --------------------------------------------------- | | Event types | **preserved, hashed, ignored** by the reducer | | Guard operators | **rejected**: lint error at authoring, throw at eval | | Definition fields | **carried and hashed, never interpreted** | ## Context - **Events must tolerate the future**: a replicated store cannot retroactively reject an event that already exists on three replicas, so the reducer is total — unknown types stay in the log (and in the hash domain) and simply do not contribute to ticket state. Ignoring is safe because an event the reducer skips cannot silently change truth. - **Guards must not tolerate the unknown**: a guard the evaluator does not understand *would* change truth if guessed at. There is no safe default for "some condition I cannot evaluate" — neither satisfied-by-default nor failed-by-default is honest. So the guard vocabulary is closed (ADR 0001, PLAN §5): unknown operators are lint errors, and evaluation throws rather than improvising. - **Definitions may carry annotations** (`:hint`, `:purpose`, lint config): they are part of the pinned bytes — two definitions differing only in annotations are different definitions, honestly — but the kernel never reads them; only lenses do. ## Consequences - Forward compatibility is asymmetric by design: new *evidence* flows through old kernels harmlessly; new *semantics* (guards) require a version bump everywhere. - `tik lint`'s closed-basis check is the enforcement point for the middle row; the reducer-totality property test enforces the top row. - Nobody gets to add meaning by sneaking a field into a definition: if the kernel doesn't interpret it, it is annotation, whatever it is named. --- # Concurrent conflicting assertions block guards; humans resolve Source: https://tik.projects.metio.wtf/decisions/0003-conflicts-block/ # ADR 0003: Conflicts are facts about disagreement ## Decision When causally concurrent assertions (neither an ancestor of the other via `:event/parents`) target the same fact path with different values, the fact becomes **conflicted**. A conflicted fact — like a disputed one — does not satisfy guards. `explain` surfaces both claims, both actors, and asks for a superseding assertion. Resolution is a new signed event: a human judgment on the record. tik ships **no conflict-resolution policy language**. No latest-wins, no role-priority, no per-process resolution rules. ## Context Most replicated systems ask "how do we make replicas converge?"; the prior question is "what does convergence mean when humans disagree?" Last-write-wins is deterministic but destroys information: an engineer asserting `severity=low` concurrently with a manager asserting `severity=critical` is a *disagreement*, and `severity = conflicted` is the actual state of the evidence — the disagreement itself is information. Silently picking a winner by timestamp hides exactly the thing the process should surface, and every resolution shortcut fails the same way: latest-wins makes clocks into authority, role-priority embeds organizational judgment in the truth engine ("why did tik choose the manager's claim?" — "because the kernel says so" is the accountability hole), and per-process resolvers grow into a second governance language hidden in configuration. The causal DAG is what makes the distinction computable: a later assertion that *observed* the earlier one (an ancestor) is a correction — history, not conflict; only causally concurrent claims disagree. ## Consequences - Dispute, retraction, and conflict are now symmetric: three reasons a fact stops satisfying guards, all visible in `explain`, all resolved by new events. With absence, that completes the fact lifecycle — retracted (withdrawn), disputed (challenged), conflicted (independent claims disagree), absent (never established) — and lets guards ask exactly one question with no per-scenario special cases: *is this fact currently trustworthy enough to derive from?* (fact-status, the choke point). - Conflict volume is a health metric, not a merge problem: chronic conflicts indicate overly broad fact paths, unclear ownership, or a noisy integration (PLAN §5, §18). A "conflict topology" lens — which paths conflict most, which actors disagree, which integration is the source — fits the governance-observability family (IDEAS). - Built on `:event/parents` (ADR 0004): the causally-maximal writes on a path conflict when they disagree. Concurrent agreement (same value from independent replicas) is corroboration, not conflict. Resolution is any write that observed all competitors — a superseding assert or a retract, either way a judgment on the record. Detection is computed from the complete log, never an incremental frontier: a backdated intermediate event would make the incremental version order-dependent, and commutativity is a law. Pinned by the corpus case `concurrent-conflict` and by `tik.conflict-test` (including the backdating counterexample). - Escape hatches (per-fact `:on-conflict`) are deferred until dogfooding demonstrates a real need; adding one later is compatible, removing one is not. (The honest operational valve already exists without kernel support: a policy bot authorized to sign superseding facts under declared rules is an accountable actor, not protocol semantics — PLAN §5.) - The job, in one line: tik does not eliminate conflict — it **makes important conflicts impossible to hide.** --- # Event parents are mandatory; the log is a Merkle DAG Source: https://tik.projects.metio.wtf/decisions/0004-mandatory-parents/ # ADR 0004: Mandatory parents — the log is a Merkle DAG ## Decision Every event carries `:event/parents`: the set of head event ids the actor observed when minting. `:ticket/create` is the unique root with `#{}`; for every other event type an empty parent set is a mint-time error. Since parents are inside the content-addressed, signed region, the log is a Merkle DAG: one head hash commits to the entire history. ## Context Parents are not metadata — they change what kind of object the log is: without them, an append-only collection of claims; with them, an **authenticated history graph** where every claim carries what its author knew. Parents were originally optional "Phase 1" metadata. Review of the axes showed that optionality here is ambiguity, not flexibility, and that mandatory parents simplify or strengthen nearly every other axis simultaneously: - **Sync** becomes head comparison + ancestry walking instead of full id enumeration. - **Concurrency** becomes structural (two events, neither an ancestor of the other) instead of heuristic — the precondition for ADR 0003's conflict semantics. - **Witnessing** collapses in cost: one countersignature over a head timestamps every ancestor event at once. - **Federation attestations** can pin claims to a head hash, making them *reproducible* (hand over the log, re-derive) rather than trust-me. - **Store integrity**: a missing ancestor is detectable corruption, not silent loss (verify ladder L0) — distinct from a missing *blob*, which is verifiable absence (L3). "History incomplete" and "artifact absent" are different failure classes and verify reports them differently. **What "observed" means, operationally.** The minting layer must track known heads; `:event/parents` states "I created this event knowing exactly these heads." Creating an event while offline or behind is not an error — a newer head existing elsewhere is precisely the distributed model, and the resulting structural concurrency is honest. The error is *pretending no head existed*: an empty parent set on a non-root event is a lie about what the actor knew, which is why it is rejected at mint time rather than tolerated as a degenerate case. ## Consequences - Porcelain must track heads (it needs them for sync anyway); the kernel gains `tik.dag`. - This changes the event schema, which is why it is decided now — before any real data exists. Old-style events without parents will never exist. - Reduction order remains `(at, id)`; parents are for integrity, concurrency detection, and sync — deliberately not for ordering, so derivation stays a pure function of the event *set*. The two concepts answer different questions and must not mix: parents answer *"what did this actor know?"*; the reducer answers *"given the complete set of evidence, what is derivable?"*. Processing children after parents would couple evaluation to causal topology and forfeit commutativity. - A **causal view** lens falls out for free — which assertions were made from which evidence, who worked from an outdated head, where branches diverged, which conflicts came from independent replicas. Pure DAG analysis, no kernel change (IDEAS). - With ADRs 0001–0003 this completes the base: truth enters only as claims, interpretation is pinned, disagreement is preserved, and claims carry their causal history. The log is not merely append-only — it is an **argument graph**, where every conclusion points back through the evidence that made it derivable. --- # SHA-256 content addressing, self-describing ids, one algorithm per store Source: https://tik.projects.metio.wtf/decisions/0006-hash-policy/ # ADR 0006: Hash policy ## Decision 1. Content addresses are SHA-256 over canonical bytes, written as self-describing ids: `sha256-`. 2. **Exactly one algorithm per store per format-version.** Verifiers reject stores mixing algorithms within a format version — hash agility in the *format*, discipline in the *policy*, because "verifier accepts whatever" is a downgrade-attack surface. 3. Migration, if ever needed, is **additive**: a format-version bump after which new events use the new prefix while old events remain exactly as signed. Mixed-prefix DAGs verify as long as the verifier implements both prefixes. No rewrite of history, ever. 4. **The format version defines the trust contract, not the identifier syntax.** A verifier implementing a format version must support exactly the hash algorithms that version requires; support for other algorithms is never inferred from a self-describing prefix alone. A generic hash-registry verifier ("dispatch on whatever the prefix says") would recreate the downgrade surface rule 2 exists to close. ## Context The argument for SHA-256 is ecosystem weight, not a prediction about cryptography: it is sufficiently deployed that the verification ecosystem around it currently outweighs the benefits of any alternative. git's own hash transition targets SHA-256 (SHA-512, SHA-512/256, BLAKE2 and K12 were considered and rejected), OCI digests, Sigstore/in-toto/DSSE, Nix SRI, FIPS 180-4 — and `sha256sum` in coreutils, which is what keeps verify level 0 checkable with tools from 1995. Should that balance ever shift, rule 3 is the exit. SHA-256's length-extension weakness is irrelevant here, and for a reason stronger than "it only affects secret-prefix MACs": tik hashes complete canonical byte sequences and never uses the hash as an authentication primitive. The division of labor is strict — **hashes answer "is this byte sequence unchanged?"; detached signatures answer "who authorized this byte sequence?"** (ADR 0007) — so the hash carries integrity only, never authenticity. Performance is irrelevant at event sizes; BLAKE3's speed does not buy anything tik needs at the price of ecosystem and compliance alignment. Unlike git — whose uniform object-id namespace makes its transition agonizing — tik's ids are strings referenced verbatim by parents, attestations, and links, which is what makes the additive migration path possible. That property is retained on purpose. ## Consequences - If witnessed.dev ever sells into CNSA 2.0 territory, the answer is a new prefix (`sha384-`/`sha512-`) under a format-version bump — a policy change, not a redesign. - The invariant future requirements must respect, in one line: **identities are stable; migrations are additive; verification rules are explicit.** This ADR does not predict cryptographic trends — it constrains how any trend gets absorbed. --- # Stage negation must be stratified Source: https://tik.projects.metio.wtf/decisions/0005-stratified-negation/ # ADR 0005: Stratified negation over stages ## Decision A stage may apply `[:not [:stage-reached X]]` only to stages in a **strictly earlier stratum** of the process graph (stratum = longest `:after` path depth). `tik lint` enforces this as an error. (A dedicated `:not-stage` alias for the same guard existed through plan v5 and was removed in the v6 subtraction — one spelling means the linter polices one shape.) ## Context `[:not [:stage-reached X]]` is negation inside a fixpoint — non-monotone. Without stratification, two stages in the same stratum can both derive in the same fixpoint sweep against the pre-sweep snapshot (e.g. `:escalated` guarded by `[:not [:stage-reached :triaged]]` co-deriving with `:triaged`), producing a state that is deterministic only by accident of iteration strategy. Datalog solved this decades ago: evaluate strata in order, negate only what earlier strata have finished deciding. Adopting the same rule makes determinism *provable* rather than incidental and connects the guard language to well-understood theory. Negation over **facts** (`[:not [:fact …]]`) is unaffected: facts are inputs to the fixpoint, not derived by it. ## Consequences - The sample support process was itself in violation and was remodeled: "escalated = 48h and not yet triaged" became "48h and no category fact" — a fact-level negation, monotone-safe, and arguably the more honest claim. - Cost: one linter check. The kernel's fixpoint is unchanged. - Lint currently detects the direct `[:not [:stage-reached X]]` spelling; arbitrarily nested negation parity is a known lint TODO, documented here so it is a tracked gap rather than a silent one. - **Stratification is necessary but not sufficient** — model checking (`spec/ChaoticFixpoint.tla`) exhibits a linter-clean process (`:d` in a later stratum negating `:c`) whose result is order-dependent under fire-one-stage-at-a-time iteration: firing `:d` before `:c` has entered yields a different fixpoint. Determinism additionally requires the evaluator to use **synchronous sweeps** (all enabled stages against the sweep-start snapshot, as `tik.stage/reached-set` does) or explicit stratum-ordered evaluation. This is a conformance requirement on any second implementation, pinned executably by the corpus case `sweep-order-negation` and explained by `spec/SweepFixpoint.tla`. --- # Stored bytes are exactly the hashed region; signatures are detached Source: https://tik.projects.metio.wtf/decisions/0007-stored-bytes-are-the-hashed-region/ # ADR 0007: Stored bytes = hashed region; detached signatures ## Decision An event file contains exactly the canonical bytes of the hashed region — the event map WITHOUT `:event/id` (the filename is the id; storing it inside would be redundant and, decisively, breaks `sha256sum(file) = filename`). Signatures never live inside the event either: `:event/key`/`:event/sig` are removed from the schema entirely; signatures are detached sidecar files (`.sig.`) verifying against the exact stored bytes, allowing multiple signatures per event without touching the hashed region. ## Context Found by the discipline this ADR family exists to protect: internal `verify` passed while an independent `sha256sum` check failed, because the file embedded the very id that its hash was supposed to equal. The claim "an auditor can check store integrity with coreutils" was almost true — and "almost verifiable" is a bug class of its own: **internal consistency without external verifiability**, a verifier validating a circular claim (`hash(bytes-with-id) == embedded id`). The fix is the standard content-addressing move stated as a rule: *the address names the object; it is never part of the object.* What the decision actually buys is a moved trust boundary — from "trust tik's verifier" to "trust mathematics + coreutils + the canonical encoding". The implementation becomes a convenience layer over a smaller primitive contract: `file bytes → hash → filename → signatures → derivation`, each step independently inspectable. The tempting alternative (`:event/id` and `:event/sig` fields, "self-contained events") fails the deeper property: a verifier must never have to trust the representation's self-description. The separation also yields a clean algebra with no privileged mutation path: the *object* is `bytes → hash → identity`; *endorsements* (`.sig.`, `.witness.`, `.ots`) accumulate around it without ever touching it — so Alice's signature still covers the exact bytes after Bob signs, which embedded signature fields cannot offer. Authority never lives inside the record (`approved_by: [alice]` is the anti-pattern); people make claims *about* the record, the same shape as every other claim in the system. ## Consequences - `sha256sum` over any event file yields its filename. Verify L0 is literally coreutils. - Clean separation of concerns: the .edn file is the *claim*; sidecars are *endorsements of the claim*. The v6 subtraction generalized this to witness countersignatures (`.witness.`) and OTS anchors (`.ots`) — one endorsement pattern for authorship, observation, and anchoring. - The verify ladder's layers become mechanically independent, each adding trust without assuming the next: L0 "do I have the object?" (coreutils, no keys, no network, no tik code); L1 "who signed these bytes?"; L2 "does it mean what it claims?" (re-derivation); L3 "do artifacts and witnesses check out?". - **Sidecar discovery is the residual footgun**: "this object has signatures" and "this signature is authoritative" are different statements. A UI that renders any valid sidecar as "signed ✓" invites the attack of attaching a valid signature from an irrelevant key. Signatures prove *identity*, never *authorization* — authorization is always signature + identity facts + role derivation + process guard, and lenses must render the distinction. - Canonicalization is the remaining sharp edge: everything downstream trusts the canonical serializer. Its defenses are the golden byte tests, independent `sha256sum` checks, the corpus, and property tests (which caught the `pr-str` map-ordering instability — exactly the failure class this layer must catch). - Readers attach `:event/id` from the filename; `verify` recomputes it from the bytes. - The Event schema shrinks. Simplification found by verification — the best kind. --- # The event-log acceptance test for all features Source: https://tik.projects.metio.wtf/decisions/0001-event-log-acceptance-test/ # ADR 0001: The event-log acceptance test ## Decision A feature proposal must be expressible as one or more of: (a) new **event or attestation types** over the existing append-only log, (b) new **guard vocabulary** evaluated purely from `(events, now)`, (c) **porcelain or lens** behavior deriving from (a) and (b). Proposals requiring stored mutable state, imperative stage transitions, or verification paths that leave the log are **rejected or redesigned**. One carve-out, so the test cannot be misread: features necessary to preserve the **integrity, authenticity, or reproducibility of the log itself** — canonical serialization, signature algorithms, hash migration, witness sidecars, store verification — are trust substrate, governed by their own ADRs (0004–0007), not by this test. The substrate may never introduce domain truth outside the log; it exists to make (a)–(c) trustworthy. "A new signature algorithm is neither an event nor a lens, therefore rejected" is a misreading, not an application, of this ADR. ## Context The kernel's value is that stage is derived, merges are union, and verification is offline-forever. Every mechanism designed so far — disputes, OIDC key/role enrollment, server countersigning, cross-instance federation, replay-based notifications, MCP/agent actors — passed this test, several by *shrinking* rather than growing. The test exists to defend the kernel from its own maintainers under deadline pressure. ## Consequences - "Reject and move back to triage" became a signed `:fact/dispute` event; regression is derived, never performed. - Identity, roles, revocation, timestamps, and federation all enter as attestation events — the trust model rides the same rails as the tickets. - Any proposal that fails the test is a design smell worth a new ADR, not a quick exception. - This ADR is the head of a chain — each successor constrains the next step of a claim's life: 0001 says where truth may enter; 0004 how history is connected; 0005 how derivation stays deterministic; 0006 how identities remain stable; 0007 what exactly is signed. Together: a claim enters → is immutable → merges safely → derives conclusions → and the conclusions are reproducible. Local exceptions ("just store the current status", "just add a transition endpoint", "just call this service during verification") each look harmless; together they recreate the workflow engine this system exists to replace — which is why the test binds maintainers, not users. --- # Tickets pin their process definition hash; migration is an event Source: https://tik.projects.metio.wtf/decisions/0002-pinned-process-versions/ # ADR 0002: Pinned process versions, explicit migration ## Decision Tickets **pin the process definition hash** in effect at creation — the hash is the identity (ADR 0006); the human-readable version number is a label carried as metadata. Re-evaluation under a newer definition happens only via an explicit, signed `:process/migrate` event carrying the new hash. `tik migrate --dry-run` shows the derived-stage diff before anyone commits to it — a migration is a consequence-bearing decision ("under the new rules, security-review is now missing"), not a version-number edit. ## Context The derivation function is part of the evidence context. The conclusion is not `events → stage` but `(events + process-definition + evaluation-time) → stage`: silently changing the definition changes the meaning of the historical record. The choice here is **reproducibility over freshness** — a live workflow product asks "what is the correct state under today's rules?"; an evidence system asks "what conclusion did *these* rules produce from *these* facts?" — and the auditor, regulator, and customer dispute all need the second question answered. The original design floated tickets to the latest version by default ("stage is derived, so migration is free re-evaluation"). External review correctly identified this as an audit integrity failure: a ticket that was `:resolved` yesterday silently ceasing to be resolved because a definition changed violates least surprise and poisons any compliance narrative built on `verify`. The inversion is cheap because migration passes ADR 0001: it is just another event — with an actor, a timestamp, parents, signatures, the new definition hash, and a derivable consequence. Mutable `ticket.process_version` metadata would instead raise "who changed it, when, was it authorized, what did it mean before?" as special questions; as an event they are ordinary log questions with no special trust path. ## Consequences - `verify` evaluates each ticket under its pinned version: reproducible audits. - **Grandfathering loophole**: pinning lets open tickets close under old, laxer rules after a security-motivated process fix. Mitigation: migration sweeps are first-class porcelain, and processes can declare `:process/migration-policy` (e.g. required-within a duration of a version bump) enforced by lint/CI and the `next` inbox. - Floating remains available as an explicit per-process opt-in for pre-1.0 process development, never as the default. The kernel never secretly overrides pinning — that would recreate the audit problem the pin exists to solve; overdue migrations are policy, surfaced by lint/CI and `next`. - The boundary in one line: **facts can accumulate and interpretations can evolve, but old interpretations must remain reproducible** — that is what makes a changing process compatible with an immutable log. --- # Contributing Source: https://tik.projects.metio.wtf/contributing/ ```sh git clone https://github.com/metio/tik.git cd tik nix develop ``` The flake carries the whole toolchain — JVM, Clojure, babashka, clj-kondo, TLC, GraalVM, ssh-keygen, Hugo — so local and CI resolve identical versions. Every command below assumes `nix develop --command`. ## The gate ```sh bb test # JVM test suite (kaocha + test.check) bb lint # clj-kondo: 0 errors, 0 warnings bb analyze # eastwood + splint bb fmt # cljfmt (bb fmt fix rewrites) bb tla # TLC model checks bb tik test processes/support-request.tests.edn reuse lint # every file carries SPDX headers (0BSD) ``` All of it is green on main and expected to stay that way. `bb tla` asserts that `ChaoticFixpoint` **fails** — a passing chaotic model means a documented counterexample was lost. Focus a single namespace or var while iterating: ```sh clojure -M:test --focus tik.stage-test clojure -M:test --focus tik.stage-test/sticky-milestone-survives-retraction ``` ## Where code belongs - **Kernel** (`src/tik/*.cljc`) — deterministic, pure, replayable forever. No I/O of any kind: no HTTP, no SQL, no environment variables, no implicit clock. Time enters as the explicit `now` argument. Everything external arrives as a signed event. - **Store** (`src/tik/store/`) — the one I/O seam, behind the EventStore protocol. - **Porcelain** (`cli/`) — may format, cache, and do I/O, and may evolve quickly, as long as nothing it caches is treated as authoritative. Dependencies point one way: porcelain depends on the kernel, never the reverse. The kernel speaks EDN; English prose belongs in lenses. ## Five test layers Each has caught a real bug the others missed, so a change to kernel semantics extends the layer that would have caught its bug: 1. **Golden byte tests** pin the canonical serialization. A change to `canonical.cljc` invalidates every event id and signature ever written. 2. **The conformance corpus** (`corpus/`) — event files plus expected derivations. The corpus, not the Clojure, is the definition of tik. 3. **Property tests against a reference kernel** — a deliberately slow prefix-replay implementation the optimized fold must agree with, with generators biased toward timestamp ties. 4. **TLA+ models** (`spec/`) for merge convergence and fixpoint semantics. 5. **Fuzzing** — the other layers feed valid input and check the answers; this one feeds garbage and checks the *manner* of failure. The contract is to fail well: structured rejection, never a raw exception, never a silent pass. ## Why things are the way they are The [decision log](/decisions/) records every load-bearing choice with the context that forced it and the consequences that follow. Several entries exist to name something tik will not do, so it is the fastest way to find out whether an idea has already been settled. ## Smells A change carrying any of these is probably changing the model rather than extending it, and wants a design discussion first: caching a derived value as authoritative, a new event type or guard keyword, ordering derived from parents, a guard that queries anything, a leader or lock or quorum on a correctness path, a self-minted event with a non-deterministic field, or kernel code doing I/O. ## Licensing Every file carries `SPDX-FileCopyrightText` and `SPDX-License-Identifier` (0BSD), inline where the format allows comments and through `REUSE.toml` where it does not.