Files
mission-control/PLAN.md
teernisse 954067a38b docs: initial Mission Control planning documents
- PLAN.md: Complete implementation plan with architecture, ACs, phases
- CLAUDE.md: Project context for AI agents

Architecture: Tauri + React, beads as universal work graph,
manual-first priority with rich decision logging.
2026-02-25 16:23:07 -05:00

62 KiB
Raw Blame History

Mission Control — Implementation Plan

Version: 1.0 Created: 2026-02-25 Status: Planning Complete, Ready for Implementation

Executive Summary

Mission Control (MC) is an ADHD-centric personal productivity hub that unifies GitLab activity, beads task tracking, and manual task management into a single, beautiful native interface. The core UX principle: surface THE ONE THING you should be doing right now.

This is NOT a dashboard of everything. It's a trusted advisor that understands your work and helps you decide what matters.


Table of Contents

  1. Vision & Principles
  2. Architecture Overview
  3. Tech Stack
  4. Data Model
  5. GitLab → Beads Bridge
  6. Views & UX
  7. Priority & Decision System
  8. Implementation Phases
  9. Acceptance Criteria
  10. Open Questions

Vision & Principles

The Core Question

"What should I actually be doing right now?"

MC answers this question. Not with 47 items. With ONE.

ADHD-Centric Design Principles

Principle Implementation
The One Thing UI's primary job is surfacing THE single most important thing. Everything else is peripheral.
Achievable Inbox Zero Every view must have a clearable state. The psychological win of "inbox zero" everywhere.
Time Decay Visibility Age is visceral, not hidden in timestamps. Fresh=bright, 3d=amber, 7d+=red pulse.
Batch Mode for Flow "You have 4 code reviews. Want to batch them? (~20 min)"
Quick Capture, Trust the System One hotkey, type it, gone. System triages later.
Context Bookmarking When you switch away, system bookmarks exactly where you were.
Ambient Awareness, Not Interruption Notifications are visible (badge, color) but never modal.

What MC Is NOT

  • Not a GitLab replacement (use GitLab for actual work)
  • Not a beads replacement (beads is the task graph, MC is the interface)
  • Not an automated prioritization system (you decide, MC learns)

Architecture Overview

┌─────────────────────────────────────────────────────────────────┐
│                     MISSION CONTROL                              │
│                     Tauri Desktop App                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │                    React Frontend                         │   │
│  │  ┌────────────┐ ┌────────────┐ ┌────────────┐            │   │
│  │  │ Focus View │ │ Queue View │ │  Timeline  │            │   │
│  │  │ (THE ONE)  │ │  (all)     │ │  (stream)  │            │   │
│  │  └────────────┘ └────────────┘ └────────────┘            │   │
│  │  ┌────────────┐ ┌────────────┐ ┌────────────┐            │   │
│  │  │ Batch Mode │ │   Inbox    │ │  Capture   │            │   │
│  │  │ (flow)     │ │  (triage)  │ │  (overlay) │            │   │
│  │  └────────────┘ └────────────┘ └────────────┘            │   │
│  └──────────────────────────────────────────────────────────┘   │
│                              │                                   │
│                              │ Tauri IPC                         │
│                              ▼                                   │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │                    Rust Backend                           │   │
│  │                                                           │   │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────────┐   │   │
│  │  │ Bridge      │  │ Data Layer  │  │ Decision Logger │   │   │
│  │  │ ──────────  │  │ ──────────  │  │ ───────────────  │   │   │
│  │  │ Watch lore  │  │ Call lore   │  │ Log all actions │   │   │
│  │  │ Create beads│  │ Call br/bv  │  │ Capture context │   │   │
│  │  │ Sync state  │  │ Read mapping│  │ Store reasons   │   │   │
│  │  └─────────────┘  └─────────────┘  └─────────────────┘   │   │
│  └──────────────────────────────────────────────────────────┘   │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘
                              │
        ┌─────────────────────┼─────────────────────┐
        ▼                     ▼                     ▼
┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐
│   lore CLI      │  │   br/bv CLI     │  │  MC Local State │
│   (--robot)     │  │   (beads)       │  │                 │
│                 │  │                 │  │  mapping.json   │
│   lore.db       │  │   .beads/       │  │  decisions.jsonl│
└─────────────────┘  └─────────────────┘  └─────────────────┘

Key Architectural Decisions

Decision Choice Reasoning
Desktop framework Tauri 2.0 Rust backend, tiny bundle (~15MB vs Electron 150MB), native APIs
Frontend React 19 + Vite Fast iteration, huge ecosystem, AI-friendly
lore integration CLI (lore --robot) Clean API boundary, no schema coupling
beads integration CLI (br commands) Battle-tested, guaranteed compatibility
Local state JSON files Simple, portable, easy to inspect/debug

Tech Stack

Layer Choice Why
Shell Tauri 2.0 Rust backend, tiny bundle, native APIs, system tray, global hotkeys
Frontend React 19 + Vite Fast iteration, huge ecosystem
Styling Tailwind + shadcn/ui Beautiful defaults, easy customization
Animations Framer Motion Smooth, spring-based, ADHD-friendly micro-interactions
State Zustand + TanStack Query Simple global state + async data fetching
Backend DB JSON files mc_state.json, mapping.json, decisions.jsonl
IPC Tauri commands + events Type-safe, bidirectional
Color scheme Dark only User preference, nail down details later

Data Model

Beads as Universal Work Graph

Everything is a bead. GitLab items, personal tasks, quick captures — all flow into the beads task graph.

