<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://gohugo.io/" version="0.163.3">Hugo</generator><title type="html">tik</title><link href="https://tik.projects.metio.wtf/" rel="alternate" type="text/html" title="html"/><link href="https://tik.projects.metio.wtf/index.xml" rel="alternate" type="application/rss+xml" title="rss"/><link href="https://tik.projects.metio.wtf/atom.xml" rel="self" type="application/atom+xml" title="atom"/><link href="https://tik.projects.metio.wtf/humans.txt" rel="alternate" type="text/plain" title="humans"/><link href="https://tik.projects.metio.wtf/foaf.rdf" rel="alternate" type="application/rdf+xml" title="foaf"/><link href="https://tik.projects.metio.wtf/llms-full.txt" rel="alternate" type="text/plain" title="llms"/><link href="https://tik.projects.metio.wtf/llms.txt" rel="alternate" type="text/plain" title="llmsindex"/><updated>2026-08-05T22:28:23+00:00</updated><author><name>metio.wtf</name></author><id>https://tik.projects.metio.wtf/</id><entry><title type="html">Claude Code skill</title><link href="https://tik.projects.metio.wtf/integrations/claude-code/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://tik.projects.metio.wtf/integrations/mcp-server/?utm_source=atom_feed" rel="related" type="text/html" title="MCP server"/><id>https://tik.projects.metio.wtf/integrations/claude-code/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-08-05T22:45:55+02:00</updated><content type="html"><![CDATA[<blockquote>Install the tik skill so Claude drives a store correctly — recording evidence and letting stages derive, instead of setting statuses.</blockquote><p>Install the tik skill into <a href="https://claude.com/claude-code">Claude Code</a>

