Headless-first surface discipline
The two binary gates
Gate 1 — before writing the first #[tauri::command] for a feature. Ask:
Does `cli/src/commands/` already have a placeholder for the parent
task? Grep: `grep -rn "TASK-N\b" cli/`.
If a placeholder exists, the CLI surface is part of the parent task's deliverable. Plan the CLI work into the same change set, not a follow-up. Examples of placeholder shapes that count:
- A
cli/src/commands/<name>.rswhose handler returnsbail!("... TASK-N"). - An
eprintln!("... see TASK-N ...")warn-and-skip in a CLI handler. - A
default_X_reader()/ similar factory incore/that returnsNonewith a doc comment naming TASK-N. - A trait in
core::diagnosticswith#[non_exhaustive]empty struct types and TASK-N comments.
If no placeholder exists, this is a green-field feature — file the CLI surface explicitly during planning.
Gate 2 — before flipping the parent task to In Review or opening a PR. Run:
grep -rn "TASK-N\b" cli/ core/src/
Any match in cli/src/commands/ or in a default_*_reader-style factory in core/ is an unfilled deliverable. The parent task isn't done. Flip the gate; ship the CLI wiring; then re-flip.
These are not heuristics. They are checks you literally execute. The cost of the two greps is seconds. The cost of skipping them, on TASK-78, was building seven Tauri commands + a full UI surface before noticing cli/src/commands/crash_dump.rs existed at all.
The rule
Every user-visible or developer-inspectable behavior ships through three layers, in this order:
- Public library API — a
pubitem incore/(or another workspace member) with a doc-comment, re-exported fromcore::preludewhen canonical. - Headless CLI — a subcommand under
cli/src/commands/with both human-text and--jsonoutput, OR an extension to an existing subcommand. Reachable without the desktop shell. - Headful UI — Tauri shell consumes the same library function the CLI does. The UI does not own behavior the CLI cannot reach.
If a feature lands in only the UI, it doesn't exist for contributors, CI, or scripted use. If it lands in only the library, the parity invariant from TASK-81 (cli/src/main.rs:5-7) silently rots. Both failure modes have happened before — this rule blocks them.
Why
- Contributor onboarding. Anyone with
cargo run -p openwhisper-cli -- memorycan repro a behavior without building Tauri, signing the bundle, or granting TCC. Lowering that floor is the difference between an OSS project and a vendor app. - CI smoke-ability. UI behavior needs Playwright + a windowed runner; library behavior runs in
cargo test. CLI behavior runs in shell. The CLI is the cheapest dependable smoke surface — TASK-81.9 already runscli transcribein CI. - Bug repro flatness. "Run this CLI invocation and paste the JSON" is faster than "open the app, click here, screenshot the panel."
- Architecture discipline. Forcing every behavior through a
pub fnkeepsapps/tauri/src-tauri/from re-growing the orchestration that TASK-81 just lifted out.
How to apply
When you propose a new feature:
- Identify the load-bearing function or type. Place it in
core/(peropenwhisper-orchestration-in-rust). - Re-export it from
core::preludeif it's a canonical type a consumer would name. - Plan a CLI surface. If the feature has something to inspect or do headlessly today, file or extend a
cli/src/commands/<name>.rsin the same task. If there's genuinely nothing to do yet (e.g. a state machine with no registered instances), write down the "lights up when X" trigger in the task notes and gate the CLI work on that follow-up. - Then wire the UI in
apps/tauri/.
When you finish a task:
Before flipping it to In Review, ask:
- Is the new behavior reachable from
cli/src/main.rs --help? If not, why not? - Is the canonical type re-exported from
core::prelude? If not, why not? - Does the Tauri command (if any) just call the library, or does it own logic the CLI can't reach?
If any answer is "no" without an explicit follow-up task, fix it before the In Review flip.
When the CLI surface waits
CLI parity can defer when the underlying library surface has no concrete instance to operate on. Example: model_lifecycle::ModelHandle<T> shipped without a CLI command because no recognizer was wrapped yet — the CLI surface (per-model rows) lights up once TASK-62.4 registers handles. The discipline still holds: file the follow-up explicitly, link the future trigger in the parent task, and don't ship a CLI subcommand that prints "no data" forever.
This is a real exception, not a loophole. If you reach for it, write the follow-up task ID into the implementation notes so the gap is visible in Backlog.
Anti-patterns this skill prevents
- Adding a Tauri
#[tauri::command]that wraps logic which exists nowhere else — the command becomes the de-facto API. - Adding a React-only feature (toast, modal, side-effect) that does work the CLI can't trigger.
- A
pubcore function that's never re-exported fromprelude— external consumers have to memorize module paths to find it. - A "Diagnostics" UI panel that shows numbers no
cargo testoropenwhisper memoryinvocation can independently confirm. - "We'll add the CLI later" without filing the follow-up — later never lands.
Why this skill didn't catch TASK-78 the first pass
A real failure case worth naming, not buried:
In the original TASK-78 implementation pass, seven Tauri commands shipped
(crashes_list, crashes_read, crashes_delete, crashes_delete_all,
crashes_mark_read, crashes_unread_count, crashes_open_folder) plus a
full React inspector — without touching the existing CLI placeholder at
cli/src/commands/crash_dump.rs. The placeholder had explicit TASK-78
deferred markers (bail!("... TASK-78"), eprintln!("see TASK-78 ..."))
and a stub default_crash_reader() -> None in core::diagnostics. The
parity gap was caught only after the user asked "this work was also added
to the CLI right?"
Three stacked failures:
- Plan didn't name the CLI surface.
backlog/docs/plans/doc-23listed "Tauri commands" as a deliverable; CLI parity lived only in this skill, not in the plan. The plan was treated as authoritative scope. - No pre-action gate. The earlier skill description said "READ before adding a Tauri command" — informational, not a checkable binary question. Adding seven commands didn't trigger me to load the skill body. Gate 1 above (now part of this skill) is the fix.
- Placeholder code was invisible. The strongest possible signal —
bail!("... TASK-78")already in tree — was never seen because no grep ever ran outsideapps/tauri/andcore/src/crashes/. Gate 2 above is the fix.
Lesson: the gates run for free. The plan + skill descriptions are fallible. Use the gates anyway, every time.
Related
openwhisper-orchestration-in-rust— the where logic lives skill. This skill is the how layers expose it skill.writing-backlog-plans— the plan author's discipline. CLI surface should be an explicit deliverable on every plan that has acli/src/commands/placeholder. Reviewer check today.- TASK-81 (
backlog/docs/specs/doc-24 - Library-API-audit-and-headless-CLI-—-design.md) — the audit that introduced the parity doctrine; this skill makes it durable. cli/src/main.rsheader doc — single-source statement of the parity invariant.core/src/prelude.rs— the canonical re-export surface; newpubtypes belong here.
Source: jimmi-joensson/OpenWhisper — distributed by TomeVault.