Skip to content

Instantly share code, notes, and snippets.

@dmarzzz
Created May 8, 2026 02:34
Show Gist options
  • Select an option

  • Save dmarzzz/c860f838b433a60a32ae6885ee4b7b14 to your computer and use it in GitHub Desktop.

Select an option

Save dmarzzz/c860f838b433a60a32ae6885ee4b7b14 to your computer and use it in GitHub Desktop.
Shape Rotator OS — Spec Walkthrough · v0.1 + amendments (2026-05-07)
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Shape Rotator OS — Spec Walkthrough · v0.1 + amendments</title>
<style>
:root {
--paper: #F1ECE7;
--paper-2: rgba(241,236,231,0.72);
--paper-3: rgba(241,236,231,0.55);
--paper-4: rgba(241,236,231,0.30);
--rule: rgba(241,236,231,0.10);
--rule-2: rgba(241,236,231,0.18);
--char: #1D191A;
--char-2: #2C2728;
--oxide: #8F220E;
--oxide-b: #B43A19;
--cyan: #8FEAF2;
--warn: #E6B97A;
--green: #6FBD8C;
--purple: #C28BFF;
--mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
--sans: "Geist", "Space Grotesk", -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
}
* { box-sizing: border-box; }
html, body { background: var(--char); color: var(--paper); margin: 0; }
body {
font-family: var(--sans);
font-size: 15px;
line-height: 1.6;
letter-spacing: -0.005em;
}
body::before {
content: "";
position: fixed; inset: 0; pointer-events: none; z-index: 0;
opacity: 0.035; mix-blend-mode: screen;
background-image:
radial-gradient(rgba(241,236,231,0.9) 1px, transparent 1px),
radial-gradient(rgba(241,236,231,0.6) 1px, transparent 1px);
background-size: 3px 3px, 7px 7px;
background-position: 0 0, 1px 2px;
}
.layout {
position: relative; z-index: 1;
display: grid;
grid-template-columns: 240px 1fr;
max-width: 1200px;
margin: 0 auto;
padding: 0;
}
nav.toc {
position: sticky; top: 0;
align-self: start;
height: 100vh;
padding: 36px 18px 36px 36px;
border-right: 1px solid var(--rule);
overflow-y: auto;
}
nav.toc h3 {
font-family: var(--mono);
font-size: 10px; letter-spacing: 0.22em; text-transform: uppercase;
color: var(--paper-3);
margin: 0 0 14px;
}
nav.toc ol {
list-style: none;
padding: 0; margin: 0;
counter-reset: section;
}
nav.toc li { counter-increment: section; padding: 4px 0; }
nav.toc a {
color: var(--paper-2);
text-decoration: none;
font-family: var(--mono);
font-size: 11.5px;
letter-spacing: 0.04em;
display: block;
padding: 4px 0;
border-left: 2px solid transparent;
padding-left: 10px;
margin-left: -10px;
transition: color 160ms ease, border-color 160ms ease;
}
nav.toc a::before {
content: counter(section, decimal-leading-zero) " ";
color: var(--oxide-b);
}
nav.toc a:hover { color: var(--paper); border-left-color: var(--oxide-b); }
nav.toc .toc-foot {
margin-top: 28px;
font-family: var(--mono);
font-size: 9.5px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--paper-4);
}
main { padding: 48px 64px 120px; }
header.hero { padding-bottom: 36px; border-bottom: 1px solid var(--rule); margin-bottom: 36px; }
header.hero .kicker {
font-family: var(--mono);
font-size: 10.5px; letter-spacing: 0.22em; text-transform: uppercase;
color: var(--oxide-b);
margin-bottom: 14px;
}
header.hero h1 {
font-family: var(--sans);
font-size: 56px;
line-height: 1;
letter-spacing: -0.02em;
font-weight: 500;
margin: 0 0 14px;
color: var(--paper);
}
header.hero .sub {
font-family: var(--mono);
font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase;
color: var(--paper-3);
margin-bottom: 18px;
}
header.hero .lede {
max-width: 60ch;
color: var(--paper-2);
font-size: 16px;
}
header.hero .amend {
margin-top: 22px;
padding: 14px 18px;
border-left: 3px solid var(--green);
background: rgba(111,189,140,0.06);
max-width: 70ch;
}
header.hero .amend .tag {
font-family: var(--mono);
font-size: 9.5px; letter-spacing: 0.22em; text-transform: uppercase;
color: var(--green);
margin-bottom: 6px;
}
header.hero .amend ul { margin: 6px 0 0 0; padding-left: 18px; color: var(--paper-2); font-size: 14px; }
header.hero .amend li { margin: 4px 0; }
section {
padding: 30px 0 14px;
border-top: 1px solid var(--rule);
counter-increment: maj;
}
section:first-of-type { border-top: 0; padding-top: 0; }
section h2 {
font-family: var(--sans);
font-size: 28px;
letter-spacing: -0.014em;
font-weight: 500;
margin: 0 0 6px;
color: var(--paper);
}
section h2::before {
content: counter(maj, decimal-leading-zero) " · ";
color: var(--oxide-b);
font-family: var(--mono);
font-size: 18px;
letter-spacing: 0.10em;
vertical-align: middle;
margin-right: 8px;
}
section .section-sub {
font-family: var(--mono);
font-size: 10.5px; letter-spacing: 0.18em; text-transform: uppercase;
color: var(--paper-3);
margin: 0 0 22px;
}
section h3 {
font-family: var(--sans);
font-size: 18px;
letter-spacing: -0.01em;
margin: 24px 0 8px;
font-weight: 500;
}
p { margin: 12px 0; max-width: 70ch; color: var(--paper-2); }
em { color: var(--oxide-b); font-style: normal; }
strong { color: var(--paper); font-weight: 500; }
a { color: var(--cyan); }
code {
font-family: var(--mono);
font-size: 0.92em;
color: var(--paper);
background: rgba(241,236,231,0.05);
padding: 1px 5px;
border-radius: 3px;
}
pre {
font-family: var(--mono);
font-size: 12.5px;
line-height: 1.55;
background: rgba(0,0,0,0.30);
border: 1px solid var(--rule);
border-left: 2px solid var(--oxide);
padding: 14px 18px;
overflow-x: auto;
color: var(--paper);
border-radius: 3px;
}
pre .k { color: var(--cyan); } /* keys */
pre .s { color: var(--warn); } /* strings */
pre .n { color: var(--oxide-b); } /* numbers */
pre .c { color: var(--paper-3); } /* comments */
pre .x { color: var(--purple); } /* keywords */
.callout {
margin: 22px 0;
padding: 14px 18px;
border-left: 3px solid var(--paper-3);
background: rgba(241,236,231,0.025);
font-size: 14px;
line-height: 1.55;
}
.callout.locked {
border-left-color: var(--oxide);
background: rgba(143,34,14,0.06);
}
.callout.decide {
border-left-color: var(--warn);
background: rgba(230,185,122,0.06);
}
.callout.note {
border-left-color: var(--cyan);
background: rgba(143,234,242,0.04);
}
.callout.resolved {
border-left-color: var(--green);
background: rgba(111,189,140,0.05);
}
.callout-tag {
font-family: var(--mono);
font-size: 9.5px; letter-spacing: 0.22em; text-transform: uppercase;
margin-bottom: 6px;
}
.callout.locked .callout-tag { color: var(--oxide-b); }
.callout.decide .callout-tag { color: var(--warn); }
.callout.note .callout-tag { color: var(--cyan); }
.callout.resolved .callout-tag { color: var(--green); }
table {
width: 100%; max-width: 100%;
border-collapse: collapse;
margin: 18px 0;
font-size: 13.5px;
}
th, td {
text-align: left;
padding: 10px 14px;
border-bottom: 1px solid var(--rule);
vertical-align: top;
}
th {
font-family: var(--mono);
font-size: 10px; letter-spacing: 0.18em; text-transform: uppercase;
color: var(--paper-3);
font-weight: normal;
border-bottom-color: var(--rule-2);
}
td.mono { font-family: var(--mono); font-size: 12.5px; color: var(--paper); }
td .pill {
display: inline-block;
font-family: var(--mono);
font-size: 9px; letter-spacing: 0.18em; text-transform: uppercase;
padding: 2px 8px;
border-radius: 999px;
border: 1px solid currentColor;
}
.pill.user { color: var(--purple); }
.pill.dmarz { color: var(--cyan); }
.pill.shared { color: var(--warn); }
.pill.exists { color: var(--paper-3); }
.pill.new { color: var(--green); }
.arch {
width: 100%;
height: auto;
max-height: 480px;
margin: 24px 0;
background: rgba(0,0,0,0.20);
border: 1px solid var(--rule);
border-radius: 3px;
}
.arch text {
font-family: var(--mono);
font-size: 11px; letter-spacing: 0.10em;
fill: var(--paper);
}
.arch text.label {
font-size: 9px; letter-spacing: 0.20em; text-transform: uppercase;
fill: var(--paper-3);
}
.arch rect.box {
fill: rgba(241,236,231,0.04);
stroke: var(--rule-2);
stroke-width: 1;
}
.arch rect.box-source {
fill: rgba(143,34,14,0.10);
stroke: var(--oxide);
}
.arch rect.box-hub {
fill: rgba(143,234,242,0.06);
stroke: var(--cyan);
}
.arch line.flow { stroke: var(--paper-3); stroke-width: 1; }
.arch line.flow.encrypted { stroke: var(--oxide-b); stroke-width: 1.4; stroke-dasharray: 4 3; }
.arch line.flow.cohort { stroke: var(--cyan); stroke-width: 1; }
.arch polygon.arrow { fill: var(--paper-3); }
.arch polygon.arrow.encrypted { fill: var(--oxide-b); }
.arch polygon.arrow.cohort { fill: var(--cyan); }
/* Naming options grid (post-decision view) */
.name-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 14px;
margin: 22px 0;
}
.name-card {
padding: 16px 18px;
background: rgba(241,236,231,0.025);
border: 1px solid var(--rule);
border-radius: 3px;
transition: opacity 200ms ease;
}
.name-card.chosen {
border-color: var(--green);
background: rgba(111,189,140,0.07);
}
.name-card.faded { opacity: 0.42; }
.name-card .pair {
font-family: var(--mono);
font-size: 13px;
color: var(--paper);
margin-bottom: 10px;
}
.name-card .pair .a { color: var(--cyan); }
.name-card .pair .b { color: var(--oxide-b); }
.name-card .vibe {
font-size: 13px;
color: var(--paper-2);
margin-bottom: 6px;
}
.name-card .meta {
font-family: var(--mono);
font-size: 9.5px; letter-spacing: 0.18em; text-transform: uppercase;
color: var(--paper-3);
}
.name-card.chosen .meta { color: var(--green); }
/* Phase timeline */
.timeline {
display: grid;
grid-template-columns: 80px 120px 1fr;
gap: 0 18px;
margin: 22px 0;
font-size: 13.5px;
}
.timeline .cell { padding: 10px 0; border-bottom: 1px solid var(--rule); }
.timeline .ph {
font-family: var(--mono);
font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase;
color: var(--oxide-b);
}
.timeline .when { font-family: var(--mono); font-size: 11px; color: var(--paper-3); }
.timeline .what { color: var(--paper-2); }
.scroll-hint {
font-family: var(--mono);
font-size: 9.5px; letter-spacing: 0.22em; text-transform: uppercase;
color: var(--paper-4);
margin-top: 60px;
text-align: center;
}
.strike {
text-decoration: line-through;
color: var(--paper-4);
}
</style>
</head>
<body>
<div class="layout">
<nav class="toc" aria-label="contents">
<h3>walkthrough</h3>
<ol>
<li><a href="#picture">the picture</a></li>
<li><a href="#components">components</a></li>
<li><a href="#bundle">the bundle envelope</a></li>
<li><a href="#visibility">surface &amp; depth</a></li>
<li><a href="#encryption">encryption &amp; key reservoir</a></li>
<li><a href="#interfaces">the interfaces (locked)</a></li>
<li><a href="#voxterm">voxterm hivemind</a></li>
<li><a href="#phases">phasing</a></li>
<li><a href="#decisions">decisions</a></li>
</ol>
<div class="toc-foot">
v0.1 + amendments<br/>
2026-05-07<br/>
docs/SHAPE-ROTATOR-OS-SPEC.md
</div>
</nav>
<main>
<header class="hero">
<div class="kicker">SHAPE ROTATOR · OS · SPEC WALKTHROUGH</div>
<h1>The architecture of the shape, in one sitting.</h1>
<div class="sub">v0.1 + amendments · 9 sections · ~6 min read</div>
<p class="lede">
The full spec is in <code>docs/SHAPE-ROTATOR-OS-SPEC.md</code>. This page is the human walkthrough — same content, easier to react to. <em>Locked contracts</em> are the surfaces between code owned by different developers; they only change by spec amendment.
</p>
<div class="amend">
<div class="tag">AMENDMENTS · 2026-05-07</div>
<ul>
<li>Cohort bundle kinds: <code>cohort.public</code> → <code>cohort.surface</code>, <code>cohort.full</code> → <code>cohort.depth</code>.</li>
<li>Depth payloads are <em>opaque</em>. Build pipeline encrypts the raw markdown file; alchemist tooling parses the plaintext directly. No JSON schema enforced at the bundle level — agents compute over blobs.</li>
<li>Voxterm clients do not sign. Each install generates a v4 UUID <code>device_id</code> on first launch; convent sink wraps + signs.</li>
</ul>
</div>
</header>
<section id="picture">
<h2>The picture</h2>
<p class="section-sub">five repos · one private data substrate · one always-on box at the convent</p>
<p>
Markdown files in a private git repo are the <em>source of truth</em>. A build script on the convent box turns them into signed swf-node bundles — <code>cohort.surface</code> (JSON, unencrypted, public-visible) and <code>cohort.depth</code> (opaque ciphertext, the raw markdown file age-encrypted to the alchemist key reservoir). swf-node propagates over LAN via mDNS. The cohort viz reads only surface; the alchemist app holds keys and decrypts depth. Voxterm dumps transcripts to a hivemind sink that lives in the same swf-node daemon.
</p>
<svg class="arch" viewBox="0 0 920 360" xmlns="http://www.w3.org/2000/svg">
<defs>
<marker id="ah" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
<polygon points="0,0 10,5 0,10" class="arrow" />
</marker>
<marker id="ahe" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
<polygon points="0,0 10,5 0,10" class="arrow encrypted" />
</marker>
<marker id="ahc" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
<polygon points="0,0 10,5 0,10" class="arrow cohort" />
</marker>
</defs>
<rect class="box box-source" x="40" y="30" width="240" height="80" rx="3"/>
<text class="label" x="160" y="52" text-anchor="middle">SOURCE OF TRUTH</text>
<text x="160" y="72" text-anchor="middle">shape-rotator-cohort-data</text>
<text x="160" y="92" text-anchor="middle" fill="rgba(241,236,231,0.55)" font-size="10">private git · markdown</text>
<rect class="box box-hub" x="340" y="20" width="240" height="100" rx="3"/>
<text class="label" x="460" y="42" text-anchor="middle">CONVENT BOX (always on)</text>
<text x="460" y="62" text-anchor="middle">swf-node + hivemind sink</text>
<text x="460" y="82" text-anchor="middle" fill="rgba(241,236,231,0.55)" font-size="10">post-receive hook → build-bundles</text>
<text x="460" y="100" text-anchor="middle" fill="rgba(241,236,231,0.55)" font-size="10">propagates over mDNS LAN</text>
<rect class="box" x="640" y="30" width="240" height="80" rx="3"/>
<text class="label" x="760" y="52" text-anchor="middle">VOXTERM CLIENTS</text>
<text x="760" y="72" text-anchor="middle">hivemind mode</text>
<text x="760" y="92" text-anchor="middle" fill="rgba(241,236,231,0.55)" font-size="10">UUID device_id · batched POST</text>
<line class="flow" x1="280" y1="70" x2="340" y2="70" marker-end="url(#ah)"/>
<line class="flow" x1="640" y1="70" x2="580" y2="70" marker-end="url(#ah)"/>
<rect class="box" x="40" y="200" width="260" height="120" rx="3"/>
<text class="label" x="170" y="222" text-anchor="middle">PARTICIPANT APP</text>
<text x="170" y="245" text-anchor="middle">shape-rotator-wrld-knwldge-viz</text>
<text x="170" y="265" text-anchor="middle" fill="rgba(241,236,231,0.55)" font-size="10">reads: surface bundles only</text>
<text x="170" y="282" text-anchor="middle" fill="rgba(241,236,231,0.55)" font-size="10">no keys; depth bytes are noise</text>
<text x="170" y="299" text-anchor="middle" fill="rgba(241,236,231,0.55)" font-size="10">surfaces: atlas / search / network / alchemy</text>
<rect class="box" x="340" y="200" width="260" height="120" rx="3"/>
<text class="label" x="470" y="222" text-anchor="middle">ALCHEMIST APP (new)</text>
<text x="470" y="245" text-anchor="middle">shape-rotator-alchemist</text>
<text x="470" y="265" text-anchor="middle" fill="rgba(241,236,231,0.55)" font-size="10">decrypts depth → parses markdown</text>
<text x="470" y="282" text-anchor="middle" fill="rgba(241,236,231,0.55)" font-size="10">writes: edits → markdown → bundles</text>
<text x="470" y="299" text-anchor="middle" fill="rgba(241,236,231,0.55)" font-size="10">surfaces: CRM editor + ledger only</text>
<rect class="box" x="640" y="200" width="240" height="120" rx="3"/>
<text class="label" x="760" y="222" text-anchor="middle">ANY OTHER VIEWER</text>
<text x="760" y="245" text-anchor="middle">future swf-node clients</text>
<text x="760" y="265" text-anchor="middle" fill="rgba(241,236,231,0.55)" font-size="10">agent bots / matrix bridges</text>
<text x="760" y="282" text-anchor="middle" fill="rgba(241,236,231,0.55)" font-size="10">consume same bundle stream</text>
<line class="flow cohort" x1="430" y1="120" x2="170" y2="200" marker-end="url(#ahc)"/>
<line class="flow encrypted" x1="460" y1="120" x2="470" y2="200" marker-end="url(#ahe)"/>
<line class="flow cohort" x1="490" y1="120" x2="760" y2="200" marker-end="url(#ahc)"/>
<line class="flow cohort" x1="40" y1="345" x2="80" y2="345"/>
<text class="label" x="90" y="349">SURFACE BUNDLE (PUBLIC, JSON)</text>
<line class="flow encrypted" x1="380" y1="345" x2="420" y2="345"/>
<text class="label" x="430" y="349">DEPTH BUNDLE (ENCRYPTED, OPAQUE)</text>
</svg>
<div class="callout note">
<div class="callout-tag">PRINCIPLE</div>
Encryption is the boundary, not the source-tree. Both Electron apps could in principle hold the same source code; what differentiates them is the keys their user holds. We ship two apps anyway because the alchemist app is much smaller and exposes only steward UI — but the security model doesn't depend on keeping source private.
</div>
</section>
<section id="components">
<h2>Components</h2>
<p class="section-sub">five repos · one operational box · clear ownership</p>
<table>
<thead>
<tr><th>repo</th><th>status</th><th>scope</th><th>owner</th></tr>
</thead>
<tbody>
<tr>
<td class="mono">searxng-wth-frnds</td>
<td><span class="pill exists">EXISTING</span></td>
<td>swf-node daemon. Extended with <code>cohort.surface</code> · <code>cohort.depth</code> · <code>transcript.batch</code> bundle handling, age verification helpers, signature verification, and the HTTP API additions.</td>
<td><span class="pill user">YOU</span></td>
</tr>
<tr>
<td class="mono">shape-rotator-wrld-knwldge-viz</td>
<td><span class="pill exists">EXISTING</span></td>
<td>Cohort participant Electron app. Atlas + Network + Search + Metrics + Alchemy(surface only).</td>
<td><span class="pill dmarz">DMARZ</span></td>
</tr>
<tr>
<td class="mono">shape-rotator-alchemist</td>
<td><span class="pill new">NEW</span></td>
<td>Slim alchemist Electron app. CRM editor (edits markdown), transcripts review, ledger. No search/atlas/etc — alchemists run the participant app for that.</td>
<td><span class="pill dmarz">DMARZ</span></td>
</tr>
<tr>
<td class="mono">shape-rotator-cohort-data</td>
<td><span class="pill new">NEW</span></td>
<td>Private git repo. Markdown source of truth. <code>schema.yml</code>, <code>.reservoir.yml</code>, <code>.alchemists.yml</code>, plus <code>people/</code>, <code>teams/</code>, <code>clusters/</code>.</td>
<td><span class="pill shared">ANDREW + TINA + DMARZ</span></td>
</tr>
<tr>
<td class="mono">voxterm</td>
<td><span class="pill exists">EXISTING</span></td>
<td>Voice transcription. Extended with hivemind mode (mDNS probe + UUID generation + batched POST to convent sink). No signing key.</td>
<td><span class="pill user">YOU</span></td>
</tr>
</tbody>
</table>
<h3>The convent box</h3>
<p>
One always-on machine at the Convent. Runs swf-node (primary peer + transcript sink), hosts a <code>cohort-data</code> clone with a post-receive hook that runs <code>build-bundles.js</code>. If it goes down, peers continue with cached data; new bundles don't propagate until it's back. Acceptable failure mode for v1.
</p>
</section>
<section id="bundle">
<h2>The bundle envelope</h2>
<p class="section-sub">spec §3.1 · the wire format · LOCKED CONTRACT</p>
<p>Every cohort or transcript bundle on swf-node has the same envelope. Only the <code>kind</code> and the payload semantics differ.</p>
<pre><span class="c">// canonical JSON · stringify with sorted keys, no whitespace</span>
{
<span class="k">"magic"</span>: <span class="s">"swf-bundle-v1"</span>,
<span class="k">"kind"</span>: <span class="s">"cohort.surface"</span> <span class="c">// or cohort.depth / transcript.batch / search.result</span>,
<span class="k">"record_id"</span>: <span class="s">"vaishnavi-dasika"</span>,
<span class="k">"version"</span>: <span class="n">7</span> <span class="c">// monotonic per record_id</span>,
<span class="k">"author"</span>: {
<span class="k">"pubkey"</span>: <span class="s">"ed25519:..."</span> <span class="c">// must be in .alchemists.yml</span>,
<span class="k">"signed_at"</span>: <span class="s">"2026-05-07T14:30:00Z"</span>
},
<span class="k">"prev_cid"</span>: <span class="s">"&lt;cid of v6, optional&gt;"</span>,
<span class="k">"encryption"</span>: <span class="x">null</span> <span class="c">// or { alg: "age-v1", recipients: [...] }</span>,
<span class="k">"payload"</span>: <span class="s">"&lt;base64 of plaintext OR opaque ciphertext&gt;"</span>,
<span class="k">"signature"</span>: <span class="s">"&lt;hex; ed25519 over canonical(everything-but-signature)&gt;"</span>
}</pre>
<p><strong>Payload semantics by kind:</strong></p>
<ul>
<li><code>cohort.surface</code> — plaintext JSON. Conventional fields per §3.4. Unencrypted.</li>
<li><code>cohort.depth</code> — <em>opaque</em> ciphertext. Plaintext format is a private convention between alchemist tooling and the build pipeline (currently: raw markdown file bytes). swf-node treats it as bytes only.</li>
<li><code>transcript.batch</code> — plaintext JSON per §3.5. Unencrypted by default (hivemind-visible).</li>
</ul>
<div class="callout locked">
<div class="callout-tag">LOCKED · §3.1</div>
Verifier MUST check: <code>author.pubkey</code> is in <code>.alchemists.yml</code>; signature is valid over the canonicalized bundle minus <code>signature</code>; <code>version</code> is strictly greater than any previously seen for this <code>record_id</code>. swf-node enforces these; apps don't need to.
</div>
</section>
<section id="visibility">
<h2>Surface &amp; depth</h2>
<p class="section-sub">spec §3.3 · two bundles per cohort record · naming resolved</p>
<p>
Each cohort record (a person, a team, a cluster) is published as <em>two</em> bundles. Surface carries a JSON projection of whitelisted fields, unencrypted, signed. Depth carries the entire raw markdown file, age-encrypted to the reservoir, signed. Same <code>record_id</code> on both; the suffix on <code>kind</code> distinguishes them.
</p>
<pre>cohort/people/vaishnavi-dasika.md
↓ build-bundles.js reads schema.yml
├─ <span class="k">cohort.surface</span> ← { name, team, role, geo, domain, links }
│ unencrypted, signed
│ cohort viz reads this
└─ <span class="k">cohort.depth</span> ← &lt;raw markdown file bytes&gt;
age-encrypted to reservoir,
signed. alchemist app decrypts → parses as markdown.</pre>
<h3>The naming question — resolved</h3>
<div class="name-grid">
<div class="name-card faded">
<div class="pair"><span class="a">cohort.public</span> + <span class="b">cohort.full</span></div>
<div class="vibe">flat, descriptive. accidentally implies public is "incomplete."</div>
<div class="meta">REJECTED</div>
</div>
<div class="name-card faded">
<div class="pair"><span class="a">cohort.public</span> + <span class="b">cohort.private</span></div>
<div class="vibe">privacy-tier framing.</div>
<div class="meta">REJECTED</div>
</div>
<div class="name-card chosen">
<div class="pair"><span class="a">cohort.surface</span> + <span class="b">cohort.depth</span></div>
<div class="vibe">geometric, on-shape-rotator-brand. surface is what you see; depth is what alchemists hold.</div>
<div class="meta">✓ CHOSEN</div>
</div>
<div class="name-card faded">
<div class="pair"><span class="a">cohort.specimen</span> + <span class="b">cohort.dossier</span></div>
<div class="vibe">museum-tag language.</div>
<div class="meta">REJECTED</div>
</div>
<div class="name-card faded">
<div class="pair"><span class="a">cohort.card</span> + <span class="b">cohort.dossier</span></div>
<div class="vibe">card = museum tag, dossier = file.</div>
<div class="meta">REJECTED</div>
</div>
<div class="name-card faded">
<div class="pair"><span class="a">cohort.face</span> + <span class="b">cohort.field</span></div>
<div class="vibe">editorial, shaperotator.xyz copy.</div>
<div class="meta">REJECTED</div>
</div>
</div>
<h3>What the schema says</h3>
<p>Single config file, <code>cohort-data/schema.yml</code>, declares which frontmatter fields are extracted into the surface bundle. Everything else stays in the depth bundle (which is the entire markdown file).</p>
<pre>schema_version: 1
people:
surface_fields: [record_id, record_type, name, team, role, geo, domain, links]
teams:
surface_fields: [record_id, record_type, name, focus, lead, geo, domain, shape, members, links]
clusters:
surface_fields: [record_id, record_type, name, label, teams, description]</pre>
<div class="callout note">
<div class="callout-tag">DEPTH IS A BLOB</div>
swf-node sees only <em>encrypted bytes</em> and metadata for depth — no schema validation on plaintext. The build pipeline currently emits raw markdown bytes, but that's a private convention. An alchemist agent can decrypt, re-shape (summarize, augment, redact), re-encrypt, and republish at v+1 without touching the spec or the swf-node API. This is the "compute over blobs" property.
</div>
<p><strong>Body content never appears in surface.</strong> If you want a public-visible blurb on a cluster, put it in a frontmatter field (see clusters' <code>description</code>).</p>
</section>
<section id="encryption">
<h2>Encryption &amp; key reservoir</h2>
<p class="section-sub">spec §3.6 · age + 20 pre-generated keypairs</p>
<p>
We pre-generate <em>20</em> X25519 keypairs at program start. Public keys are committed to <code>cohort-data/.reservoir.yml</code>. Private keys are distributed to alchemists individually as needed; unallocated keys go to cold storage. Every depth bundle is encrypted to <em>all 20</em> reservoir pubkeys.
</p>
<pre>schema_version: 1
generated_at: 2026-05-07T...
keys:
- id: alc-001 pubkey: age1xxxx... distributed_to: Andrew
- id: alc-002 pubkey: age1yyyy... distributed_to: Tina
- id: alc-003 pubkey: age1zzzz... distributed_to: dmarz
- id: alc-004 pubkey: age1qqqq... distributed_to: Alexis
- id: alc-005 pubkey: age1eeee... distributed_to: <span class="c">null # unallocated</span>
...
- id: alc-020 pubkey: age1tttt... distributed_to: <span class="c">null # vault</span></pre>
<div class="callout note">
<div class="callout-tag">WHY 20</div>
Way more than 4 alchemists. Lost-key recovery: hand the alchemist an unallocated key — old bundles still decrypt because they were encrypted to <em>all</em> reservoir pubkeys originally. New alchemist next year? Hand them an unallocated key. No re-encryption ever needed for additions. Cost: any of the 20 keys could in principle decrypt everything, so the unallocated 16 must stay in cold storage.
</div>
<p>
Compromised key: rotate by encrypting <em>new</em> bundles to the reservoir minus the compromised key. Bumps <code>reservoir_version</code> in <code>.reservoir.yml</code>. Old bundles encrypted to the compromised key remain readable by it — acceptable for a 10-week program.
</p>
<h3>Signing is separate</h3>
<p>
Signing uses <em>Ed25519</em> (different keypair from encryption). Each alchemist holds one signing keypair; pubkeys are in <code>cohort-data/.alchemists.yml</code>. Both apps embed this list at build time. Adding/removing an alchemist means an app rebuild — fine for a fixed-cohort 10-week program.
</p>
</section>
<section id="interfaces">
<h2>The interfaces (locked)</h2>
<p class="section-sub">spec §4 · the seams between parallel work · LOCKED CONTRACTS</p>
<p>These surfaces need to be agreed before swf-node and the apps begin in parallel. Once locked, change only by spec amendment.</p>
<h3>swf-node HTTP — read</h3>
<pre><span class="x">GET</span> /bundles?kind=cohort.surface&since=&lt;v&gt;&record_id=&lt;id&gt;
→ { bundles: [...], next_since: &lt;int|null&gt; }
<span class="x">GET</span> /bundles/by_cid/&lt;cid&gt;
→ &lt;single bundle envelope&gt;
<span class="x">GET</span> /bundles/subscribe?kind=cohort.surface <span class="c">// SSE</span>
event: bundle
data: { ...envelope... }</pre>
<h3>swf-node HTTP — write</h3>
<pre><span class="x">POST</span> /bundles
body: &lt;complete signed envelope&gt;
→ 201 { cid: "..." }
→ 400 if envelope malformed
→ 403 if author.pubkey not in alchemists list
→ 409 if version not strictly greater than known</pre>
<h3>Cohort app's sole entry point</h3>
<p>Inside the cohort viz, every renderer goes through one file:</p>
<pre><span class="c">// visualizer/src/renderer/cohort-source.js</span>
<span class="x">export async function</span> getCohortSurface(): <span class="x">Promise</span>&lt;{
people: SurfaceRecord[], teams: SurfaceRecord[], clusters: SurfaceRecord[],
}&gt;;
<span class="x">export function</span> subscribeToCohortChanges(
cb: (record) =&gt; <span class="x">void</span>
): () =&gt; <span class="x">void</span>;</pre>
<p>Today calls swf-node. Tomorrow could call anything. Renderers don't care.</p>
<h3>Alchemist app's richer interface</h3>
<pre><span class="x">export async function</span> getCohortDepth(): <span class="x">Promise</span>&lt;{
people, teams, clusters, transcripts, <span class="c">// each: { frontmatter, body, ...metadata }</span>
}&gt;;
<span class="x">export async function</span> publishRecord(
type, markdownContent, alchemistKey
): <span class="x">Promise</span>&lt;{ surface_cid, depth_cid }&gt;;
<span class="c">// gates on git push success — git is the conflict-resolver, not bundles.</span></pre>
<div class="callout locked">
<div class="callout-tag">LOCKED · §4</div>
These interfaces (HTTP API, voxterm hivemind protocol, cohort-source.js shape) are the <em>only</em> things that have to be right before parallel work starts. Everything else can iterate.
</div>
</section>
<section id="voxterm">
<h2>Voxterm hivemind</h2>
<p class="section-sub">spec §3.5 §4.3 · default-public · UUID identity · sink resigns</p>
<p>
Voxterm clients on the LAN probe via mDNS for service <code>_shape-rotator-hivemind._tcp.local</code>. The convent box advertises this when run with <code>--hivemind-sink</code>. On first launch each voxterm install generates a <em>v4 UUID</em>, persists to its config dir, and includes it as <code>origin_device</code> on every batch. Every ~60 seconds (or every 30 segments), voxterm posts:
</p>
<pre><span class="x">POST</span> /hivemind/transcripts
{
<span class="k">"record_id"</span>: <span class="s">"transcript-2026-05-07-1430-convent-room-a"</span>,
<span class="k">"batch_index"</span>: <span class="n">0</span>,
<span class="k">"started_at"</span>: <span class="s">"2026-05-07T14:30:00Z"</span>,
<span class="k">"ended_at"</span>: <span class="s">"2026-05-07T14:31:08Z"</span>,
<span class="k">"location"</span>: <span class="s">"convent-room-a"</span>,
<span class="k">"origin_device"</span>: <span class="s">"550e8400-e29b-41d4-a716-446655440000"</span>,
<span class="k">"segments"</span>: [
{ <span class="k">t</span>: <span class="n">0.0</span>, <span class="k">speaker</span>: <span class="s">"Tina"</span>, <span class="k">text</span>: <span class="s">"..."</span> },
{ <span class="k">t</span>: <span class="n">4.7</span>, <span class="k">speaker</span>: <span class="s">"Andrew"</span>, <span class="k">text</span>: <span class="s">"..."</span> }
]
}</pre>
<p>
The sink validates the schema, wraps in a <code>transcript.batch</code> envelope, signs with the convent box's alchemist Ed25519 key, posts to its own swf-node. Default visibility is <em>unencrypted</em> — hivemind public. An alchemist can later publish a redaction bundle to seal a meeting retroactively.
</p>
<div class="callout resolved">
<div class="callout-tag">RESOLVED · §3.7</div>
Voxterm clients do NOT sign. UUID-based <code>device_id</code> is provenance metadata only — it's opaque, forgeable, and not authentication. The convent sink is the single trust bottleneck for transcript ingestion. Acceptable for v1.
</div>
</section>
<section id="phases">
<h2>Phasing</h2>
<p class="section-sub">spec §6 · who does what, in roughly what order</p>
<div class="timeline">
<div class="cell ph">PHASE 0</div>
<div class="cell when">this week</div>
<div class="cell what">
<strong>Substrate.</strong> Create the cohort-data repo. Seed schema + reservoir + alchemists. Convert gist into markdown records. <em>Emergency cleanup</em> of the cohort viz — strip alchemist-only fields from <code>alchemy.js</code>, replace with a JSON fixture. Currently leaks if shipped.
</div>
<div class="cell ph">PHASE 1</div>
<div class="cell when">this week</div>
<div class="cell what">
<strong>Build pipeline, no swf-node.</strong> <code>build-bundles.js</code> → <code>cohort-surface.json</code> + <code>cohort-depth.tar</code> as flat files. Cohort viz consumes via <code>cohort-source.js</code>. Iterate UI against real data.
</div>
<div class="cell ph">PHASE 2</div>
<div class="cell when">~week 2</div>
<div class="cell what">
<strong>swf-node bundle types.</strong> <span class="pill user">YOU</span> — implement bundle envelope, <code>cohort.surface</code> · <code>cohort.depth</code> · <code>transcript.batch</code> handlers. Depth is opaque bytes; only verify envelope + sig + version. HTTP API. Test with two swf-node peers on LAN.
</div>
<div class="cell ph">PHASE 3</div>
<div class="cell when">~week 2</div>
<div class="cell what">
<strong>Cohort viz reads from swf-node.</strong> <code>cohort-source.js</code> flips from JSON-fixture to swf-node HTTP. SSE for live updates.
</div>
<div class="cell ph">PHASE 4</div>
<div class="cell when">~week 3</div>
<div class="cell what">
<strong>Alchemist app.</strong> New repo. Key onboarding (X25519 + Ed25519 in keychain). CRM editor that writes markdown. Decrypts depth bundles, parses as markdown.
</div>
<div class="cell ph">PHASE 5</div>
<div class="cell when">~week 3-4</div>
<div class="cell what">
<strong>Voxterm hivemind.</strong> <span class="pill user">YOU</span> — UUID generation on first launch, mDNS probe, batched POST. Convent box runs swf-node with <code>--hivemind-sink</code>.
</div>
<div class="cell ph">PHASE 6</div>
<div class="cell when">~week 3-4</div>
<div class="cell what">
<strong>Convent box ops.</strong> Provisioned, post-receive hook installed, end-to-end test.
</div>
<div class="cell ph">PHASE 7+</div>
<div class="cell when">later</div>
<div class="cell what">
<strong>Deferred.</strong> See <code>docs/alchemy-roadmap.md</code> — sorting hat, kind taxonomy, retroactive ledger UI, transcript projection, gravity-well evolution.
</div>
</div>
</section>
<section id="decisions">
<h2>Decisions</h2>
<p class="section-sub">resolved this draft + still open</p>
<h3>Resolved</h3>
<div class="callout resolved">
<div class="callout-tag">D1 · §3.3 NAMING ✓</div>
<strong>Chosen: <code>cohort.surface</code> + <code>cohort.depth</code>.</strong> Geometric pair, on-shape-rotator-brand. Surface = what you see; depth = what alchemists hold.
</div>
<div class="callout resolved">
<div class="callout-tag">D3 · §3.7 VOXTERM SIGNING ✓</div>
<strong>Sink resigns; voxterm clients don't sign.</strong> Each install carries a v4 UUID <code>device_id</code> as provenance. UUIDs are opaque, forgeable, not authentication.
</div>
<div class="callout resolved">
<div class="callout-tag">NEW · §3.3 DEPTH PAYLOAD SCHEMA ✓</div>
<strong>No schema.</strong> Depth payloads are opaque encrypted bytes; the build pipeline currently emits raw markdown file bytes as the plaintext, but the format is a private convention between alchemist tooling and the build pipeline, free to evolve without spec amendment.
</div>
<h3>Still open</h3>
<div class="callout decide">
<div class="callout-tag">D2 · §3.5 TRANSCRIPT KIND NAME</div>
Keep <code>transcript.batch</code>, or rename to <code>transcript.fragment</code> / <code>.shard</code> / <code>.scroll</code>?
</div>
<div class="callout decide">
<div class="callout-tag">D4 · COHORT-DATA REPO LOCATION</div>
New private repo on <code>dmarzzz</code>? A different org? Andrew's existing notes repo retrofitted? This blocks Phase 0.
</div>
<div class="callout decide">
<div class="callout-tag">D5 · CONVENT BOX HARDWARE</div>
Existing laptop or new RPi 5? Affects power-management story and how loud we can be about "convent has its own server."
</div>
<div class="callout decide">
<div class="callout-tag">D6 · RESERVOIR SIZE</div>
20 keys feels right for 4 active alchemists + buffer. Confirm or bump to 30. Doesn't matter much; just commit.
</div>
<div class="callout decide">
<div class="callout-tag">D7 · COHORT-PARTICIPANT WRITE-BACK</div>
Phase 4 or later? Recommend later — read-only cohort viz for v1. Alchemist app handles all writes.
</div>
<div class="callout decide">
<div class="callout-tag">D8 · COMMIT THE EXISTING ALCHEMY WORK?</div>
The repo has uncommitted alchemy implementation (alchemy tab with legend/shapes/pulse/constellation, search-bar fixes, "ask my agent" button). Bundle into Phase 0 cleanup commit, or land separately first?
</div>
<div class="callout decide">
<div class="callout-tag">D9 · DEPTH PAYLOAD FORMAT EVOLUTION (NEW)</div>
Should the depth plaintext include a <code>payload_format</code> header byte/field so version skew between alchemist app and build pipeline is detectable? Today both ship together; tomorrow they might not. Not blocking for v1.
</div>
<p class="scroll-hint">end of walkthrough · v0.1 + amendments · 2026-05-07</p>
</section>
</main>
</div>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment