Created
May 8, 2026 02:34
-
-
Save dmarzzz/c860f838b433a60a32ae6885ee4b7b14 to your computer and use it in GitHub Desktop.
Shape Rotator OS — Spec Walkthrough · v0.1 + amendments (2026-05-07)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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 & depth</a></li> | |
| <li><a href="#encryption">encryption & 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">"<cid of v6, optional>"</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">"<base64 of plaintext OR opaque ciphertext>"</span>, | |
| <span class="k">"signature"</span>: <span class="s">"<hex; ed25519 over canonical(everything-but-signature)>"</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 & 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> ← <raw markdown file bytes> | |
| 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 & 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=<v>&record_id=<id> | |
| → { bundles: [...], next_since: <int|null> } | |
| <span class="x">GET</span> /bundles/by_cid/<cid> | |
| → <single bundle envelope> | |
| <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: <complete signed envelope> | |
| → 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><{ | |
| people: SurfaceRecord[], teams: SurfaceRecord[], clusters: SurfaceRecord[], | |
| }>; | |
| <span class="x">export function</span> subscribeToCohortChanges( | |
| cb: (record) => <span class="x">void</span> | |
| ): () => <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><{ | |
| people, teams, clusters, transcripts, <span class="c">// each: { frontmatter, body, ...metadata }</span> | |
| }>; | |
| <span class="x">export async function</span> publishRecord( | |
| type, markdownContent, alchemistKey | |
| ): <span class="x">Promise</span><{ surface_cid, depth_cid }>; | |
| <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