# 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 `<project>/.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/<name>` 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.