┌─────────────────────────────────────────────────────────────────┐
│                         BEADS                                    │
│                (Universal Task Graph)                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌──────────────┐     ┌──────────────┐     ┌──────────────┐     │
│  │  MR Review   │────▶│  My Feature  │────▶│  Deploy PR   │     │
│  │  !847        │     │  (manual)    │     │  (manual)    │     │
│  │ [from gitlab]│     │              │     │              │     │
│  └──────────────┘     └──────────────┘     └──────────────┘     │
│                                                                  │
│  Source types:                                                   │
│  • gitlab (auto-created from lore sync via MC bridge)           │
│  • manual (your tasks, quick captures)                          │
│                                                                  │
│  Dependencies span everything. bv's graph algos work on all.    │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                     MISSION CONTROL                              │
│              (The interface to your work graph)                  │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  • Shows THE ONE THING (manually curated by you)                │
│  • Suggests what to work on (bv recommendations as hints)       │
│  • You decide. You drag. You defer. You're in control.          │
│  • Quick capture → creates bead → you triage                    │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

MC Does NOT Modify Beads Schema

Beads stays clean. MC layers its own tracking on top via mapping files.


GitLab → Beads Bridge

Event → Bead Mapping

GitLab Event Bead Created Mapping Key Format
MR review requested to you Review MR !{iid}: {title} mr_review:{host}:{project_id}:{iid}
Issue assigned to you Issue #{iid}: {title} issue:{host}:{project_id}:{iid}
MR you authored (opened) Your MR !{iid}: {title} mr_authored:{host}:{project_id}:{iid}
Mention in discussion Mentioned in {type} #{iid}: {snippet} mention:{host}:{project_id}:{iid}:{note_id}
Comment on your authored MR Respond to @{actor} on MR !{iid} feedback:{host}:{project_id}:{iid}:{note_id}

Key format rationale: We use numeric project_id instead of project path because paths can change (renames, transfers). Numeric IDs are immutable.

Data Source

All events come from lore --robot me, specifically the since_last_check section which tracks new activity since the cursor was last advanced.

This is key: We don't track resolution status or complex thread state. We use since_last_check — "someone said something to you" = create bead. You close it when handled.

Item Lifecycle States

Each mapped item has a lifecycle state:

                    ┌─────────────────────────────────────┐
                    │                                     │
                    ▼                                     │
┌────────┐     ┌────────┐     ┌─────────────┐     ┌──────┴───┐
│ (new)  │────▶│ active │────▶│ suspect_    │────▶│  closed  │
│ event  │     │        │     │ orphan      │     │          │
└────────┘     └────────┘     └─────────────┘     └──────────┘
                    │                                     ▲
                    │         (user closes bead)          │
                    └─────────────────────────────────────┘
State Meaning
active Bead exists, GitLab item is open
suspect_orphan Missing from lore for 1 reconciliation cycle
closed Bead closed (by user OR auto-close), entry removed from map

Two-Strike Close Rule

Problem: What if GitLab's API hiccups and temporarily says "you have no reviews"? Without protection, we'd delete all your tasks.

Solution: Items must be missing for TWO consecutive reconciliations before auto-close.

Check #1 Check #2 Result
Missing Missing Close the task (confirmed gone)
Missing Found Keep it (was just a glitch)
Found Keep it (still active)

This prevents false closes from transient API failures or partial fetches.

Mapping File Schema

~/.local/share/mc/gitlab_bead_map.json
{
  "schema_version": 1,
  "cursor": {
    "last_check_timestamp": "2026-02-25T10:30:00Z",
    "last_reconciliation": "2026-02-25T06:00:00Z"
  },
  "mappings": {
    "mr_review:gitlab.com:12345:847": {
      "bead_id": "br-x7f",
      "created_at": "2026-02-23T14:00:00Z",
      "suspect_orphan": false,
      "pending": false
    },
    "issue:gitlab.com:12345:312": {
      "bead_id": "br-c9d",
      "created_at": "2026-02-24T09:00:00Z",
      "suspect_orphan": true,
      "pending": false
    },
    "mr_authored:gitlab.com:12345:903": {
      "bead_id": null,
      "created_at": "2026-02-25T10:29:00Z",
      "suspect_orphan": false,
      "pending": true
    }
  }
}

Field meanings:

  • bead_id: The beads task ID, or null if creation was interrupted
  • suspect_orphan: true if item was missing in last reconciliation (first strike)
  • pending: true if bead creation is in-flight (crash recovery will retry)

Bridge Flow

lore.db changes (file watcher on mtime)
         │
         ▼
MC calls: lore --robot me
         │
         ▼
MC iterates: since_last_check events
         │
         ├── Event key in mapping? → Skip (already have bead)
         │
         └── Event key NOT in mapping? → Create bead via br CLI
                  │
                  ▼
         Store mapping: {key} → {bead_id, created_at, suspect_orphan: false}

Reconciliation

Problem: since_last_check is incremental. If MC is offline, clock skews, or lore's cursor gets stale, events can be missed.

Solution: Periodic full reconciliation pass.

Trigger Action
App startup Full reconciliation
Every 6 hours Full reconciliation
since_last_check empty but items exist Full reconciliation

Full Reconciliation Algorithm:

  1. Fetch all open items from lore --robot me --issues and lore --robot me --mrs
  2. Build set of expected keys from lore response
  3. For each key in map:
    • If key in expected AND suspect_orphan=true → clear flag
    • If key NOT in expected AND suspect_orphan=false → set suspect_orphan=true
    • If key NOT in expected AND suspect_orphan=true → close bead, remove from map
  4. For each key in expected:
    • If key NOT in map → create bead, add to map

Cursor Semantics

Operation Cursor Update
Successful incremental sync Advance last_check_timestamp
Successful full reconciliation Advance last_reconciliation
Partial/failed sync Do not advance (retry will reprocess)

Recovery: If cursor appears stale (since_last_check empty but open items exist), trigger full reconciliation and reset cursor.

Crash-Safe Operation Ordering

Problem: If MC crashes mid-sync, we risk duplicates (bead created but not mapped) or lost events (cursor advanced but bead not created).

Solution: Write-ahead pattern with idempotent operations.

For each new event:

1. Check if key exists in mapping → if yes, skip (idempotent)
2. Write mapping entry FIRST: {key} → {bead_id: null, pending: true}
3. Create bead via `br create`
4. Update mapping: {bead_id: actual_id, pending: false}
5. On success of all events: advance cursor

