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.jsonstraight 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:findwhose 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
contextsession-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)withctxcarrying 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
warnwith 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. Nameos-statusoveros-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
adr_system_present(ctx)— port ofplugins/os-adr/hooks/session_start.pylogic, including its.os-adr/suppress+ once-per-day behavior and the Eval-B-tuned PRESENT_NOTE wording verbatim.vault_hub_note_present(ctx)— project has a hub note in the vault (lookup strategy: matchproject/<name>facet tag or configured hub slug in~/Documents/SecondBrain); missing →warnnaming the corrective skill; present →ok(no injection — see Non-Goals).subagent_model_env_override(ctx)—warnifCLAUDE_CODE_SUBAGENT_MODELis in the process environment or in theenvblock 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/configwins over inference); a falsewarnis 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
- Build os-status complete with tests green (no hook registered yet).
- Register os-status in the local-plugins marketplace manifest, install, refresh, verify via
claude plugin details. - Cut over os-adr (D5) in one commit; refresh; verify cache; fresh-session smoke test.
- 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.