cc-os/docs/plans/c3-spike-findings.md

115 lines
7.0 KiB
Markdown

# C3 spike findings: `--setting-sources` interaction + overlay file placement
_Task: C3 in [system-prompt-profiles-tasks.md](system-prompt-profiles-tasks.md). Feeds B2._
## Method note (important correction mid-spike)
Initial attempts asked the model to self-report whether ORCHESTRATION.md/skills were
injected ("ORCH=yes/no SKILL=yes/no"). This was **unreliable** — a default run that the
transcript later proved *did* receive the orchestration injection had the model answer
`ORCH=no`. Model self-report is not a valid signal for this spike.
Switched to a deterministic signal: the session transcript JSONL at
`~/.claude/projects/<slug>/<session_id>.jsonl` records hook execution as
`attachment.type == "hook_additional_context"` entries (one per SessionStart hook that
returned `additionalContext`) and the full injected skill list as
`attachment.type == "skill_listing"`. Both are populated *before* the model ever sees the
turn, so they're a ground-truth substitute for `/context`.
## Spike (a): `--setting-sources` vs symlinked local plugins + absolute-path hooks
4 headless haiku runs from an empty scratch dir
(`/tmp/claude-1000/.../scratchpad/c3spike`), `claude -p --model haiku --output-format json
"reply OK"`, varying `--setting-sources`. For each, inspected the resulting transcript
JSONL for hook-injected additionalContext count and whether plugin-specific skills
(`os-adr`, `os-vault`) appear in the injected skill listing (built-in skills like
`autoresearch`/`caveman` appear regardless — only plugin skill presence is diagnostic).
| `--setting-sources` value | additionalContext entries (hooks fired) | os-adr/os-vault skills present |
|---|---|---|
| (no flag, default) | 1 (confirmed in first exploratory run) | yes |
| `""` (none) | 0 | no |
| `user` | 1 | yes |
| `project,local` | 0 | no |
Interpretation:
- Hooks in this setup are wired **only** in `~/.claude/settings.json` (a **user**-scope
settings file), by absolute path into `cc-os/`. They fire if and only if the `user`
settings source is included. `--setting-sources ""` and `--setting-sources
project,local` both silently drop them — no error, no warning, just no
`additionalContext`. This is a real footgun for a future `cyolo` profile wrapper: any
profile that passes `--setting-sources` explicitly must include `user` or it silently
loses ORCHESTRATION.md injection, os-status checks, os-vault hooks, and every other
hook in the plugin family.
- Plugin skill registration (the `os-adr:find` style slash-command/skill listing) tracks
the same axis: present with `user` included, absent without it. This makes sense —
`enabledPlugins` lives in the user-scope `~/.claude/settings.json`, so a
`--setting-sources` value that excludes `user` also excludes plugin enablement, not
just hooks.
- Symlinks themselves are a non-issue: once the `user` settings source is included, the
symlinked plugin (`~/.claude/plugins/os-adr -> cc-os/plugins/os-adr`) resolves and
loads identically to the default case — no special-casing needed for the symlink or
for the hooks' absolute paths into `cc-os/`. The failure mode is entirely about which
settings **source** is included, not about symlinks or path resolution.
**Verdict (a):** `--setting-sources` interacts cleanly with symlinked plugins and
absolute-path hooks in the sense that nothing breaks or errors — but it is an all-or-
nothing gate: excluding `user` from `--setting-sources` silently disables every cc-os
plugin (hooks AND skills) with no error surfaced. Any profile wrapper must always include
`user` in `--setting-sources` (or simply not pass the flag) to keep the plugin family
alive; only `project`/`local` inclusion should be varied per profile.
## Spike (b): where should per-profile settings overlay files live?
Read `bin/refresh-plugins` (Ruby). It touches exactly these paths, all read-only except
for the plugin cache itself:
- Reads `~/.claude/plugins/known_marketplaces.json` (marketplace list/config)
- Reads `~/.claude/plugins/installed_plugins.json` (which plugins are installed per
marketplace)
- For each installed plugin under a `directory`-source marketplace, shells out to
`claude plugin uninstall <plugin>@<marketplace>` / `claude plugin install
<plugin>@<marketplace>` — this rewrites `~/.claude/plugins/cache/<marketplace>/
<plugin>/<version>/` (via Claude Code's own plugin manager, not directly) and updates
`installed_plugins.json`'s install record for that plugin.
- Verifies via `diff -rq` between the cache dir and the plugin's source dir under
`source_path` (from marketplace config, e.g. `~/dev/cc-os/plugins` or
`~/dev/cc-plugins`).
It never touches `~/.claude/settings.json`, never touches any file under a
`~/.claude/profiles/` style directory, and never touches arbitrary `--settings <file>`
overlays — its blast radius is strictly `known_marketplaces.json`,
`installed_plugins.json`, and the `plugins/cache/<marketplace>/<plugin>/` tree.
**Recommendation:** put per-profile settings overlay JSONs at `~/.claude/profiles/*.json`
(e.g. `~/.claude/profiles/cc-os-design.json`, `client-dev.json`, `brainstorm.json`),
passed via `claude --settings ~/.claude/profiles/<name>.json`. This path is structurally
disjoint from everything `refresh-plugins` reads or writes (`plugins/cache/`,
`plugins/known_marketplaces.json`, `plugins/installed_plugins.json`) — there is no
plausible future edit to `refresh-plugins` that would need to reach into
`~/.claude/profiles/`, since that script's whole job is cache-vs-source verification for
marketplace-installed plugins, not settings resolution.
Interaction with `enabledPlugins`: `--settings <file>` is merged with (not a full
replacement for) the resolved settings from the sources selected by `--setting-sources`
(per spike (a), keep `user` in the source list so the base `~/.claude/settings.json`
`enabledPlugins`/hooks/env still apply). The profile overlay should therefore only need
to express a *diff* — an `enabledPlugins` map with the profile's unwanted plugins set to
`false` (or, if this repo prefers positive lists, the full set with only the
wanted ones `true`) — rather than duplicating the whole settings file. This diff-only
overlay is exactly what B2 should generate per profile. One caution to carry into B2:
verify at build time whether CC deep- or shallow-merges the `enabledPlugins` object
across `--settings` sources (not tested in this spike) — either way, a plugin disabled
via the overlay's `enabledPlugins: {"name": false}` does not require touching
`known_marketplaces.json` or `installed_plugins.json`, since `enabledPlugins` toggles are
independent of the marketplace/install-record identity those files hold (the files
`refresh-plugins` owns).
**Verdict (b):** `~/.claude/profiles/*.json` (passed via `--settings`) is safe — it lives
entirely outside the three paths `refresh-plugins` reads/writes/rewrites
(`known_marketplaces.json`, `installed_plugins.json`, `plugins/cache/`), and
`enabledPlugins` toggles in an overlay are orthogonal to the marketplace/cache identity
`refresh-plugins` verifies, so cache refreshes and profile overlays can never clobber
each other.