Crash recovery (on startup):

1. Scan mapping for entries with pending: true
2. For each pending entry:
   - If bead_id is null → retry `br create`, update mapping
   - If bead_id exists but pending → verify bead exists, clear pending flag
3. Do NOT advance cursor until all pending entries resolved

Why this works:

  • Step 1 is idempotent (duplicate events skip)
  • Step 2 happens before bead creation (we know we intend to create)
  • Step 5 only advances cursor after ALL events processed
  • Recovery finds incomplete work and finishes it

Bridge Invariants

These must ALWAYS hold. Violations are bugs.

ID Invariant
INV-1 No duplicate beads. Each mapping key maps to exactly one bead ID.
INV-2 No orphan beads. Every bead ID in the map exists in beads.
INV-3 No false closes. Items only auto-closed after missing in TWO reconciliations.
INV-4 Cursor monotonicity. Cursor only advances forward, never backward.

Single-Instance Lock

MC enforces single-instance operation via an OS advisory lock:

~/.local/share/mc/mc.lock

Implementation: Use flock(2) (Unix) or equivalent OS advisory lock — NOT "file exists" semantics.

Startup behavior:

  1. Open mc.lock (create if missing)
  2. Attempt flock(fd, LOCK_EX | LOCK_NB) (non-blocking exclusive lock)
  3. If EWOULDBLOCK → another instance holds lock → show error dialog, exit
  4. If lock acquired → proceed, OS auto-releases on process exit/crash

Why advisory lock over "file exists":

  • Automatically released on crash (no stale lockfiles)
  • No cleanup needed on abnormal exit
  • Race-free (OS handles atomicity)

Rationale: Atomic file writes protect against mid-write crashes, but not concurrent writers. Lock file prevents race conditions in bead creation.

CLI Contract Testing

Risk: lore/br/bv robot output schema can drift, silently breaking MC.

Mitigation:

  • Store fixture files of expected CLI outputs in tests/fixtures/
  • Contract tests validate MC's parsing against real CLI outputs
  • On CLI version bump, regenerate fixtures and verify parsing still works
  • Rust types with #[serde(deny_unknown_fields)] to catch unexpected fields

Bead Creation via br CLI

// MC shells out to br for all bead operations
fn create_bead(title: &str, bead_type: &str) -> Result<String> {
    let output = Command::new("br")
        .args(["create", "--title", title, "--type", bead_type, "--json"])
        .output()?;
    let result: BrCreateResult = serde_json::from_slice(&output.stdout)?;
    Ok(result.id)
}

fn close_bead(id: &str, reason: &str) -> Result<()> {
    Command::new("br")
        .args(["close", id, "--reason", reason])
        .output()?;
    Ok(())
}

Auto-Close on GitLab State Change

When GitLab state changes (MR merged, issue closed), the corresponding bead closes via the two-strike rule:

GitLab State Change Detection Bead Action
MR merged Item disappears from open list Two-strike close with reason "MR merged in GitLab"
MR closed Item disappears from open list Two-strike close with reason "MR closed in GitLab"
Issue closed Item disappears from open list Two-strike close with reason "Issue closed in GitLab"

Error Handling

Error Handling
lore CLI unavailable Log error, skip sync, retry next cycle
br create fails Log error, do NOT add to map (will retry next sync)
br close fails Log error, keep in map as suspect_orphan (will retry)
JSON parse error Log error, skip that event, continue processing others
Map file corrupted Load backup, log warning, trigger full reconciliation

Backup Strategy: Before each write to gitlab_bead_map.json, copy current file to .bak, write to .tmp, then atomic rename.


Views & UX

Window Behavior: B+D Hybrid

  1. Menu bar icon with badge count (always present)
  2. Popover for quick glance (click menu bar icon)
  3. Full window for deep work (hotkey or button in popover)
  4. Optional floating widget (toggle in settings) for constant visibility
Menu Bar:  [other items]                    🔴3  MC  │ 🔋 ...
                                                 │
                                    click ───────┘
                                                 ▼
                              ┌─────────────────────────────┐
                              │  THE ONE THING              │
                              │  Review MR !847             │
                              │  2d waiting · @sarah        │
                              │                             │
                              │  [Start]  [Defer]  [Skip]   │
                              ├─────────────────────────────┤
                              │  Queue: 4  Inbox: 3         │
                              │  ⌘⇧F Full window            │
                              └─────────────────────────────┘

View 1: Focus View (Home)

The default. Shows THE ONE THING.

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│                    ┌───────────────────┐                    │
│                    │  🔴  MR REVIEW    │                    │
│                    └───────────────────┘                    │
│                                                             │
│     Fix authentication token refresh logic                  │
│     ─────────────────────────────────────                   │
│                                                             │
│     !847 in platform/core • 47 lines changed                │
│                                                             │
│     ┌─────────────────────────────────────────────────┐     │
│     │  @sarah requested 2 days ago                    │     │
│     │  "Can you take a look? I need this for the      │     │
│     │   release tomorrow"                             │     │
│     └─────────────────────────────────────────────────┘     │
│                                                             │
│     ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌───────┐    │
│     │  Start   │  │ 1 hour   │  │ Tomorrow │  │ Skip  │    │
│     │    ↵     │  │   ⌘1     │  │    ⌘2    │  │  ⌘S   │    │
│     └──────────┘  └──────────┘  └──────────┘  └───────┘    │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│  Queue: 3 more reviews • 2 assigned issues • 5 mentions     │
└─────────────────────────────────────────────────────────────┘

Behavior:

  • "Start" opens GitLab in browser
  • Defer options: 1 hour, tomorrow, custom
  • Skip removes from today's list (logged with reason)
  • Keyboard-driven: Enter to start, numbers for defer, S to skip

View 2: Queue View

All pending work, organized by type.

