- **Context**: The `/os-vault:onboard-project` skill's onboarding flow ran `graphify extract . --backend ollama --model qwen2.5-coder:7b` directly. graphify does NOT honor `.gitignore`; it uses a separate `.graphifyignore` file (same syntax — see `docs/graphify/02-installation-setup.md`). With no `.graphifyignore`, onboarding `viking-warrior-training-log` (135 tracked files) walked `node_modules/` (5,858 files / 161MB) and routed every non-code file there through the Ollama doc pass. The run was killed after ~1 hour with no `graph.json` produced. Cost driver: only non-code files hit the Ollama LLM; code uses the free tree-sitter AST pass.
- **Decision**: Onboarding is now assessment-first. Before running `graphify extract`, the skill surveys the repo, classifies directories and file types into include/exclude (weighing non-code file count since only non-code files cost LLM time), confirms the proposed ignore list with the user, writes `.graphifyignore` at the repo root, ensures `graphify-out/` is in the project's `.gitignore`, then runs extract. The ignore list is generated per-project — what counts as noise varies by stack — not from a static template.
- **Rationale**: graphify's `.gitignore` blindness is by design; the correct lever is `.graphifyignore`, not a workaround. Assessment before extraction surfaces borderline dirs (migrations, seeds, fixtures, sample data) that need a human call. Per-project generation avoids the false safety of a shared template that silently mismatches a new stack.
- **Alternatives rejected**: **Static `.graphifyignore` template** — rejected; dependency/build/cache dirs vary by project stack; a template would both over-exclude (suppressing wanted source) and under-exclude (missing stack-specific dirs) for any given project. **Auto-write without user confirmation** — rejected; borderline directories (e.g. migrations, seeds, fixtures) require human judgment; mechanical exclusion would silently drop legitimate content. **AST-only mode (skip doc pass entirely)** — deferred; the `--backend ollama` flag stays; the ignore list is the correct cost lever, not backend switching.
- **Default-exclude taxonomy is type-based, not name-based.** The skill's assessment step organizes exclude candidates into 11 categories by KIND (fetched deps, build/generated output, caches, VCS internals, editor/AI-tooling dirs, lockfiles, coverage/logs, bulk data/databases, binaries, secrets, graphify-out/) with a meta-principle: index what a human authored; exclude anything fetched, generated, cached, tooling config, bulk data/binary, or secret. Example names within each category are illustrative, not exhaustive — the goal is to recognize the kind even in an unfamiliar stack. See SKILL.md for the full table.
- **Onboarding uses the 16k-context model.** The `graphify extract` command now passes `--model qwen25-coder-7b-16k` (the `ollama_model` from config.yaml), a 16k-context build of qwen2.5-coder:7b. Inference is GPU-bound (~65 tok/s); the main speed lever is chunk-count × generation — fewer chunks per doc via a larger context window. `GRAPHIFY_OLLAMA_NUM_CTX` does not propagate through graphify's OpenAI-compatible endpoint; the larger context must come from the model's Modelfile.
## ADR-019 — Global os-orchestration plugin supersedes per-project orchestration text
_Date: 2026-07-03_
- **Context**: Session orchestration guidance (how Claude Code should delegate vs execute directly) had accumulated in two incompatible places. (1) A permissive global plugin (`os-orchestration`, moved into cc-os from a standalone `~/dev/cc-plugins/orchestration/` repo) injects `ORCHESTRATION.md` as SessionStart additionalContext carrying the rule: "do single-file/≤2-tool-call ops directly; delegate only when work is parallelizable across independent files, spans many files, or needs isolated/large context." (2) cc-os's own `CLAUDE.md` carried a stricter per-project override: "Delegate all file I/O and shell commands to subagents via the Agent tool. No exceptions by default." The two rules contradicted each other and lived in different places, creating confusion about which one applies when.
- **Decision**: **Adopt the plugin's permissive rule as the canonical global default.** cc-os relinquishes its stricter local override entirely. Every project now follows the plugin's behavior: single-file/≤2-tool-call ops are executed directly; larger or cross-file work is delegated. The `os-orchestration` plugin is migrated into cc-os (`plugins/os-orchestration/`) following the same pattern as `os-vault` (git-tracked, symlinked into `~/.claude/plugins/`, registered via `local-plugins` marketplace).
- **Rationale**: One authoritative orchestration rule, maintained in one place (the plugin, under version control), is simpler than per-project copy-paste text that drifts. The permissive rule reflects actual practice (small direct edits are efficient; large multi-file refactors warrant delegation). Consolidation into a global plugin means all projects get the same behavior without duplication, and the rule can be evolved once and picked up everywhere.
- **Alternatives rejected**:
- **Keep cc-os's stricter override as a local addition on top of the global plugin**: would recreate the incompatibility problem; cc-os would be a special case instead of a normal project.
- **Make the strict version the global default instead**: reverses the decision in favor of the permissive rule. The permissive approach scales better (most tasks fit the single-file/≤2-tool threshold); strict delegation adds ceremony for no gain on small work. The plugin's default was chosen by the user for good reason.
- **Cross-references**: ADR-016 (os-vault plugin sourced from cc-os git repo, establishing the git-tracked plugin pattern), ADR-018 (plugin marketplace mechanics for local plugins).
- **Status**: Accepted 2026-07-03. cc-os's CLAUDE.md updated to remove the stricter orchestration section and note that behavior is now global; `os-orchestration` component added to Implemented Components.
## ADR-020 — os-adr plugin built; implementation-time resolutions of the design's open questions
_Date: 2026-07-03_
- **Context**: The `os-adr` plugin (OpenSpec change `add-os-adr-plugin`, built from `docs/adr-system/04-plugin-requirements.md` + `05-plugin-prd.md`) was implemented. Its design deliberately left two questions open — the migration pilot's low-confidence flag-rate threshold, and whether ADR frontmatter needs a Graphify tag/edge convention for retrieval layer 3 — and the pilot forced several heuristic refinements within the design's declared mechanical/LLM boundary.
- **Decision**: (1) **Pilot gate threshold: 25%** low-confidence flags, recorded in `Adr::MigrationReport::FLAG_RATE_THRESHOLD` and printed in every migration report. Final pilot rates (sandboxed copies of the three surveyed projects): viking-warrior-training-log 0%, delta-refinery 0%, llf-schema 8.3% — gate passed. (2) **No Graphify tag/edge convention for ADRs**: retrieval layer 3 expands the *query* file paths one hop through `graphify-out/graph.json` node `source_file` attributes and matches the expanded set against `affected-paths` frontmatter; ADRs never need to exist as graph nodes. Verified against the real llf-schema graph. (3) **Heuristic-boundary refinements from the pilot**: heading-synonym fallback mappings (Why/Rationale/Problem/Background→Context, Impact/Implications/Trade-offs→Consequences) are mechanical copies but cap the file at `migration_confidence: medium`; status synonyms SETTLED/Confirmed→Accepted, OPEN→Proposed map mechanically; bold-label paragraphs (`**Context.** …`) and bulleted labels (`- **Status:** …`) parse as sections; multi-decision files *without* mechanically splittable unit headings classify `unrecognized` (report-only) rather than converting as one blob.
- **Rationale**: The threshold was set empirically at pilot time per the PRD ("do not ship on an unmeasured guess"); 25% sits well above the observed worst case (8.3%) while still failing a genuinely broken heuristic run (pre-tightening iterations hit 100% on delta-refinery and llf-schema). Query-path graph expansion keeps ADRs plain markdown with zero indexing obligations — retrieval reuses the existing project graph instead of imposing a new convention on it (and sidesteps the open ADR-014 facet-edge question entirely). Capping synonym-mapped files at medium keeps the mechanical-vs-judgment boundary honest: the copy is mechanical, the mapping is a judgment call, so it stays flagged.
- **Alternatives rejected**:
- **Indexing ADRs as Graphify nodes with a tag/edge convention**: adds an extraction pass and a freshness obligation for no retrieval gain at current corpus sizes; revisit only if one-hop query expansion proves too shallow.
- **Treating heading synonyms as high-confidence mechanical fills**: hides an interpretive mapping from the flag mechanism.
- **Auto-splitting multi-Status files that lack unit headings**: the split points would themselves be guesses; `unrecognized` + manual handling is the non-destructive answer.
- **Cross-references**: `docs/adr-system/04-plugin-requirements.md` (locked requirements), `05-plugin-prd.md` (phases), `06-eval-scenarios.md` (held-out eval sketches — the eval itself stays deferred), ADR-013 (build-first/migrate-incrementally precedent), ADR-018 (three-place plugin registration, followed during install), ADR-019 (plugin conventions).
- **Status**: Accepted 2026-07-03. Plugin live at `cc-os/plugins/os-adr/`, installed as `os-adr@local-plugins`. Rollout order locked: pilot projects' real migration (interactive, per project) → cc-os retrofit → wider.
## ADR-021 — Model-tier skill-execution eval for os-adr (Eval A), run in-session via subagents
_Date: 2026-07-03_
- **Context**: The os-adr build validated every model-touching surface (skill-following, migrate fills, find judgment) only at the session model used to build it (Fable). Confidence that the skills execute correctly on the *weakest* tier (Haiku) would imply confidence on all stronger tiers. The locked eval methodology (`04-plugin-requirements.md`) covers only the deferred held-out unprompted-behavior eval (requirements 4–5, "Eval B") and says nothing about model tiers. Separately, headless `claude -p` runs consume more of the user's subscription credit than in-session work.
- **Decision**: (1) A second, non-held-out eval — **Eval A: prompted skill-execution across model tiers** — lives at `plugins/os-adr/eval/`: two fixture projects (generated with the plugin's own CLIs), six scenarios (S1 create, S2 create+supersede, S3 find/conflict, S4 find/distractor, S5 init, S6 migrate+fills), a deterministic Ruby checker (`bin/check`, structural invariants only — never prose quality), sandbox + headless-runner scripts, and a runner-prompt template. (2) **Primary run mode is in-session**: a driver session spawns Agent-tool subagents with `model:` pinned to the tier under test; each subagent reads the SKILL.md file directly (uniform across tiers; dispatch is deterministic plumbing). Headless `claude -p` is the fidelity fallback only. (3) The **`autoresearch` skill (Classic mode)** wraps the grid as its metric to iterate SKILL.md *wording only* — checker, fixtures, scenarios, and runner prompt are frozen during a loop, as the guard against metric-gaming. (4) **Eval B gains a model axis** when it eventually runs: scenario × model tier, pass rate per tier as the autoresearch metric.
- **Rationale**: The deterministic core (CLIs, hook, index) is model-independent and already tested; what varies by tier is instruction-following, so the cheap, high-signal eval is exactly that surface with machine-checkable pass criteria (the plugin's invariants double as scoring rules). In-session subagents preserve the user's credits and parallelize; the fidelity gap (no SessionStart context, no slash dispatch) is irrelevant for *explicitly invoked* skills. Eval A prompts are deliberately not held-out — only Eval B's are — so they can be iterated on freely without contaminating the deferred methodology.
- **Alternatives rejected**:
- **Headless-only harness**: higher subscription cost per run, no parallel driver, no benefit for prompted-execution scoring; kept as fallback for full-fidelity checks.
- **Folding tier-testing into Eval B**: conflates two questions (can the model *execute* a skill it was told to run vs. does it *recognize* when to run one) and would burn held-out scenarios on mechanical failures.
- **LLM-judged scoring**: reintroduces the model-capability variable into the scorer; structural invariants are sufficient and reproducible.
- **Cross-references**: `plugins/os-adr/eval/README.md` (procedure + autoresearch invocation), `docs/adr-system/06-eval-scenarios.md` (Eval B sketches, now noting the model axis), ADR-020 (pilot gate the S6 fixture mirrors).
- **Status**: Accepted 2026-07-03. Harness built and self-tested (all six scenarios: perfect-run PASS, untouched-sandbox FAIL). The grid itself has not yet been run against haiku/sonnet.
## ADR-022 — os-status plugin: aggregated in-process SessionStart status checks
_Date: 2026-07-06_
- **Context**: Two real incidents showed that cc-os plugins have per-project and per-environment preconditions nothing verifies: (1) `CLAUDE_CODE_SUBAGENT_MODEL=haiku` in `~/.claude/settings.json` silently downgraded all 23 verified subagent spawns found by the WS1 orchestration audit — and the E1 canary proved the downgrade is unobservable to the model at spawn time, so protection must be deterministic; (2) projects routinely lack artifacts plugins assume (ADR system, vault hub note), with each plugin inventing its own SessionStart reminder in isolation (os-adr, os-doc-hygiene) or having none. The WS3 plan (`docs/plans/ws3-status-convention-plugin.md`) proposed a per-plugin status-check convention plus a glue plugin; two perspective reviews (simplifier, implementer, 2026-07-06) pressure-tested where check code should live.
- **Decision**: New global plugin **`os-status`** (`plugins/os-status/`), one SessionStart hook running all checks **in-process**: plain Python functions in `hooks/checks.py` with the uniform signature `check(ctx) -> CheckResult(status, message)` and a single registry list. Three result states: `ok` (silent), `note` (near-zero-token line, never snoozed/suppressed — exists so os-adr's Eval-B-tuned usage note survives conversion verbatim), `warn` (aggregated into at most one banner; once-per-day snooze + permanent suppress in a gitignored per-project `.cc-os/` dir — state only, never code). Initial checks: `subagent-model-env-override` (env + settings.json `env` block; runs outside git projects too), `adr-system-present` (verbatim port of os-adr's hook, honoring legacy `.os-adr/suppress`), `vault-hub-note-present` (config-first slug in `.cc-os/config`, else `type/hub` + `project/<name>` facet-tag scan of the vault). os-adr's own SessionStart hook was removed atomically in the same change, with the cutover verified against the refreshed plugin cache.
- **Alternatives rejected**:
- **Option A — per-project code copies** (`.cc-os/status.d/*.py`): copies drift; the 2026-07-04 stale-cache incident is exactly this failure class; adds a per-project install step.
- **Option B as sketched — per-plugin scripts + subprocess/JSON enumeration**: overbuilt for three trivially cheap probes with one author in one repo (spawn/parse/timeout/error-normalization protocol for three registrants); and enumeration from plugin caches would silently run stale code — os-vault is wired by absolute path to git source and its cache copy is already stale (implementer finding). The uniform function signature is kept as the seam so extraction to per-plugin checks later is mechanical.
- **Blanket silent-on-ok**: would regress os-adr's present-state usage note; hence the third `note` state.
- **Deferred**: `context` injection field (no consumer yet), consolidation of `.os-adr/`/`.dochygiene/` state dirs, conversion of os-doc-hygiene's reminder, Phase 2 cache-freshness "collection manager" duties, subprocess enumeration.
- **Cross-references**: OpenSpec change `add-os-status-plugin` (proposal/design/specs/tasks), `plugins/os-status/invariants.md`, WS1 findings (`docs/orchestration-audit/2026-07-06-findings.md`), ADR-018 (marketplace-manifest procedure followed for install).
## ADR-023 — Backlog process management ships as a new `os-backlog` plugin (not an os-vault extension)
_Date: 2026-07-10_
- **Context**: The Planka backlog pilot (vault note `vault-backlog-pilot-plan`) reached its plugin phase with one gated decision open: build the AI's process-management surface (skills for capture/list/tick/board ops, deterministic scripts over the `planka-api` gem, card-triage/board-audit named agents, hooks) as a new plugin or as an extension of os-vault. The decision also surfaced a broader framing question: what cc-os *is* relative to `~/dev/cc-plugins`.
- **Decision**: (1) New global plugin **`os-backlog`** (`plugins/os-backlog/`). os-vault is not extended. Naming per `cc-os-plugin-skill-naming-convention.md`: verb-first skills invoked as `/os-backlog:<verb>`; no `name:` frontmatter; no `commands/` dispatchers. (2) **Ecosystem role made explicit**: cc-os is the user's *personal operating layer* for Claude Code — the family of always-on, globally installed, mutually aware `os-*` plugins running on every machine; `~/dev/cc-plugins` is the shelf of optional, as-needed plugins. Cross-plugin awareness and cooperation (including notifications, ADR-024) is an explicit long-term goal of the cc-os family, not an accident.
- **Rationale**: Separation of concerns — os-vault is the memory/knowledge domain (what do I know), os-backlog is the workflow/process domain (what state is work in): different backing stores (SecondBrain vault vs. Planka Postgres via REST), different failure modes (local files vs. network service), different maturity (os-vault is eval-hardened and stable; backlog skills will churn while the process settles). Every comparably sized domain in the family already got its own plugin (os-adr, os-status, os-doc-hygiene). Hook hygiene: a broken/unreachable Planka must not degrade vault sync hooks.
- **Alternatives rejected**: **Extending os-vault** — the only benefit is one fewer plugin to maintain, a cost already amortized by `bin/refresh-plugins` and the marketplace procedure (ADR-018); it would couple a churning surface to a frozen, eval-tuned one and blur the domain boundary the tag taxonomy itself encodes. Future skills needing both domains compose by invoking each other's skills, not by merging plugins.
- **Cross-references**: vault notes `vault-backlog-pilot-plan` (pilot plan + execution status), `cc-os-plugin-skill-naming-convention`; ADR-018 (install procedure); ADR-024 (notification direction the plugin will consume).
- **Status**: Accepted 2026-07-10. Design pass (skill/agent/hook inventory) next; build delegated to sonnet after design.
## ADR-024 — Apprise replaces Pushover as the notification transport (supersedes the Pushover-direct plan)
_Date: 2026-07-10_
- **Context**: The `planka-api` gem's tick/digest delivery was specified as Pushover-direct (`PUSHOVER_API_TOKEN`/`PUSHOVER_USER_KEY`, falls back to stdout when unset — see the gem's `docs/issues/README.md` and the P0 board card "Provision Pushover digest credentials"). Separately, Planka itself ships Apprise as a notification integration, and the user's real need is multi-service: Discord, Telegram, Bark, Home Assistant, Gnome desktop, Zoom, and Pushover are all in active use, and any AI agent (Claude Code, Hermes) on any machine should be able to notify on whichever service fits the moment.
- **Decision**: (1) **Apprise becomes the single notification transport**: an Apprise API server deployed as a docker-compose service on the OVH production server, following the established service pattern. (2) **This iteration's scope is deliberately minimal**: stand up the server and swap the gem's Pushover P0 for a minimal Apprise client (one `notify` call against the API) — the pilot's critical path does not grow. (3) The **expansion is captured as P2/P3 backlog cards, to be built soon**: an Apprise client gem + CLI wrapper, an `os-notify` cc-os plugin so any session can route notifications by service/need (per ADR-023's ecosystem framing, notifications are cross-plugin infrastructure and belong in cc-os), multi-service routing config, and wiring Planka's native Apprise integration to the same server.
- **Rationale**: One integration covers N services — adding a service becomes an Apprise URL/tag config change, not new code; service selection becomes a runtime parameter, which is exactly the "tell the AI where to notify" requirement. Planka speaking Apprise natively means board events and agent-initiated notifications converge on one server. The minimal client is *less* work than the Pushover gem would have been (Apprise absorbs per-service quirks). Notification-blindness policy (pull beats push, silent days stay silent) is unchanged — this ADR changes the transport, not the policy.
- **Alternatives rejected**: **Pushover gem as planned** — single-service dead end; every future service would be a new integration. **Deferring Apprise entirely** — the server + minimal client is comparable effort to the Pushover path, so there is no cheaper interim.
## ADR-025 — Standard Ruby structure for cc-os plugins (lib/ + single-dispatcher bin/, fail-soft, installed-gem-only dependencies)
_Date: 2026-07-10_
- **Context**: Two plugins ship Ruby (os-adr, os-backlog) and more will follow (os-notify is queued). implementation-status.md informally calls os-backlog's layout "the os-adr `lib/`+`bin/` pattern", but no ADR pins it, and the two plugins already diverge: os-adr uses one `bin/` script per verb (`adr-new`, `adr-find`, …) invoked as `ruby ${CLAUDE_PLUGIN_ROOT}/bin/adr-new`, inline `abort` error handling, stdlib only; os-backlog uses a single dispatcher `bin/os-backlog <subcommand>` invoked without a `ruby` prefix, a centralized `fail_soft` helper, and a lazily `require`d installed gem (`planka-api`) guarded by `rescue LoadError`. Without a recorded standard, each new plugin re-decides and the surfaces drift.
- **Decision**: New cc-os plugin Ruby follows this standard (deviate only with a recorded reason):
1.**Library**: `lib/<domain>.rb` + `lib/<domain>/*.rb`, one `module <Domain>` namespace, loaded via `require_relative`, stdlib-first. No gemspec/Gemfile inside a plugin — plugins are source trees, not gems.
2.**CLI**: a single dispatcher `bin/os-<domain>` with subcommands (os-backlog style), executable with a `#!/usr/bin/env ruby` shebang; skills invoke `${CLAUDE_PLUGIN_ROOT}/bin/os-<domain> <subcommand>` with no `ruby` prefix. One entry point per plugin keeps skill docs uniform and namespaces the verbs.
3.**Errors**: every user-reachable failure goes through one fail-soft helper printing exactly one line, `os-<domain>: <message>`, exit 1. Never a raw stack trace from a skill-invoked path.
4.**External gems**: consumed as *installed* gems, `require`d lazily inside the method that needs them with `rescue LoadError` → fail-soft. Never vendored, never source-coupled to a sibling repo, never shelled out to another gem's binary when the client classes suffice. Each required gem is named in the plugin README and implementation-status.md (installation is an environment prerequisite, so it must be discoverable).
5.**Tests**: minitest, `tests/all.rb` + `tests/test_helper.rb`, with in-memory fakes for any network service so the suite runs on machines without the gem or credentials (os-backlog's `FakePlankaClient` is the template).
- **Rationale**: os-backlog is the newer, deliberate iteration of the pattern and its choices earned their place: the dispatcher scales without `bin/` sprawl, fail-soft is mandatory for network-touching plugins (ADR-023's hook-hygiene argument), and lazy installed-gem requires keep pure subcommands (e.g. `resolve`) working on machines where the gem is absent — verified in practice 2026-07-10.
- **Alternatives rejected**: **Per-verb bin scripts (os-adr style)** — fine at 4 verbs, sprawl at 9; grandfathered in os-adr, not worth a retrofit until os-adr is next touched for other reasons. **Packaging plugins as real gems** — adds release/version ceremony for code that ships by symlink + cache refresh (ADR-018). **Vendoring external gems** — duplicates code and hides the dependency instead of documenting it.
## ADR-026 — Unified project setup/update command: `/os-status:fix` remediates what the os-status checks flag
_Date: 2026-07-12_
- **Context**: Bringing a project up to cc-os standards currently requires knowing and running each plugin's own onboarding surface separately (`/os-adr:init`, `/os-vault:onboard-project`, a manual hub-note write, the os-backlog routing skill — which is itself still unbuilt, issue #14, so the os-status tracker warning points at a skill that does not exist). The SessionStart warnings (ADR-022) name a different remediation per line, and only os-adr's names a real command. The user wants one command that brings any project up to date with the current cc-os approach, without per-plugin ceremony. A literal `/cc-os:init` is impossible under the naming convention (`os-[domain]` plugins only).
- **Decision**: (1) New skill **`/os-status:fix`** in the os-status plugin: it runs the same check registry the SessionStart hook runs, then for each non-`ok` result drives the owning plugin's remediation — invoking that plugin's existing skill (`/os-adr:init`, `/os-vault:onboard-project`, the routing skill once built) or performing the small direct action (hub-note creation via `/os-vault:write` conventions). Decision-bearing steps (tracker destination, ignore-list confirmation) keep their human gates; mechanical steps run autonomously. (2) **Idempotent by construction**: re-running `fix` on a configured project is the update path — no separate update command. (3) Each `Check` in `hooks/checks.py` gains a **remediation pointer** so check and fix stay co-located in one registry; SessionStart warn output suggests `/os-status:fix` as the single entry point (per-check detail remains for context). (4) `.cc-os/config` gains a **`version` key** stamped by `fix`; a future check can warn when a project was configured under an older approach.
- **Rationale**: os-status already owns the per-project precondition registry (ADR-022); the fix skill is the write-side of the same seam, so checks and remediations cannot drift apart. Idempotent fix collapses setup/update into one verb. Per-plugin init skills remain the implementation (composition per ADR-023's cooperation goal) — `fix` orchestrates, it does not reimplement.
- **Alternatives rejected**: **New `os-project` plugin with `/os-project:init`** — reads slightly more naturally but splits remediations from the check registry they mirror and adds a seventh plugin for glue. **`/os-status:configure`** — same home, weaker verb; `fix` reads as "fix what the status check flagged", which is exactly the contract. **Literal `/cc-os:init`** — violates the `os-[domain]` naming convention.
## ADR-027 — All cc-os per-project state lives under `.cc-os/<plugin>/`
_Date: 2026-07-12_
- **Context**: The credvault smoke-test of `/os-status:fix` (issue #21) surfaced per-project dotfile sprawl: `.cc-os/` (os-status config/snoozes/state, os-backlog tracker key), `.dochygiene/` (os-doc-hygiene state), a planned `.os-adr/`, plus non-cc-os dirs (`.claude/`, `graphify-out/`). Each cc-os plugin inventing its own root-level dotdir means N gitignore entries, N places to look for state, and ad-hoc gitignore hygiene per session (observed in the credvault audit, session cd1ae1cd).
- **Decision**: (1) All cc-os-owned per-project state lives under **`.cc-os/`**: the shared `config` file stays at `.cc-os/config` (cross-plugin key=value contract, ADR-026); each plugin's private state lives in **`.cc-os/<plugin-domain>/`** (e.g. `.cc-os/dochygiene/`, `.cc-os/status/`, `.cc-os/adr/`). No new root-level dotdirs. (2) Existing plugins migrate with a **backward-compat fallback read** of the old path (auto-migrate on first write, then remove the old dir): os-doc-hygiene `.dochygiene/` → `.cc-os/dochygiene/`; os-status's own `snooze-*`/`state.json` → `.cc-os/status/`. (3) **`graphify-out/` is exempt** — external-tool output, disposable/rebuildable, path baked into os-vault hooks; moving it is deferred as aesthetic-only churn. `.claude/` is Claude Code's own dir, out of scope. (4) One gitignore entry — `.cc-os/` — covers all cc-os state; ensuring it is present becomes an explicit step of `/os-status:fix` (issue #23), not improvised per session.
- **Rationale**: One predictable home for the operating layer's per-project state matches the cc-os family framing (ADR-023: mutually aware, cooperating plugins) and extends the shared-config seam ADR-026 established. Fallback-read migration makes rollout painless across already-onboarded projects.
- **Alternatives rejected**: **Status quo (per-plugin root dotdirs)** — sprawl grows linearly with the plugin family. **Moving `graphify-out/` too** — touches the os-vault onboard skill, SessionStart reconcile, and docs for zero functional gain.
| MemPalace (L4) | Storage not readable markdown; isolated drawers (knowledge not interconnected); fights self-managing + cross-linking goals |
| Recall / LightRAG (L5) | Content knowledge bases / deep research, not operational memory; Recall = hosted, you don't own data; LightRAG = enterprise overkill |
| OpenBrain / Mem0 (L6) | Always-remote DB → latency + cost; conflicts with local-fast lazy-sync; only pays off for real-time cross-tool memory (user: overkill) |
| Postgres / Milvus server | Unnecessary — Graphify's local graph (knowledge) + Milvus Lite (memsearch episodic) cover everything locally with no Docker |
| Ruby/SQLite tag index CLI; QMD vector layer | Superseded by Graphify before build — one knowledge graph replaces both the structured index and the deferred semantic layer (ADR-010) |