diff options
| author | Adam Malczewski <[email protected]> | 2026-06-12 18:57:39 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-12 18:57:39 +0900 |
| commit | 6f45dc177540d6c6ae7596427209091d4c7adc20 (patch) | |
| tree | 72786e6cf8679ca303881e6a8d998a11aa8030f9 | |
| download | unbox-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-- | .gitignore | 13 | ||||
| -rw-r--r-- | .skills/add-extension.md | 32 | ||||
| -rw-r--r-- | .skills/add-protocol.md | 21 | ||||
| -rw-r--r-- | .skills/nested-run.md | 19 | ||||
| -rw-r--r-- | .unbox/extension-agent.md | 36 | ||||
| -rw-r--r-- | .unbox/package-agent.md | 39 | ||||
| -rw-r--r-- | .unbox/rules/listener-lifetime.md | 6 | ||||
| -rw-r--r-- | .unbox/rules/no-translation-layers.md | 6 | ||||
| -rw-r--r-- | .unbox/rules/unit-registration.md | 6 | ||||
| -rw-r--r-- | .unbox/rules/wlroots-include.md | 5 | ||||
| -rw-r--r-- | AGENTS.md | 74 | ||||
| -rw-r--r-- | GLOSSARY.md | 63 | ||||
| -rw-r--r-- | ORCHESTRATOR.md | 123 | ||||
| -rw-r--r-- | README.md | 33 | ||||
| -rw-r--r-- | notes/plan.md | 151 | ||||
| -rw-r--r-- | tasks.md | 30 |
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 |