┌─────────────────────────────────────────────────────────────┐
│  Queue                                          ⌘K filter   │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  REVIEWS (4)                           [Batch All · 25min]  │
│  ┌─────────────────────────────────────────────────────┐    │
│  │ 🔴 !847  Fix auth token refresh          2d  @sarah │    │
│  │ 🟡 !902  Add rate limiting middleware    1d  @mike  │    │
│  │ 🟢 !915  Update README badges            4h  @alex  │    │
│  │ 🟢 !918  Typo fix in error messages      2h  @bot   │    │
│  └─────────────────────────────────────────────────────┘    │
│                                                             │
│  ASSIGNED ISSUES (2)                                        │
│  BEADS (3)                                                  │
│  MANUAL TASKS (1)                                           │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Behavior:

  • Items colored by staleness (fresh=green, aging=amber, stale=red)
  • Click to make it THE ONE THING
  • Drag to reorder (manual priority)
  • "Batch All" enters batch mode

View 3: Timeline View

Chronological activity stream.

View 4: Batch Mode

Full-screen focus for clearing similar items rapidly.

┌─────────────────────────────────────────────────────────────┐
│                     BATCH: CODE REVIEWS                      │
│                        1 of 4 · 25 min                       │
│                     ━━━━━━━━━━░░░░░░░░░░                     │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│     Fix authentication token refresh logic                  │
│     !847 in platform/core                                   │
│                                                             │
│     47 lines changed across 3 files                         │
│                                                             │
│     ┌───────────────┐  ┌───────────────┐  ┌───────────┐    │
│     │  Open in GL   │  │     Done      │  │   Skip    │    │
│     │      ⌘O       │  │      ⌘D       │  │    ⌘S     │    │
│     └───────────────┘  └───────────────┘  └───────────┘    │
│                                                             │
│                        ESC to exit batch                     │
└─────────────────────────────────────────────────────────────┘

View 5: Inbox View

New stuff requiring triage.

View 6: Quick Capture (Overlay)

Global hotkey (⌘⇧C) summons this from anywhere:

              ┌────────────────────────────────────────┐
              │                                        │
              │  ┌────────────────────────────────┐    │
              │  │ Quick thought...               │    │
              │  │                                │    │
              │  │ Need to check if webhook       │    │
              │  │ retry uses exponential backoff │    │
              │  └────────────────────────────────┘    │
              │                                        │
              │  ⏎ Save & close     ESC Cancel         │
              │                                        │
              └────────────────────────────────────────┘

Creates a bead immediately. You triage later.


Priority & Decision System

Philosophy: Manual-First, Learn from Data

We do NOT know the right prioritization algorithm yet. Instead:

  1. You manually set THE ONE THING
  2. MC logs every decision with context and reasoning
  3. Post-process logs to extract patterns
  4. Eventually codify patterns into suggestions

Decision Log

~/.local/share/mc/decision_log.jsonl

Every action gets logged:

{
  "timestamp": "2026-02-25T10:30:00Z",
  "action": "set_focus",
  "bead_id": "br-x7f",
  "reason": "Sarah pinged me on Slack, she's blocked",
  "tags": ["blocking", "urgent"],
  "context": {
    "previous_focus": "br-a3b",
    "queue_size": 12,
    "time_of_day": "morning",
    "day_of_week": "Tuesday",
    "available_items": ["br-x7f", "br-a3b", "br-c9d"],
    "item_ages_days": {"br-x7f": 2, "br-a3b": 5, "br-c9d": 1},
    "items_completed_today": 3,
    "focus_session_duration_min": 45
  }
}

Actions to Log

Action What to Capture
set_focus Which bead, why, what else was available
reorder Old order, new order, why
defer Which bead, duration, why
snooze Which bead, until when, why
skip Which bead, why (explicitly chose not to do it)
complete Which bead, duration if tracked, notes
create_manual New bead from quick capture
change_priority Old priority, new priority, why

"Why" Capture UX

Every significant action prompts for optional reason:

┌─────────────────────────────────────────────────────────────┐
│  Setting focus to: Review MR !847                            │
│                                                              │
│  Why? (optional, helps learn your patterns)                  │
│  ┌────────────────────────────────────────────────────────┐  │
│  │ Sarah pinged me, she's blocked on release              │  │
│  └────────────────────────────────────────────────────────┘  │
│                                                              │
│  Quick tags: [Blocking] [Urgent] [Context switch] [Energy]   │
│                                                              │
│  [Confirm]                                    [Skip reason]  │
└─────────────────────────────────────────────────────────────┘

Quick tags = one-click common reasons Skip reason = don't force it, but make it easy

Context Snapshot

Each log entry captures decision context:

struct DecisionContext {
    queue_snapshot: Vec<QueueItem>,      // All items at decision time
    time_of_day: TimeOfDay,              // morning/afternoon/evening
    day_of_week: Weekday,
    focus_session_duration: Option<Duration>,
    items_completed_today: u32,
    last_sync_age: Duration,
}

Log Retention & Privacy (Post-v1)

The decision log can grow indefinitely. For v1, we accept unbounded growth since:

  • Single user, local storage only
  • Log entries are small (~500 bytes each)
  • 100 decisions/day × 365 days × 500 bytes = ~18 MB/year

Post-v1 retention policy (when needed):

Policy Implementation
Size cap Rotate when log exceeds 50 MB (keep last N entries)
Time-based Optional: prune entries older than 1 year
Export before prune Always export to dated backup before any deletion

Privacy considerations (post-v1):

  • Reasons may contain sensitive context (names, project details)
  • Add optional redaction: strip free-text reasons, keep only tags
  • Add export-with-redaction for sharing anonymized patterns

Future: Pattern Extraction

Once we have enough data:

  1. Cluster reasons → discover priority categories
  2. Correlate context → time-of-day patterns, staleness thresholds
  3. Train simple model → suggest priority, you confirm/override
  4. Eventually: auto-prioritize with confidence scores

Implementation Phases

TDD Rule: Every task below follows RED → GREEN. Write failing test first, then implement.