and Claude gains working knowledge of tik: it recognises a store, records
facts with <code>tik set</code> rather than inventing a status field, reads
<code>tik explain</code> before asking a person anything, and authors process
definitions whose stages derive from evidence instead of listing
activities.</p>
<p>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.</p>
<p>The skill lives in this repository under <code>skills/tik/</code>, packaged as a
Claude Code plugin by the manifests in <code>.claude-plugin/</code>.</p>
<h2 id="install">Install</h2>
<p>Add this repository as a plugin marketplace, then install the plugin:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">/plugin marketplace add metio/tik
</span></span><span class="line"><span class="cl">/plugin install tik@tik
</span></span></code></pre></div><p>Claude activates the skill whenever a repository holds a <code>tickets/</code> or
<code>.tik/</code> directory, an <code>actors</code> registry, or <code>processes/*.edn</code> — or when
you mention tik, tickets, processes, or stages.</p>
<h2 id="what-it-grants-claude">What it grants Claude</h2>
<p>The skill teaches the CLI surface and the model behind it:</p>
<ul>
<li><strong>The daily loop</strong> — <code>tik ls</code>, <code>tik next</code>, <code>tik new</code>, <code>tik set</code>,
<code>tik explain</code>, <code>tik status</code>, and the selector grammar for filtering a
board.</li>
<li><strong>Corrections</strong> — retract, dispute, and the fact that a dispute is
answered only by a <em>different</em> value, so Claude does not try to clear
one by retyping the same fact.</li>
<li><strong>Authoring</strong> — <code>tik author</code>, the guard vocabulary, and the lint,
simulate, and test loop that proves a definition before a real ticket
depends on it.</li>
<li><strong>The one design law</strong> — a stage is defined by what must be TRUE to
reach it, never by who moved what.</li>
</ul>
<p>It also knows when <em>not</em> to act: an unreachable step in <code>explain</code> means
waiting is pointless and the definition or the role register needs
fixing, not patience.</p>
<h2 id="for-agents-generally">For agents generally</h2>
<p>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:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">tik agent actions <span class="m">3184</span> --actor bot
</span></span><span class="line"><span class="cl">tik agent <span class="nb">set</span> <span class="m">3184</span> <span class="nv">severity</span><span class="o">=</span>:high --actor bot
</span></span></code></pre></div><p>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.</p>
<p>This site also publishes <code>/llms.txt</code> and <code>/llms-full.txt</code>, so a model can
read the documentation directly.</p>
]]></content><category scheme="https://tik.projects.metio.wtf/tags/claude" term="claude" label="claude"/><category scheme="https://tik.projects.metio.wtf/tags/skill" term="skill" label="skill"/><category scheme="https://tik.projects.metio.wtf/tags/plugin" term="plugin" label="plugin"/><category scheme="https://tik.projects.metio.wtf/tags/agents" term="agents" label="agents"/></entry><entry><title type="html">Derived beats declared</title><link href="https://tik.projects.metio.wtf/concepts/derived-beats-declared/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://tik.projects.metio.wtf/concepts/stages/?utm_source=atom_feed" rel="related" type="text/html" title="Stages"/><id>https://tik.projects.metio.wtf/concepts/derived-beats-declared/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-08-05T22:45:55+02:00</updated><content type="html"><![CDATA[<blockquote>The one law — if something follows from the log, storing it as authoritative state is a bug.</blockquote><p><strong>If something can be derived, storing it as authoritative state is a
bug.</strong> That is the whole law, and every design question in tik resolves
against it.</p>
<p>A ticket&rsquo;s stage is the obvious case. It is never written down anywhere. It
is <code>f(events, now)</code> — a pure function of the ticket&rsquo;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.</p>
<h2 id="what-the-law-buys">What the law buys</h2>
<p><strong>Correctness by construction.</strong> A stored status can disagree with the
facts; a derived one cannot. When somebody retracts the approval that a
ticket&rsquo;s <code>:approved</code> 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.</p>
<p><strong>Every question about every moment.</strong> Because <code>now</code> is an argument rather
than an ambient clock, &ldquo;what was true on March 1&rdquo; is answered by evaluating
March 1:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">tik status <span class="m">3184</span> --at 2026-03-01T00:00:00Z
</span></span></code></pre></div><p><strong>Replication without coordination.</strong> 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
<a href="/concepts/replication/">Replication</a>
.</p>
<p><strong>Auditability that survives the tool.</strong> The conclusion is reproducible
from the signed bytes by anyone, including someone who does not run tik.
<code>tik bundle</code> packs one ticket into a tarball that verifies with coreutils
and <code>ssh-keygen</code> alone.</p>
<h2 id="what-the-law-costs">What the law costs</h2>
<p>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.</p>
<p>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.</p>
<h2 id="the-companion-law">The companion law</h2>
<p><strong>Coordination-free by construction: no leader, no lock, no consensus.</strong>
Every operation is correct on arbitrarily many replicas that share nothing
and reconcile only by eventually unioning their grow-only,
content-addressed event sets.</p>
<p>Reads consult one ticket&rsquo;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 <em>derivation</em>:
two competing claims about a fact reduce to <code>:conflicted</code>, a derived state,
rather than a lock to be won.</p>
<p>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.</p>
<h2 id="what-the-kernel-refuses-to-answer">What the kernel refuses to answer</h2>
<p><strong>The kernel answers &ldquo;what follows from these signed facts?&rdquo; It never
answers &ldquo;what should happen next?&rdquo;</strong> There are no workflow transitions, no
scheduler, no policy engine, and no external queries in the core. A guard
consults one ticket&rsquo;s log and nothing else — never a service, never another
ticket, never a clock of its own.</p>
<p>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.</p>
]]></content><category scheme="https://tik.projects.metio.wtf/tags/law" term="law" label="law"/><category scheme="https://tik.projects.metio.wtf/tags/derivation" term="derivation" label="derivation"/><category scheme="https://tik.projects.metio.wtf/tags/design" term="design" label="design"/></entry><entry><title type="html">Events and facts</title><link href="https://tik.projects.metio.wtf/concepts/events-and-facts/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://tik.projects.metio.wtf/cli/recording-work/?utm_source=atom_feed" rel="related" type="text/html" title="Recording work"/><id>https://tik.projects.metio.wtf/concepts/events-and-facts/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-08-05T22:45:55+02:00</updated><content type="html"><![CDATA[<blockquote>Seven event types, five fact statuses, and the content addressing that makes a store auditable with coreutils.</blockquote><p>A ticket is an append-only set of events. Everything else about it is
computed.</p>
<h2 id="seven-event-types">Seven event types</h2>
<p>The vocabulary is closed and versioned, because the semantics of a
verifiable kernel have to be enumerable:</p>
<table>
	<thead>
			<tr>
					<th>Event</th>
					<th>Meaning</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>:ticket/create</code></td>
					<td>the ticket exists, pinned to a process definition by hash</td>
			</tr>
			<tr>
					<td><code>:fact/assert</code></td>
					<td>a claim about a path: <code>[:severity] = :high</code></td>
			</tr>
			<tr>
					<td><code>:fact/retract</code></td>
					<td>withdraw a claim, with no replacement</td>
			</tr>
			<tr>
					<td><code>:fact/dispute</code></td>
					<td>reject a claim, with a reason</td>
			</tr>
			<tr>
					<td><code>:artifact/attach</code></td>
					<td>a file, stored and addressed by its hash</td>
			</tr>
			<tr>
					<td><code>:attestation/add</code></td>
					<td>a signed claim the kernel does not interpret</td>
			</tr>
			<tr>
					<td><code>:process/migrate</code></td>
					<td>re-pin this ticket to a newer definition</td>
			</tr>
	</tbody>
</table>
<p>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
<code>[:link …]</code> path. Work records are <code>:work</code> attestation claims. Witness
countersignatures are detached sidecars over a head rather than events,
because an event would move the very head it witnesses.</p>
<h2 id="content-addressing">Content addressing</h2>
<p>An event&rsquo;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:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">sha256sum tickets/*/events/*.edn   <span class="c1"># filename must equal the digest</span>
</span></span></code></pre></div><p>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.</p>
<p>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 <code>(at, id)</code>
over the event <em>set</em>, which is what makes the reducer total, commutative,
and idempotent.</p>
<h2 id="fact-status">Fact status</h2>
<p>One function decides why a fact does or does not satisfy a guard, and
guards consult nothing else:</p>
<table>
	<thead>
			<tr>
					<th>Status</th>
					<th>Meaning</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>:present</code></td>
					<td>a value stands</td>
			</tr>
			<tr>
					<td><code>:absent</code></td>
					<td>nothing has been claimed</td>
			</tr>
			<tr>
					<td><code>:retracted</code></td>
					<td>withdrawn, no replacement offered</td>
			</tr>
			<tr>
					<td><code>:disputed</code></td>
					<td>rejected with a reason, and unusable until corrected</td>
			</tr>
			<tr>
					<td><code>:conflicted</code></td>
					<td>causally concurrent claims disagree</td>
			</tr>
	</tbody>
</table>
<h2 id="what-a-dispute-means">What a dispute means</h2>
<p>A dispute rejects <strong>the value that stood when it was raised</strong>, so the
assertion that answers it has to claim something else. Re-asserting the
rejected value verbatim leaves the path <code>:disputed</code> — otherwise the party a
dispute holds accountable could clear it in one command by retyping the
same fact.</p>
<p>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.</p>
<p>Disputes accumulate: several people may reject the same claim, and each
withdraws only their own.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">tik dispute <span class="m">3184</span> category --reason <span class="s2">&#34;this is billing, not technical&#34;</span>
</span></span><span class="line"><span class="cl">tik dispute <span class="m">3184</span> category --withdraw     <span class="c1"># takes back your own objection</span>
</span></span></code></pre></div><h2 id="what-a-conflict-means">What a conflict means</h2>
<p>When two replicas write the same path without having seen each other, and
they disagree, the fact reads <code>:conflicted</code> 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.</p>
<p>Concurrent writes that happen to <em>agree</em> are not a conflict — there is no
disagreement to surface.</p>
]]></content><category scheme="https://tik.projects.metio.wtf/tags/events" term="events" label="events"/><category scheme="https://tik.projects.metio.wtf/tags/facts" term="facts" label="facts"/><category scheme="https://tik.projects.metio.wtf/tags/disputes" term="disputes" label="disputes"/><category scheme="https://tik.projects.metio.wtf/tags/storage" term="storage" label="storage"/></entry><entry><title type="html">Explain</title><link href="https://tik.projects.metio.wtf/concepts/explain/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://tik.projects.metio.wtf/cli/reading-a-store/?utm_source=atom_feed" rel="related" type="text/html" title="Reading a store"/><id>https://tik.projects.metio.wtf/concepts/explain/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-08-05T23:28:02+02:00</updated><content type="html"><![CDATA[<blockquote>The product surface — what evidence is missing, who may supply it, and when waiting is pointless.</blockquote><p>A traditional tracker exposes state. tik exposes <strong>justification</strong>: what is
true, why it is true, and what evidence is missing next. <code>tik explain</code> is
where that surfaces, and every other view is a rendering of it.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> tik explain <span class="m">3184</span>
</span></span><span class="line"><span class="cl"><span class="go">To reach :resolved:
</span></span></span><span class="line"><span class="cl"><span class="go">  ✓ [:fact [:resolution :ref]]
</span></span></span><span class="line"><span class="cl"><span class="go">  ✗ attach an artifact whose path starts with &#34;repro/&#34;
</span></span></span><span class="line"><span class="cl"><span class="go">  blocks: :closed
</span></span></span><span class="line"><span class="cl"><span class="go">  (see: kb/runbooks/support-request-resolved.md)
</span></span></span></code></pre></div><p>Nothing in that block is speculation. The checkmarks are guards that
already hold, the crosses are structured reasons produced by guard
evaluation, <code>blocks</code> is the downstream closure, and the hint is the
<a href="/runbooks/">runbook</a>
 the definition declares for that stage.</p>
<h2 id="reasons-are-data">Reasons are data</h2>
<p>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:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">tik explain <span class="m">3184</span> --edn
</span></span></code></pre></div><p>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 <em>are</em> the guards.</p>
<p>Reasons are sorted by <strong>who can act on them right now</strong>: values anyone can
supply first, then corrections, artifacts, specific people, attestations,
other stages, and finally time, which is nobody&rsquo;s to act on.</p>
<h2 id="who-can-act">Who can act</h2>
<p><code>--actor</code> filters a block to what one person can do, and counts the rest
rather than hiding it:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> tik explain <span class="m">3184</span> --actor alice
</span></span><span class="line"><span class="cl"><span class="go">To reach :resolved:
</span></span></span><span class="line"><span class="cl"><span class="go">  ✗ attach an artifact whose path starts with &#34;repro/&#34;
</span></span></span><span class="line"><span class="cl"><span class="go">  … 1 step(s) waiting on others or time
</span></span></span></code></pre></div><p><code>tik next</code> rotates the same derivation into an inbox — for a person, or for
a whole role:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">tik next --actor alice
</span></span><span class="line"><span class="cl">tik next --role :triager
</span></span></code></pre></div><p>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.</p>
<h2 id="waiting-versus-impossible">Waiting versus impossible</h2>
<p>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.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> tik explain 9c21
</span></span><span class="line"><span class="cl"><span class="go">To reach :approved (unreachable):
</span></span></span><span class="line"><span class="cl"><span class="go">  ⊘ fact [:sign-off] must be asserted by a member of role :auditor
</span></span></span><span class="line"><span class="cl"><span class="go">      (currently by &#34;seb&#34;) — nobody can ever do this
</span></span></span></code></pre></div><p>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
<code>✗</code>.</p>
<h2 id="proving-it">Proving it</h2>
<p><code>tik causal</code> answers the auditor&rsquo;s question — which signed events made each
reached stage true, with negations and time saying so honestly:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">tik causal <span class="m">3184</span>
</span></span></code></pre></div><p>And <code>tik whatif</code> asks the counterfactual without writing anything:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">tik whatif <span class="m">3184</span> <span class="nv">severity</span><span class="o">=</span>:low +P2D retract:category
</span></span></code></pre></div>]]></content><category scheme="https://tik.projects.metio.wtf/tags/explain" term="explain" label="explain"/><category scheme="https://tik.projects.metio.wtf/tags/next" term="next" label="next"/><category scheme="https://tik.projects.metio.wtf/tags/inbox" term="inbox" label="inbox"/><category scheme="https://tik.projects.metio.wtf/tags/reasons" term="reasons" label="reasons"/></entry><entry><title type="html">Install</title><link href="https://tik.projects.metio.wtf/get-started/install/?utm_source=atom_feed" rel="alternate" type="text/html"/><id>https://tik.projects.metio.wtf/get-started/install/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-08-05T22:45:55+02:00</updated><content type="html"><![CDATA[<blockquote>Run the native binary or the uberjar, run from source with babashka, or work inside the nix devshell.</blockquote><p>Each release publishes two downloads, a <code>SHA256SUMS</code> file covering both,
and a cosign keyless signature over those checksums.</p>
<h2 id="download-a-release">Download a release</h2>
<p>From the <a href="https://github.com/metio/tik/releases">releases page</a>
:</p>
<ul>
<li><strong><code>tik-linux-amd64-glibc</code></strong> — the native binary, no runtime needed. It is
labelled for exactly what it is: linux/amd64 against glibc.</li>
<li><strong><code>tik.jar</code></strong> — the uberjar, for macOS, Windows, and arm. Runs on any
JDK 21: <code>java -jar tik.jar --help</code>. The container image is the universal
deployment path.</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">chmod +x tik-linux-amd64-glibc
</span></span><span class="line"><span class="cl">./tik-linux-amd64-glibc --help
</span></span></code></pre></div><p>Verify before trusting. The checksums cover the downloads, and the cosign
bundle covers the checksums:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">sha256sum --check SHA256SUMS
</span></span><span class="line"><span class="cl">cosign verify-blob SHA256SUMS <span class="se">\
</span></span></span><span class="line"><span class="cl">  --bundle SHA256SUMS.bundle <span class="se">\
</span></span></span><span class="line"><span class="cl">  --certificate-identity-regexp <span class="s1">&#39;^https://github.com/metio/tik/&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --certificate-oidc-issuer https://token.actions.githubusercontent.com
</span></span></code></pre></div><h2 id="from-source-with-babashka">From source with babashka</h2>
<p>The CLI runs on <a href="https://babashka.org/">babashka</a>
, so a checkout is enough:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">git clone https://github.com/metio/tik.git
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> tik
</span></span><span class="line"><span class="cl">bb tik --help
</span></span></code></pre></div><p>Every command in this documentation is written as <code>tik</code>; substitute
<code>bb tik</code> when running this way.</p>
<h2 id="the-nix-devshell">The nix devshell</h2>
<p>Contributors get the whole toolchain — JVM, Clojure, babashka, clj-kondo,
TLC, GraalVM, ssh-keygen — from the flake:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">nix develop --command bb <span class="nb">test</span>
</span></span></code></pre></div><p><a href="/contributing/">Contributing</a>
 describes the gate a change is expected to
pass.</p>
<h2 id="signing-your-writes">Signing your writes</h2>
<p>A store works unsigned, and signing turns authorship into evidence anyone
can check offline. Register yourself as an actor and point <code>TIK_KEY</code> at an
ed25519 private key:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">ssh-keygen -t ed25519 -f ~/.config/tik/id_ed25519
</span></span><span class="line"><span class="cl">tik actor add alice ~/.config/tik/id_ed25519.pub
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">TIK_KEY</span><span class="o">=</span>~/.config/tik/id_ed25519
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">TIK_ACTOR</span><span class="o">=</span>alice
</span></span></code></pre></div><p>Signatures are detached sidecars produced by <code>ssh-keygen -Y</code>, so <code>tik verify</code> checks them with stock OpenSSH and nothing else. The public
registry (<code>actors</code>) belongs in version control; the private key stays
outside the store.</p>
<h2 id="where-a-store-lives">Where a store lives</h2>
<p>Commands find a store the way git does: <code>TIK_ROOT</code> wins, otherwise the
nearest ancestor directory holding <code>tickets/</code>, <code>tik.db</code>, or <code>.tik/</code>,
otherwise the current directory. <code>tik init</code> marks one explicitly —
<code>--sqlite</code> for the single-file backend, <code>--hidden</code> to keep everything
inside <code>.tik/</code> when the store sits above several repositories.</p>
]]></content><category scheme="https://tik.projects.metio.wtf/tags/install" term="install" label="install"/><category scheme="https://tik.projects.metio.wtf/tags/binary" term="binary" label="binary"/><category scheme="https://tik.projects.metio.wtf/tags/babashka" term="babashka" label="babashka"/><category scheme="https://tik.projects.metio.wtf/tags/nix" term="nix" label="nix"/></entry><entry><title type="html">Lint, simulate, test</title><link href="https://tik.projects.metio.wtf/authoring/lint-simulate-test/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://tik.projects.metio.wtf/authoring/writing-a-definition/?utm_source=atom_feed" rel="related" type="text/html" title="Writing a definition"/><id>https://tik.projects.metio.wtf/authoring/lint-simulate-test/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-08-05T22:45:55+02:00</updated><content type="html"><![CDATA[<blockquote>Proving a definition does what you meant — before a real ticket depends on it.</blockquote><p>Three checks, in the order you reach for them.</p>
<h2 id="lint">Lint</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">tik lint processes/support-request.edn
</span></span></code></pre></div><p>The linter enforces what the kernel cannot:</p>
<ul>
<li><strong>The closed guard basis</strong> — only operators the declared
<code>:process/guard-vocab</code> admits.</li>
<li><strong>Graph sanity</strong> — every <code>:after</code> names a stage that exists, and no
cycles.</li>
<li><strong>Stratified negation</strong> — <code>[:not [:stage-reached …]]</code> may only name a
stage in a strictly earlier stratum, which is what makes the fixpoint
provably deterministic.</li>
<li><strong>Facts over flags</strong> — a warning where a bare boolean stands in for
information worth recording.</li>
<li><strong>Prefix boundaries</strong> — an <code>:artifact</code> prefix that does not end at a path
boundary matches more than its author expects.</li>
</ul>
<p>With no argument it lints the <em>store</em> instead: open tickets missing
descriptions, titles, or signatures.</p>
<h2 id="simulate">Simulate</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">tik sim processes/support-request.edn
</span></span></code></pre></div><p>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.</p>
<h2 id="test">Test</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">tik <span class="nb">test</span> processes/support-request.tests.edn
</span></span></code></pre></div><p>Scripted cases: evidence in, expected stages out. Deterministic — a fixed
epoch, pure derivation, no store — and a failing case prints <code>explain</code>, so
the process itself says why a stage did not derive.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-clojure" data-lang="clojure"><span class="line"><span class="cl"><span class="p">{</span><span class="ss">:test/process</span> <span class="s">&#34;support-request.edn&#34;</span>
</span></span><span class="line"><span class="cl"> <span class="ss">:test/cases</span>
</span></span><span class="line"><span class="cl"> <span class="p">[{</span><span class="ss">:case/name</span> <span class="s">&#34;facts alone do not triage — the triager role must sign&#34;</span>
</span></span><span class="line"><span class="cl">   <span class="ss">:case/steps</span> <span class="p">[[</span><span class="ss">:actor</span> <span class="s">&#34;rando&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">                <span class="p">[</span><span class="ss">:set</span> <span class="p">[</span><span class="ss">:category</span><span class="p">]</span> <span class="ss">:technical</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">                <span class="p">[</span><span class="ss">:set</span> <span class="p">[</span><span class="ss">:severity</span><span class="p">]</span> <span class="ss">:high</span><span class="p">]]</span>
</span></span><span class="line"><span class="cl">   <span class="ss">:case/expect</span> <span class="p">{</span><span class="ss">:excludes</span> <span class="o">#</span><span class="p">{</span><span class="ss">:triaged</span><span class="p">}}}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="p">{</span><span class="ss">:case/name</span> <span class="s">&#34;uncategorized tickets escalate after 48h&#34;</span>
</span></span><span class="line"><span class="cl">   <span class="ss">:case/steps</span> <span class="p">[[</span><span class="ss">:now</span> <span class="s">&#34;+PT49H&#34;</span><span class="p">]]</span>
</span></span><span class="line"><span class="cl">   <span class="ss">:case/expect</span> <span class="p">{</span><span class="ss">:includes</span> <span class="o">#</span><span class="p">{</span><span class="ss">:escalated</span><span class="p">}}}]}</span>
</span></span></code></pre></div><p><code>[:now &quot;+PT49H&quot;]</code> moves the evaluation clock, so time-gated stages are
testable without waiting and without a mock. <code>[:actor &quot;rando&quot;]</code> switches
who is signing, which is how a <code>:signed-by</code> guard gets tested from both
sides.</p>
<p>Write the negative cases. &ldquo;A triager categorizing reaches <code>:triaged</code>&rdquo; is
the easy half; &ldquo;facts alone do not triage&rdquo; is the half that catches a
<code>:signed-by</code> you dropped.</p>
<h2 id="publish">Publish</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">tik process sign support-request
</span></span></code></pre></div><p>Signing archives the definition by content hash and signs those canonical
bytes. The hash stays the identity; the signature is the authority behind
it.</p>
]]></content><category scheme="https://tik.projects.metio.wtf/tags/authoring" term="authoring" label="authoring"/><category scheme="https://tik.projects.metio.wtf/tags/lint" term="lint" label="lint"/><category scheme="https://tik.projects.metio.wtf/tags/testing" term="testing" label="testing"/><category scheme="https://tik.projects.metio.wtf/tags/simulation" term="simulation" label="simulation"/></entry><entry><title type="html">MCP server</title><link href="https://tik.projects.metio.wtf/integrations/mcp-server/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://tik.projects.metio.wtf/integrations/claude-code/?utm_source=atom_feed" rel="related" type="text/html" title="Claude Code skill"/><id>https://tik.projects.metio.wtf/integrations/mcp-server/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-08-05T22:45:55+02:00</updated><content type="html"><![CDATA[<blockquote>The frontier as an agent&rsquo;s gated tool surface, over stdio.</blockquote><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="nv">TIK_ACTOR</span><span class="o">=</span>bot <span class="nv">TIK_KEY</span><span class="o">=</span>~/.config/tik/bot_ed25519 tik mcp
</span></span></code></pre></div><p><code>tik mcp</code> 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
<em>is</em> the authorization boundary.</p>
<h2 id="why-the-boundary-holds">Why the boundary holds</h2>
<p>An agent&rsquo;s permission to act is derived from the same guards a person&rsquo;s
is. A step whose <code>:signed-by</code> 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.</p>
<p>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.</p>
<h2 id="accountability">Accountability</h2>
<p>Every write the agent makes is an ordinary signed event with the agent&rsquo;s
own actor identity. The log therefore distinguishes what a person claimed
from what an agent claimed, permanently and without a separate audit
trail. <code>tik causal</code> 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.</p>
<h2 id="task-specifications-from-guards">Task specifications from guards</h2>
<p>An agent asking &ldquo;what should I do?&rdquo; gets the same structured answer every
other lens renders:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">tik explain <span class="m">3184</span> --actor bot --edn
</span></span></code></pre></div><p>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.</p>
]]></content><category scheme="https://tik.projects.metio.wtf/tags/mcp" term="mcp" label="mcp"/><category scheme="https://tik.projects.metio.wtf/tags/agents" term="agents" label="agents"/><category scheme="https://tik.projects.metio.wtf/tags/authorization" term="authorization" label="authorization"/></entry><entry><title type="html">Reading a store</title><link href="https://tik.projects.metio.wtf/cli/reading-a-store/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://tik.projects.metio.wtf/concepts/explain/?utm_source=atom_feed" rel="related" type="text/html" title="Explain"/><link href="https://tik.projects.metio.wtf/cli/recording-work/?utm_source=atom_feed" rel="related" type="text/html" title="Recording work"/><link href="https://tik.projects.metio.wtf/cli/administration/?utm_source=atom_feed" rel="related" type="text/html" title="Store administration"/><id>https://tik.projects.metio.wtf/cli/reading-a-store/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-08-05T22:45:55+02:00</updated><content type="html"><![CDATA[<blockquote>The inbox, the board, explain, selectors, history, and counterfactuals.</blockquote><p>Every view here is computed when you ask, so none of them can be stale.</p>
<h2 id="what-should-i-do">What should I do?</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">tik next --actor alice
</span></span><span class="line"><span class="cl">tik next --role :triager
</span></span><span class="line"><span class="cl">tik explain <span class="m">3184</span> --actor alice
</span></span></code></pre></div><p><code>next</code> ranks by how much downstream work each step unlocks, with quiet
tickets rising. <code>explain</code> is the per-ticket answer: what is missing, whose
signature it needs, and which stages it blocks. See
<a href="/concepts/explain/">Explain</a>
 for the model behind them.</p>
<h2 id="the-board">The board</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">tik ls
</span></span><span class="line"><span class="cl">tik ls --all --long
</span></span><span class="line"><span class="cl">tik ls --where <span class="s1">&#39;stage=:blocked and fact:severity=:high and not disputed&#39;</span>
</span></span><span class="line"><span class="cl">tik search firefox login
</span></span></code></pre></div><p>A selector is space-separated terms, all ANDed, each optionally negated:
<code>stage=:blocked</code>, <code>fact:severity</code>, <code>fact:severity=:high</code>, <code>actor=seb</code>,
<code>disputed</code>, <code>conflicted</code>, <code>unsigned</code>, <code>derived-from=&lt;hash&gt;</code>, <code>~text</code>. The
same grammar drives <code>search</code>, and <code>tik dupes</code> reports near-title
lookalikes.</p>
<h2 id="one-ticket">One ticket</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">tik status <span class="m">3184</span>
</span></span><span class="line"><span class="cl">tik status <span class="m">3184</span> --at 2026-03-01T00:00:00Z
</span></span><span class="line"><span class="cl">tik log <span class="m">3184</span>
</span></span><span class="line"><span class="cl">tik diff <span class="m">3184</span> <span class="m">5</span>
</span></span></code></pre></div><p><code>status</code> reports the derived stage, the facts behind it, links, and what is
next. <code>--at</code> answers the same question about any past moment by evaluating
that moment. <code>diff</code> shows the evidence gained over the last few events.</p>
<h2 id="why-and-what-if">Why, and what if</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">tik causal <span class="m">3184</span>
</span></span><span class="line"><span class="cl">tik whatif <span class="m">3184</span> <span class="nv">severity</span><span class="o">=</span>:low +P2D retract:category
</span></span><span class="line"><span class="cl">tik debug <span class="m">3184</span>
</span></span></code></pre></div><p><code>causal</code> names the signed events that made each reached stage true —
including negations and time saying so honestly. <code>whatif</code> shows the stage
diff a change would produce and writes nothing. <code>debug</code> shows the fixpoint
with its working: every sweep, every guard verdict.</p>
<h2 id="the-roadmap-and-the-wider-picture">The roadmap and the wider picture</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">tik plan
</span></span><span class="line"><span class="cl">tik plan roadmap.html
</span></span><span class="line"><span class="cl">tik roles
</span></span><span class="line"><span class="cl">tik work week --actor alice
</span></span></code></pre></div><p><code>plan</code> derives the dependency-link roadmap — ready, blocked, done, cyclic,
the critical path, and each item&rsquo;s unlock impact. <code>roles</code> shows who gates
what: every role on the open board, its effective members, and the stages
waiting on its signature.</p>
<h2 id="sharing-what-you-see">Sharing what you see</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">tik board board.html
</span></span><span class="line"><span class="cl">tik serve --port <span class="m">8080</span>
</span></span><span class="line"><span class="cl">tik bundle <span class="m">3184</span> --out ticket-3184.tgz
</span></span></code></pre></div><p><code>board</code> renders the whole store into one dependency-free HTML file you can
mail or archive. <code>serve</code> publishes it live, read-only, with <code>/tickets.edn</code>
and <code>/explain/&lt;id&gt;.edn</code> for tools. <code>bundle</code> packs one ticket — events,
signatures, witness marks, the pinned ruleset, and a <code>verify.sh</code> — into a
tarball a third party checks with coreutils and <code>ssh-keygen</code>, no tik
required.</p>
]]></content><category scheme="https://tik.projects.metio.wtf/tags/cli" term="cli" label="cli"/><category scheme="https://tik.projects.metio.wtf/tags/explain" term="explain" label="explain"/><category scheme="https://tik.projects.metio.wtf/tags/next" term="next" label="next"/><category scheme="https://tik.projects.metio.wtf/tags/board" term="board" label="board"/><category scheme="https://tik.projects.metio.wtf/tags/selectors" term="selectors" label="selectors"/></entry><entry><title type="html">Recording work</title><link href="https://tik.projects.metio.wtf/cli/recording-work/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://tik.projects.metio.wtf/concepts/events-and-facts/?utm_source=atom_feed" rel="related" type="text/html" title="Events and facts"/><link href="https://tik.projects.metio.wtf/cli/reading-a-store/?utm_source=atom_feed" rel="related" type="text/html" title="Reading a store"/><link href="https://tik.projects.metio.wtf/cli/administration/?utm_source=atom_feed" rel="related" type="text/html" title="Store administration"/><id>https://tik.projects.metio.wtf/cli/recording-work/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-08-05T22:45:55+02:00</updated><content type="html"><![CDATA[<blockquote>Creating tickets, asserting facts, attaching evidence, and correcting the record.</blockquote><p>You record evidence. Stages follow.</p>
<h2 id="create-a-ticket">Create a ticket</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">tik new support-request --title <span class="s2">&#34;login fails on Firefox&#34;</span>
</span></span></code></pre></div><p>The ticket pins the process definition&rsquo;s content hash at creation, so it is
judged by the rules it was minted under until somebody deliberately moves
it with <code>tik reprocess</code>.</p>
<p>Created beneath a store, a ticket inherits its context as signed facts:
<code>repo=&lt;name&gt;</code> from the enclosing git repository, plus any <code>.tik-facts.edn</code>
maps on the way down, nearest winning, with anything explicit beating both.</p>
<h2 id="assert-facts">Assert facts</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">tik <span class="nb">set</span> <span class="m">3184</span> <span class="nv">severity</span><span class="o">=</span>:high resolution.ref<span class="o">=</span>abc123def456
</span></span></code></pre></div><p>Dotted keys nest, so <code>parked.reason=&quot;waiting on legal&quot;</code> writes the path
<code>[:parked :reason]</code>. Values parse as EDN, and a bare word becomes a
keyword — <code>severity=high</code> and <code>severity=:high</code> mean the same thing, which
keeps facts out of the swamp of free-form strings.</p>
<p>Links are facts too:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">tik <span class="nb">set</span> <span class="m">3184</span> link.depends-on<span class="o">=</span>9c21f0a4
</span></span></code></pre></div><p><code>tik next</code> then holds 3184 back while 9c21 is unsettled, and <code>tik status</code>
names the blocker.</p>
<h2 id="attach-evidence">Attach evidence</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">tik attach <span class="m">3184</span> ./crash-repro.sh
</span></span><span class="line"><span class="cl">tik comment <span class="m">3184</span> reproduced on a clean profile, video attached
</span></span></code></pre></div><p>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.</p>
<h2 id="correct-the-record">Correct the record</h2>
<p>Nothing is edited or deleted. Corrections are new events:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">tik <span class="nb">set</span> <span class="m">3184</span> <span class="nv">severity</span><span class="o">=</span>:critical              <span class="c1"># supersedes; history retained</span>
</span></span><span class="line"><span class="cl">tik retract <span class="m">3184</span> resolution.ref --reason <span class="s2">&#34;wrong commit&#34;</span>
</span></span><span class="line"><span class="cl">tik dispute <span class="m">3184</span> category --reason <span class="s2">&#34;this is billing&#34;</span>
</span></span><span class="line"><span class="cl">tik dispute <span class="m">3184</span> category --withdraw         <span class="c1"># take back your own objection</span>
</span></span></code></pre></div><p>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 <em>different</em> value answers it. In
every case the stage regresses by derivation — there is no rollback step.</p>
<h2 id="signed-claims-and-attestations">Signed claims and attestations</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">tik attest <span class="m">3184</span> <span class="o">{</span>:ci :green<span class="o">}</span>
</span></span></code></pre></div><p>An attestation is a signed claim whose meaning the kernel does not
interpret. Lenses read it, and the <code>:attested-within</code> guard checks that a
fresh-enough one exists — a replayed &ldquo;CI green&rdquo; from last month is
cryptographically valid and fails that guard honestly.</p>
<h2 id="derive-facts-from-the-world">Derive facts from the world</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">tik probe <span class="m">3184</span>
</span></span></code></pre></div><p>A probe is any executable that prints <code>key=value</code> lines. It runs with its
working directory in the ticket&rsquo;s <code>[:repo]</code> repository, and changed values
land as ordinary signed facts, so a ticket regresses on its own when
reality does. The environment carries <code>TIK_TICKET</code>, <code>TIK_REPO</code>, and every
present fact as <code>TIK_FACT_&lt;PATH&gt;</code>, which is what lets one repository hold
many subjects — a package, tenant, or workload per ticket.</p>
<h2 id="recurring-and-bulk-work">Recurring and bulk work</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">tik recur weekly-review --period 2026-W32
</span></span><span class="line"><span class="cl">tik rollout dependency-audit --parent-title <span class="s2">&#34;Q3 audit&#34;</span>
</span></span></code></pre></div><p><code>recur</code> mints this period&rsquo;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. <code>rollout</code> 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&rsquo;s evidence.</p>
]]></content><category scheme="https://tik.projects.metio.wtf/tags/cli" term="cli" label="cli"/><category scheme="https://tik.projects.metio.wtf/tags/facts" term="facts" label="facts"/><category scheme="https://tik.projects.metio.wtf/tags/artifacts" term="artifacts" label="artifacts"/><category scheme="https://tik.projects.metio.wtf/tags/corrections" term="corrections" label="corrections"/></entry><entry><title type="html">Replication</title><link href="https://tik.projects.metio.wtf/concepts/replication/?utm_source=atom_feed" rel="alternate" type="text/html"/><id>https://tik.projects.metio.wtf/concepts/replication/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-08-05T22:45:55+02:00</updated><content type="html"><![CDATA[<blockquote>Merging by set union — no leader, no lock, no consensus, and no file-level conflicts to resolve by hand.</blockquote><p>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.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">git pull <span class="o">&amp;&amp;</span> git push     <span class="c1"># this is the replication protocol</span>
</span></span></code></pre></div><h2 id="why-the-merge-is-trivial">Why the merge is trivial</h2>
<p>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 <em>identical</em> claim produce byte-identical files with
the same name, and the union keeps one — deduplication for free.</p>
<p>That is why merging never requires a human to resolve a text conflict
inside a ticket. What it can produce is a <em>derived</em> disagreement, and that
is the point.</p>
<h2 id="disagreement-resolves-by-derivation">Disagreement resolves by derivation</h2>
<p>When two replicas wrote the same fact path without having seen each other,
and their claims differ, the fact reads <code>:conflicted</code>. Every guard reading
it fails with a reason naming the competing claims, and <code>tik explain</code> asks
for a value that supersedes them:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> tik explain <span class="m">3184</span>
</span></span><span class="line"><span class="cl"><span class="go">To reach :triaged:
</span></span></span><span class="line"><span class="cl"><span class="go">  ✗ fact [:category] has conflicting concurrent assertions — one must
</span></span></span><span class="line"><span class="cl"><span class="go">    supersede (ADR 0003)
</span></span></span></code></pre></div><p>Nobody wins an election. A later write that observed both competitors
settles it, and the conflict disappears from the derivation. Parents are
what make &ldquo;observed&rdquo; a checkable claim rather than a guess.</p>
<h2 id="no-leader-no-lock-no-consensus">No leader, no lock, no consensus</h2>
<p>No operation needs a leader, a distributed lock, or a quorum to be correct:</p>
<ul>
<li><strong>Reads shard without limit.</strong> A derivation reads one ticket&rsquo;s own log
and nothing else. Guards never query across tickets, so N stateless
replicas across M shards scale reads with zero coordination.</li>
<li><strong>Writes never resolve by lock.</strong> A write is either a content-addressed
event that is a pure function of its intent, or an append whose
contention resolves by derivation.</li>
<li><strong>Self-minted events are deterministic.</strong> Anything a replica mints on its
own — a recurring ticket, a scheduled probe — derives its id and every
byte, <code>:at</code> included, from its inputs. Two replicas firing the same
schedule concurrently mint the same event, and the union keeps one.</li>
</ul>
<p>A tik replica is stateless by construction, so horizontal scaling is the
preferred deployment.</p>
<h2 id="partial-logs-say-so">Partial logs say so</h2>
<p>Between syncs, a replica&rsquo;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 <code>:conflicted</code>, and explain asks somebody to resolve a conflict
that does not exist.</p>
<p>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.</p>
<h2 id="backends">Backends</h2>
<p>The file store is the signed interchange format: a <code>tickets/</code> tree that
<code>sha256sum</code> audits and git replicates. The SQLite backend keeps the same
events in a single file when that suits operations better. Convert in place
with <code>tik store migrate --to sqlite|file</code> — events and their detached
signatures both travel.</p>
]]></content><category scheme="https://tik.projects.metio.wtf/tags/replication" term="replication" label="replication"/><category scheme="https://tik.projects.metio.wtf/tags/git" term="git" label="git"/><category scheme="https://tik.projects.metio.wtf/tags/crdt" term="crdt" label="crdt"/><category scheme="https://tik.projects.metio.wtf/tags/conflicts" term="conflicts" label="conflicts"/><category scheme="https://tik.projects.metio.wtf/tags/scaling" term="scaling" label="scaling"/></entry><entry><title type="html">Roles and authority</title><link href="https://tik.projects.metio.wtf/authoring/roles-and-authority/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://tik.projects.metio.wtf/cli/administration/?utm_source=atom_feed" rel="related" type="text/html" title="Store administration"/><id>https://tik.projects.metio.wtf/authoring/roles-and-authority/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-08-05T22:45:55+02:00</updated><content type="html"><![CDATA[<blockquote>Who may sign what, and keeping membership current without re-pinning every ticket.</blockquote><p>A role is a name a definition uses in its guards. Membership is store
state that decides who is in that role today.</p>
<h2 id="declaring-a-role">Declaring a role</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-clojure" data-lang="clojure"><span class="line"><span class="cl"><span class="ss">:process/roles</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span><span class="ss">:triager</span> <span class="p">{</span><span class="ss">:members</span> <span class="p">[</span><span class="s">&#34;seb&#34;</span><span class="p">]}</span>
</span></span><span class="line"><span class="cl"> <span class="ss">:approver</span> <span class="p">{</span><span class="ss">:members</span> <span class="p">[</span><span class="s">&#34;alice&#34;</span> <span class="s">&#34;bob&#34;</span><span class="p">]}}</span>
</span></span></code></pre></div><p>Guards refer to the name:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-clojure" data-lang="clojure"><span class="line"><span class="cl"><span class="ss">:guards</span> <span class="p">[[</span><span class="ss">:signed-by</span> <span class="ss">:triager</span> <span class="p">[</span><span class="ss">:category</span><span class="p">]]]</span>
</span></span></code></pre></div><p>That guard holds when the fact at <code>[:category]</code> was asserted by a member of
<code>:triager</code>. It is a statement about evidence — who put this claim on the
record — rather than a permission check performed somewhere else.</p>
<h2 id="membership-lives-in-the-register">Membership lives in the register</h2>
<p>A definition declares which roles exist and who starts in them. The store&rsquo;s
register decides who is in one now:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">tik roles                          <span class="c1"># who gates what, with effective members</span>
</span></span><span class="line"><span class="cl">tik roles add approver carol
</span></span><span class="line"><span class="cl">tik roles remove approver bob
</span></span></code></pre></div><p>The register overrides a definition role by role, and takes effect on
in-flight tickets immediately — no version bump, no <code>reprocess</code>. 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.</p>
<p>Overriding is whole-role: the register&rsquo;s entry replaces the definition&rsquo;s
members for that role rather than merging with them, because a departure
has to be expressible. The first <code>tik roles add</code> on a role says so.</p>
<p>A store with no register derives exactly as before, so a definition&rsquo;s
declared members are a working default rather than something to restate.</p>
<p>Resolution takes no <code>now</code>. Time-aware validity — &ldquo;was this actor a member
last March&rdquo; — is a separate concern that wants signed bindings rather than
a mutable file, so a re-derivation at a past instant reads today&rsquo;s
membership.</p>
<h2 id="separation-of-duties">Separation of duties</h2>
<p>Two guards express the common controls without a policy engine.</p>
<p><strong>Four eyes.</strong> <code>:different-person</code> holds when two facts are present and
were asserted by distinct actors:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-clojure" data-lang="clojure"><span class="line"><span class="cl"><span class="p">[</span><span class="ss">:different-person</span> <span class="p">[</span><span class="ss">:proposal</span><span class="p">]</span> <span class="p">[</span><span class="ss">:approval</span><span class="p">]]</span>
</span></span></code></pre></div><p>Nobody approves their own proposal, and <code>explain</code> 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.</p>
<p><strong>Fresh evidence.</strong> <code>:attested-within</code> holds when an attestation of a claim
exists and is recent enough:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-clojure" data-lang="clojure"><span class="line"><span class="cl"><span class="p">[</span><span class="ss">:attested-within</span> <span class="p">{</span><span class="ss">:claim</span> <span class="ss">:ci-green</span><span class="p">}</span> <span class="s">&#34;P7D&#34;</span><span class="p">]</span>
</span></span></code></pre></div><p>A replayed attestation from last month is cryptographically valid and
fails this guard honestly, which is exactly the distinction a stale-evidence
control needs.</p>
<h2 id="an-empty-role-is-a-dead-end">An empty role is a dead end</h2>
<p>A role with no members can never sign, so every stage behind it is
unreachable. <code>explain</code> says that rather than leaving somebody waiting:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="go">To reach :approved (unreachable):
</span></span></span><span class="line"><span class="cl"><span class="go">  ⊘ fact [:sign-off] must be asserted by a member of role :auditor
</span></span></span><span class="line"><span class="cl"><span class="go">      — nobody can ever do this
</span></span></span></code></pre></div><p>The fix is <code>tik roles add</code>, or a definition that does not demand a
signature nobody can give.</p>
<h2 id="signing">Signing</h2>
<p>Authority only means something once writes are signed. Register a signer
once, then let every write carry authorship:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">tik actor add alice ~/.config/tik/id_ed25519.pub
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">TIK_KEY</span><span class="o">=</span>~/.config/tik/id_ed25519
</span></span></code></pre></div><p>Signatures are detached sidecars from <code>ssh-keygen -Y</code>, checked by
<code>tik verify</code> with stock OpenSSH. A signature is an authorship claim, which
is why <code>tik sign</code> refuses to sign somebody else&rsquo;s events.</p>
]]></content><category scheme="https://tik.projects.metio.wtf/tags/roles" term="roles" label="roles"/><category scheme="https://tik.projects.metio.wtf/tags/authority" term="authority" label="authority"/><category scheme="https://tik.projects.metio.wtf/tags/signatures" term="signatures" label="signatures"/><category scheme="https://tik.projects.metio.wtf/tags/separation-of-duties" term="separation-of-duties" label="separation-of-duties"/></entry><entry><title type="html">Stages</title><link href="https://tik.projects.metio.wtf/concepts/stages/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://tik.projects.metio.wtf/concepts/derived-beats-declared/?utm_source=atom_feed" rel="related" type="text/html" title="Derived beats declared"/><id>https://tik.projects.metio.wtf/concepts/stages/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-08-05T22:45:55+02:00</updated><content type="html"><![CDATA[<blockquote>Position in a process as a fixpoint over the reached set — synchronous sweeps, sticky milestones, and regression by derivation.</blockquote><p>A process defines stages. Each stage names its prerequisites (<code>:after</code>) and
the conditions that must hold to reach it (<code>:guards</code>). Reaching a stage is
never an action somebody takes; it is a conclusion.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-clojure" data-lang="clojure"><span class="line"><span class="cl"><span class="p">{</span><span class="ss">:stage/id</span> <span class="ss">:resolved</span>
</span></span><span class="line"><span class="cl"> <span class="ss">:after</span> <span class="p">[</span><span class="ss">:triaged</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"> <span class="ss">:guards</span> <span class="p">[[</span><span class="ss">:fact</span> <span class="p">[</span><span class="ss">:resolution</span> <span class="ss">:ref</span><span class="p">]]</span>
</span></span><span class="line"><span class="cl">          <span class="p">[</span><span class="ss">:or</span> <span class="p">[</span><span class="ss">:not</span> <span class="p">[</span><span class="ss">:fact=</span> <span class="p">[</span><span class="ss">:category</span><span class="p">]</span> <span class="ss">:technical</span><span class="p">]]</span>
</span></span><span class="line"><span class="cl">           <span class="p">[</span><span class="ss">:stage-reached</span> <span class="ss">:reproducible</span><span class="p">]]]}</span>
</span></span></code></pre></div><p>That second guard reads &ldquo;technical implies reproduced&rdquo; — material
implication written out, because the vocabulary has <code>:or</code> and <code>:not</code> and
needs no separate conditional.</p>
<h2 id="the-reached-set-is-a-fixpoint">The reached set is a fixpoint</h2>
<p>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.</p>
<p>Each iteration is a <strong>synchronous sweep</strong>: 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.</p>
<p>Because <code>[:not [:stage-reached …]]</code> is negation inside a fixpoint, the
linter enforces <strong>stratified negation</strong>: a definition may only negate
stages in a strictly earlier stratum. That is what makes determinism
provable rather than incidental.</p>
<h2 id="regression-is-by-derivation">Regression is by derivation</h2>
<p>Nothing rolls a ticket back. Withdraw the evidence and the conclusion stops
following:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> tik retract <span class="m">3184</span> resolution.ref --reason <span class="s2">&#34;wrong commit&#34;</span>
</span></span><span class="line"><span class="cl"><span class="gp">$</span> tik status <span class="m">3184</span>
</span></span><span class="line"><span class="cl"><span class="go">stage:   triaged (reached: received, triaged)
</span></span></span></code></pre></div><p>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.</p>
<h2 id="sticky-milestones">Sticky milestones</h2>
<p>Some stages are milestones: reaching them once is a historical fact that
later evidence does not undo. <code>:stage/sticky? true</code> says so, and the fold
carries such a stage forward once any prefix of the log reached it.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-clojure" data-lang="clojure"><span class="line"><span class="cl"><span class="p">{</span><span class="ss">:stage/id</span> <span class="ss">:closed</span> <span class="ss">:after</span> <span class="p">[</span><span class="ss">:resolved</span><span class="p">]</span> <span class="ss">:stage/sticky?</span> <span class="nv">true</span>
</span></span><span class="line"><span class="cl"> <span class="ss">:guards</span> <span class="p">[[</span><span class="ss">:fact</span> <span class="p">[</span><span class="ss">:customer</span> <span class="ss">:ack</span><span class="p">]]]}</span>
</span></span></code></pre></div><p>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.</p>
<p>Sticky is monotone <strong>in fold position</strong>, 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.</p>
<h2 id="time">Time</h2>
<p>Time enters derivation as an explicit <code>now</code> argument. The kernel reads no
clock of its own, which is what makes a re-derivation years from now
reproducible.</p>
<p>Guards read the claimed clock — the <code>:at</code> an actor asserted — by default,
and each fold step evaluates at its event&rsquo;s <code>:at</code> <strong>clamped to the read&rsquo;s
<code>now</code></strong>. No step acts as though more time has passed than actually has, so
a postdated event cannot buy the 48 hours an <code>:elapsed-since</code> guard is
waiting for. The clamp lifts by itself once that time really passes.</p>
<h2 id="several-stages-at-once">Several stages at once</h2>
<p>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. <code>tik status</code>
reports the reached set and the current tips; <code>tik debug</code> shows every
sweep and every guard verdict that produced them.</p>
]]></content><category scheme="https://tik.projects.metio.wtf/tags/stages" term="stages" label="stages"/><category scheme="https://tik.projects.metio.wtf/tags/fixpoint" term="fixpoint" label="fixpoint"/><category scheme="https://tik.projects.metio.wtf/tags/sticky" term="sticky" label="sticky"/><category scheme="https://tik.projects.metio.wtf/tags/derivation" term="derivation" label="derivation"/></entry><entry><title type="html">Store administration</title><link href="https://tik.projects.metio.wtf/cli/administration/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://tik.projects.metio.wtf/cli/reading-a-store/?utm_source=atom_feed" rel="related" type="text/html" title="Reading a store"/><link href="https://tik.projects.metio.wtf/cli/recording-work/?utm_source=atom_feed" rel="related" type="text/html" title="Recording work"/><link href="https://tik.projects.metio.wtf/authoring/roles-and-authority/?utm_source=atom_feed" rel="related" type="text/html" title="Roles and authority"/><id>https://tik.projects.metio.wtf/cli/administration/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-08-05T22:45:55+02:00</updated><content type="html"><![CDATA[<blockquote>Identity, roles, verification, migration, storage backends, and alerts.</blockquote><h2 id="identity">Identity</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">tik actor add alice ~/.config/tik/id_ed25519.pub
</span></span><span class="line"><span class="cl">tik sign <span class="m">3184</span>
</span></span><span class="line"><span class="cl">tik witness <span class="m">3184</span>
</span></span></code></pre></div><p><code>actor add</code> registers a signer in the store&rsquo;s allowed-signers registry.
Exporting <code>TIK_KEY</code> signs every write as it happens; <code>sign</code> catches up on
your own earlier events, and only your own — signing somebody else&rsquo;s would
assert something false. <code>witness</code> countersigns a ticket&rsquo;s head, and one
signature timestamps the entire ancestry beneath it.</p>
<h2 id="roles">Roles</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">tik roles
</span></span><span class="line"><span class="cl">tik roles add triager alice
</span></span><span class="line"><span class="cl">tik roles remove triager bob
</span></span></code></pre></div><p>A process definition declares which roles exist and who starts in them; the
store&rsquo;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 <a href="/authoring/roles-and-authority/">Roles and authority</a>
.</p>
<h2 id="verification">Verification</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">tik verify
</span></span><span class="line"><span class="cl">tik verify --changed
</span></span><span class="line"><span class="cl">tik root --witness
</span></span></code></pre></div><p><code>verify</code> runs the ladder: content addressing, schema, signatures, and
re-derivation. <code>--changed</code> skips unchanged heads for a fast drift check
rather than the full audit. <code>root</code> prints one hash committing to the entire
store, optionally countersigned, optionally anchored to a third-party
timestamp.</p>
<h2 id="moving-tickets-to-newer-rules">Moving tickets to newer rules</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">tik reprocess <span class="m">3184</span> processes/support-request.edn
</span></span><span class="line"><span class="cl">tik reprocess <span class="m">3184</span> processes/support-request.edn --apply
</span></span></code></pre></div><p>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.
<code>--apply</code> records the re-pin as a signed event, so the log keeps which
rules judged the ticket when.</p>
<h2 id="storage">Storage</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">tik init --sqlite
</span></span><span class="line"><span class="cl">tik store migrate --to file
</span></span><span class="line"><span class="cl">tik <span class="nb">export</span> ./audit-copy
</span></span><span class="line"><span class="cl">tik pack
</span></span><span class="line"><span class="cl">tik gc --apply
</span></span></code></pre></div><p>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. <code>export</code> materializes any store
as the file format. <code>pack</code> consolidates settled tickets into one
content-addressed pack each, and <code>gc</code> removes archived definitions no
ticket pins any more.</p>
<h2 id="alerts-out-mail-in">Alerts out, mail in</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">tik effects run --config effects.edn --dry-run
</span></span><span class="line"><span class="cl">tik bridge email &lt; message.eml
</span></span></code></pre></div><p><code>effects run</code> 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.</p>
<p>The email bridge turns messages into tickets and comments, and replies
carrying <code>tik&gt; key=value</code> lines become facts, which closes the loop for
people who never leave their inbox.</p>
<h2 id="the-gated-agent-surface">The gated agent surface</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">tik agent actions <span class="m">3184</span> --actor bot
</span></span><span class="line"><span class="cl">tik agent <span class="nb">set</span> <span class="m">3184</span> <span class="nv">severity</span><span class="o">=</span>:high --actor bot
</span></span></code></pre></div><p>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.</p>
]]></content><category scheme="https://tik.projects.metio.wtf/tags/cli" term="cli" label="cli"/><category scheme="https://tik.projects.metio.wtf/tags/verify" term="verify" label="verify"/><category scheme="https://tik.projects.metio.wtf/tags/roles" term="roles" label="roles"/><category scheme="https://tik.projects.metio.wtf/tags/migration" term="migration" label="migration"/><category scheme="https://tik.projects.metio.wtf/tags/effects" term="effects" label="effects"/></entry><entry><title type="html">Writing a definition</title><link href="https://tik.projects.metio.wtf/authoring/writing-a-definition/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://tik.projects.metio.wtf/authoring/lint-simulate-test/?utm_source=atom_feed" rel="related" type="text/html" title="Lint, simulate, test"/><id>https://tik.projects.metio.wtf/authoring/writing-a-definition/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-08-05T22:45:55+02:00</updated><content type="html"><![CDATA[<blockquote>The shape of a process definition, the closed guard vocabulary, and the design law that separates a process from a task list.</blockquote><h2 id="the-design-law">The design law</h2>
<p><strong>A stage is defined by what must be TRUE to reach it, never by who moved
what.</strong> 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 &ldquo;somebody said so&rdquo;, then make <em>that</em> the fact — a named person&rsquo;s
signature on a named claim — rather than a status somebody sets.</p>
<p>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.</p>
<h2 id="the-shape">The shape</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-clojure" data-lang="clojure"><span class="line"><span class="cl"><span class="p">{</span><span class="ss">:process/id</span> <span class="ss">:support-request</span>
</span></span><span class="line"><span class="cl"> <span class="ss">:process/version</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl"> <span class="ss">:process/guard-vocab</span> <span class="mi">2</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="ss">:process/roles</span>
</span></span><span class="line"><span class="cl"> <span class="p">{</span><span class="ss">:triager</span> <span class="p">{</span><span class="ss">:members</span> <span class="p">[</span><span class="s">&#34;seb&#34;</span><span class="p">]}</span>
</span></span><span class="line"><span class="cl">  <span class="ss">:billing</span> <span class="p">{</span><span class="ss">:members</span> <span class="p">[</span><span class="s">&#34;billing&#34;</span><span class="p">]}}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="ss">:process/facts</span>
</span></span><span class="line"><span class="cl"> <span class="p">{[</span><span class="ss">:category</span><span class="p">]</span>        <span class="p">[</span><span class="ss">:enum</span> <span class="ss">:billing</span> <span class="ss">:technical</span> <span class="ss">:account</span> <span class="ss">:abuse</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">  <span class="p">[</span><span class="ss">:severity</span><span class="p">]</span>        <span class="p">[</span><span class="ss">:enum</span> <span class="ss">:low</span> <span class="ss">:normal</span> <span class="ss">:high</span> <span class="ss">:critical</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">  <span class="p">[</span><span class="ss">:resolution</span> <span class="ss">:ref</span><span class="p">]</span> <span class="p">[</span><span class="ss">:string</span> <span class="p">{</span><span class="ss">:min</span> <span class="mi">8</span><span class="p">}]}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="ss">:process/stages</span>
</span></span><span class="line"><span class="cl"> <span class="p">[{</span><span class="ss">:stage/id</span> <span class="ss">:received</span>
</span></span><span class="line"><span class="cl">   <span class="ss">:hint</span> <span class="s">&#34;kb/runbooks/support-request-received.md&#34;</span>
</span></span><span class="line"><span class="cl">   <span class="ss">:guards</span> <span class="p">[]}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="p">{</span><span class="ss">:stage/id</span> <span class="ss">:triaged</span>
</span></span><span class="line"><span class="cl">   <span class="ss">:after</span> <span class="p">[</span><span class="ss">:received</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">   <span class="ss">:hint</span> <span class="s">&#34;kb/runbooks/support-request-triaged.md&#34;</span>
</span></span><span class="line"><span class="cl">   <span class="ss">:guards</span> <span class="p">[[</span><span class="ss">:fact</span> <span class="p">[</span><span class="ss">:category</span><span class="p">]]</span>
</span></span><span class="line"><span class="cl">            <span class="p">[</span><span class="ss">:fact</span> <span class="p">[</span><span class="ss">:severity</span><span class="p">]]</span>
</span></span><span class="line"><span class="cl">            <span class="p">[</span><span class="ss">:signed-by</span> <span class="ss">:triager</span> <span class="p">[</span><span class="ss">:category</span><span class="p">]]]}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="p">{</span><span class="ss">:stage/id</span> <span class="ss">:closed</span>
</span></span><span class="line"><span class="cl">   <span class="ss">:after</span> <span class="p">[</span><span class="ss">:resolved</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">   <span class="ss">:stage/sticky?</span> <span class="nv">true</span>
</span></span><span class="line"><span class="cl">   <span class="ss">:guards</span> <span class="p">[[</span><span class="ss">:fact</span> <span class="p">[</span><span class="ss">:customer</span> <span class="ss">:ack</span><span class="p">]]]}]}</span>
</span></span></code></pre></div><p><code>:process/facts</code> declares the schema for each path, which is what lets
<code>explain</code> 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.</p>
<p><code>:hint</code> names a runbook for the stage. Those files are published here under
<a href="/runbooks/">Runbooks</a>
, so the hint a person sees in <code>explain</code> resolves to
a page.</p>
<h2 id="the-guard-vocabulary">The guard vocabulary</h2>
<p>Twelve operators, closed and versioned. New keywords need a version bump,
because the semantics of a verifiable kernel have to be enumerable:</p>
<table>
	<thead>
			<tr>
					<th>Operator</th>
					<th>Holds when</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>[:fact path]</code></td>
					<td>a value stands at <code>path</code></td>
			</tr>
			<tr>
					<td><code>[:fact= path v]</code></td>
					<td>that value equals <code>v</code></td>
			</tr>
			<tr>
					<td><code>[:artifact &quot;prefix&quot;]</code></td>
					<td>an attached artifact&rsquo;s path starts with <code>prefix</code></td>
			</tr>
			<tr>
					<td><code>[:signed-by :role path]</code></td>
					<td>the fact at <code>path</code> was asserted by a member of <code>:role</code></td>
			</tr>
			<tr>
					<td><code>[:stage-reached :id]</code></td>
					<td>that stage is in the reached set</td>
			</tr>
			<tr>
					<td><code>[:elapsed-since :ticket/create &quot;PT48H&quot;]</code></td>
					<td>that much time has passed</td>
			</tr>
			<tr>
					<td><code>[:attested-within claim &quot;P7D&quot;]</code></td>
					<td>a fresh-enough attestation of <code>claim</code> exists</td>
			</tr>
			<tr>
					<td><code>[:different-person path-a path-b]</code></td>
					<td>two facts came from distinct actors</td>
			</tr>
			<tr>
					<td><code>[:malli schema]</code></td>
					<td>the fact map satisfies a schema</td>
			</tr>
			<tr>
					<td><code>[:and …]</code> <code>[:or …]</code> <code>[:not …]</code></td>
					<td>the connectives</td>
			</tr>
	</tbody>
</table>
<p><code>:different-person</code> is the four-eyes principle as a derivable condition.
<code>:attested-within</code> closes the stale-evidence gap: a replayed &ldquo;CI green&rdquo;
from last month is cryptographically valid and fails the guard honestly.</p>
<p>There is no conditional operator, because material implication already
spells one — &ldquo;technical implies reproduced&rdquo; is:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-clojure" data-lang="clojure"><span class="line"><span class="cl"><span class="p">[</span><span class="ss">:or</span> <span class="p">[</span><span class="ss">:not</span> <span class="p">[</span><span class="ss">:fact=</span> <span class="p">[</span><span class="ss">:category</span><span class="p">]</span> <span class="ss">:technical</span><span class="p">]]</span>
</span></span><span class="line"><span class="cl"> <span class="p">[</span><span class="ss">:stage-reached</span> <span class="ss">:reproducible</span><span class="p">]]</span>
</span></span></code></pre></div><h2 id="guards-never-query">Guards never query</h2>
<p>A guard reads one ticket&rsquo;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.</p>
<h2 id="facts-over-flags">Facts over flags</h2>
<p>Prefer a fact that carries information to a boolean that carries a
decision. <code>[:customer :ack] = true</code> 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.</p>
<h2 id="versions-and-pinning">Versions and pinning</h2>
<p>A ticket pins the definition&rsquo;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 <code>tik reprocess</code>. The version number is a human label; the hash is the
identity.</p>
]]></content><category scheme="https://tik.projects.metio.wtf/tags/authoring" term="authoring" label="authoring"/><category scheme="https://tik.projects.metio.wtf/tags/processes" term="processes" label="processes"/><category scheme="https://tik.projects.metio.wtf/tags/guards" term="guards" label="guards"/><category scheme="https://tik.projects.metio.wtf/tags/edn" term="edn" label="edn"/></entry></feed>