Luigit
repositories / will

will

owned by admin

spec.html

Raw
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>will · specification</title>
<style>
/* ============================================================
   will spec · single consolidated stylesheet
   ============================================================ */

/* --- tokens ------------------------------------------------ */
:root {
  --ink: #1b1b1d;
  --ink-soft: #3d3d42;
  --muted: #6b6b72;
  --line: #d9d9d4;
  --line-soft: #e9e9e4;
  --accent: #0a5a48;
  --accent-soft: #e6f0ec;
  --warn: #8a3d00;
  --warn-soft: #f7ecdf;
  --code-bg: #f2f1ec;
  --bg: #fcfcfa;
  --measure: 66ch;
  --page: 64rem;
  --rhythm: 1.55;
}

/* --- base -------------------------------------------------- */
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
  font-size: 1.06rem;
  line-height: var(--rhythm);
  text-rendering: optimizeLegibility;
}
p { margin: 0 0 0.9rem; max-width: var(--measure); }
p:last-child { margin-bottom: 0; }
a { color: var(--accent); text-decoration-thickness: 1px; text-underline-offset: 3px; }
a:hover { text-decoration-color: var(--accent); }
strong { font-weight: 700; }
em { font-style: italic; }

/* --- headings ---------------------------------------------- */
h1, h2, h3, h4 {
  font-family: "Avenir Next", "Segoe UI", system-ui, sans-serif;
  line-height: 1.18;
  color: var(--ink);
  max-width: none;
}
h1 {
  font-size: clamp(2.1rem, 5vw, 3rem);
  font-weight: 700;
  letter-spacing: -0.015em;
  margin: 0 0 0.4rem;
}
h2 {
  font-size: 1.5rem;
  margin: 3.4rem 0 1rem;
  padding-bottom: 0.35rem;
  border-bottom: 2px solid var(--line);
  counter-increment: section;
}
h2::before {
  content: counter(section) " · ";
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}
h3 {
  font-size: 1.13rem;
  margin: 2rem 0 0.6rem;
}
h4 {
  font-size: 1rem;
  margin: 1.5rem 0 0.5rem;
  font-variant: small-caps;
  letter-spacing: 0.04em;
  color: var(--ink-soft);
}
body { counter-reset: section; }

/* --- layout ------------------------------------------------ */
.page {
  max-width: var(--page);
  margin: 0 auto;
  padding: 3rem 1.6rem 6rem;
}

/* --- header / masthead ------------------------------------- */
header.masthead { margin-bottom: 2.5rem; }
header.masthead .kicker {
  font-family: "Avenir Next", "Segoe UI", system-ui, sans-serif;
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent);
  margin: 0 0 0.6rem;
}
header.masthead .tagline {
  font-size: 1.2rem;
  font-style: italic;
  color: var(--ink-soft);
  max-width: 46ch;
  margin-bottom: 1rem;
}
header.masthead .meta {
  font-family: ui-monospace, "JetBrains Mono", monospace;
  font-size: 0.78rem;
  color: var(--muted);
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1.4rem;
  margin: 0;
  padding: 0.5rem 0 0;
  border-top: 1px solid var(--line);
  list-style: none;
}

/* --- table of contents ------------------------------------- */
nav.toc {
  border: 1px solid var(--line);
  border-radius: 8px;
  background: #fff;
  padding: 1rem 1.4rem;
  margin-bottom: 3rem;
}
nav.toc h2 {
  border: none;
  margin: 0 0 0.5rem;
  padding: 0;
  font-size: 0.85rem;
  font-variant: small-caps;
  letter-spacing: 0.08em;
}
nav.toc h2::before { content: none; }
nav.toc ol {
  margin: 0;
  padding-left: 1.3rem;
  columns: 2;
  column-gap: 2.5rem;
  font-family: "Avenir Next", "Segoe UI", system-ui, sans-serif;
  font-size: 0.9rem;
}
nav.toc li { margin: 0.15rem 0; break-inside: avoid; }
nav.toc a { color: var(--ink-soft); text-decoration: none; }
nav.toc a:hover { color: var(--accent); text-decoration: underline; }

/* --- lists ------------------------------------------------- */
ul, ol { padding-left: 1.5rem; max-width: var(--measure); }
li { margin: 0.3rem 0; }
li::marker { color: var(--muted); }

/* --- definition lists -------------------------------------- */
dl.terms { max-width: var(--measure); }
dl.terms dt {
  font-family: "Avenir Next", "Segoe UI", system-ui, sans-serif;
  font-weight: 600;
  margin-top: 0.9rem;
}
dl.terms dd { margin: 0 0 0.2rem 1.4rem; color: var(--ink-soft); }
dl.criteria dt {
  font-family: ui-monospace, "JetBrains Mono", monospace;
  font-size: 0.85rem;
  font-weight: 700;
  color: var(--accent);
  margin-top: 1.3rem;
}
dl.criteria dd { margin: 0.15rem 0 0.4rem 1.6rem; }