Phase 0: Test Infrastructure (0.5 day)

  • Configure Vitest for frontend unit/component tests
  • Configure Playwright for E2E tests
  • Set up Rust test harness with trait-based mocking
  • Create fixture directory structure
  • Capture initial CLI fixtures from real lore/br outputs
  • Add test commands to package.json and Cargo.toml

Phase 1: Foundation (2-3 days)

  • Scaffold Tauri + Vite + React project
  • Basic window with system tray icon
  • Global hotkey to toggle window (⌘⇧M)
  • RED: Write lore.rs parsing tests against fixtures
  • GREEN: Implement lore CLI wrapper, parse lore --robot me
  • RED: Write file watcher tests (mock filesystem events)
  • GREEN: File watcher on lore.db to trigger refresh
  • Display raw data in UI (no test needed — visual verification)

Phase 2: Bridge + Data Layer (2-3 days)

  • RED: Write state machine transition tests (all 6 scenarios from Testing Architecture)
  • RED: Write invariant assertion helper
  • GREEN: Implement GitLab → Beads bridge state machine
  • RED: Write mapping file read/write tests (atomic writes, schema validation)
  • GREEN: Create mapping file structure with atomic writes
  • RED: Write br create/br close wrapper tests against fixtures
  • GREEN: Bead creation via br CLI
  • RED: Write two-strike auto-close tests
  • GREEN: Auto-close beads on GitLab state change
  • RED: Write reconciliation tests (startup, periodic, cursor recovery)
  • GREEN: Full reconciliation pass (startup + periodic)
  • RED: Write crash recovery tests (all 3 scenarios)
  • GREEN: Implement write-ahead pattern with pending flag
  • RED: Write single-instance lock tests
  • GREEN: Implement flock-based single-instance lock
  • RED: Write decision log append/read tests
  • GREEN: Decision log infrastructure
  • Verify all invariants pass after each test

Phase 3: Focus View (1-2 days)

  • RED: Write FocusCard component tests (render, Start/Defer/Skip, keyboard)
  • GREEN: THE ONE THING card component
  • RED: Write focus selection state tests
  • GREEN: Manual focus selection
  • RED: Write action tests (open URL, defer timing, skip logging)
  • GREEN: Basic actions (open in browser, defer, skip)
  • RED: Write ReasonPrompt tests (text capture, tags, submit/cancel)
  • GREEN: Decision logging with reason capture
  • GREEN: Quick tags for common reasons

Phase 4: Queue + Inbox (2-3 days)

  • RED: Write QueueList tests (render sections, staleness colors)
  • GREEN: Queue list with sections
  • RED: Write drag-reorder tests (order persisted, decision logged)
  • GREEN: Drag to reorder (manual priority)
  • RED: Write click-to-focus tests
  • GREEN: Click to set as focus
  • RED: Write Inbox triage tests
  • GREEN: Inbox with triage actions
  • RED: Write filter/search tests
  • GREEN: Filter/search (⌘K)

Phase 5: Batch Mode (1-2 days)

  • RED: Write BatchMode tests (progress, cycling, completion)
  • GREEN: Full-screen batch interface
  • GREEN: Progress tracking
  • GREEN: Rapid completion flow
  • GREEN: Completion celebration

Phase 6: Quick Capture (1 day)

  • RED: Write QuickCapture tests (overlay, text input, bead creation)
  • GREEN: Global hotkey overlay (⌘⇧C)
  • GREEN: Instant bead creation
  • GREEN: Appears over other apps
  • E2E: Write quick-capture E2E test

Phase 7: Polish + E2E (ongoing)

  • Animations (Framer Motion) — visual verification
  • RED: Write staleness color tests
  • GREEN: Staleness visualization (color decay)
  • Badge counts in menu bar — visual verification
  • Settings UI — visual verification
  • Floating widget (optional)
  • E2E: Write focus-flow E2E test
  • E2E: Write batch-mode E2E test
  • E2E: Write sync-status E2E test
  • Verify 90% Rust bridge coverage, 85% hooks coverage, 70% component coverage

Post-v1: Deferred Features

Timeline View (deferred)

  • Chronological activity view
  • Smart grouping (today, yesterday, older)
  • Expand/collapse

Rationale: Timeline is "nice to have" but not core to the "ONE THING" thesis. Defer until trust metrics on bridge reliability are solid.

Future Intelligence (deferred)

Once decision log has sufficient data:

  • Cluster reasons → discover priority categories
  • Correlate context → time-of-day patterns, staleness thresholds
  • Train simple model → suggest priority, user confirms/overrides

Acceptance Criteria

AC-001: Bridge Creates Beads from GitLab Events

Given lore has synced new GitLab activity When MC detects lore.db change and processes since_last_check Then beads are created for: MR reviews requested, issues assigned, mentions, comments on authored MRs And mapping file is updated with {event_key} → {bead_id} And duplicate events do not create duplicate beads

AC-002: Bridge Auto-Closes Beads (Two-Strike Rule)

Given a bead exists for a GitLab MR or issue When that item is missing from lore for TWO consecutive reconciliations Then the corresponding bead is closed via br close And reason includes GitLab state change (e.g., "MR merged in GitLab")

Note: The two-strike rule prevents false closes from transient API failures. First miss sets suspect_orphan=true, second miss triggers close.

AC-002b: Reconciliation Heals Missed Events

Given MC was offline or since_last_check cursor is stale When MC starts up or 6-hour reconciliation timer fires Then full reconciliation runs against all open items from lore And missing beads are created for items not in mapping And items missing from lore are marked suspect_orphan (first strike) And items already marked suspect_orphan that are still missing are closed (second strike)

AC-003: Focus View Shows THE ONE THING

Given user has set a focus item When viewing Focus View Then that single item is prominently displayed And actions (Start, Defer, Skip) are available And keyboard shortcuts work

AC-004: Manual Priority via Drag Reorder

Given user is in Queue View When user drags an item to new position Then order is persisted And decision is logged with context And user is prompted for optional reason

AC-005: Decision Logging Captures Context

