# Design: add-os-status-plugin ## Context cc-os ships four global plugins (os-vault, os-adr, os-orchestration, os-doc-hygiene), all in one repo, maintained by one person. Each has per-project or per-environment preconditions; today two plugins carry their own SessionStart reminders (os-adr existence check, os-doc-hygiene staleness reminder) and two carry none (os-vault hub-note presence, os-orchestration env-override detection). The WS3 plan (`docs/plans/ws3-status-convention-plugin.md`) proposed a convention: per-plugin status checks aggregated into one banner. Two 2026-07-06 perspective reviews (perspectives:simplifier, perspectives:implementer, both sonnet) pressure-tested the plan's central tension — where check code lives. Their verified findings shape this design: - Per-project code copies (the plan's Option A) are the proven failure class: the 2026-07-04 stale-cache incident was copy-drift. - A subprocess + JSON stdout enumeration protocol (Option B as sketched) is overbuilt for three trivially cheap checks (path-exists, env read, settings grep) with one author. - The three existing plugins already disagree on hook wiring: os-adr/os-doc-hygiene run from the plugin cache via `hooks/hooks.json` + `${CLAUDE_PLUGIN_ROOT}`; os-vault is wired by absolute path in `~/.claude/settings.json` straight to git source, and its cache copy is already stale. Any design that enumerates other plugins' caches inherits this hazard. - "Silent on ok" as a blanket rule would regress os-adr's present-state usage note (a near-zero-token pointer to `/os-adr:create` + `/os-adr:find` whose wording was tuned by the Eval B campaign and validated by Eval C). - Migration must cut over hooks atomically per plugin or both old and new hooks fire (double banner), and cache must be verified post-cutover (`claude plugin details`), not just source-edited. User decision 2026-07-06: adopt the in-process, convention-shaped design. ## Goals / Non-Goals **Goals:** - One aggregated SessionStart status pass over all cc-os precondition checks; at most one short warning banner per session. - Catch, deterministically and at session start, the two incident classes that motivated this: missing per-project artifacts (ADR system, vault hub note) and silent model-forcing env overrides (`CLAUDE_CODE_SUBAGENT_MODEL`). - Preserve os-adr's tuned present-state usage note verbatim (no behavioral regression). - Keep adding a check cheap: one function + one registry entry in one file, same repo. - Model-free tests + invariants.md (same rigor as os-adr's hook; no behavioral eval — the code is deterministic). **Non-Goals:** - No subprocess boundary, no JSON wire contract, no installed-plugin enumeration (revisit only if a future check needs isolation: slow, network-touching, or conflicting deps). - No `context` session-injection field (no consumer yet; injection economics unexamined). - No consolidation of `.os-adr/` / `.dochygiene/` state dirs (works today, separable). - No conversion of os-doc-hygiene's reminder in this change (follow-up; keeps migration surface to one plugin). - No cache-freshness "collection manager" duties (Phase 2, separate change). - No fixing of os-vault's absolute-path hook wiring here (documented hazard, not a blocker — this design reads nothing from other plugins' caches). ## Decisions ### D1 — Code location: in-process functions in the `os-status` plugin All checks are plain Python functions living in `plugins/os-status/hooks/checks.py`, imported and called in a loop by a single SessionStart entry point. No per-plugin `status/check.py`, no discovery. - Why over Option A (project-local `.cc-os/status.d/*.py`): copies drift; per-project install step; 2026-07-04 incident is this exact class. - Why over Option B-as-sketched (per-plugin source + subprocess enumeration): three checks, one author, one repo — enumeration adds a protocol (spawn, parse, timeout, error normalization) with only three registrants; and enumeration-from-cache would silently run os-vault's stale cache copy (implementer finding). In-process reads nothing from other plugins' installs. - Convention preserved as a seam: every check has the uniform signature `check(ctx) -> CheckResult(status, message)` with `ctx` carrying project root + config. Extraction to subprocess later is mechanical because the contract already exists as a function boundary. ### D2 — Result semantics: `ok` / `note` / `warn` (three states, not two) - `ok` → contributes nothing, silent. - `note` → near-zero-token informational line, NOT subject to snooze/suppress; exists precisely so os-adr's present-state usage note survives conversion unchanged (implementer finding: blanket silent-on-ok is a regression). - `warn` → aggregated into the single banner with the corrective action ("X missing — run /os-…:… to fix"); subject to snooze/suppress. - A check that raises or exceeds its time budget is reported as `warn` with a generic message; it never blocks the session and never suppresses other checks' results (per-check try/except in the runner). ### D3 — Ship as a new `os-status` plugin, hooks.json-wired New plugin `plugins/os-status/` with `hooks/hooks.json` using `${CLAUDE_PLUGIN_ROOT}` (matching os-adr/os-doc-hygiene — the cache-correct pattern), thin `session_start.py` entry point + deep modules (`checks.py`, `state.py`), mirroring os-vault's module style. - Why a new plugin over piggybacking on os-orchestration (simplifier floated this): os-orchestration's domain is session-orchestration wording; status checking is a distinct domain and the naming convention (`os-[domain]`) favors a dedicated home. Name `os-status` over `os-core`: narrower, honest about scope; "core/collection manager" duties are Phase 2 and can justify a rename or a second plugin then. ### D4 — State: `.cc-os/` per-project dir, state only Snooze (once-per-day) and permanent suppress files live in `/.cc-os/` (gitignored; the check runner adds it to `.git/info/exclude` opportunistically or documents the gitignore line). Never code. Existing `.os-adr/`, `.dochygiene/` untouched. Outside a git project, the hook exits silently (os-adr precedent). ### D5 — Atomic os-adr cutover in this change os-adr's SessionStart entry is deleted from its `hooks/hooks.json` in the same change that os-status ships its equivalent check (existence → init/migrate suggestion with once-per-day snooze honoring the existing `.os-adr/suppress`; presence → the verbatim usage note as a `note`). Post-cutover verification: `bin/refresh-plugins`, then `claude plugin details os-adr@local-plugins` must show the SessionStart hook gone from the resolved cache, then a fresh-session smoke test shows exactly one emission. ### D6 — Initial check registry 1. `adr_system_present(ctx)` — port of `plugins/os-adr/hooks/session_start.py` logic, including its `.os-adr/suppress` + once-per-day behavior and the Eval-B-tuned PRESENT_NOTE wording verbatim. 2. `vault_hub_note_present(ctx)` — project has a hub note in the vault (lookup strategy: match `project/` facet tag or configured hub slug in `~/Documents/SecondBrain`); missing → `warn` naming the corrective skill; present → `ok` (no injection — see Non-Goals). 3. `subagent_model_env_override(ctx)` — `warn` if `CLAUDE_CODE_SUBAGENT_MODEL` is in the process environment or in the `env` block of `~/.claude/settings.json`. ## Risks / Trade-offs - [Adding a check means editing os-status, not the owning plugin] → Accepted: one author, one repo; the uniform signature keeps a later extraction mechanical. Revisit at the first check that needs isolation. - [os-status must know about other plugins' domains (hub notes, ADR dirs)] → Accepted coupling; checks are read-only path/env probes, not imports of other plugins' code. - [Double emission during cutover if caches go stale] → D5's verify-the-cache step is a mandatory task, not cleanup; the 2026-07-04 incident is the template for what happens otherwise. - [Hub-note lookup heuristic may mis-detect] → keep the matcher trivial (explicit config in `.cc-os/config` wins over inference); a false `warn` is one banner line, suppressible. - [SessionStart latency] → in-process, three cheap probes, one interpreter spawn; budget the hook at 5s like os-adr's. No parallelism machinery needed at this scale. ## Migration Plan 1. Build os-status complete with tests green (no hook registered yet). 2. Register os-status in the local-plugins marketplace manifest, install, refresh, verify via `claude plugin details`. 3. Cut over os-adr (D5) in one commit; refresh; verify cache; fresh-session smoke test. 4. Rollback = revert the commit + `bin/refresh-plugins` (restores os-adr's own hook). ## Open Questions - Does the hub-note check also fire usefully in non-project cwds (e.g. `~`)? Current answer: git-project-only, matching os-adr; revisit with real usage. - os-vault's absolute-path hook wiring (stale cache copy) — normalize onto hooks.json in a separate housekeeping change? Recommended but out of scope here.