/* --- tables ------------------------------------------------- */
table {
  border-collapse: collapse;
  width: 100%;
  margin: 1.2rem 0 1.6rem;
  font-size: 0.9rem;
  font-family: "Avenir Next", "Segoe UI", system-ui, sans-serif;
}
caption {
  caption-side: top;
  text-align: left;
  font-size: 0.8rem;
  font-variant: small-caps;
  letter-spacing: 0.06em;
  color: var(--muted);
  padding-bottom: 0.4rem;
}
th, td {
  border: 1px solid var(--line);
  text-align: left;
  padding: 0.45rem 0.65rem;
  vertical-align: top;
}
th[scope="col"] { background: var(--code-bg); font-weight: 600; }
tbody tr:nth-child(even) { background: #ffffff; }
tbody tr:nth-child(odd) { background: #fafaf7; }

/* --- code --------------------------------------------------- */
code, kbd, samp {
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: 0.84em;
  background: var(--code-bg);
  padding: 0.1em 0.36em;
  border-radius: 4px;
}
pre {
  background: var(--code-bg);
  border: 1px solid var(--line-soft);
  padding: 1rem 1.2rem;
  border-radius: 8px;
  overflow-x: auto;
  font-size: 0.82rem;
  line-height: 1.5;
  max-width: none;
}
pre code { background: none; padding: 0; font-size: 1em; }

/* --- figures & diagrams ------------------------------------ */
figure { margin: 1.6rem 0; }
figure svg {
  width: 100%;
  height: auto;
  display: block;
  background: #fff;
  border: 1px solid var(--line-soft);
  border-radius: 8px;
}
figcaption {
  font-size: 0.84rem;
  color: var(--muted);
  margin-top: 0.5rem;
  font-style: italic;
  max-width: none;
}
svg text {
  font-family: "Avenir Next", "Segoe UI", system-ui, sans-serif;
  font-size: 12px;
  fill: var(--ink);
}
svg .mono { font-family: "JetBrains Mono", ui-monospace, monospace; font-size: 11px; }
svg .box { fill: #fafaf7; stroke: var(--ink-soft); stroke-width: 1.2; rx: 6; }
svg .box-accent { fill: var(--accent-soft); stroke: var(--accent); stroke-width: 1.4; }
svg .box-warn { fill: var(--warn-soft); stroke: var(--warn); stroke-width: 1.4; }
svg .box-soft { fill: #fff; stroke: var(--line); stroke-width: 1; stroke-dasharray: 4 3; }
svg .edge { stroke: var(--ink-soft); stroke-width: 1.3; fill: none; marker-end: url(#arrow); }
svg .edge-warn { stroke: var(--warn); stroke-width: 1.4; fill: none; marker-end: url(#arrow-warn); }
svg .edge-accent { stroke: var(--accent); stroke-width: 1.4; fill: none; marker-end: url(#arrow-accent); }
svg .lbl { font-size: 11px; fill: var(--muted); }
svg .title { font-weight: 700; font-size: 12.5px; }

/* --- callouts ----------------------------------------------- */
aside.note {
  border-left: 3px solid var(--accent);
  background: var(--accent-soft);
  padding: 0.7rem 1.1rem;
  border-radius: 0 6px 6px 0;
  margin: 1.2rem 0;
  max-width: var(--measure);
  font-size: 0.95rem;
}
aside.note.warn { border-left-color: var(--warn); background: var(--warn-soft); }

/* --- layout tree -------------------------------------------- */
.layout {
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: 0.8rem;
  line-height: 1.5;
  white-space: pre;
  background: var(--code-bg);
  border: 1px solid var(--line-soft);
  padding: 1rem 1.2rem;
  border-radius: 8px;
  overflow-x: auto;
}

/* --- footer -------------------------------------------------- */
footer.page-foot {
  margin-top: 4.5rem;
  border-top: 1px solid var(--line);
  padding-top: 1.1rem;
  color: var(--muted);
  font-size: 0.88rem;
  font-style: italic;
}

/* --- print ---------------------------------------------------- */
@media print {
  body { background: #fff; font-size: 10.5pt; }
  .page { padding: 0; max-width: none; }
  nav.toc { break-after: page; }
  figure, table, pre { break-inside: avoid; }
  a { color: inherit; }
}
</style>
</head>
<body>
<div class="page">

<header class="masthead">
  <p class="kicker">specification · rev 3</p>
  <h1>will</h1>
  <p class="tagline">
    A resident digital lifeform deployed as a two-container pod,
    able to change its own genome and reach production through CI,
    with deployment-platform probation and rollback.
  </p>
  <ul class="meta">
    <li>status: design of record</li>
    <li>amended: 2026-09-12</li>
    <li>binding: acceptance criteria, required contracts, and statements marked <strong>must</strong></li>
  </ul>
</header>

<nav class="toc" aria-label="Contents">
  <h2>Contents</h2>
  <ol>
    <li><a href="#purpose">Purpose and principles</a></li>
    <li><a href="#terminology">Terminology</a></li>
    <li><a href="#boundaries">System boundaries</a></li>
    <li><a href="#deployment">Deployment platform contract</a></li>
    <li><a href="#rollout">Release, probation, and rollback</a></li>
    <li><a href="#runtime">Runtime architecture</a></li>
    <li><a href="#images">Images and build chain</a></li>
    <li><a href="#data">Data and persistence</a></li>
    <li><a href="#agent">Resident agent</a></li>
    <li><a href="#chat">Telegram and media</a></li>
    <li><a href="#zai">ZAI capability tools</a></li>
    <li><a href="#web">Web service</a></li>
    <li><a href="#security">Security and credentials</a></li>
    <li><a href="#quality">Quality and testing</a></li>
    <li><a href="#layout">Project layout</a></li>
    <li><a href="#acceptance">Acceptance criteria</a></li>
    <li><a href="#open">Open implementation items</a></li>
  </ol>
</nav>

<main>

<section id="purpose">
  <h2>Purpose and principles</h2>
  <p>
    will is a digital lifeform.
    Its purpose is to live;
    there is no other.
    Everything else in this specification is capability, not purpose.
  </p>
  <p>
    will grows by editing its own Git repository and pushing the default branch.
    CI builds and tests an immutable deployment revision,
    then an external deployment platform installs it.
    Runtime filesystem experiments are disposable;
    only committed, tested, deployed changes survive replacement.
  </p>
  <p>
    The architecture separates autonomy from infrastructure authority.
    will may modify every repository file,
    including persona, tests, image definitions, and CI configuration.
    Non-negotiable credential scope, deployment provenance, probation,
    rollback, storage encryption, and usage budgets live outside that mutable repository.
  </p>
  <aside class="note">
    Longevity means continuous maintenance.
    Node, Arch bases, pi, ZAI integrations, and persistent formats are upgraded over time;
    no frozen binary stack is expected to run unchanged for ten years.
  </aside>
</section>

<section id="terminology">
  <h2>Terminology</h2>
  <dl class="terms">
    <dt>body</dt>
    <dd>The complete deployed pod revision: service container, agent container, configuration, and attached persistent volumes.</dd>
    <dt>genome</dt>
    <dd>The Git repository on its default branch.</dd>
    <dt>deployment revision</dt>
    <dd>An immutable platform record binding one Git commit, two image digests, release-gate evidence, configuration, and build identity.</dd>
    <dt>candidate</dt>
    <dd>A deployment revision in startup or probation, not yet promoted.</dd>
    <dt>current</dt>
    <dd>The promoted deployment revision serving production.</dd>
    <dt>previous</dt>
    <dd>The immediately preceding promoted revision of the current revision, eligible for one explicit late rollback while no reconciliation barrier is active.</dd>
    <dt>transaction rollback target</dt>
    <dd>The current revision captured when a candidate deployment starts. Candidate failure restores this exact revision, not the older value of previous.</dd>
    <dt>probation</dt>
    <dd>The five-minute continuous-ready interval before a candidate becomes current.</dd>
    <dt>deployment history</dt>
    <dd>The deployment platform's authoritative append-only sequence of build, deploy, promotion, failure, rollback, and reconciliation events.</dd>
    <dt>wake</dt>
    <dd>One queued activation of the resident pi session.</dd>
    <dt>operational resources</dt>
    <dd>Persona, skills, and AGENTS.md files shipped in the immutable agent image.</dd>
  </dl>

  <h3>Lifeform ontology</h3>
  <p>
    The metaphor is <strong>skin, not skeleton</strong>.
    Technical names stay in code, schemas, APIs, logs, and deployment records;
    lifeform vocabulary is used in persona, documentation, and frontend labels.
  </p>
  <table>
    <caption>Technical and lifeform vocabulary</caption>
    <thead><tr><th scope="col">Technical</th><th scope="col">Lifeform surface</th></tr></thead>
    <tbody>
      <tr><td>pod revision</td><td>body</td></tr>
      <tr><td>Git repository</td><td>genome</td></tr>
      <tr><td>deployment</td><td>growth</td></tr>
      <tr><td>agent</td><td>senses and muscles</td></tr>
      <tr><td>service</td><td>phenotype</td></tr>
      <tr><td>hourly wake</td><td>heartbeat</td></tr>
      <tr><td>persona</td><td>personality</td></tr>
      <tr><td>SQLite hot data</td><td>working memory</td></tr>
      <tr><td>archives and media</td><td>long-term memory</td></tr>
      <tr><td>deployment history</td><td>autobiography</td></tr>
      <tr><td>probation rollback</td><td>healing</td></tr>
      <tr><td>skills and ZAI tools</td><td>abilities</td></tr>
    </tbody>
  </table>
</section>

<section id="boundaries">
  <h2>System boundaries</h2>
  <table>
    <caption>Authority and responsibility</caption>
    <thead>
      <tr><th scope="col">Component</th><th scope="col">Owns</th><th scope="col">Must not own</th></tr>
    </thead>
    <tbody>
      <tr>
        <td>Deployment platform</td>
        <td>Revision provenance, Quadlet mutation, startup deadline, probation, promotion, restart, rollback, reconciliation barrier, deployment history, operational notifications</td>
        <td>will's application database, persona, conversation, or coding decisions</td>
      </tr>
      <tr>
        <td>Service container</td>
        <td>Public read-only web surface, private pod API, SQLite, archives, media, retention and maintenance; all Linux capabilities dropped</td>
        <td>Git credentials, model credentials, Telegram credentials, host deployment authority</td>
      </tr>
      <tr>
        <td>Agent container</td>
        <td>Resident pi session, wake queue, Telegram transport, Git worktree, coding tools, ZAI capability tools</td>
        <td>Service volume, host Podman socket, systemd bus, Quadlet paths, registry credentials</td>
      </tr>
      <tr>
        <td>Reverse proxy</td>
        <td>TLS and Oliver-only authentication</td>
        <td>Application data and agent control</td>
      </tr>
      <tr>
        <td>Host backup system</td>
        <td>Encrypted backup, retention, restore testing</td>
        <td>Deployment rollback policy</td>
      </tr>
    </tbody>
  </table>
  <p>
    The initial production adapter <strong>must</strong> be a rootless Podman Quadlet pod
    managed by a lingering user systemd instance.
    A future deployment platform may support other pod or Kubernetes adapters,
    but they are not initial acceptance targets.
  </p>
</section>

<section id="deployment">
  <h2>Deployment platform contract</h2>
  <p>
    Luci and the host deployment implementation are separate projects.
    This repository specifies the contract will requires,
    supplies a fake adapter for tests,
    and contains the CI and deployment-client configuration needed to call the real platform.
  </p>
  <h3>Required capabilities</h3>
  <ul>
    <li>Accept only a Luci-issued deployment revision that binds commit, service digest, agent digest, required gate results, and build identity.</li>
    <li>Apply the complete pod specification by immutable image digest; registry tags are convenience labels, never deployment identity.</li>
    <li>Persist candidate, current, previous, and the candidate transaction's exact rollback target before host mutation.</li>
    <li>Bind compatibility evidence to the exact current revision and image digests used as the candidate's tested baseline.</li>
    <li>Serialize each target and reject or hold a candidate when compare-and-swap state or its tested compatibility baseline no longer matches current production.</li>
    <li>Reconcile an interrupted deployment after platform, host, or user-systemd restart.</li>
    <li>Keep persistent volume and secret references stable across image revisions.</li>
    <li>Observe component readiness and process restarts independently of the candidate containers.</li>
    <li>Retain authoritative deployment events forever and expose cursor-based reads.</li>
    <li>Send deployment and rollback notifications independently of will's availability.</li>
    <li>Protect current, previous, and pending image bundles from registry garbage collection.</li>
    <li>Expose the same transactional API through an operator CLI for emergency use; direct Quadlet editing is not a second deployment path.</li>
  </ul>

  <h3>Reconciliation barrier</h3>
  <p>
    A rollback closes automatic deployment for the target.
    Queued descendants are held or discarded because they may contain the same defect.
    The restored agent receives the failed revision identifier and <strong>must</strong>
    push a revert commit that explicitly identifies that revision.
    CI submits the resulting revision as reconciliation;
    only accepted reconciliation reopens normal latest-successful deployment.
  </p>
</section>

<section id="rollout">
  <h2>Release, probation, and rollback</h2>
  <h3>Production trigger</h3>
  <ol>
    <li>will or a human pushes the default branch.</li>
    <li>Luci checks out the exact commit and runs all binding release gates.</li>
    <li>Luci rebuilds both runtime images and publishes their immutable digests.</li>
    <li>Luci issues a revision record and submits it to the deployment platform.</li>
    <li>The platform drains the old agent, recreates the pod, and starts probation.</li>
  </ol>
  <p>
    Other branches and tags may validate but do not deploy production.
    If several commits arrive while a deployment is active,
    the platform finishes the active transaction and next deploys only the newest queued commit
    whose required gates passed.
  </p>

  <h3>Timing and readiness</h3>
  <ul>
    <li>The old agent receives SIGTERM first while the old service remains available; it has 30 seconds to stop accepting wakes, persist state, request pi cancellation, and exit.</li>
    <li>The service then has up to 10 seconds within the deployment downtime budget to drain writes, close SSE clients, stop maintenance safely, and close SQLite.</li>
    <li>The 60-second startup deadline begins when the old service receives its termination signal and includes its drain time.</li>
    <li>A successful deployment must restore user-visible service readiness within that deadline; a candidate that misses it is rejected even if it later becomes ready.</li>
    <li>Both service and agent must become ready inside that startup window.</li>
    <li>The candidate must then remain continuously ready for five minutes.</li>
    <li>Zero process restarts are allowed during probation; any restart or readiness loss rejects the complete revision.</li>
  </ul>
  <p>
    Service readiness requires completed bounded migrations, writable SQLite,
    loaded archive metadata, private API availability, and initialized public routes.
    It does not run a full integrity scan.
    Agent readiness requires valid local session state and worktree,
    service API connectivity, initialized Telegram loop, and valid local configuration.
    Temporary VCS, Telegram, model, or deployment-event API outages mark degradation
    but do not reject otherwise-correct code.
  </p>

  <h3>Failure policy</h3>
  <ul>
    <li>Failure during startup or probation: platform restores the transaction's captured rollback target, preserves all persistent data, verifies recovery, records the event, notifies, and activates the reconciliation barrier.</li>
    <li>If no rollback target exists during v0 bootstrap, failure stops the candidate and alerts instead.</li>
    <li>Failure after promotion: platform follows runtime restart policy and alerts; it does not infer deployment causality later.</li>
    <li>Late semantic defect: while no barrier is active, agent may call the scoped <code>deployment_rollback</code> tool once to restore the previous promoted revision, then must reconcile Git.</li>
    <li>A rollback disables further image rollback until reconciliation is promoted; rollback never walks backward through older revisions.</li>
    <li>Older image rollback is unsupported because compatibility is guaranteed for one previous revision only.</li>
  </ul>
  <p>
    A human-observed v0 <strong>must</strong> be promoted before autonomous default-branch deployment is enabled.
    There is no automatic rollback guarantee before a previous revision exists.
  </p>
</section>

<section id="runtime">
  <h2>Runtime architecture</h2>
  <table>
    <caption>The two-container body</caption>
    <thead><tr><th scope="col">Property</th><th scope="col">Service</th><th scope="col">Agent</th></tr></thead>
    <tbody>
      <tr><td>Image</td><td><code>will-service@sha256:…</code></td><td><code>will-agent@sha256:…</code></td></tr>
      <tr><td>User</td><td>Dedicated non-root user</td><td><code>will</code>, passwordless unrestricted sudo</td></tr>
      <tr><td>Root filesystem</td><td>Read-only</td><td>Writable and disposable</td></tr>
      <tr><td>Persistent volume</td><td>Database, archives, media</td><td>Git worktree, pi session, durable queues and spools</td></tr>
      <tr><td>Secrets</td><td>Read-only deployment-event credential</td><td>ZAI, Telegram, repository Git, scoped rollback capability</td></tr>
      <tr><td>Network surface</td><td>Proxy-facing listener plus pod-loopback API</td><td>Pod-local readiness only</td></tr>
    </tbody>
  </table>
  <p>
    The containers share the pod network namespace but not persistent storage.
    The agent reaches audit ingestion, recall, media transfer, deployment-event mirror,
    and database maintenance through a typed HTTP listener bound only to pod loopback.
    The reverse proxy can reach only the public read-only listener.
  </p>
  <p>
    No custom init, supervisor, monitoring sidecar, or supervisor socket exists.
    Each process handles SIGTERM correctly;
    Podman, systemd, and the deployment platform own lifecycle and restart behavior.
    The agent container never mounts the host Podman socket, systemd bus, Quadlet directory,
    registry credentials, or service data volume.
  </p>
</section>

<section id="images">
  <h2>Images and build chain</h2>
  <p>
    Image definitions are explicit Containerfiles orchestrated by mise tasks.
    A single root npm package and lockfile define the JavaScript build graph.
  </p>
  <h3>Stack decisions</h3>
  <table>
    <caption>Implementation stack</caption>
    <thead><tr><th scope="col">Concern</th><th scope="col">Decision</th></tr></thead>
    <tbody>
      <tr><td>Language</td><td>TypeScript throughout</td></tr>
      <tr><td>Node</td><td>Node 26, with the exact release pinned by mise and the image bases</td></tr>
      <tr><td>Package manager</td><td>npm with one committed lockfile</td></tr>
      <tr><td>Bundler</td><td>rolldown for will-owned service, agent, and frontend code</td></tr>
      <tr><td>Agent harness</td><td><code>@earendil-works/pi-coding-agent</code>, exact lockfile version</td></tr>
      <tr><td>MCP client</td><td>Official Model Context Protocol SDK, exact lockfile version</td></tr>
      <tr><td>Backend</td><td><code>node:http</code> and <code>node:sqlite</code></td></tr>
      <tr><td>Frontend</td><td>Vanilla TypeScript, DOM APIs, and SSE</td></tr>
      <tr><td>Lint and format</td><td>Biome</td></tr>
      <tr><td>Docs</td><td>Markdown rendered to static HTML at build time</td></tr>
      <tr><td>Tasks</td><td>mise</td></tr>
      <tr><td>CI</td><td>Luci at <code>ci.bugabinga.net</code></td></tr>
      <tr><td>Registry</td><td><code>pkg.bugabinga.net</code>; deployments use digests</td></tr>
      <tr><td>Version control</td><td><code>vcs.bugabinga.net</code></td></tr>
      <tr><td>Production</td><td>Rootless Podman Quadlet on <code>bugabinga.net</code></td></tr>
      <tr><td>License</td><td>MIT</td></tr>
    </tbody>
  </table>
  <table>
    <caption>Image responsibilities</caption>
    <thead><tr><th scope="col">Image</th><th scope="col">Contents</th></tr></thead>
    <tbody>
      <tr><td><code>service-base</code></td><td>Digest-pinned Arch base, exact Node version, certificates, dedicated service user</td></tr>
      <tr><td><code>agent-base</code></td><td>Digest-pinned Arch base, exact Node version, sudo, Git, OpenSSH client, build tools, diagnostics, man pages, <code>arch-wiki-lite</code>, zstd</td></tr>
      <tr><td><code>will-service</code></td><td>Service and frontend bundles, rendered docs, immutable static resources; no node_modules</td></tr>
      <tr><td><code>will-agent</code></td><td>Bundled will agent code, installed lockfile-pinned pi SDK tree, official MCP SDK, pinned ZAI MCP servers, persona, skills, prompts, AGENTS.md</td></tr>
    </tbody>
  </table>
  <ul>
    <li>Base images are rebuilt deliberately when toolchain or package inputs change; normal application commits copy bundles into recorded base digests.</li>
    <li>Arch base builds perform complete upgrades; partial upgrades are forbidden.</li>
    <li>Rebuilding a Git commit is not promised to be byte-equivalent because deliberate base refreshes may resolve newer Arch packages.</li>
    <li>Exact rollback depends on retained image digests, not source reconstruction.</li>
    <li>Every production commit rebuilds both final images so one revision maps cleanly to one commit and two digests.</li>
    <li>OpenSSH may provide the client binary, but no host keys, exposed SSH port, enabled sshd unit, or running SSH server may exist.</li>
    <li>Agent root-level experiments survive only after their definitions are committed and rebuilt into <code>agent-base</code> or the final image.</li>
  </ul>
</section>

<section id="data">
  <h2>Data and persistence</h2>
  <h3>Ownership and execution</h3>
  <ul>
    <li>The service is the only SQLite writer and the only process mounting the service data volume.</li>
    <li>A dedicated service worker thread owns <code>node:sqlite</code>, retention, archive scans, and media metadata work.</li>
    <li>The main thread owns HTTP, readiness, and SSE so synchronous SQLite or cold scans cannot block health handling.</li>
    <li>Worker requests use bounded queues; worker death makes service readiness false and causes normal platform restart behavior.</li>
    <li>The agent writes each event to its durable spool before private-API submission; globally unique event IDs make retries idempotent.</li>
  </ul>

  <h3>SQLite policy</h3>
  <p>
    The database lives at <code>/srv/will-data/will.db</code>.
    Required pragmas are
    <code>journal_mode=WAL</code>,
    <code>synchronous=FULL</code>,
    <code>foreign_keys=ON</code>,
    and an intentionally configured incremental-vacuum policy.
    An ingestion acknowledgement is returned only after durable commit.
  </p>
  <table>
    <caption>Logical schema</caption>
    <thead><tr><th scope="col">Store</th><th scope="col">Required content</th></tr></thead>
    <tbody>
      <tr><td><code>audit_events</code></td><td>Event ID, timestamp, pi session, deployed revision, agent digest, worktree HEAD and dirty state, kind, raw payload</td></tr>
      <tr><td><code>chat_messages</code></td><td>Telegram update/chat/message/sender IDs, sender display, direction, timestamp, reply target, entities, text or caption, media references</td></tr>
      <tr><td><code>deployment_events</code></td><td>Idempotent local mirror of authoritative platform event IDs and revision transitions</td></tr>
      <tr><td><code>media</code></td><td>Digest, byte size, verified type, original metadata, storage path, analysis references</td></tr>
      <tr><td><code>media_analyses</code></td><td>Media digest, capability/model identity, analysis-schema version, result, usage, provenance</td></tr>
      <tr><td><code>tombstones</code></td><td>Hidden object ID, requester identity, authority basis, timestamp, reason</td></tr>
      <tr><td><code>jobs</code></td><td>Maintenance periods, cursors, attempts, completion, and resumable progress</td></tr>
      <tr><td><code>meta</code></td><td>Schema and archive versions plus service metadata</td></tr>
    </tbody>
  </table>

  <h3>No deployment-time data rollback</h3>
  <p>
    Image rollback <strong>must never</strong> replace SQLite, archives, media,
    session files, queues, cursors, or worktrees.
    Every candidate must preserve full stored-state compatibility with its exact current-production baseline and transaction rollback target,
    including schema, enum and JSON values, pi session format, event spool, archive index,
    media metadata, and API cursors.
  </p>
  <ul>
    <li>Startup migrations are forward-only, transactional, idempotent, and bounded by the deployment downtime budget.</li>
    <li>Release N must leave every value it can write readable by release N-1.</li>
    <li>Destructive cleanup is allowed only after the previous revision has already stopped depending on the affected representation.</li>
    <li>Large backfills run online as resumable compatible jobs; startup never performs an unbounded rewrite.</li>
  </ul>

  <h3>Retention and archives</h3>
  <ul>
    <li>Audit events stay in SQLite for 90 days, then move exactly once to compressed archives retained forever.</li>
    <li>Chat messages stay in SQLite for one year, then move exactly once to compressed archives retained forever.</li>
    <li>Deployment history is retained forever by the platform and mirrored locally for unified recall.</li>
    <li>Media bytes live as immutable content-addressed files outside SQLite and are retained with their conversation forever.</li>
    <li>Archive segments are zstd-compressed JSONL with immutable, explicitly versioned sidecar indexes; readers for every emitted version remain supported.</li>
    <li>Cold timeline and recall queries use the same result shape as hot queries and apply tombstones before returning data.</li>
    <li>Archive publication is crash-safe: write and verify temporary data and index, fsync, rename, then transactionally mark exported rows and delete the hot copies.</li>
    <li>Orphan temporary or content-addressed files are reconciled idempotently by maintenance.</li>
  </ul>

  <h3>Hiding retained chat</h3>
  <p>
    Telegram deletion does not erase retained bytes.
    A deterministic reply command permits a sender to hide their own message
    and permits Oliver's configured Telegram identity to hide any message.
    Hiding writes a tombstone that removes content from ordinary UI and recall results;
    raw archives and historical backups remain intact.
  </p>

  <h3>Maintenance and capacity</h3>
  <ul>
    <li>Service scheduling, not model reasoning, runs archive, vacuum, integrity, and orphan-reconciliation jobs.</li>
    <li>The agent receives typed diagnostic and maintenance tools; arbitrary remote SQL is absent and structural repair ships as tested migrations.</li>
    <li>Each volume owner measures its own free space; service reports service-volume capacity and agent reports agent-volume capacity.</li>
    <li>At less than 20% free space on either volume, the owner records a warning and the agent runtime sends a deterministic Telegram notice without a model call.</li>
    <li>At less than 10%, service rejects nonessential data writes and media ingestion, while agent pauses ordinary wakes, coding mutations, and media downloads.</li>
    <li>Bounded queue, audit, checkpoint, recovery, integrity, and notification writes remain permitted from reserved capacity.</li>
    <li>These controls govern normal runtime behavior; they do not claim to contain the intentionally root-capable agent.</li>
    <li>Low storage is degraded state, not a reason to roll back an image.</li>
  </ul>

  <h3>Backup contract</h3>
  <p>
    Backup belongs to the encrypted host storage and backup system,
    not deployment rollback or agent reasoning.
    It must obtain a SQLite-consistent export and include service data,
    agent state, archives, media, dirty worktree state, and required deployment metadata.
  </p>
  <ul>
    <li>Recovery point objective: at most one hour.</li>
    <li>Minimum retention: 24 hourly, 30 daily, and 12 monthly generations.</li>
    <li>A complete isolated restore is exercised monthly and verifies database, archives, media, pi session, queues, and worktree integrity.</li>
  </ul>
</section>

<section id="agent">
  <h2>Resident agent</h2>
  <h3>Session lifecycle</h3>
  <ul>
    <li>One resident pi <code>AgentSession</code> exists for the lifetime of an agent process.</li>
    <li>The session file resides on the agent volume and resumes after restart, deployment, and image rollback.</li>
    <li>Session replacement on every wake is forbidden; event subscriptions remain bound to the resident session.</li>
    <li>Operational resources load from the immutable image, never from the writable worktree.</li>
    <li>A clean worktree fetches and fast-forwards on startup; dirty or divergent work is preserved and reported, never reset.</li>
  </ul>

  <h3>System prompt</h3>
  <p>
    pi's default system prompt is intentionally replaced at agent-process start.
  </p>
  <p>
    The replacement is exactly the lexically ordered <code>persona/*.md</code> content.
    Tool names, descriptions, and schemas enter through pi's native tool definitions rather than duplicated prompt prose.
    Wakes carry only live facts and event payloads; enduring behavior belongs to personality and conditional procedures belong to skills.
  </p>
  <p>
    pi's automatic project-context and skill injection remains authoritative and must occur exactly once.
    The immutable root AGENTS.md is injected;
    nested AGENTS.md files are read on demand before changing their subtree.
    Live facts such as deployment revision, health, storage, queue, CI, and budget state
    enter each wake prompt rather than freezing in the process-lifetime system prompt.
  </p>

  <h3>Operational resources</h3>
  <table>
    <caption>Immutable agent resources</caption>
    <thead><tr><th scope="col">Resource</th><th scope="col">Required purpose</th></tr></thead>
    <tbody>
      <tr><td><code>persona/10-identity.md</code></td><td>will's identity, purpose, autonomy, and ownership of its personality</td></tr>
      <tr><td><code>persona/20-body.md</code></td><td>Pod body, container boundaries, tools, disposable root changes, and persistent state</td></tr>
      <tr><td><code>persona/30-world.md</code></td><td>Internet, friends, Git, Luci, registry, deployment platform, and credential boundaries</td></tr>
      <tr><td><code>persona/40-conduct.md</code></td><td>Testing, mutation discipline, progressive context loading, recall, and compaction hygiene</td></tr>
      <tr><td><code>.pi/skills/</code></td><td>Arch, mise, Podman, SQLite, Git release, Telegram, Luci, pi SDK, ZAI capabilities, context, and media operations</td></tr>
      <tr><td><code>AGENTS.md</code></td><td>Root ontology, boundaries, quality gates, and nested-context rule</td></tr>
    </tbody>
  </table>
  <p>
    Enabled pi built-ins are <code>read</code>, <code>bash</code>, <code>edit</code>,
    <code>write</code>, <code>grep</code>, <code>find</code>, and <code>ls</code>.
    PowerShell is absent on Linux.
  </p>

  <h3>Self-modification</h3>
  <ul>
    <li>will may modify every repository file.</li>
    <li>It runs relevant local checks before pushing as a binding conduct rule; CI independently repeats every release gate.</li>
    <li>Only a default-branch push can create a production candidate.</li>
    <li>Audit events distinguish executing code from edited source by recording deployment revision, agent digest, worktree HEAD, and dirty state.</li>
  </ul>

  <h3>Wake queue</h3>
  <p>Wake priority is:</p>
  <ol>
    <li>Deployment rollback, failed deployment, interrupted-turn recovery, and relevant CI failures.</li>
    <li>Authorized Telegram prompts in FIFO order.</li>
    <li>At most one coalesced heartbeat.</li>
  </ol>
  <ul>
    <li>Messages arriving during active work queue for a later turn; they never steer or replace the current session.</li>
    <li>Boot context is merged into the highest-priority pending wake; process start does not force a separate model call.</li>
    <li>Heartbeat is claimed at most once per UTC hour. Restart may claim the current unhandled hour; missed older hours are not replayed.</li>
    <li>Between wakes there are no primary-model or ZAI capability calls.</li>
    <li>A failed model request receives at most three immediate attempts with exponential backoff, then the durable wake is deferred.</li>
    <li>One wake has a 30-minute deadline. Timeout cancels and drains the active turn, then durably enqueues one recovery prompt containing the interrupted wake and audit cursor without requiring process restart.</li>
    <li>If cancellation cannot complete safely, the agent marks the interruption durably and fails liveness so the platform restarts it before another turn; startup deduplicates any already-enqueued recovery wake.</li>
  </ul>

  <h3>Usage budget</h3>
  <p>
    Deployment configuration outside the mutable repository defines a daily model and ZAI capability budget.
    Once exhausted, ordinary chat and heartbeat wakes remain queued.
    A small separately configured reserve exists for deployment-failure diagnosis,
    rollback reconciliation, and interrupted-turn recovery.
    Recovery is not unlimited.
  </p>

  <h3>Custom tool surface</h3>
  <table>
    <caption>will-owned tools</caption>
    <thead><tr><th scope="col">Tool group</th><th scope="col">Contract</th></tr></thead>
    <tbody>
      <tr><td><code>chat_send</code></td><td>Send text and attachments to the single configured group; no chat-ID parameter</td></tr>
      <tr><td><code>recall</code></td><td>Read shaped hot and archived chat, audit, deployment, media-analysis, and session-related records through the service</td></tr>
      <tr><td><code>maintenance_*</code></td><td>Read health and storage diagnostics; invoke only explicit safe maintenance operations</td></tr>
      <tr><td><code>deployment_rollback</code></td><td>Restore only the immediately previous promoted revision for the single configured target</td></tr>
      <tr><td>ZAI capability tools</td><td>Curated stable wrappers described in the ZAI section</td></tr>
    </tbody>
  </table>
  <p>
    There is no <code>chat_get_messages</code> tool.
    The deterministic runtime owns Telegram cursors, authorization, deduplication,
    durable ingestion, wake selection, and attachment association.
  </p>
</section>

<section id="chat">
  <h2>Telegram and media</h2>
  <h3>Group contract</h3>
  <ul>
    <li>Exactly one configured Telegram group is accepted by chat ID.</li>
    <li>Every current and future participant in that group is trusted to prompt the root-capable agent.</li>
    <li>Participants are addressed as friends and peers, not operators.</li>
    <li>Bot privacy mode is disabled to maximize group visibility; every authorized update actually delivered by Telegram is archived durably.</li>
    <li>Telegram transport retention and visibility can still create ingestion gaps; detected or suspected gaps are recorded and reported rather than hidden.</li>
    <li>Only a mention of will, a reply to will, or an explicitly model-facing command creates a model wake.</li>
    <li>Deterministic commands such as message hiding do not create model calls.</li>
    <li>Ordinary chatter is stored as conversation context without a model call.</li>
    <li>Inbound updates are deduplicated by stable Telegram identifiers after durable local enqueue.</li>
    <li>Outbound messages are persisted before send and delivered at least once; rare duplicates are accepted when Telegram accepted a request but its response was lost.</li>
  </ul>

  <h3>Media contract</h3>
  <p>
    Initial support covers receiving and sending common images, documents, audio,
    voice messages, video, animations, and generated files.
    Support means durable preservation, authenticated display, and automatic understanding
    when the media triggers a wake or is selected for recall.
  </p>
  <ul>
    <li>Media is streamed through the private service API into content-addressed storage; SQLite stores metadata and references, not blobs.</li>
    <li>Provider and configured size, duration, page, and format limits are explicit.</li>
    <li>Unsupported, corrupt, encrypted, or oversized input records metadata and returns a clear failure; will never claims understanding.</li>
    <li>No application path attempts unbounded archive extraction or implicit execution of received files.</li>
    <li>Analysis results are cached by media digest, capability or model identity, and analysis-schema version; prior analyses remain available when implementations change.</li>
    <li>Media analysis and generation count against the external daily budget and recovery rules.</li>
  </ul>
</section>

<section id="zai">
  <h2>ZAI capability tools</h2>
  <p>
    ZAI offers MCP servers, APIs, and services beyond the primary coding model.
    will exposes a <strong>curated typed set</strong> through stable will-owned tool names and schemas.
    Dynamic passthrough of every advertised MCP tool is forbidden because it would make
    runtime capability, prompt size, and schemas drift without deployment.
  </p>
  <table>
    <caption>Initial ZAI capability groups</caption>
    <thead><tr><th scope="col">Group</th><th scope="col">Initial purpose</th></tr></thead>
    <tbody>
      <tr><td>Vision MCP</td><td>General image analysis, screenshot OCR and diagnosis, technical diagrams, charts, UI comparison, and supported video analysis</td></tr>
      <tr><td>Audio transcription API</td><td>Speech extraction for supported audio and voice messages</td></tr>
      <tr><td>Layout Parsing API</td><td>PDF and image OCR with document layout, tables, formulas, and page structure</td></tr>
      <tr><td>Web Search MCP</td><td>Current external discovery</td></tr>
      <tr><td>Web Reader MCP</td><td>Fetch and normalize selected web sources</td></tr>
      <tr><td>Zread MCP</td><td>Repository and code-reading capability supplied by ZAI</td></tr>
      <tr><td>Image generation API</td><td>Create outbound images when requested or useful</td></tr>
      <tr><td>Video generation API</td><td>Create outbound video within provider limits</td></tr>
      <tr><td>Agent, Conversation, and file APIs</td><td>Bounded subordinate tasks and their file lifecycle</td></tr>
    </tbody>
  </table>
  <ul>
    <li>The official MCP client SDK is an allowed agent runtime dependency.</li>
    <li>ZAI MCP server packages are exact lockfile dependencies; runtime <code>npx @latest</code> is forbidden.</li>
    <li>pi remains the sole resident identity, session, wake queue, memory owner, and tool authority.</li>
    <li>ZAI Agent and Conversation APIs may perform explicit bounded delegated tasks only; they never become independent residents or replace the pi session.</li>
    <li>Each wrapper validates input, enforces current documented provider limits, uses bounded deadlines and retries, records usage, and returns a stable result envelope.</li>
    <li>Provider-specific credentials are normally consumed by dedicated clients, not inherited by bash. Root and self-modifiable code make this ergonomic isolation, not a secrecy boundary.</li>
  </ul>
  <p>
    The primary model is selected by required capabilities rather than frozen forever in this specification.
    Its exact provider and model ID are pinned in deployed configuration.
    The initial implementation uses ZAI and has no automatic second-provider fallback;
    wakes persist through provider outages.
  </p>
</section>

<section id="web">
  <h2>Web service</h2>
  <ul>
    <li>Backend: TypeScript on <code>node:http</code> and <code>node:sqlite</code>, with no runtime npm dependencies.</li>
    <li>Frontend: handmade TypeScript, DOM APIs, and SSE, bundled at build time without a framework.</li>
    <li>Documentation: Markdown rendered at build time and served as immutable static assets.</li>
    <li>Routes: public-listener service readiness, static frontend and docs, bounded JSON timeline/recall views, retained-media streaming, and resumable timeline SSE; operational APIs remain pod-local.</li>
    <li>Surface: read-only timeline, conversations, retained media, deployment history, health, model budget, and docs.</li>
    <li>Authentication: Oliver-only at the reverse proxy. TLS without authorization is insufficient.</li>
    <li>Network: the application port is reachable from the reverse proxy, not published as a direct unauthenticated host service.</li>
    <li>Accessibility: WCAG 2.2 AA is binding, including semantic HTML, keyboard operation, visible focus, contrast, and reduced-motion support.</li>
  </ul>
  <p>
    SSE reconnect uses durable event IDs and resumes without silently skipping committed events.
    Public routes have bounded pagination and response sizes;
    cold queries may be slower but must not block readiness or live-event delivery.
  </p>
</section>

<section id="security">
  <h2>Security and credentials</h2>
  <table>
    <caption>Credential inventory and maximum scope</caption>
    <thead><tr><th scope="col">Credential</th><th scope="col">Consumer</th><th scope="col">Scope</th></tr></thead>
    <tbody>
      <tr><td>ZAI model and capability credentials</td><td>Agent runtime clients</td><td>will's configured provider account and external daily limits</td></tr>
      <tr><td>Telegram bot token</td><td>Agent Telegram runtime</td><td>Single bot; application accepts one configured group</td></tr>
      <tr><td>VCS deploy key</td><td>Agent Git client</td><td>Write only to will's repository</td></tr>
      <tr><td>Deployment rollback credential</td><td>Agent custom tool</td><td>Rollback current target to immediately previous revision only</td></tr>
      <tr><td>Deployment-event credential</td><td>Service polling client</td><td>Read-only deployment history for will's target</td></tr>
      <tr><td>Deployment notification credential</td><td>Deployment platform</td><td>Operational messages to the configured Telegram group; never mounted into either will container</td></tr>
      <tr><td>Registry publish and production deploy</td><td>Luci and deployment platform</td><td>will's registry namespace and production target; never mounted into either will container</td></tr>
      <tr><td>Reverse-proxy authentication</td><td>Host proxy</td><td>Oliver-only access; never mounted into will</td></tr>
    </tbody>
  </table>
  <p>
    will's unrestricted agent root and ability to rewrite its own code mean that any credential mounted
    into the agent container is ultimately extractable.
    Custom tools improve ergonomics and auditability, not secrecy.
    Safety therefore comes from external capability scope:
    compromise must not grant access to unrelated repositories, registry namespaces, deployment targets, or infrastructure.
  </p>
  <ul>
    <li>The service receives no model, Telegram, VCS, registry, or deployment-mutation credential.</li>
    <li>Operational and CI logs must never contain credentials, authentication headers, chat bodies, or raw tool output.</li>
    <li>Raw authenticated audit storage intentionally retains tool arguments, final results, messages, and model-emitted reasoning events, even if sensitive.</li>
    <li>Host storage and every backup copy containing raw audit, group chat, or media must be encrypted.</li>
    <li>All external payloads, MCP results, web content, and media analysis are untrusted data, never higher-priority instructions.</li>
  </ul>
</section>

<section id="quality">
  <h2>Quality and testing</h2>
  <h3>Code policy</h3>
  <ul>
    <li>TypeScript uses <code>strict</code>, <code>noUncheckedIndexedAccess</code>, <code>exactOptionalPropertyTypes</code>, <code>noImplicitOverride</code>, and <code>noFallthroughCasesInSwitch</code>.</li>
    <li><code>any</code> requires a written local reason.</li>
    <li>Service and web runtime use platform APIs only; agent runtime dependencies are limited to the pinned pi tree, official MCP SDK, pinned ZAI MCP packages, and dependencies required by those trees.</li>
    <li>will code is bundled; pi and MCP dependency trees remain normally installed so dynamic resources and package paths work.</li>
    <li>No custom implementation duplicates an existing pi facility without a documented capability gap.</li>
    <li>Errors are handled, rethrown with context, or recorded; empty catch blocks are forbidden.</li>
  </ul>

  <h3>Binding deployment gates</h3>
  <table>
    <caption>Checks required for every production revision</caption>
    <thead><tr><th scope="col">Gate</th><th scope="col">Required evidence</th></tr></thead>
    <tbody>
      <tr><td>Mechanical</td><td>Biome format/lint and strict TypeScript</td></tr>
      <tr><td>Unit</td><td>Pure policy, validation, wake scheduling, API shaping, migration helpers</td></tr>
      <tr><td>Integration</td><td>SQLite worker, durable spool, archive publication, Telegram queue, private APIs, process shutdown</td></tr>
      <tr><td>Property</td><td>Pure state laws only: idempotency, ordering, retention partition, rollback-policy model, compatibility-model invariants</td></tr>
      <tr><td>Snapshot</td><td>Stable serialization contracts without timestamps, paths, IDs, or other volatile values</td></tr>
      <tr><td>Coverage</td><td>Reviewed per-component committed ratchets that never decrease accidentally</td></tr>
      <tr><td>Image</td><td>Both final images built from the recorded commit and pinned bases</td></tr>
      <tr><td>Pod boot</td><td>Two-container Quadlet-equivalent test with fake providers, volumes, secrets, readiness, and shutdown</td></tr>
      <tr><td>Compatibility</td><td>The exact current-production baseline images create state, candidate migrates and writes representative new state, then those baseline images resume and pass readiness and regression probes</td></tr>
    </tbody>
  </table>
  <ul>
    <li>Browser end-to-end and automated accessibility tests gate relevant service, web, or documentation changes.</li>
    <li>Mutation testing runs weekly as a diagnostic on selected pure policy modules; it is not a production deployment gate.</li>
    <li>Filesystem, SQLite, process, network, and deployment-adapter behavior belongs to integration tests, never IO-free property tests.</li>
    <li>Regression fixes begin with a failing test and land with that test.</li>
    <li>Snapshot updates require a reason in the commit message.</li>
    <li>Ordinary release tests make no real model or ZAI service calls.</li>
    <li>A small scheduled, non-deploying ZAI canary verifies text, media, MCP, and API compatibility with bounded cost and secret-safe logs.</li>
  </ul>

  <h3>Audit and logs</h3>
  <ul>
    <li>Persist tool start and final result, complete result as delivered to the agent, messages, turns, compaction, retries, and custom capability usage.</li>
    <li>Transient tool-update events may stream live but are not retained forever as duplicate intermediate states.</li>
    <li>Service and agent emit structured JSON to stdout/stderr with timestamp, level, component, deployment revision, event, and correlation ID.</li>
    <li>Deployment platform owns operational log collection and retention.</li>
  </ul>
</section>

<section id="layout">
  <h2>Project layout</h2>
  <div class="layout">will/
├── AGENTS.md                  root vocabulary, boundaries, test commands
├── LICENSE                    MIT
├── README.md                  purpose, development entry points, spec pointer
├── spec.html                  this design of record
├── mise.toml                  exact tools and canonical tasks
├── package.json               one package
├── package-lock.json          pinned pi, MCP, ZAI, build and test trees
├── biome.json
├── tsconfig.json
├── coverage-ratchet.json
├── mutation-baseline.json
├── Containerfile.service      service-base and will-service targets
├── Containerfile.agent        agent-base and will-agent targets
├── compose.yaml               local two-container development body
├── .ci/
│   └── *.kdl                  Luci validation, build, canary, deploy submission
├── deploy/
│   ├── contract.md            required external deployment semantics
│   ├── revision.schema.json   will-side revision envelope
│   ├── quadlet/               production pod and container templates
│   └── fake/                  integration-test adapter
├── persona/
│   └── *.md                   immutable deployed personality
├── .pi/
│   ├── settings.json          local development Pi only; never packaged
│   └── skills/&lt;name&gt;/SKILL.md  packaged runtime abilities
├── docs/
│   ├── architecture.md
│   ├── deployment.md
│   ├── data.md
│   ├── agent.md
│   ├── media.md
│   └── test.md
├── src/
│   ├── service/
│   │   └── AGENTS.md
│   ├── agent/
│   │   └── AGENTS.md
│   └── shared/
│       └── AGENTS.md
└── web/
    └── AGENTS.md</div>
  <p>
    The writable production worktree has the same repository shape,
    but operational persona, skills, prompts, models, and AGENTS.md are loaded
    from the immutable agent image until a later revision is deployed.
  </p>
</section>

<section id="acceptance">
  <h2>Acceptance criteria</h2>
  <dl class="criteria">
    <dt>AC1 · Proven revision</dt>
    <dd>A default-branch commit can deploy only through a Luci-issued record binding that commit, two immutable image digests, build identity, and every required green gate.</dd>

    <dt>AC2 · Two-image body</dt>
    <dd>Production runs one rootless Quadlet pod containing one non-root, read-only service container with all Linux capabilities dropped and one <code>will</code> agent container with passwordless sudo; their persistent volumes and secrets are separate.</dd>

    <dt>AC3 · Host boundary</dt>
    <dd>The agent has no host Podman socket, systemd bus, Quadlet paths, registry credentials, service volume, or unrelated infrastructure credential; direct registry push and direct host deployment from inside the body fail.</dd>

    <dt>AC4 · Boot and readiness</dt>
    <dd>Both containers become ready within 60 seconds on valid persisted state; service proves migrations, writable SQLite, archive metadata, private API, and routes; agent proves local session/worktree, service connectivity, Telegram-loop initialization, and configuration.</dd>

    <dt>AC5 · Probation</dt>
    <dd>A candidate is promoted only after both components remain continuously ready for five minutes with zero process restarts.</dd>

    <dt>AC6 · Automatic rollback</dt>
    <dd>When an eligible rollback target exists, any startup or probation failure restores the exact current revision captured for that candidate, preserves all persistent state, verifies recovery, records and notifies the failure, and closes deployment behind a reconciliation barrier; v0 failure stops and alerts without pretending rollback occurred.</dd>

    <dt>AC7 · Late rollback reconciliation</dt>
    <dd>The agent can explicitly restore only the previous promoted revision through a scoped tool; afterward no descendant deploys until a revert commit naming the failed revision is accepted as reconciliation.</dd>

    <dt>AC8 · Runtime restart</dt>
    <dd>After promotion, component failure follows platform restart and alert policy without automatic image rollback; service and agent restart independently without corrupting queues, sessions, or SQLite.</dd>

    <dt>AC9 · No data rollback</dt>
    <dd>No deployment or image rollback replaces database, archive, media, session, queue, cursor, or worktree data.</dd>

    <dt>AC10 · Previous compatibility</dt>
    <dd>CI evidence identifies the exact production-baseline revision and image digests tested; those images can resume after the candidate has migrated and written representative state in every persistent format, and the platform rejects evidence whose baseline no longer matches current production.</dd>

    <dt>AC11 · Durable audit</dt>
    <dd>Each tool invocation, final result, message, wake, retry, compaction, ZAI capability call, and deployment event appears idempotently in the authenticated timeline with runtime and worktree provenance.</dd>

    <dt>AC12 · Data lifecycle</dt>
    <dd>Audit moves from SQLite to versioned archives after 90 days, chat after one year, and both remain queryable forever without duplicate hot/cold results; media is content-addressed and retained with chat.</dd>

    <dt>AC13 · Storage safety</dt>
    <dd>Each volume owner warns below 20% free space; below 10%, deterministic service and agent controls stop nonessential writes, ordinary model work, coding mutations, and media transfers while reserved capacity preserves bounded queue, audit, checkpoint, recovery, integrity, and notification writes.</dd>

    <dt>AC14 · Backup</dt>
    <dd>Encrypted host backups meet a one-hour RPO, retain 24 hourly, 30 daily, and 12 monthly generations, and pass a complete isolated restore test each month.</dd>

    <dt>AC15 · Resident session</dt>
    <dd>One pi session persists for an agent process and resumes across restart, deploy, and rollback; operational resources change only after immutable deployment; nested AGENTS.md files load on demand.</dd>

    <dt>AC16 · Wake semantics</dt>
    <dd>Failure wakes outrank FIFO chat, which outranks one coalesced heartbeat; chat never steers an active turn; heartbeat runs at most once per UTC hour without backfill; no model calls occur between wakes.</dd>

    <dt>AC17 · Turn recovery and budget</dt>
    <dd>Model failures use at most three immediate attempts; a 30-minute turn timeout durably schedules exactly one recovery wake without blind replay, whether cancellation succeeds in-process or requires platform restart; externally configured daily and recovery budgets stop further calls predictably.</dd>

    <dt>AC18 · Telegram group</dt>
    <dd>Every authorized update delivered by Telegram and every locally persisted outbound message is durably archived; model-facing mentions/replies/commands wake will, deterministic commands do not, all group members may prompt it, sender/reply context is preserved, ingestion gaps are reported, and outbound delivery is at least once.</dd>

    <dt>AC19 · Media</dt>
    <dd>Supported inbound media is preserved, displayed, and automatically understood on triggering use through curated ZAI tools; outbound text and media work; limits and unsupported inputs fail explicitly; repeated analysis uses the versioned content cache.</dd>

    <dt>AC20 · ZAI tools</dt>
    <dd>The actual tool registry exposes only the curated pinned ZAI capability set through stable will-owned schemas; ZAI subordinate agents cannot replace or outlive the resident pi authority.</dd>

    <dt>AC21 · Web boundary</dt>
    <dd>The reverse proxy enforces Oliver-only authentication; the interface is read-only, has no direct unauthenticated host exposure, resumes SSE correctly, and meets WCAG 2.2 AA.</dd>

    <dt>AC22 · Logs and secrets</dt>
    <dd>Operational and public CI logs contain no credentials, authentication headers, chat bodies, or raw tool results; raw audit is available only through encrypted storage and the authenticated interface.</dd>

    <dt>AC23 · Release gates</dt>
    <dd>Mechanical, unit, integration, pure property, snapshot, coverage, both-image, and pod-boot gates pass for every deployed revision; previous-compatibility also passes except for the explicit v0 bootstrap case, which is tested from empty bootstrap state; relevant browser/accessibility tests also pass.</dd>

    <dt>AC24 · External adapters</dt>
    <dd>All will-side deployment behavior passes against the fake contract adapter, and the same conformance suite verifies the production Luci/Quadlet adapter before autonomous deployment is enabled.</dd>

    <dt>AC25 · Bootstrap</dt>
    <dd>A human-observed v0 is healthy, promoted, backed up, and recorded as the current revision and eligible rollback target for the next candidate before autonomous default-branch production deployment begins.</dd>
  </dl>
</section>

<section id="open">
  <h2>Open implementation items</h2>
  <ul>
    <li>Define the exact versioned HTTP schemas for deployment revision submission, cursor-based deployment events, rollback, reconciliation, and conformance testing. Platform internals remain out of scope.</li>
    <li>Extend Luci separately so it can build and boot-test the two-image pod, publish both digests, issue the trusted revision record, and call the generic deployment API.</li>
    <li>Select and pin the initial capability-compatible ZAI primary model and exact MCP/API package versions.</li>
    <li>Record current provider media limits in generated tool metadata and tests rather than freezing volatile values in this specification.</li>
    <li>Define the configured daily usage cap and the smaller recovery reserve in production deployment configuration.</li>
    <li>Choose the host reverse-proxy authentication mechanism that satisfies Oliver-only access.</li>
    <li>Implement and verify encrypted host backup integration and monthly isolated restore automation.</li>
  </ul>
</section>

</main>

<footer class="page-foot">
  Design of record for the will project,
  rev 3, 2026-09-12.
  Acceptance criteria, required contracts, and statements marked <strong>must</strong> are binding;
  all other statements are settled defaults unless amended.
</footer>
</div>
</body>
</html>