Given user performs any significant action (set_focus, reorder, defer, skip, complete) When action is executed Then decision_log.jsonl is appended with: timestamp, action, bead_id, reason (if provided), tags, full context snapshot

AC-006: Quick Capture Creates Bead

Given user presses global hotkey (⌘⇧C) from any app When user types text and presses Enter Then bead is created via br create And overlay dismisses And user returns to previous context

AC-007: Menu Bar Badge Shows Counts

Given MC is running When there are pending items Then menu bar icon shows badge with count And clicking icon opens popover And popover shows THE ONE THING and queue summary

AC-008: Batch Mode Enables Flow State

Given user has multiple items of same type (e.g., reviews) When user enters Batch Mode Then items are presented one at a time And progress bar shows completion And Done/Skip advances to next And ESC exits batch

AC-009: Sync Status Visible

Given lore cron syncs periodically When viewing any MC screen Then sync status is visible (last sync time, success/failure) And errors are surfaced with actionable info

AC-010: Staleness Visualization

Given items have different ages When viewing Queue or Focus Then fresh items appear bright/green And 1-2 day items appear normal And 3-6 day items appear amber And 7+ day items appear red/pulsing


File Structure

mission-control/
├── src-tauri/              # Rust backend
│   ├── src/
│   │   ├── main.rs
│   │   ├── commands/       # Tauri command handlers
│   │   │   ├── mod.rs
│   │   │   ├── work_items.rs
│   │   │   ├── actions.rs
│   │   │   ├── capture.rs
│   │   │   └── decisions.rs
│   │   ├── bridge/         # GitLab → Beads bridge
│   │   │   ├── mod.rs
│   │   │   ├── sync.rs
│   │   │   └── mapping.rs
│   │   ├── data/           # Data layer
│   │   │   ├── mod.rs
│   │   │   ├── lore.rs     # Shell to lore CLI
│   │   │   ├── beads.rs    # Shell to br CLI
│   │   │   └── state.rs    # MC-local state
│   │   ├── logging/        # Decision logging
│   │   │   ├── mod.rs
│   │   │   └── decision_log.rs
│   │   └── lib.rs
│   ├── tests/              # Rust integration tests
│   │   ├── bridge_test.rs
│   │   ├── mapping_test.rs
│   │   ├── crash_recovery_test.rs
│   │   └── fixtures/       # CLI output fixtures
│   │       ├── lore/
│   │       │   ├── me_empty.json
│   │       │   ├── me_with_reviews.json
│   │       │   └── me_stale_cursor.json
│   │       └── br/
│   │           ├── create_success.json
│   │           └── create_error.json
│   ├── Cargo.toml
│   └── tauri.conf.json
│
├── src/                    # React frontend
│   ├── components/
│   │   ├── ui/             # shadcn components
│   │   ├── FocusCard.tsx
│   │   ├── QueueList.tsx
│   │   ├── Timeline.tsx
│   │   ├── BatchMode.tsx
│   │   ├── Inbox.tsx
│   │   ├── QuickCapture.tsx
│   │   └── ReasonPrompt.tsx
│   ├── views/
│   │   ├── FocusView.tsx
│   │   ├── QueueView.tsx
│   │   ├── TimelineView.tsx
│   │   └── InboxView.tsx
│   ├── hooks/
│   │   ├── useWorkItems.ts
│   │   ├── useTauriEvents.ts
│   │   ├── useKeyboard.ts
│   │   └── useDecisionLog.ts
│   ├── store/
│   │   └── index.ts        # Zustand store
│   ├── lib/
│   │   ├── tauri.ts        # Tauri invoke wrappers
│   │   └── utils.ts
│   ├── App.tsx
│   └── main.tsx
│
├── tests/                  # Frontend tests
│   ├── unit/               # Vitest unit tests
│   │   ├── store.test.ts
│   │   └── utils.test.ts
│   ├── components/         # Component tests
│   │   ├── FocusCard.test.tsx
│   │   ├── QueueList.test.tsx
│   │   └── ReasonPrompt.test.tsx
│   └── e2e/                # Playwright E2E
│       ├── focus-flow.spec.ts
│       ├── batch-mode.spec.ts
│       └── quick-capture.spec.ts
│
├── scripts/
│   └── regenerate-fixtures.sh  # Capture real CLI outputs
│
├── package.json
├── tailwind.config.js
├── vite.config.ts
├── vitest.config.ts        # Vitest configuration
├── playwright.config.ts    # Playwright configuration
├── PLAN.md                 # This document
└── README.md

Local State Files

~/.local/share/mc/
├── gitlab_bead_map.json    # {event_key} → {bead_id}
├── decision_log.jsonl      # Append-only decision log
├── state.json              # Current focus, queue order, UI state
└── settings.json           # User preferences

State File Reliability

Atomic Writes: All JSON state files use write-to-temp + rename pattern to prevent corruption on crash.

fn write_state_atomic(path: &Path, data: &impl Serialize) -> Result<()> {
    let tmp = path.with_extension("json.tmp");
    let file = File::create(&tmp)?;
    serde_json::to_writer_pretty(file, data)?;
    fs::rename(&tmp, path)?;  // Atomic on POSIX
    Ok(())
}

Crash Recovery: On startup, check for .json.tmp files — if found, previous write was interrupted. Delete tmp and use existing .json (last known good state).

Schema Versioning: Each JSON file includes a "schema_version": 1 field. On load, migrate if version < current.


Testing Architecture

TDD Philosophy

Every feature is implemented RED → GREEN:

  1. RED: Write failing test that specifies behavior
  2. GREEN: Write minimal code to pass
  3. REFACTOR: Clean up while tests stay green

Tests are written BEFORE implementation. If you can't write the test first, the spec isn't clear enough.

Test Directory Structure

