161 lines
9.0 KiB
Markdown
161 lines
9.0 KiB
Markdown
|
|
# 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.
|