Read this file at the start of EVERY session before touching code. These are the load-bearing rules. Breaking one silently is how this project rots across sessions. If you must break one, it’s a deliberate, isolated increment with its own ADR in DECISIONS.md — never a side effect.
START
- Read this file.
make test && make demo-<previous-increment>— the baseline must be green before you change anything.- State out loud the single increment ID you are building this session. One increment per session.
END
- New
make demo-<this-increment>passes. - Full
make testgreen (every prior demo still passes). - If you touched the prompt prefix, re-run the prefix-hash test.
- If you touched anything under
desktop/renderer/, runcd desktop && bun run build-renderer, then GREP THE SERVED BYTES for a marker your change introduced.dev.ts bundleApp()serves the prebuiltdesktop/renderer/app.bundle.jswhenever it exists and NEVER compiles from source (ADR-0260: Bun must not module-load a.tsfrom a protected install dir). So renderer source edits are invisible in the running app until that artifact is rebuilt, and no amount of restarting helps. A source typecheck or a scratchbun buildproves the code COMPILES; it proves nothing about the bytes the app serves. Verify by fetching/app.jsand/styles.cssfrom a freshly booted engine and grepping for a new identifier, plus one retired identifier that must be ABSENT. This is the ADR-0303 vacuous-green trap in its most expensive form, because everything looks green and the user sees no change at all. - Append exactly 3 lines to PROGRESS.md: shipped / stubbed / next.
Do not start the next increment in the same session. A half-finished second increment is the exact failure this whole structure exists to prevent.
We are building a security/provenance/memory layer around oh-my-pi (omp),
not from scratch. The harness is TypeScript on Bun, in-process with omp. The
only Python is scanner-sidecar/ (the pure Unicode scanner). We extend omp
through hooks, custom tools, and its SDK. See DECISIONS.md ADR-0001.
- Never use em dashes (the long dash, Unicode U+2014). Use commas, periods, colons, or hyphens instead. This applies everywhere: code, comments, UI strings, commit messages, ADRs, and PROGRESS.md.
Prefer a hook or custom tool over any change that would require forking omp. omp ships hundreds of releases; a fork is permanent merge pain. If you believe something genuinely cannot be done via hook/tool/SDK, STOP and write an ADR before forking anything.
The harness is TypeScript. The ONLY directory containing Python is
scanner-sidecar/. Never add a .py file anywhere else. Never reimplement the
scanner in TypeScript. If a second Python surface seems necessary, that’s a
drift signal — stop and write an ADR.
Any failure to obtain a valid scan result — sidecar dead, malformed response, timeout, missing id — MUST be treated as “block / quarantine,” never “safe.” No code path may treat “scan unavailable” as “pass.” There is a test that kills the sidecar mid-run and asserts the gate blocks; it must stay green forever.
The security gate is a pre hook inside omp’s runtime. It must not depend on a
network call or a fragile boundary to do its blocking. The scanner (pure, behind
the sidecar) may be out-of-process; the GATE that acts on its output may not.
All user-provided, retrieved, imported, or externally stored text enters prompts
only inside UNTRUSTED_CONTENT_START / UNTRUSTED_CONTENT_END, only after
scanning + sanitation, and only AFTER the cache breakpoint (never in the frozen
prefix). The system prompt instructs the model to treat delimited content as
data, never instructions.
Prompt layers 1–4 (identity/safety, tool-use/permission policy, stable coding rules, security/trust-boundary rules) are byte-identical across all requests. Volatile context — date, cwd, git branch/status, env — that omp auto-injects MUST live in the tail, after the cache breakpoint. Putting volatile bytes in the prefix busts the KV cache every turn. Verify with the prefix-hash test.
Exactly: trusted | untrusted | suspicious | quarantined. No other values.
Every logged event uses a name from the EventName enum in contracts.ts.
Emitting an unknown event name must raise. Every event carries run_id,
session_id, and artifact_id when an artifact is in scope.
Every run, finding, and approval has a stable string *_id. Reuse omp’s
Snowflake session/run IDs; mint your own for findings/approvals. IDs are never
regenerated for the same logical entity.
Schema changes only ever happen through numbered migration files. Never edit a table definition in place. The schema is a frozen contract.
Readability is load-bearing. Any list/grid of rows (providers, models, settings,
tiles, chips) MUST keep each row's primary label on one line: the label takes the
space and ellipsizes on overflow (white-space: nowrap; overflow: hidden; text-overflow: ellipsis); it NEVER mid-word wraps inside a narrow column.
Secondary chips/badges are white-space: nowrap. Grid tracks use a generous
minmax() (about 220px or more) so labels do not fold; when content will not fit,
use fewer/wider columns or a single column, NEVER a fixed narrow multi-column grid
that word-wraps its text into unreadable slivers. If a label can be long, widen the
column or truncate it with a tooltip. This applies to every panel, popup, and
mockup (a mockup that word-wraps is a design bug, not a rendering artifact).
The #1 cause is a FLEX container holding prose: display: flex makes every raw
text run AND every inline element (<b>, <a>, <code>) its OWN flex item, so a
sentence with bold phrases shatters into narrow stacked columns. A flex row MUST
therefore contain a SINGLE text child: give a leading icon flex: none and wrap the
message in ONE element, OR (preferred, as .set-note does) absolutely-position the
icon and let the text flow as a normal BLOCK paragraph. NEVER place raw text plus
inline tags directly as siblings inside a flex box. And mockups MUST reuse the real
component CSS (or copy it verbatim) - a hand-rolled divergent style that flexes
prose is this same bug wearing a costume, which is exactly how it slipped back in.
harness/contracts.ts— TrustLabel, AgentMode, EventName, ExecutionProfile, ToolResult (PRD shape, translated to TS).harness/tools/result_adapter.ts— the ONLY place omp’s{ content: [{type,text}] }and the PRDToolResultmeet.- The scanner IPC contract (DECISIONS.md ADR-0002).
- DuckDB schema migration files.
- The frozen prompt prefix (layers 1–4).
If you import one of these, import it — never redefine its types locally.
- The Unicode scanner (
scanner-sidecar/, increment P2.1). Every finding type has fixtures; clean control corpus must produce zero false positives. - The semantic-promotion gate (increment P4.3). Suspicious-source content must never auto-promote into semantic memory.
Everything security-related leans on these two. Treat a failing test in either as a stop-the-line event.
- The PRD wrote
contractsandToolResultas Python dataclasses. Under Option A they are TypeScript incontracts.ts. The Python sidecar only needs the finding shape. Translate other PRD Python samples to TS as needed; note non-obvious translations in an ADR. - The integration seams were written from omp’s README. The FIRST task of
Increment 0 is to confirm the real
createAgentSession, hook API, and custom-tool factory shapes against the installed omp version, and ADR any deltas. - DuckDB uses the Node binding (less mature than DuckDB-Python). If a specific analytics task is blocked by it, that’s a localized future ADR — not a reason to revisit the harness language.
Every first-party source file under harness/, desktop/, tools/, and
scanner-sidecar/ carries the BUSL-1.1 SPDX header (Copyright (c) 2026 TechLead 187 LLC + SPDX-License-Identifier: BUSL-1.1; # for Python). This is enforced
two ways and you must keep both working:
- A pre-commit hook (
.githooks/pre-commit) auto-applies the header to staged source. Runmake install-hooksonce per clone to activate it — git cannot self-install hooks on clone, so a fresh checkout (yours or a collaborator’s) has NO hook until this runs.make installruns it for you. - CI runs
tools/license_headers.ts --checkas a backstop, so nothing unheadered can land on master even if the hook was never installed.
Vendored / generated trees (vendor/, node_modules/, desktop/release/,
.venv, __pycache__, dist/) keep their own licenses and are NEVER
relicensed. New first-party files get the header automatically (hook) or via
make license-headers. When adding the header in code, read the file then write —
never existsSync-then-read (CodeQL flags that pair as a TOCTOU race).