mission-control/
├── src-tauri/
│   ├── src/
│   │   └── ...
│   └── tests/                    # Rust integration tests
│       ├── bridge_test.rs        # Bridge state machine tests
│       ├── mapping_test.rs       # Mapping file operations
│       ├── crash_recovery_test.rs
│       └── fixtures/             # CLI output fixtures
│           ├── lore/
│           │   ├── me_empty.json
│           │   ├── me_with_reviews.json
│           │   ├── me_with_issues.json
│           │   └── me_mixed.json
│           └── br/
│               ├── create_success.json
│               ├── create_error.json
│               ├── close_success.json
│               └── list.json
│
├── src/
│   └── ...
└── tests/                        # Frontend tests
    ├── unit/                     # Vitest unit tests
    │   ├── store.test.ts
    │   ├── utils.test.ts
    │   └── hooks/
    │       ├── useWorkItems.test.ts
    │       └── useDecisionLog.test.ts
    ├── components/               # React component tests
    │   ├── FocusCard.test.tsx
    │   ├── QueueList.test.tsx
    │   └── ReasonPrompt.test.tsx
    └── e2e/                      # Playwright E2E tests
        ├── focus-flow.spec.ts
        ├── batch-mode.spec.ts
        └── quick-capture.spec.ts

Mocking Strategy

Rust backend — trait-based mocking:

// Define traits for external dependencies
trait LoreCli {
    fn me(&self) -> Result<LoreMeResponse>;
    fn me_issues(&self) -> Result<Vec<LoreIssue>>;
    fn me_mrs(&self) -> Result<Vec<LoreMr>>;
}

trait BeadsCli {
    fn create(&self, title: &str, bead_type: &str) -> Result<String>;
    fn close(&self, id: &str, reason: &str) -> Result<()>;
    fn exists(&self, id: &str) -> Result<bool>;
}

// Production implementation shells out to CLI
struct RealLoreCli;
impl LoreCli for RealLoreCli {
    fn me(&self) -> Result<LoreMeResponse> {
        let output = Command::new("lore").args(["--robot", "me"]).output()?;
        // ...
    }
}

// Test implementation returns fixtures
struct MockLoreCli {
    responses: HashMap<&'static str, String>,
}
impl LoreCli for MockLoreCli {
    fn me(&self) -> Result<LoreMeResponse> {
        let json = self.responses.get("me").unwrap();
        Ok(serde_json::from_str(json)?)
    }
}

Frontend — MSW for Tauri IPC mocking:

// Mock Tauri invoke calls
import { mockIPC } from '@tauri-apps/api/mocks';

beforeEach(() => {
  mockIPC((cmd, args) => {
    if (cmd === 'get_work_items') {
      return fixtures.workItems;
    }
    if (cmd === 'set_focus') {
      return { ok: true };
    }
  });
});

Fixture Strategy

CLI Output Fixtures — Real outputs captured from actual CLIs, stored as JSON files:

Fixture Contents Used By
lore/me_empty.json Empty since_last_check, no open items Cursor recovery test
lore/me_with_reviews.json 3 MR reviews in since_last_check New event processing test
lore/me_stale_cursor.json Empty since_last_check but has open items Reconciliation trigger test
br/create_success.json Successful bead creation response Happy path tests
br/create_error.json Error response (e.g., validation failure) Error handling tests

Fixture Regeneration:

# Capture real CLI outputs for fixtures
lore --robot me > tests/fixtures/lore/me_current.json
br create --title "Test" --type task --json > tests/fixtures/br/create_success.json

Contract Validation: On CI, compare fixture schema against current CLI version:

#[test]
fn lore_me_fixture_matches_schema() {
    let fixture = include_str!("fixtures/lore/me_with_reviews.json");
    let parsed: LoreMeResponse = serde_json::from_str(fixture)
        .expect("Fixture should match current schema");
}

State Machine Test Scenarios

Each state transition gets an explicit test. Write these RED first.

Lifecycle Transitions:

Test Name Initial State Action Expected State Invariants Checked
new_event_creates_active (no entry) Process new MR review event active INV-1 (no duplicates)
duplicate_event_skips active Process same event again active (unchanged) INV-1
missing_once_sets_suspect active Reconciliation, item missing suspect_orphan=true INV-3
missing_twice_closes suspect_orphan=true Reconciliation, item still missing Entry removed, bead closed INV-3
reappears_clears_suspect suspect_orphan=true Reconciliation, item reappears suspect_orphan=false
user_close_removes active User closes bead manually Entry removed

Crash Recovery Transitions:

Test Name Initial State Simulated Crash Point Recovery Action Expected
crash_before_br_create pending=true, bead_id=null After map write, before br Startup recovery Retries br create, updates map
crash_after_br_create pending=true, bead_id="br-xxx" After br, before pending clear Startup recovery Verifies bead exists, clears pending
crash_before_cursor_advance Multiple pending=false After all beads, before cursor Startup recovery Re-processes events (idempotent), advances cursor

Invariant Tests

Automated verification of INV-1 through INV-4 after every operation:

fn assert_invariants(mapping: &Mapping, beads: &impl BeadsCli) -> Result<()> {
    // INV-1: No duplicate beads
    let bead_ids: Vec<_> = mapping.values().map(|e| &e.bead_id).collect();
    let unique: HashSet<_> = bead_ids.iter().filter_map(|id| id.as_ref()).collect();
    assert_eq!(bead_ids.len(), unique.len(), "INV-1 violated: duplicate bead IDs");

    // INV-2: No orphan beads (every bead_id exists)
    for entry in mapping.values() {
        if let Some(id) = &entry.bead_id {
            assert!(beads.exists(id)?, "INV-2 violated: bead {} not found", id);
        }
    }

    // INV-3: No false closes (tested via state machine tests)
    // INV-4: Cursor monotonicity (tested via cursor tests)

    Ok(())
}

// Run after every test
#[test]
fn reconciliation_maintains_invariants() {
    let mut bridge = setup_bridge_with_mock();
    bridge.reconcile();
    assert_invariants(&bridge.mapping, &bridge.beads).unwrap();
}

Error Path Tests

Every error in the error handling table gets a test:

