cc-os/openspec/changes/archive/2026-07-10-add-os-status-pl.../design.md

9.0 KiB

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.