# Tasks: registry and debloat One task = one agent (`specs/agents/opencode-runbook.md`). `[ ]` open. Spec: `spec.md` (sections D1 to D13). Numbers: `measurements.md`. Rules: `AGENTS.md`, `docs/CODE-MAP.md`. No new crate: everything new is `crates/engine/src/addons/` (not `registry`, that name is the command registry). **First tool call rule**: after at most three reads, write the first OWNS file in your SECOND tool call: a compiling stub with the GOAL signatures and ONE failing test, then make it pass. **Standing VERIFY**: `export PATH="$HOME/.cargo/bin:$PATH"`; `cargo test -p `; `cargo clippy -p --all-targets -- -D warnings`; `rustfmt --edition 2024 `; UI/i18n tasks add `cargo xtask i18n`; the last task of a phase adds `cargo xtask layers` and `cargo xtask tree`. **Standing DO NOT**: edit Cargo files (tasks marked LEAD do), files outside OWNS, git state, `cargo fmt --all`; add `allow` attributes; use `unwrap/expect/panic/[i]/[a..b]` on input-derived data; add a dependency; write outside `/registry/` and `.notescraft/registry.json`. **Blocked on owner**: task 3 and everything that installs (7 onward) wait for open question 1 (ed25519). Tasks 1, 2, 4, 5, 6 and Phase 0 do not. ## Phase 0 - size fixes without a registry (do these first, they are 25 % of the binary) - [x] 0.1 LEAD: release profile. OWNS: root `Cargo.toml` (`[profile.release]` only), `packaging/macos/*.sh` and `packaging/windows/*` only if they strip separately. GOAL: add `strip = "symbols"`; confirm Linux `package.sh` strip is now redundant but harmless. VERIFY: `CARGO_TARGET_DIR=target/agent-size cargo build --release -p notescraft`, `ls -l` is at most 34.9 MB (measured 34,851,464), the app still starts (`measure.py` in `measurements.md`), `cargo xtask ci`. DO NOT: set `panic = "abort"` (the never-crash policy and tests rely on unwinding). - [x] 0.2 Share fonts (brief `briefs/29-font-dedupe.md`). - [x] 0.3 Drop egui `default_fonts`. OWNS: `apps/notescraft/Cargo.toml` (eframe feature list only, LEAD edit), `crates/ui-egui/src/design/fonts/mod.rs`, `assets/fonts/NotoEmoji-Regular.ttf` (copy from `epaint_default_fonts`, with its OFL text), `crates/ui-egui/tests/fonts_emoji.rs`. FILES TO OPEN: `crates/ui-egui/src/design/fonts/mod.rs` (`install`), `~/.cargo/registry/src/*/epaint_default_fonts-*/fonts/`. GOAL: `eframe` without `default_fonts`, `FontDefinitions::empty()` plus Inter, JetBrains Mono, STIX, NotoEmoji (last in every chain), the system fallbacks as today. TESTS: kittest renders `😀 ★ → ✓` in proportional and monospace with no missing-glyph box (compare against the egui replacement glyph); the snapshot of the Settings and Explorer screens differs only by sub-pixel noise. VERIFY: standing + `cargo run -p notescraft-ui-egui --example snapshot -- --vault --out a.png --size 1440x900` before and after, read both PNGs; binary shrinks by at least 0.9 MiB. DO NOT: change any font size or weight. - [ ] 0.4 LEAD: `opt-level = "s"` decision. GOAL: measure on `target/agent-size` with the profile env vars: index build of the 6500-note vault (CPU seconds, `measure.py --large-ok`), `cargo test --release -p notescraft-ui-egui --test perf_code_blocks -- --ignored --nocapture`, a graph timelapse frame time; adopt only if none is worse than 10 %. Write the numbers in `measurements.md` section 1.3. DO NOT: adopt without numbers. - [x] 0.5 Lazy system fallback fonts. OWNS: `crates/ui-egui/src/design/fonts/mod.rs`, `.../scripts.rs` (add Hebrew and Arabic to the on-demand table if missing), `crates/ui-egui/src/design/fonts/tests.rs`. FILES TO OPEN: same, `note_text` / `apply_pending`. GOAL: remove `SYSTEM_FALLBACKS` eager read at `install`; Hebrew and Arabic load through `note_text` like other scripts, so launch does not read `SFHebrew.ttf` and `SFArabic.ttf`. TESTS: a note with Hebrew shows no box after one frame loop (existing test style in `tests.rs`); a note without them never reads the files (counter in the injected `file_size`). VERIFY: standing. DO NOT: change the Latin chain. ## Phase 1 - pure registry core (engine, no IO unless stated) - [x] 1. Index model, validation, version compare (brief `briefs/30-registry-index-model.md`). - [x] 2. Fileset guard: path and size rules (brief `briefs/31-registry-fileset-guard.md`). - [x] 3. Signature canonical text and verify. OWNS: `crates/engine/src/addons/verify.rs`, `crates/engine/src/addons/keys.rs`, `crates/engine/tests/addons_verify.rs`. FILES TO OPEN: `spec.md` D3, `crates/engine/src/addons/model.rs` (task 1), `crates/syntax/src/validate.rs` (`sha256_hex`). GOAL: `pub trait Verifier { fn verify(&self, entry: &Entry) -> Result<()>; }`, `pub fn canonical_text(entry: &Entry) -> String`, `pub struct Ed25519 { keys }` implementing it with the dependency the owner approved, `TRUSTED_KEYS` with the owner's public key (placeholder key in tests only), a `FakeVerifier` for other tasks. TESTS: known-answer signature generated by the xtask tool (task 18) verifies; one flipped hash byte fails; unknown `key_id` fails; wrong kind or version in the text fails; files in a different order produce the same text. VERIFY: standing. DO NOT: put a private key anywhere; accept an entry without `signature`. - [x] 4. State and store layout. OWNS: `crates/engine/src/addons/state.rs`, `crates/engine/src/addons/store.rs`, `crates/engine/tests/addons_state.rs`. FILES TO OPEN: `spec.md` D4, `crates/engine/src/activity/outbox.rs` (versioned JSON pattern), `crates/engine/src/grammars/mod.rs` (`grammar_dir`, settings dir), `craft-libs/crates/atomic/src/lib.rs` (`atomic_write`, `write_new`). GOAL: `State { installed: BTreeMap }` with `parse/to_json` (version 1, cap 512, unknown version `Err`), `Store::open(data_dir)`, `Store::version_dir(kind, id, version) -> PathBuf` built from validated components, `Store::gc(&State)` removing version dirs not in state and `staging/`, `commit(&State)` via `atomic_write`, `VaultFile { enabled, wanted }` with `parse/to_json` (cap 256 each). TESTS: round trip; corrupt JSON returns `Err` and leaves the file byte-identical; gc keeps `version` and `previous` and deletes the rest; commit is the only writer of `state.json`. VERIFY: standing. DO NOT: touch the vault; delete anything outside `/registry/`. - [x] 5. Transport and bounded download. OWNS: `crates/engine/src/addons/net.rs`, `crates/engine/tests/addons_net.rs`. FILES TO OPEN: `crates/syntax/src/fetch.rs` (`Transport`, `Curl`), `crates/syntax/src/validate.rs` (`check_url`), `crates/engine/src/sync/` (how `ureq` is used), `spec.md` D5, D7. GOAL: `pub trait Transport { fn get(&self, url: &str, max_bytes: u64, if_none_match: Option<&str>) -> Result; }`, `Response { status, etag, bytes }` (304 allowed), `Ureq` implementation (https only, no redirects, total timeout, reads at most `max_bytes + 1` and fails above), `FakeTransport` for tests, `download_file(&dyn Transport, &File, cap) -> Result>` that checks host allow-list, size equals entry size, sha256 equals entry. TESTS: host not allow-listed; `http://`; credentials; IP literal; redirect status; body one byte too long; one byte short; wrong sha256; 304 keeps cache; error maps to `offline`. VERIFY: standing; no real network in tests. DO NOT: spawn `curl`; follow redirects; add a header with an identifier beyond `NotesCraft/`. - [x] 6. Per-kind content checks. OWNS: `crates/engine/src/addons/kinds.rs`, `crates/engine/tests/addons_kinds.rs`. FILES TO OPEN: `spec.md` D8, `crates/syntax/src/validate.rs` (`check_grammar`), `crates/themes/src/load.rs` (`Theme::from_toml` diagnostics), `crates/plugins/src/manifest.rs`, `crates/plugins/src/lib.rs`. GOAL: `check(kind, &Entry, &[(path, bytes)]) -> Result<()>` dispatching to grammar, theme, plugin (manifest equals entry: id, version, permissions, command ids; wasm magic `\0asm`), font (magic bytes), snippet (UTF-8, no NUL), pack (each file by its subfolder). TESTS: one good and one bad fixture per kind; plugin manifest with extra permission fails; theme reusing a built-in id fails; `.tmLanguage` renamed to `.sublime-syntax` fails. VERIFY: standing. DO NOT: instantiate wasm here (task 12 does the dry load). - [x] 7. BLOCKED on 3. Install, update, remove, rollback pipeline. OWNS: `crates/engine/src/addons/install.rs`, `crates/engine/tests/addons_install.rs`. FILES TO OPEN: `spec.md` D6, tasks 1 to 6 outputs, `crates/engine/src/grammars/mod.rs` (`save`). GOAL: `install(&Ctx, &Index, kind, id, version) -> Result`, `update`, `remove`, `rollback` exactly as D6 steps 1 to 7; `Ctx { store, transport: &dyn Transport, verifier: &dyn Verifier, app_version, seed: Option<&SeedDir> }`; seed file used when its sha256 equals the entry. TESTS: the list in `spec.md` D13 IO bullet (hash mismatch, short and long body, crash before the state flip leaves the old state and gc removes the new dir, update keeps `previous`, rollback, remove, seed offline, revoked, app too old, bad signature, cap reached). VERIFY: standing. DO NOT: write anywhere but the store and `state.json`; delete the old version before the new state is committed. - [x] 8. Commands, gate, command list. OWNS: `crates/engine/src/addons/commands.rs`, `crates/engine/src/addons/mod.rs`, one `mod addons;` in `crates/engine/src/lib.rs`, one `register_addons(reg)` call in `crates/engine/src/commands.rs::register_core`, `crates/engine/src/ai/assistant_gate.rs`, `crates/engine/tests/core_commands.rs` (`CORE_IDS`), `crates/engine/tests/addons_commands.rs`. FILES TO OPEN: `spec.md` D9, `crates/engine/src/bases/commands.rs` (group shape), `crates/engine/src/ai/assistant_gate.rs` (`READ_COMMANDS`, `WRITE_COMMANDS`, `DELETE_COMMANDS`), `docs/CODE-MAP.md`. GOAL: the nine commands of D9, error codes, session fields `addons_transport` and `addons_verifier` injectable like `grammar_transport`. TESTS: list joins state and cache; `registry.install` through the registry with a fake transport; read-only session refuses mutating ids; gate: `list|info|suggest` READ, `enable|disable` WRITE, the five others need `allow_delete`; offline `check` leaves the cache. VERIFY: standing + `--test core_commands --test addons_commands`. DO NOT: touch UI; change the `grammar.*` codes. - [ ] 9. CLI sugar. OWNS: `apps/notescraft-cli/src/args.rs`, `apps/notescraft-cli/src/commands/addons.rs`, `apps/notescraft-cli/tests/registry.rs`. FILES TO OPEN: `apps/notescraft-cli/src/commands.rs`, `apps/notescraft-cli/src/commands/notes.rs`, `apps/notescraft-cli/src/output.rs`. GOAL: `notescraft registry list|info|check|install|update|remove|rollback` mapped to the commands, `--json` like the rest, exit code 3 only for write commands while the app holds the lock (`list` and `info` use `open_read_only`). TESTS: parse table; `list --json` shape; `install` against a fake transport env var is not allowed: test the argument-to-params mapping only. VERIFY: standing, `-p notescraft-cli`. DO NOT: add network code to the CLI. ## Phase 2 - consumers - [x] 10. Grammar consumer and legacy adoption. OWNS: `crates/engine/src/grammars/mod.rs` (call the shared installer, keep codes), `crates/engine/src/addons/consumers.rs`, `crates/engine/tests/grammars.rs` (extend), `crates/engine/tests/addons_grammars.rs`. FILES TO OPEN: `crates/engine/src/grammars/mod.rs`, `crates/engine/src/vault/session.rs` (`load_grammars`), `crates/syntax/src/load.rs`. GOAL: `Session::load_grammars` feeds `set_installed` from `state.json` grammars plus legacy `AppSettings::grammars` once per generation; legacy entries show as `source: legacy`; `grammar.install` and `grammar.remove` call the registry code when the id is in the index. TESTS: legacy install still works; registry install appears in `grammar.info`; rebuild happens once for N installs in one frame; corrupt grammar file skipped. VERIFY: standing + `--test grammars --test grammars_hostile`. DO NOT: delete legacy files; change `MAX_GRAMMARS`. - [x] 11. Theme consumer. OWNS: `crates/engine/src/prefs/themes.rs`, `crates/engine/tests/addons_themes.rs`. FILES TO OPEN: `crates/engine/src/prefs/themes.rs`, `crates/themes/src/load.rs`. GOAL: `Session::user_themes` also reads installed themes from the store (still capped by `MAX_USER_THEMES`, still refusing built-in ids); a selected id that is not installed resolves to the built-in of the same variant plus a diagnostic. TESTS: installed theme resolves; missing selected theme falls back with a diagnostic; duplicate id between settings folder and store: store loses. VERIFY: standing. DO NOT: change the built-ins. - [x] 12. Plugin consumer: wire the wasm host into the app. OWNS: `crates/engine/src/plugin_host/plugins.rs` (scan installed plugins listed in the vault `enabled`), `crates/engine/src/vault/session.rs` (call `load_plugins` after open and after enable/disable; one line), `crates/engine/tests/addons_plugins.rs`. FILES TO OPEN: `crates/engine/src/plugin_host/plugins.rs`, `plugin_run.rs`, `crates/engine/tests/plugins.rs`, `crates/engine/src/builtin/plugin_commands.rs`, `plugins/dice/`. GOAL: `load_plugins` merges vault-local plugins (today's behaviour) with installed ones; commands `plugin..` register; the approval flow is unchanged; a dry `Plugin::load` runs inside `registry.install` (task 6 left it). TESTS: install dice from a fake transport, enable, run `plugin.dice.roll` in a `TempVault`; disabled plugin not registered; permission not approved refuses; a plugin removed while enabled unloads. VERIFY: standing; then re-measure the binary and write the wasmi delta into `measurements.md` section 1.4. DO NOT: change ABI v1; give a plugin more permissions than approved. - [x] 13. Pack consumer. OWNS: `crates/engine/src/addons/consumers.rs` (pack arm), `crates/engine/tests/addons_packs.rs`. GOAL: a `pack` entry's `grammars/`, `themes/` files flow to the grammar and theme consumers; `icons/` is read by the Lottie loader only when task 21 lands. TESTS: a pack with two grammars and one theme installs and removes atomically as one unit; one bad file fails the whole pack. DO NOT: install per-file. ## Phase 3 - UI - [x] 14. Registry browser view. OWNS: `crates/ui-egui/src/views/registry_view/mod.rs`, `.../list.rs`, `.../detail.rs`, `crates/ui-egui/src/text/views/registry.rs`, `[ui.registry]` at the END of `crates/i18n/locales/en.toml`, `crates/ui-egui/tests/registry_view.rs`. FILES TO OPEN: `spec.md` D10, `crates/ui-egui/src/views/settings_view/categories.rs`, `crates/ui-egui/src/views/share_dialog/mod.rs` (modal pattern), `.claude/skills/notescraft-egui-screen/`. GOAL: the view returns `Action`s only (`Check`, `Install(kind,id)`, `Update`, `Remove`, `Rollback`, `Enable(id)`), draws rows by state, the drawer with permission sentences, the offline banner; no engine calls inside the view. TESTS: kittest: each state renders its button; Install emits one action; search filters; offline banner; permissions listed as sentences. VERIFY: standing + `cargo xtask i18n`. DO NOT: use raw egui buttons or inputs; hardcode strings; fetch from the view. - [x] 15. Shell wiring: category, commands, snapshot flag. OWNS: `crates/ui-egui/src/views/settings_view/categories.rs` (one Registry entry), `crates/ui-egui/src/shell/app/vault/registry.rs`, `crates/ui-egui/examples/snapshot.rs` (`--registry`). GOAL: actions call the engine commands on a worker thread (poll like `grammar_dialog.rs`), results update the view data; `registry.list` on open reads the cache only; Check/Install are the network triggers. TESTS: kittest: opening the category makes zero network calls (counter on the fake transport); Install runs one command. VERIFY: standing + snapshot PNG read. DO NOT: block the UI thread. - [x] 16. Code card, first-run card, update badge. OWNS: `crates/ui-egui/src/views/note_view/blocks/code_card.rs` (the Add-grammar link only), `crates/ui-egui/src/shell/app/vault/first_run.rs`, `crates/ui-egui/src/text/views/registry.rs` (append). GOAL: D10 bullets 5 and 6; card dismissal remembered in `UiState`. TESTS: code card shows "Install Twig grammar" when `registry.suggest` hits, the old dialog link otherwise; first-run card never fetches; badge only after a check. VERIFY: standing + `cargo xtask i18n`. DO NOT: install without a click, unless `registry.auto_install_for_vault` is on and the file comes from the seed. - [x] 17. Vault `wanted` scan and banner. OWNS: `crates/engine/src/addons/wanted.rs`, `crates/engine/tests/addons_wanted.rs`, `crates/ui-egui/src/shell/app/vault/wanted_banner.rs`. FILES TO OPEN: `crates/engine/src/vault/session.rs`, the content-search code that uses `ignore`/`grep-*`, `spec.md` D11 step 4. GOAL: `scan_wanted(session, budget_ms) -> Vec` over fence labels and `$`/`$$` in the vault with a 3 s budget, run once per vault and registry generation, written to `.notescraft/registry.json` `wanted`; banner lists not-installed entries with size. TESTS: a vault with a ```` ```twig ```` fence yields `grammar:twig`; budget stops a 6500-note scan early without error; installed items are not listed; no network. VERIFY: standing. DO NOT: read notes over 2 MiB; write outside `.notescraft/registry.json`. ## Phase 4 - first-party content and release - [x] 18. Registry repo layout and signing tool. OWNS: `xtask/src/registry_sign.rs`, one subcommand line in `xtask/src/main.rs`, `xtask/tests/registry_sign.rs`; outside this repo (separate commit by the lead): `notescraft-registry/README.md` (schema v2, `id` not `name`), `index.json`, `files/`. FILES TO OPEN: `spec.md` D1 to D3, `/Volumes/Storage/Projects/notescraft-registry/README.md`, `xtask/src/main.rs`. GOAL: `cargo xtask registry-sign --dir --key ` rebuilds `index.json` from `files////` plus a small `entry.toml` per item (title, license, aliases), fills size and sha256, signs; `--check` verifies an existing index. TESTS: sign then verify with the engine verifier; tampered file fails `--check`. VERIFY: standing, `-p xtask`. DO NOT: read the key from the repo; print it. - [ ] 19. Move the 25 grammars out. OWNS: `crates/syntax/src/embedded.rs` (shrinks to an empty list or removed), `crates/syntax/grammars/` (moved to the registry repo and to the seed), `crates/syntax/src/load.rs` (drop the embedded loop), `crates/engine/tests/addons_migration_grammars.rs`. GOAL: only after 7, 10, 16, 17, 18, 23 are done; registry entries `twig`, `blade`, ... one per file; tests per D11 step 5; the TOML and TypeScript grammars stay if the owner wants highlighting out of the box (they are the two most common: decide with the owner, default keep). TESTS: for every moved id: suggest finds it, seed verifies, offline install from the seed makes `is_known` true; removal restores plain text. VERIFY: standing + measure `load::current()` (expect about 75 ms instead of 110 ms) and binary delta (about -69 KB). DO NOT: remove a grammar that has no seed copy. - [ ] 20. STIX Two Math as a font entry. OWNS: `crates/ui-egui/src/design/fonts/mod.rs` (STIX loaded from the store when present), `crates/ui-egui/src/views/math_view.rs` (Install button when absent), `assets/fonts/STIXTwoMath-Regular.ttf` (moves to the seed), tests. GOAL: after 7, 16, 18, 23; math without the font uses the existing Inter/JetBrains chain and shows "Install math font (376 KB)". TESTS: with and without the font; rendering never panics; the migration test of D11. VERIFY: binary delta about -0.36 MiB; snapshot of a note with display math both ways. - [ ] 21. Lottie icons as a pack. OWNS: `crates/ui-egui/src/design/lottie_icon/files.rs` (store lookup, static fallback), `assets/icons-anim/` (moves to the seed), tests. GOAL: after 13, 18, 23; `icons-animated` pack; without it every animated icon draws its static Lucide icon (the skill `notescraft-icon-motion` rules still hold). TESTS: each icon id has a static fallback; motion tests run with the pack installed from a temp store. VERIFY: binary delta about -0.14 MiB. DO NOT: change any animation JSON. - [x] 22. (this repo's side only: test via the signing tool with a throwaway key built at test time, nothing committed; the registry repo's `files/plugin/dice/` and the four themes are still to publish) First plugins and themes in the registry. OWNS (registry repo, plus this repo's tests only): `files/plugin/dice/1.0.0/`, `files/theme/{catppuccin-frappe,catppuccin-macchiato,solarized-light,solarized-dark}/1.0.0/` (themes copied from `crates/themes/src/builtin.rs` and kept built-in until a migration test passes), `plugins/dice/README.md` (point to the registry), `crates/engine/tests/addons_first_party.rs`. GOAL: dice builds with `plugins/dice/build.sh`, its `manifest.json` equals the entry; citations follows when roadmap 10 starts. TESTS: dice installs offline from the seed, rolls, `2d6+3` result in range. DO NOT: remove built-in themes in this task. - [x] 23. Seed packaging. OWNS: `packaging/macos/*`, `packaging/windows/*`, `packaging/linux/package.sh` (copy `registry-seed/` next to the binary), `crates/engine/src/addons/seed.rs`, `crates/engine/tests/addons_seed.rs`. FILES TO OPEN: `packaging/README.md`, `packaging/macos/`. GOAL: `SeedDir::locate()` finds `Contents/Resources/registry-seed` (macOS), `registry-seed/` beside the exe (Windows, Linux), verified signed snapshot of moved items only. TESTS: locate with a fake exe path; missing seed is fine (network install still works); tampered seed file rejected. DO NOT: put anything unsigned in the seed. - [ ] 24. Docs, numbers, status. OWNS: `ROADMAP.md`, `docs/ARCHITECTURE.md`, `docs/CODE-MAP.md` (addons rows), `SPEC.md` sections 6.1 and 7 (a "Registry" subsection, community store no longer deferred), `specs/registry/measurements.md` (new numbers), `specs/plugins-parity/roadmap.md` (add the rows). VERIFY: `cargo xtask ci`. DO NOT: add features. ## Order and parallelism Phase 0 in parallel with 1, 2, 4, 5, 6 (disjoint folders). 3 starts when the owner answers question 1. 7 after 3 to 6. 8 and 9 after 7. 10 to 13 are disjoint after 8. 14 to 17 after 8 (UI can start on a fake command layer). 18 can start at any time and unblocks 3's known-answer test. 19 to 22 only after their dependencies, one at a time, each with its migration test. The first three agent briefs are `specs/plugins-parity/briefs/29..31`.