Test Name Error Simulated Expected Behavior
lore_cli_unavailable Mock returns Err(CommandNotFound) Logs error, skips sync, no crash
br_create_fails Mock returns error JSON Logs error, entry NOT added to map
br_close_fails Mock returns error Logs error, keeps suspect_orphan=true
json_parse_error Malformed fixture Logs error, skips event, continues
map_file_corrupted Invalid JSON in map file Loads backup, triggers reconciliation
lock_held Lock file already locked Shows error dialog, exits cleanly

Frontend Test Strategy

Unit Tests (Vitest):

Module What to Test
store/index.ts State mutations, selectors, computed values
lib/utils.ts Staleness calculation, time formatting
hooks/useWorkItems.ts Data fetching, caching, error states

Component Tests (React Testing Library):

Component Test Cases
FocusCard Renders item, handles Start/Defer/Skip, keyboard shortcuts
QueueList Renders list, drag reorder, click to focus, staleness colors
ReasonPrompt Shows prompt, captures text, handles quick tags, submit/cancel
BatchMode Progress bar, item cycling, completion state

Example Component Test (RED first):

// tests/components/FocusCard.test.tsx
describe('FocusCard', () => {
  it('calls onStart when Start button clicked', async () => {
    const onStart = vi.fn();
    render(<FocusCard item={mockItem} onStart={onStart} />);

    await userEvent.click(screen.getByRole('button', { name: /start/i }));

    expect(onStart).toHaveBeenCalledWith(mockItem.id);
  });

  it('calls onStart when Enter pressed', async () => {
    const onStart = vi.fn();
    render(<FocusCard item={mockItem} onStart={onStart} />);

    await userEvent.keyboard('{Enter}');

    expect(onStart).toHaveBeenCalledWith(mockItem.id);
  });

  it('shows amber color for 3-day-old items', () => {
    const oldItem = { ...mockItem, createdAt: daysAgo(4) };
    render(<FocusCard item={oldItem} />);

    expect(screen.getByTestId('staleness-indicator')).toHaveClass('bg-amber-500');
  });
});

E2E Test Strategy (Playwright + Tauri)

Full app tests using @tauri-apps/cli test mode:

Test Flow
focus-flow.spec.ts Launch app → See focus card → Click Start → Verify browser opens
batch-mode.spec.ts Queue with 4 reviews → Enter batch → Complete all → See celebration
quick-capture.spec.ts Global hotkey → Type text → Enter → Verify bead created
sync-status.spec.ts Mock lore failure → Verify error shown → Mock success → Verify recovers

E2E Test Setup:

// tests/e2e/focus-flow.spec.ts
import { test, expect } from '@playwright/test';
import { spawn } from 'child_process';

test.beforeAll(async () => {
  // Start Tauri app in test mode with mocked CLIs
  process.env.MC_TEST_MODE = 'true';
  process.env.MC_MOCK_LORE = 'fixtures/lore/me_with_reviews.json';
});

test('clicking Start opens GitLab URL', async ({ page }) => {
  // Tauri exposes window at localhost:1420 in dev mode
  await page.goto('http://localhost:1420');

  // Wait for focus card to load
  await expect(page.getByTestId('focus-card')).toBeVisible();

  // Click start
  const [newPage] = await Promise.all([
    page.waitForEvent('popup'),
    page.click('button:has-text("Start")'),
  ]);

  // Verify GitLab URL opened
  expect(newPage.url()).toContain('gitlab.com');
});

Test Commands

# Rust tests
cargo test                           # All unit + integration tests
cargo test bridge                    # Bridge tests only
cargo test --test crash_recovery     # Crash recovery integration tests

# Frontend tests
npm run test                         # Vitest unit + component tests
npm run test:watch                   # Watch mode
npm run test:coverage                # Coverage report

# E2E tests
npm run test:e2e                     # Playwright tests
npm run test:e2e -- --headed         # With browser visible

# All tests (CI)
npm run test:all                     # Runs everything

CI Test Matrix

# .github/workflows/test.yml
jobs:
  rust-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: cargo test --all-features

  frontend-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run test -- --coverage
      - run: npm run lint

  e2e-tests:
    runs-on: macos-latest  # Tauri E2E needs native
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run tauri build -- --debug
      - run: npm run test:e2e

  contract-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      # Install actual lore/br CLIs
      - run: cargo install lore br
      # Regenerate fixtures and compare
      - run: ./scripts/regenerate-fixtures.sh
      - run: git diff --exit-code tests/fixtures/

Coverage Requirements

Layer Minimum Coverage Focus Areas
Rust bridge 90% State transitions, crash recovery, error paths
Rust data layer 80% CLI parsing, file I/O
Frontend hooks 85% Data fetching, state management
Frontend components 70% User interactions, edge cases
E2E N/A (scenario coverage) Critical user flows

Open Questions

  1. Floating widget details — Size, position, what info to show?
  2. Notification behavior — When to notify vs. just badge?
  3. Calendar integration — Worth adding for energy/time awareness?
  4. Mobile surface — Any desire for iOS widget in future?
  5. bv recommendations — Show as separate "suggestions" section or inline hints?

Changelog

Date Change
2026-02-25 Initial plan document created
2026-02-25 Added reliability improvements: reconciliation pass, cursor semantics, CLI contract testing, atomic writes, schema versioning. Deferred Timeline to post-v1.
2026-02-25 Integrated Bridge State Machine spec: lifecycle states, two-strike close rule, stable mapping keys (project_id), cursor semantics, invariants, single-instance lock, error handling.
2026-02-25 Added crash-safe operation ordering (write-ahead pattern with pending flag). Fixed AC-002/AC-002b to match two-strike rule. Added decision log retention policy (post-v1). Clarified flock-based lock semantics.
2026-02-25 Added comprehensive Testing Architecture: TDD philosophy, test directory structure, mocking strategy (trait-based Rust, MSW frontend), fixture strategy, state machine test scenarios, invariant tests, error path tests, frontend test strategy, E2E test strategy, CI matrix, coverage requirements. Updated Implementation Phases with explicit RED→GREEN TDD tasks.