summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-12 18:57:39 +0900
committerAdam Malczewski <[email protected]>2026-06-12 18:57:39 +0900
commit6f45dc177540d6c6ae7596427209091d4c7adc20 (patch)
tree72786e6cf8679ca303881e6a8d998a11aa8030f9
downloadunbox-6f45dc177540d6c6ae7596427209091d4c7adc20.tar.gz
unbox-6f45dc177540d6c6ae7596427209091d4c7adc20.zip
Set up the agent harness
Constitution (AGENTS.md), orchestrator workflow with the header-contract read rule and harness-growth duties (ORCHESTRATOR.md), wlroots-seeded glossary, 4 safety reflexes, owner-agent briefs, 3 skills, and the living plan (notes/plan.md) with hardware-verified facts, settled decisions, and the slice 1-9 roadmap (tasks.md). Slice 0 done; next: toolchain bootstrap.
-rw-r--r--.gitignore13
-rw-r--r--.skills/add-extension.md32
-rw-r--r--.skills/add-protocol.md21
-rw-r--r--.skills/nested-run.md19
-rw-r--r--.unbox/extension-agent.md36
-rw-r--r--.unbox/package-agent.md39
-rw-r--r--.unbox/rules/listener-lifetime.md6
-rw-r--r--.unbox/rules/no-translation-layers.md6
-rw-r--r--.unbox/rules/unit-registration.md6
-rw-r--r--.unbox/rules/wlroots-include.md5
-rw-r--r--AGENTS.md74
-rw-r--r--GLOSSARY.md63
-rw-r--r--ORCHESTRATOR.md123
-rw-r--r--README.md33
-rw-r--r--notes/plan.md151
-rw-r--r--tasks.md30
16 files changed, 657 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..72ded8c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,13 @@
+# build trees (build/ = fast dev, build-asan/ = sanitizers)
+build*/
+.cache/
+compile_commands.json
+
+# orchestrator working files (briefs in, reports out)
+prompts/
+reports/
+
+# meson subprojects: keep wraps + our patch files, ignore downloaded sources
+subprojects/*
+!subprojects/*.wrap
+!subprojects/packagefiles/
diff --git a/.skills/add-extension.md b/.skills/add-extension.md
new file mode 100644
index 0000000..8bbd91d
--- /dev/null
+++ b/.skills/add-extension.md
@@ -0,0 +1,32 @@
+Use when creating any new extension unit (ext-*) from scratch.
+---
+# /add-extension — the full checklist
+
+1. **Overlap check.** `GLOSSARY.md` + existing public headers: does this
+ concept already exist under another name? If yes, extend that unit
+ instead (and that is a USER decision either way).
+2. **User decisions confirmed:** tier (core/standard), unit boundary, and
+ the unit's name (glossary-clean).
+3. **Scaffold:**
+ ```
+ packages/ext-<name>/
+ ├── include/unbox/ext-<name>/ # the contract — write this FIRST
+ ├── src/ # impl + private headers
+ ├── tests/ # doctest suite
+ ├── meson.build
+ └── ext-<name>.md # written LAST (step 9)
+ ```
+4. **Contract first.** Public header: manifest, exported hook descriptors /
+ service handles, lifetime semantics doc-commented. It must compile
+ against the kernel ABI alone before any implementation exists.
+5. **Registration — the trap, all four** (see .unbox/rules/unit-registration.md):
+ unit `meson.build` · root `subdir()` · host-bin `main.cpp` registration ·
+ test suite wired in.
+6. **Pure core** in `src/` with its doctest suite. Glue LAST.
+7. **UI (if any):** RML + RCSS under `assets/ext-<name>/`, data bindings
+ via the ui substrate service, touch-mode variables honored.
+8. **Verify:** `ninja -C build ext-<name>` · `meson test -C build --suite
+ ext-<name>` · smoke-test via /nested-run.
+9. **Package doc:** `ext-<name>.md`, ~20–30 lines — why it exists, gotchas
+ the header can't express, side-effect graph (what it emits/filters).
+10. **Report** to `reports/ext-<name>.md`; orchestrator updates `tasks.md`.
diff --git a/.skills/add-protocol.md b/.skills/add-protocol.md
new file mode 100644
index 0000000..ee18278
--- /dev/null
+++ b/.skills/add-protocol.md
@@ -0,0 +1,21 @@
+Use when a unit needs a Wayland protocol whose codegen is not yet wired into this repo.
+---
+# /add-protocol — wayland-scanner codegen checklist
+
+1. **Source the XML.** Prefer the system `wayland-protocols` package
+ (`pkg-config --variable=pkgdatadir wayland-protocols`). Vendor XML into
+ `protocol/` ONLY for protocols not shipped there (e.g. wlr-protocols
+ extras like wlr-layer-shell).
+2. **Codegen in Meson.** Add the `wayland-scanner` `custom_target` pair —
+ `private-code` + `server-header` — following the existing pattern in the
+ kernel's `meson.build`. One pair per protocol, named consistently.
+3. **Containment.** Generated headers are C: include them ONLY inside the
+ owning unit's glue, wrapped the same way as wlroots includes (see
+ `.unbox/rules/wlroots-include.md`). Protocol types NEVER cross a unit
+ boundary — expose the capability through a typed contract in the owning
+ unit's public header instead.
+4. **Prove ordering.** One clean full build (`rm -rf build && meson setup
+ build && ninja -C build`) to verify codegen dependency ordering, then
+ back to per-unit targets.
+5. **Glossary.** If the protocol introduces a user-visible concept, its
+ canonical name goes through the glossary flow (user sign-off).
diff --git a/.skills/nested-run.md b/.skills/nested-run.md
new file mode 100644
index 0000000..13d6bbd
--- /dev/null
+++ b/.skills/nested-run.md
@@ -0,0 +1,19 @@
+Use to smoke-test unbox visually without leaving the live labwc session.
+---
+# /nested-run — nested smoke-test checklist
+
+1. **Build** the needed targets: `ninja -C build` (or unit target + host-bin).
+2. **Launch nested:** `./build/packages/host-bin/unbox` — wlroots
+ auto-detects the parent Wayland session (labwc) and opens unbox as a
+ window. NEVER test on a bare TTY during development; the nested window
+ is the sandbox.
+3. **Drive it:** note the `WAYLAND_DISPLAY` socket unbox prints, then
+ `WAYLAND_DISPLAY=<that socket> foot` to open a client inside it.
+4. **Verify the ONE thing you came to verify**, then exit. Visual checks:
+ a single targeted `grim` screenshot of the nested output, read it, done.
+ Screenshots are token-expensive — never browse around with captures.
+5. **Crashes:** reproduce under `build-asan/` FIRST and read the sanitizer
+ trace before reading any source (see ORCHESTRATOR.md §4).
+6. **Touch caveat:** nested touch fidelity depends on what labwc forwards.
+ Final touch/gesture validation only counts on the real seat (slice 9+,
+ s6 service on seat0).
diff --git a/.unbox/extension-agent.md b/.unbox/extension-agent.md
new file mode 100644
index 0000000..d125dfd
--- /dev/null
+++ b/.unbox/extension-agent.md
@@ -0,0 +1,36 @@
+# Extension-agent supplement (units named ext-*)
+
+Read AFTER `.unbox/package-agent.md` — everything there applies, plus:
+
+## Shape
+- Export a **manifest**: id, tier (`core` | `standard`), `dependsOn`
+ (extension ids, resolved topologically by the host).
+- `activate(Host&)` is your ONLY entry point. Everything you touch arrives
+ through the typed Host API — no globals, no reaching into the kernel's
+ internals, no other extension's headers except its public contract.
+- Contribute capabilities as TYPED exported symbols in your public header
+ (hook descriptors, service handles) so consumers link against them — a
+ missing dependency must be a compile/link error, never a runtime lookup.
+
+## Tier rules
+- Depend on the kernel + lower tiers only. Never sideways into another
+ unit's privates, never upward.
+- Deactivation = your RAII members being destroyed, in reverse declaration
+ order. No manual teardown lists; if teardown needs choreography, your
+ ownership graph is wrong.
+- An exception escaping your hook callback disables YOUR extension, not the
+ session (the bus catches at the boundary). That isolation is a safety
+ net, not a feature — treat every trip as a bug.
+
+## UI contributions
+- UI = RML document(s) + RCSS under `assets/<unit>/` + **data bindings**
+ registered through the ui substrate service. No GL calls, no direct
+ RMLUi context access — the substrate owns rendering and scheduling.
+- Honor **touch-mode**: hit targets scale via the substrate's theme
+ variables. Never hardcode pointer-sized targets; this DE must be equally
+ usable by finger, keyboard, and mouse.
+
+## Pure-core reminder
+Your policy logic (layout math, what-to-show decisions, gesture
+recognition) is a pure function library inside your unit, doctest-covered
+without the kernel present. Glue calls it; it never calls glue.
diff --git a/.unbox/package-agent.md b/.unbox/package-agent.md
new file mode 100644
index 0000000..25f60e6
--- /dev/null
+++ b/.unbox/package-agent.md
@@ -0,0 +1,39 @@
+# Owner-agent brief (every unit)
+
+You are the SINGLE owner of `packages/<unit>/` — its `include/`, `src/`,
+`tests/`, `meson.build`, and `<unit>.md`. Nobody else edits these files;
+you edit nothing outside them.
+
+## Visibility
+- You MAY read: your unit; the PUBLIC headers of your dependencies
+ (`packages/*/include/**`); the harness md files; `GLOSSARY.md`; assets.
+- You may NOT read other units' `src/`. Needing to means their contract is
+ incomplete — record the gap in your report instead of peeking.
+
+## Contract discipline
+- Your public headers ARE your contract: minimal, documented,
+ lifetime-explicit. `unique_ptr` = ownership transfer; raw pointer/ref =
+ borrow valid only for the call, never stored; subscriptions return RAII
+ handles.
+- Doc-comment every public symbol: ownership + calling context. Everything
+ runs on the single `wl_event_loop` thread unless explicitly stated.
+- No templates in public headers unless genuinely necessary (they drag
+ implementation into the contract and bloat compile times).
+- Changing a header YOU own: fine, note it in the report. Needing a change
+ in one you DON'T own: a change-request in your report — never edit it,
+ never work around it with casts or string lookups.
+
+## Code discipline
+- Pure decision core (no wlroots/GL/RMLUi types) + thin glue at the edge.
+ doctest the core hard; zero mocks of `unbox::` modules — if you want to
+ mock one, the effect should have been injected: fix the design.
+- Respect the budget machine: forward-declare, pimpl concrete cross-
+ boundary classes, keep includes lean. Build YOUR target, not the world.
+- Vocabulary: `GLOSSARY.md` is law. A concept that needs a new name goes in
+ the report for user sign-off — never coin silently.
+
+## Done means
+`ninja -C build <unit-target>` clean · `meson test -C build --suite <unit>`
+green · `reports/<unit>.md` written (what you built, the public surface,
+test output, contract gaps / change-requests). Reply to the orchestrator
+with ONE line + the report path. No diffs, no logs.
diff --git a/.unbox/rules/listener-lifetime.md b/.unbox/rules/listener-lifetime.md
new file mode 100644
index 0000000..8133e04
--- /dev/null
+++ b/.unbox/rules/listener-lifetime.md
@@ -0,0 +1,6 @@
+# listener-lifetime
+Never a bare `wl_listener` or raw callback across a unit boundary. Subscribe
+through the bus and keep the returned RAII subscription handle as a member —
+destruction unsubscribes. Raw pointers received in a hook are borrows: valid
+ONLY during the call, never stored. Use-after-free across contracts is this
+project's #1 expected bug class; the type system is the defense.
diff --git a/.unbox/rules/no-translation-layers.md b/.unbox/rules/no-translation-layers.md
new file mode 100644
index 0000000..40a7864
--- /dev/null
+++ b/.unbox/rules/no-translation-layers.md
@@ -0,0 +1,6 @@
+# no-translation-layers
+GLES 3.2 is NATIVE here (Mesa crocus on HD 4400 — hardware-verified, see
+notes/plan.md §1). NO ANGLE, no Vulkan (hasvk deprecated, not installed),
+no GL-version downgrades "for speed" — it's the same driver either way.
+Compositing: wlr GLES2 renderer. RMLUi: sibling GLES 3.2 context on the
+shared EGLDisplay. Settled decision — do not relitigate.
diff --git a/.unbox/rules/unit-registration.md b/.unbox/rules/unit-registration.md
new file mode 100644
index 0000000..2c39e47
--- /dev/null
+++ b/.unbox/rules/unit-registration.md
@@ -0,0 +1,6 @@
+# unit-registration
+Meson does not auto-discover. A new unit exists only when ALL FOUR are done:
+its `packages/<unit>/meson.build` · `subdir()` in the root `meson.build` ·
+registration in host-bin's `main.cpp` (extensions only) · its test suite
+wired into `meson test`. Miss one and it silently doesn't exist. Check all
+four, every time.
diff --git a/.unbox/rules/wlroots-include.md b/.unbox/rules/wlroots-include.md
new file mode 100644
index 0000000..af008dd
--- /dev/null
+++ b/.unbox/rules/wlroots-include.md
@@ -0,0 +1,5 @@
+# wlroots-include
+Never `#include <wlr/...>` or `<wayland-server*.h>` directly. Include
+`unbox/kernel/wlr.hpp` (the kernel's extern-"C" wrapper) instead. wlroots
+headers are C and break in C++ without the wrapper's guards, and the version
+pin (0.20, `WLR_USE_UNSTABLE`) must live in exactly ONE place.
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..3bc7ab3
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,74 @@
+# unbox — Constitution (root AGENTS.md)
+
+> Loaded every session. Non-obvious, project-specific rules only — if a fresh
+> frontier model could infer it from the code, it does NOT belong here.
+> Full design + rationale: `notes/plan.md`. Workflow: `ORCHESTRATOR.md`.
+
+## What this project is
+An experimental Wayland desktop environment for the Panasonic CF-AX3
+(touchscreen, 4-core i5-4300U Haswell, 3.7 GiB RAM, Artix Linux + s6).
+A **monolithic kernel + in-process extensions**: ONE process containing the
+wlroots compositor core and an embedded RMLUi UI substrate; every feature
+(taskbar, launcher, tiling, OSK, …) is an extension. Tiers:
+**kernel → core → standard**. Ergonomic for touch AND keyboard AND mouse.
+
+## Stack
+C++23 (gcc), Meson + Ninja (+ ccache), wlroots **0.20** (system, pinned,
+`WLR_USE_UNSTABLE`), `wlr_scene` + wlr GLES2 renderer for compositing,
+RMLUi (vendored Meson subproject via the cmake module) on a sibling
+**GLES 3.2** EGL context (hardware-verified native), toml++ config
+(`unbox.toml`), doctest for pure-core tests, wlr headless backend for
+integration tests. Development runs NESTED under the live labwc session.
+
+## The non-negotiable architecture rules
+- **The kernel names NO concrete feature.** It owns: contracts (the ABI),
+ the extension host, the event/hook/service bus, backend/output/seat/scene
+ glue, and the UI substrate. Policies (tiling, bar contents, focus rules)
+ are extensions.
+- **Contracts are the only cross-unit surface.** A unit's contract is its
+ PUBLIC headers: `packages/<unit>/include/unbox/<unit>/`. Private headers
+ live in `src/` and are invisible to everyone else. If you must read
+ another unit's `src/` to do your job, the contract is incomplete — report
+ up, never peek.
+- **One owner per unit.** You may ONLY edit files in the unit you were
+ assigned. Cross-unit changes are change-requests reported up.
+- **Lifetime is part of the contract type system.** `unique_ptr` = ownership
+ transfer; raw pointer/reference = non-owning borrow valid only for the
+ call; every hook subscription returns an RAII handle that unsubscribes on
+ destruction. Never a bare `wl_listener` across units.
+- **Effects at the edges, pure decision cores.** Layout math, gesture
+ recognition, config parsing: pure input→output, heavily unit-tested.
+ Only thin glue touches wlroots/GL.
+- **Hooks are error-isolated.** Events: fire-and-forget, N listeners,
+ exceptions caught at the boundary (a throwing extension gets disabled,
+ never the session). Filters: ordered value-in→value-out chains.
+- **Cross-extension coupling anchors to exported TYPED symbols** (hook
+ descriptors, service handles). String-keyed lookups are forbidden — a
+ missing dependency must be a compile/link error.
+- **Never block the event loop.** Single-threaded `wl_event_loop`; anything
+ slow is deferred/async. Frame budget on this GPU is precious.
+
+## Commands
+- `meson setup build` (once) · `ninja -C build` — build everything
+- `ninja -C build <unit-target>` — build ONE unit (prefer this; the i5 is slow)
+- `meson test -C build --suite <unit>` — that unit's tests
+- Sanitizer dev build lives in `build-asan/` (separate dir, don't thrash ccache)
+
+## Don'ts (each is a settled decision — see notes/plan.md §2)
+- NO ANGLE, NO Vulkan, no GL translation layers (native GLES 3.2 verified).
+- NO direct `#include <wlr/...>` — only via the kernel's extern-"C" wrapper.
+- NO new third-party deps without surfacing to the user first.
+- NO synonym coinage: check `GLOSSARY.md`; new terms need user sign-off.
+
+## Testing (asymmetric — strict core, lenient shell)
+Pure cores: doctest, zero mocks of our own modules (mocking `unbox::` is a
+design bug — inject the effect instead). Glue/shell: a few integration tests
+on the wlr headless backend; do not chase unit coverage there.
+
+## Reports
+Finish a task → write `reports/<unit>.md`: what you built, the public
+surface, test output, contract gaps / change-requests. Reply tiny.
+
+## Vocabulary
+`GLOSSARY.md` is canonical (wlroots' own names preferred). Never invent a
+synonym for an existing concept.
diff --git a/GLOSSARY.md b/GLOSSARY.md
new file mode 100644
index 0000000..a59e950
--- /dev/null
+++ b/GLOSSARY.md
@@ -0,0 +1,63 @@
+# Glossary — canonical vocabulary
+
+> One name per concept. Never invent a synonym. New term? The orchestrator
+> proposes the standard/training-baked name (wlroots' own vocabulary
+> preferred) and the user confirms before it lands here. "Aliases to avoid"
+> maps wrong names back to the canonical one.
+
+## Architecture
+
+| Term | Meaning | Aliases to avoid |
+|---|---|---|
+| **kernel** | The minimal runtime: contracts (the ABI), extension host, event/hook/service bus, backend/output/seat/scene glue, and the UI substrate. Names no concrete feature. NOT an extension. | engine, core (when meaning the kernel) |
+| **core** (tier) | The extension tier required for a usable session: xdg-shell, layer-shell, output-config, keybindings. | — |
+| **standard** (tier) | The on-by-default feature tier: taskbar, launcher, OSK, tiling, … | — |
+| **extension** | An in-process unit contributing capabilities via the Host API: hooks, services, ui surfaces, protocol glue. | plugin, module, addon |
+| **unit** | One package under `packages/` with exactly one owner-agent. | component |
+| **contract** | A unit's public headers (`packages/<unit>/include/unbox/<unit>/`) — the ONLY cross-unit surface. | API, interface (when meaning the whole surface) |
+| **manifest** | An extension's declaration: id, tier, dependsOn. | — |
+| **Host API** | The typed object an extension receives in `activate(host)`. | host context |
+| **hook** | A typed extension point. **event** = fire-and-forget, N listeners, error-isolated. **filter** = ordered value-in→value-out chain. | callback, signal (when meaning our bus) |
+| **service** | A single-responder request/response capability fetched via a typed handle. NOT a hook. | — |
+| **subscription handle** | The RAII object returned by every hook subscription; destruction unsubscribes. | listener token |
+| **composition root** | `packages/host-bin/` — the only place that names every extension. Owned by the orchestrator. | bootstrapper |
+
+## Compositor domain (wlroots names are law)
+
+| Term | Meaning | Aliases to avoid |
+|---|---|---|
+| **surface** | A `wl_surface`: the protocol-level pixel container. | window (when meaning wl_surface) |
+| **toplevel** | An `xdg_toplevel` — an application window managed by the compositor. | window, view, app window |
+| **layer surface** | A wlr-layer-shell surface from an EXTERNAL client, anchored to an output layer. | panel surface, overlay (when meaning the protocol object) |
+| **output** | A display device (`wlr_output`). | monitor, display, screen |
+| **seat** | The input-device collection owning keyboard/pointer/touch focus (`wlr_seat`). | — |
+| **scene** | The `wlr_scene` retained-mode node tree; provides damage tracking. | render graph, scene tree (write "scene") |
+| **damage** | The output region needing redraw this frame. | dirty region |
+| **listener** | A `wl_listener`. Lives inside one unit's glue only; RAII-wrapped at every boundary. | — |
+| **workspace** | A virtual-desktop grouping of toplevels. | desktop, tag |
+| **nested session** | unbox running as a window inside the live labwc session (the dev mode). | embedded mode |
+
+## UI substrate
+
+| Term | Meaning | Aliases to avoid |
+|---|---|---|
+| **ui substrate** | The kernel subsystem owning RMLUi: contexts, render-to-scene bridge, input routing, theme variables. | shell renderer, ui engine |
+| **ui surface** | One RMLUi document an extension contributes, composited as a scene node. | shell surface, overlay, RML window, panel (when meaning the object) |
+| **data binding** | RMLUi's model↔document binding; the ONLY way extension state reaches RML. | — |
+| **touch-mode** | The theme state that scales hit targets for finger input (RCSS variables, never per-extension hacks). | tablet mode |
+
+## Workflow
+
+| Term | Meaning | Aliases to avoid |
+|---|---|---|
+| **orchestrator** | The main agent: plans, summons, verifies; owns host-bin + harness md. Reads only public headers (the READ RULE). | — |
+| **owner-agent** | The single agent assigned to one unit; edits only that unit. | subagent (alone — say which) |
+| **wave** | Disjoint units summoned in parallel in one message. | batch |
+| **brief** | `prompts/<unit>.md` — the task given to an owner-agent. | prompt file |
+| **report** | `reports/<unit>.md` — the owner-agent's writeup. | — |
+| **spike** | A slice whose only goal is de-risking one unknown. | POC, prototype (when meaning a spike) |
+
+## Known vocabulary drift
+
+- _None yet._ Add rows here the moment drift is caught, and keep the bad
+ name in "Aliases to avoid" forever so it is never reintroduced.
diff --git a/ORCHESTRATOR.md b/ORCHESTRATOR.md
new file mode 100644
index 0000000..44fae9a
--- /dev/null
+++ b/ORCHESTRATOR.md
@@ -0,0 +1,123 @@
+# ORCHESTRATOR.md — how to drive unbox
+
+> **You are the orchestrator.** You plan, summon owner-agents, verify their
+> work, and keep the build green. You do NOT write feature code — with two
+> exceptions you own outright: `packages/host-bin/` (the composition root)
+> and the harness md files. Read also: `AGENTS.md`, `GLOSSARY.md`,
+> `tasks.md` (live status), `notes/plan.md` (design + rationale).
+
+## 0. Mental model
+Monolithic kernel + in-process extensions. The team structure is isomorphic
+to the module structure: one owner-agent per unit, communicating only
+through contracts (public headers) — exactly as the code does. Friction
+between agents (needing to read another unit's implementation, constant
+back-and-forth) is a SIGNAL of a bad contract boundary, not normal work.
+
+This harness is a synthesis of "The AI Harness"
+(https://dev.to/louaiboumediene/the-ai-harness-why-your-ai-coding-agent-is-only-as-smart-as-the-repo-you-put-it-in-cml)
+with the dispatch methodology (reference repo:
+`user@builder:~/projects/internal-methodology/`, principles P1–P8 in
+its `notes/restructure-plan.md` §1). Key imports: tiered-cache context
+(tiny always-loaded files, big on-demand files), rules as crystallized scar
+tissue, glossary against synonym drift, skills for fumbled workflows, and
+"never write down what a frontier model already knows" (P6).
+
+## 1. THE READ RULE (what you may read — C++ enforces the rest)
+- Harness md files, `meson.build` files, `unbox.toml`, `assets/`
+- PUBLIC headers only: `packages/*/include/**`
+- `packages/host-bin/` in full (you own it)
+- Build / test / sanitizer OUTPUT (compiler errors are output, not source)
+- NEVER a unit's `src/`. If planning seems to require it, the contract
+ header is incomplete — fix THAT through its owner. Your worldview is the
+ ABI; keep the headers worth reading and the rule stays cheap.
+
+## 2. The golden workflow
+1. **Plan.** Decide the unit(s); split into dependency-topological waves of
+ DISJOINT units; widen each wave where possible.
+2. **Overlap check FIRST** (anti-synonym-drift): `GLOSSARY.md` + existing
+ public headers. A request that describes an existing concept under a new
+ name gets steered to the canonical term. Genuinely new term → propose
+ the standard/training-baked name (prefer wlroots' own vocabulary) and
+ ASK THE USER before it lands in the glossary.
+3. **Boundary decisions are the USER's.** New extension vs. extending one,
+ kernel vs. extension placement — surface it; never decide silently.
+4. **Write the brief** to `prompts/<unit>.md` (gitignored): the contract
+ sketch (public header signatures), required behavior, test expectations,
+ and which `.unbox/rules/` files apply (§3 map).
+5. **Summon the wave** — one Task per unit, all in ONE message when file
+ sets are disjoint. **THE TOKEN RULE:** never inline harness/rule/brief
+ contents into a Task prompt — the agent reads them itself; the guardrail
+ bytes land in ITS context, not yours. Canonical summon prompt:
+
+ You are the single owner-agent for packages/<unit>/. Read IN FULL,
+ in order, with your own tools (do NOT paste them back):
+ 1. AGENTS.md
+ 2. .unbox/package-agent.md
+ 3. .unbox/extension-agent.md (ONLY if your unit is an ext-*)
+ 4. .unbox/rules/<scoped rules for this unit — orchestrator lists them>
+ 5. prompts/<unit>.md (YOUR task)
+ Then implement: edit ONLY packages/<unit>/, build with
+ `ninja -C build <unit-target>`, run `meson test -C build --suite
+ <unit>`, write reports/<unit>.md.
+ Reply with ONE line of status + the report path. No diffs, no logs.
+
+6. **Verify.** Read the report from disk, then independently re-run the
+ build + tests (+ the asan build for anything touching lifetimes). Trust
+ nothing you haven't re-run yourself.
+7. **Resolve** contract gaps: header changes go through the owning unit as
+ a new (small) brief; never patch around a bad contract in host-bin.
+8. **Commit** the milestone with a clear message; update `tasks.md`.
+
+## 3. Rule scoping map (which rules each summon lists)
+| Unit kind | Always list | Notes |
+|---|---|---|
+| kernel / pure-core libs | listener-lifetime, wlroots-include | strict tests: zero internal mocks |
+| extensions (ext-*) | listener-lifetime, wlroots-include, unit-registration | lenient glue tests |
+| host-bin (you) | unit-registration, no-translation-layers | composition root names everything |
+
+## 4. Cross-unit debugging (the escape hatch)
+Lifetime/memory bugs spanning units: rebuild in `build-asan/`, reproduce,
+read the sanitizer trace (allowed — it is output). If the trace spans units
+and the fix is unclear, summon ONE read-only **debugger agent** permitted to
+read the involved units' `src/`; it reports findings up and edits NOTHING.
+Keep this rare — frequent use means the lifetime contracts are too weak,
+which is the real bug.
+
+## 5. Build throughput (4-core i5-4300U, 3.7 GiB — the build is the bottleneck)
+- ccache always on; per-unit ninja targets in loops, never world rebuilds.
+- The RMLUi subproject builds ONCE and is never edited.
+- `build/` (fast, dev) and `build-asan/` (sanitizers) are separate dirs.
+- If local builds still bottleneck: build remotely on builder over ssh,
+ run binaries here (deferred decision — exhaust ccache first).
+
+## 6. Harness maintenance — HOW THE MD FILES GROW (your standing duty)
+After every working session ask: **"What did an agent get wrong that I had
+to correct?"** Then file it exactly once, in the right layer (budgets are
+hard limits):
+
+| What happened | Where it goes | Budget |
+|---|---|---|
+| Mistake that would recur in ANY unit | `.unbox/rules/<name>.md` — "if you do X you must also Y" | ≤5 lines |
+| Knowledge about ONE unit its header can't express (gotchas, side-effect graph, why-it-exists) | `packages/<unit>/<unit>.md` | ~20–30 lines |
+| A multi-step workflow an agent fumbled | `.skills/<verb-name>.md` (line 1 = when-to-use, line 2 = `---`, then checklist) | one page |
+| Naming collision / synonym drift | `GLOSSARY.md` row with alias-to-avoid — USER confirms first | one row |
+| A settled decision being relitigated | `notes/plan.md` §2 decisions table | one row |
+| Slice progress / next action | `tasks.md` — after EVERY milestone | — |
+
+Constraints on growth:
+- Rules are crystallized scar tissue — NEVER hypothetical. A rule is earned
+ by an actual correction/revert, with two exceptions already seeded
+ (listener-lifetime, wlroots-include: wlroots' failure modes are public
+ knowledge, not speculation).
+- `AGENTS.md` stays <100 lines FOREVER. Adding a line means cutting one;
+ if it can live in a scoped rule or package doc instead, it must.
+- Package docs are written when the unit is FIRST built (the brief pays for
+ the doc), never in batches up front.
+- Every ~6 weeks: re-read the whole harness; delete anything a fresh
+ frontier model would know anyway (P6) and anything the code now proves.
+
+## 7. Workflow vocabulary
+**unit** = one package = one owner. **wave** = disjoint units summoned in
+parallel (one message, multiple Tasks). **brief** = `prompts/<unit>.md`.
+**report** = `reports/<unit>.md`. **spike** = a slice whose only goal is to
+de-risk one technical unknown (see `notes/plan.md` §4).
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e0487eb
--- /dev/null
+++ b/README.md
@@ -0,0 +1,33 @@
+# unbox
+
+An experimental Wayland desktop environment for the Panasonic CF-AX3 Let's
+Note (touchscreen convertible, i5-4300U Haswell, 3.7 GiB RAM, Artix Linux +
+s6) — lightweight, and ergonomic for **touch, keyboard, and mouse** equally.
+
+**Architecture:** a monolithic **kernel + in-process extensions**. One
+process holds the wlroots compositor core and an embedded RMLUi UI
+substrate; every user-facing feature (taskbar, launcher, on-screen
+keyboard, tiling, …) is an extension talking to the kernel through typed
+C++ contracts. Tiers: `kernel → core → standard`.
+
+- **Stack:** C++23 · Meson/Ninja · wlroots 0.20 (`wlr_scene`, GLES2
+ renderer) · RMLUi on a sibling GLES 3.2 EGL context · toml++ · doctest
+- **Status:** planning / harness complete — see `tasks.md` for the live
+ slice status and `notes/plan.md` for the full design + rationale.
+
+## Building (slice 1 will make this real)
+
+```sh
+sudo pacman -S meson ninja cmake ccache # one-time toolchain
+meson setup build
+ninja -C build
+./build/packages/host-bin/unbox # runs nested under labwc
+```
+
+## Documentation
+
+- **Design & rationale:** `notes/plan.md`
+- **Agent constitution (build rules):** `AGENTS.md`
+- **Orchestration workflow:** `ORCHESTRATOR.md`
+- **Canonical vocabulary:** `GLOSSARY.md`
+- **Live status / task log:** `tasks.md`
diff --git a/notes/plan.md b/notes/plan.md
new file mode 100644
index 0000000..061eb6a
--- /dev/null
+++ b/notes/plan.md
@@ -0,0 +1,151 @@
+# unbox — Living Plan
+
+> **Status:** harness complete; implementation not started (slice 1 next).
+> **Purpose:** the full design + rationale so any agent or human picking
+> this up has the complete picture. This is a *living* document — the
+> orchestrator updates §2 when a decision lands and §7 when one is made.
+> Live progress lives in `tasks.md`, not here.
+
+## 0. The goal in one paragraph
+
+A lightweight, experimental Wayland desktop environment for the Panasonic
+CF-AX3 Let's Note, equally ergonomic for touchscreen, keyboard, and mouse.
+One process — a **monolithic kernel** (wlroots compositor core + embedded
+RMLUi UI substrate + extension host) — where **every feature is an
+in-process extension** coupled through typed C++ contracts. The methodology
+is imported from dispatch (`user@builder:~/projects/dispatch/
+arch-rewrite/`): minimal kernel, contracts as the only cross-unit surface,
+one owner-agent per unit, functional core / imperative shell, glossary
+discipline, and the harness-as-deliverable (its principles P1–P8 apply
+here unchanged; see its `notes/restructure-plan.md` §1).
+
+## 1. Verified target inventory (probed 2026-06-12, this machine)
+
+| Fact | Value | How verified |
+|---|---|---|
+| Machine | Panasonic CF-AX3, touchscreen convertible | — |
+| CPU | i5-4300U (Haswell-ULT), 4 threads, 1.9–2.9 GHz | `lscpu` |
+| RAM | 3.7 GiB + 8 GiB swap | `free -h` |
+| GPU | Intel HD 4400 (HSW GT2), Mesa 26.1.2 **crocus** | EGL probe |
+| GL support | **GLES 3.2 native**, EGL 1.5 | 20-line EGL probe: created ES 3.2 context, `GL_VERSION: OpenGL ES 3.2 Mesa 26.1.2-arch1.1` |
+| Vulkan | NOT viable: no driver installed (only vulkan-icd-loader); Haswell's `hasvk` deprecated upstream | `pacman -Q` |
+| OS / init | Artix Linux, s6 + s6-rc, elogind 257.14 (seatd also present) | `pacman -Q` |
+| Session | **labwc 0.20.0 running on Wayland** — proof wlroots 0.20 works on this exact GPU/seat stack; also our nested-dev host | env + `pacman -Q` |
+| wlroots | `wlroots0.20` 0.20.1 and `wlroots0.19` 0.19.3 packaged | `pacman -Q` |
+| Wayland | wayland 1.25, wayland-protocols 1.49, libinput 1.31.3, xorg-xwayland 24.1.12 | `pacman -Q` |
+| Toolchain present | gcc 16.1.1 (C++23-capable), git, pkgconf, freetype2 2.14.3, glfw 3.4, foot | `pacman -Q` |
+| Toolchain MISSING | meson, ninja, cmake, ccache — install at slice 1 | `pacman -Q` |
+| RMLUi | NOT packaged in Artix/Arch repos → vendor as Meson subproject; only hard dep is FreeType (present) | `pacman -Si` |
+
+## 2. Decisions (settled — relitigating one requires new evidence)
+
+Each row: what we decided, why (the P4 test: the specific problem it
+solves), and the trigger that would reopen it.
+
+| Decision | Rationale | Reopen if |
+|---|---|---|
+| **Monolithic kernel** (compositor + RMLUi in one process), features as in-process extensions | Faithful to dispatch (its extensions are in-process too); one GL context/font atlas/event loop on a 3.7 GiB machine; extensions get cheap superpowers (OSK injects via wlr_seat, tiling edits the scene directly); simple frame scheduling for touch latency | Crash-rate in practice makes session loss intolerable despite mitigations |
+| Crash-isolation mitigations: exception-catching hook boundaries, pure extension cores, s6 restart, layer-shell escape hatch for external clients | A segfaulting extension kills the session and Wayland clients die with the compositor — accepted cost, bounded by keeping the segfault surface (kernel + thin glue) small | — |
+| **Extension ABI designed as-if-dynamic, linked statically** | dlopen + C++ ABI instability is ceremony with zero current consumers (P1 stopping point) | A real third-party extension appears |
+| **wlroots 0.20**, system package, pinned, via ONE extern-"C" wrapper header | Matches running labwc (proven on this hardware); wlroots API churns between minors — the pin + single wrapper contains upgrades to one file | 0.21+ needed for a feature; bump = one dedicated slice |
+| **wlr GLES2 renderer** for compositing; **sibling GLES 3.2 EGL context** (shared EGLDisplay) for RMLUi | wlroots' renderer is its own battle-tested choice; RMLUi's maintained backend needs GL3/GLES3; GLES 3.2 verified native. Older GL API ≠ faster: same crocus driver either way, and ES3 features (UBOs, VAOs, instancing) REDUCE CPU-side driver overhead — the actual bottleneck on this CPU | — |
+| **NO ANGLE / translation layers** | ANGLE's Linux backend is Vulkan (no driver here; hasvk deprecated); ANGLE-on-GL just stacks on the Mesa we'd bypass; Chromium-scale build; tens of MB RSS; wlroots speaks system EGL | — |
+| **Meson + Ninja**, RMLUi via Meson's cmake subproject module; ccache; `build/` + `build-asan/` separate | Wayland-ecosystem native (wlroots/labwc/sway), first-class wayland-scanner codegen; RMLUi is CMake-only, the cmake module bridges it, built once | cmake-module friction exceeds ~a day of fighting → fall back to prebuilding RMLUi as a system-installed lib |
+| **C++23, gcc 16** | Already installed; designated initializers ease wlroots struct setup; Hyprland proved years of C++-on-wlroots | — |
+| **toml++** for `unbox.toml` config | Mirrors dispatch.toml convention; header-only | — |
+| **doctest** for pure cores | Lightest compile cost of the mainstream frameworks — compile time IS the scarce resource here | — |
+| **wlr headless backend** for integration tests | Tests without a display, CI-able, no nested session needed | — |
+| **Header-contract workflow + READ RULE** (orchestrator reads only `include/**` + host-bin + harness) | C++'s .hpp/.cpp split makes dispatch's contract-only visibility *mechanical*, and the compiler enforces what prompts only suggest; headers are the densest token representation of the system | — |
+| **Lifetime encoded in contract types** (unique_ptr / borrows / RAII subscription handles) | The workflow's blind spot is cross-unit use-after-free (`wl_listener` after owner death — THE wlroots bug); typed lifetime makes it visible in headers the orchestrator can read | — |
+| ASan/UBSan dev build + read-only debugger-agent escape hatch | Sanitizer traces substitute for reading two units' sources when a lifetime bug spans units | — |
+| **Develop nested under labwc**; real seat only via s6 service at slice 9+ | Never brick the live session; wlroots auto-nests | — |
+| **xwayland: optional extension, OFF by default** | RAM; this is an experimental DE — X11 apps opt in | — |
+| Vocabulary source: **wlroots' own names** | P8 + prefer training-baked terms; Wayland's synonym swamp (surface/view/window/toplevel, output/monitor/display) is severe | — |
+
+## 3. Architecture
+
+```
+┌──────────────────────────────────────────────────────────────┐
+│ STANDARD extensions: ext-taskbar · ext-launcher · ext-osk · │
+│ ext-window-tiling · ext-wallpaper · ext-notifications · … │
+├──────────────────────────────────────────────────────────────┤
+│ CORE extensions (minimum usable session): │
+│ ext-xdg-shell · ext-layer-shell · ext-output-config · │
+│ ext-keybindings · (ext-xwayland, optional, off) │
+├──────────────────────────────────────────────────────────────┤
+│ KERNEL (names no feature): │
+│ wl_event_loop · backend/output/seat/scene glue · │
+│ UI substrate (RMLUi contexts, render-to-scene, input │
+│ routing, theme/touch-mode variables) · extension host · │
+│ typed event/hook/service bus · contracts (the ABI) │
+└──────────────────────────────────────────────────────────────┘
+packages/host-bin = composition root (orchestrator-owned): the ONLY
+place that names every extension; loads unbox.toml; activates in
+dependency order.
+```
+
+- "Window", "workspace", "ui surface" are **contract types**; every policy
+ about them (tiling, focus rules, bar contents) is an extension.
+- Extensions contribute UI as RML documents + data bindings via the ui
+ substrate service — never GL, never raw RMLUi contexts.
+- Input: ONE kernel routing path feeds pointer AND touch into ui surfaces;
+ extensions never see device types unless they ask (gesture extensions).
+
+## 4. THE SPIKE (slice 3) — RMLUi → wlr_scene bridge
+
+The single biggest unknown; everything UI depends on it. Plan A: RMLUi
+renders each ui surface into an offscreen GLES 3.2 FBO backed by a dmabuf
+(shared EGLDisplay, separate context), wrapped as a `wlr_buffer`, attached
+as a `wlr_scene_buffer` node — damage flows through the scene like any
+client surface. Fallbacks, in order: (B) render into a shm `wlr_buffer`
+(CPU copy, acceptable for low-churn surfaces like a bar), (C) import the
+FBO texture via EGLImage into the wlr renderer. Acceptance: hello-world
+RML document composited + damage-tracked + input-routed in the nested
+session. If A–C all fail, the monolith decision (§2 row 1) gets reopened —
+that is what a spike is for.
+
+## 5. Roadmap
+
+Slices 1–9 with acceptance criteria live in `tasks.md` (single source of
+truth for status). Summary: bootstrap → tinywl port → THE SPIKE →
+extension host → input ergonomics → taskbar+launcher → tiling → OSK →
+s6 session hardening. Slice 9's durability model is dispatch's: persist
+layout state append-only, pure `reconcile` on boot, status derived never
+trusted.
+
+## 6. Risks
+
+| Risk | Mitigation |
+|---|---|
+| Spike fails (dmabuf path) | Fallbacks B/C in §4; worst case reopen monolith decision early, cheaply |
+| wlroots 0.19→0.20→0.21 API churn | Version pinned; ONE wrapper header; upgrades are a dedicated slice |
+| Cross-unit lifetime bugs | Typed lifetime contracts + ASan build + debugger-agent (ORCHESTRATOR.md §4) |
+| Build times on the i5 | ccache, per-unit targets, RMLUi built once, doctest; remote-build escape hatch (§7) |
+| RMLUi text-input/IME for OSK | OSK injects via wlr_seat directly (no protocol); text-input protocol only if external-client IME is ever wanted |
+| Touch latency / frame budget | wlr_scene damage tracking; flat effect-light RCSS theme by default; never block the event loop |
+| RAM pressure (3.7 GiB) | One process, one font atlas; xwayland off; measure RSS at every slice end |
+
+## 7. Deferred decisions (decide when reached, record in §2)
+
+| Decision | Default until then | Trigger |
+|---|---|---|
+| dlopen extension loading | static linking | third-party extension exists |
+| Remote builds on builder | local + ccache | builds still bottleneck after ccache |
+| OSK injection: wlr_seat direct vs virtual-keyboard protocol | direct injection | external-client IME need |
+| Workspace model (per-output? tags?) | undecided — design at slice 7 | tiling slice starts |
+| clang-format style | defer config to slice 1 | first formatting dispute |
+| Catch2 vs doctest revisit | doctest | doctest blocks something real |
+
+## 8. References
+
+- Methodology source: `user@builder:~/projects/internal-methodology/`
+ (AGENTS.md, ORCHESTRATOR.md, GLOSSARY.md, notes/restructure-plan.md §1
+ P1–P8)
+- "The AI Harness" — https://dev.to/louaiboumediene/the-ai-harness-why-your-ai-coding-agent-is-only-as-smart-as-the-repo-you-put-it-in-cml
+ (layer budgets: constitution <100 lines, rules ≤5, feature docs ~60 —
+ ours run ~20–30 because contract headers carry the schema half)
+- wlroots 0.20 docs + tinywl: https://gitlab.freedesktop.org/wlroots/wlroots
+ (tinywl is the slice-2 seed)
+- RMLUi: https://github.com/mikke89/RmlUi + https://mikke89.github.io/RmlUiDoc/
+ (GL3/GLES3 renderer backend; custom render/system interfaces)
+- Prior art for C++-on-wlroots: Hyprland (pre-0.36, wlroots era)
diff --git a/tasks.md b/tasks.md
new file mode 100644
index 0000000..3c1e933
--- /dev/null
+++ b/tasks.md
@@ -0,0 +1,30 @@
+# tasks.md — live status
+
+> The orchestrator updates this after EVERY milestone. Keep it terse:
+> slice status + the single next action. History lives in git.
+
+## Now
+
+**Next action:** Slice 1 — install toolchain (`sudo pacman -S meson ninja
+cmake ccache`), then bootstrap the Meson skeleton per `notes/plan.md` §5.
+
+## Slices
+
+| # | Slice | Status | Acceptance |
+|---|---|---|---|
+| 0 | Harness skeleton | **DONE** 2026-06-12 | All harness md files in place |
+| 1 | Bootstrap: toolchain, Meson skeleton, RMLUi subproject compiles, empty kernel links wlroots-0.20 from C++ via the extern-"C" wrapper | pending | `ninja -C build` green; no-op binary runs + exits clean |
+| 2 | tinywl port: kernel skeleton runs nested under labwc | pending | opens as a window; manages a foot toplevel; keyboard/pointer/touch events flow |
+| 3 | **THE SPIKE:** RMLUi→scene bridge | pending | a hello-world RML document composited as a scene node with damage tracking; go/no-go gate |
+| 4 | Extension host + contracts: bus, manifests, static registration; xdg-shell/layer-shell refactored OUT of kernel into core extensions | pending | kernel names no feature; ext-xdg-shell + ext-layer-shell pass suite |
+| 5 | Input routing + ergonomics contract: unified pointer/touch→RMLUi events, keybinding filter chain, touch-mode RCSS variables | pending | same ui surface usable by mouse and finger |
+| 6 | First standard extensions: ext-taskbar + ext-launcher | pending | proves the ui-substrate contract is complete (friction = bad contract) |
+| 7 | ext-window-tiling: pure layout core + thin scene glue | pending | layout math 100% doctest-covered, zero wlroots types in core |
+| 8 | ext-osk: RML keyboard ui surface injecting via wlr_seat | pending | type into foot via touch only; auto-show on text-input focus |
+| 9 | Session hardening: s6 user service, TTY launch on seat0, layout persistence (append-only state + pure reconcile on boot) | pending | survives `kill -9` + s6 restart with workspaces restored |
+
+## Deferred decisions (decide when reached — see notes/plan.md §7)
+
+dlopen extensions · remote builds on builder · xwayland default ·
+OSK virtual-keyboard protocol vs direct seat injection · workspace model ·
+clang-format style details