Skip to content

feat(context): PR-C — wiki override + agent/command install widening (ADR-0008)#624

Merged
memtomem merged 4 commits intomainfrom
feat/pr-c
Apr 30, 2026
Merged

feat(context): PR-C — wiki override + agent/command install widening (ADR-0008)#624
memtomem merged 4 commits intomainfrom
feat/pr-c

Conversation

@memtomem
Copy link
Copy Markdown
Owner

Summary

ADR-0008 PR-C in three commits — install widening, vendor override
resolution for skills, and the wiki-side seed CLI.

  • commit 1 widens mm context install to skill | agent | command
    and migrates fan-out enumeration to read both legacy flat
    (agents/<name>.md) and new directory layout (agents/<name>/agent.md,
    commands/<name>/command.md) per ADR-0008 — without moving any
    existing user files. New canonicals land in directory layout;
    legacy flat keeps working.
  • commit 2 ships the override resolver (context/override.py)
    and the per-vendor hook in skills.py. OVERRIDE_FORMATS is the
    full 9-cell matrix; the resolver activates only skills in v1 via
    _PR_C_ACTIVE_TYPES = frozenset({"skills"}). Auxiliary files
    (scripts/, references/) stay from canonical when an override is
    staged (Invariant 4 says byte-copy that file — singular).
  • commit 3 adds mm wiki skill override <name> --vendor <V> with
    --force (creates a .bak sibling) and --editor. Stdout prints
    three pieces — human line, bare absolute path for shell capture,
    and a git add / git commit hint that names the exact paths to
    run in the wiki repo.

A byte-identical regression test (test_context_override.py) was
written first as a guard for PR-B fan-out semantics: with no overrides
on disk, every vendor's SKILL.md MUST equal canonical byte-for-byte.
The test stays green across all three commits.

Why directory layout for agents/commands

ADR-0008 commits to <type>/<name>/overrides/<vendor>.<ext> — that
requires a directory shape around each canonical file. Before PR-C,
agents/commands canonical was flat (<name>.md) with no place for
an overrides/ subdir without breaking fan-out. PR-B's install.py
docstring already called this out:

Agent and command install land in PR-C alongside override resolution
— without override-aware extraction the snapshot exists on disk but
does not flow through fan-out, which would surprise users into
thinking install is broken.

Closing the gap without a file migration: list_canonical_* returns
list[tuple[Path, Layout]] and enumerates both layouts; dir wins on
collision (with WARN); parse_canonical_* takes layout= and uses
path.parent.name for dir form (avoids the brittle path.stem == "agent" heuristic); extract_*_to_canonical preserves the existing
layout per name. mm context migrate (PR-D) will be the supported
consolidation path for users who want to move flat → dir.

Test plan

  • test_context_override.py byte-identical regression — green
  • test_context_install_widening.py — install_agent/install_command + CLI dispatch
  • test_context_agents_bc_layout.py + commands twin — flat-only / dir-only / both-present collision warning / parse layout dispatch / extract preserves layout / extract warns on coexist
  • test_context_override.py resolver unit + skills override apply (per-vendor + all three) + scripts untouched + Invariant 1 no-wiki + agents/commands gate
  • test_wiki_cmd_override.py — happy path, refuse + force + .bak, unknown vendor, missing skill, missing wiki, editor on/off, stdout contract
  • Full regression: uv run pytest packages/memtomem/tests/ -m "not ollama" — 3444 passed, 0 failed
  • uv run ruff check && uv run ruff format --check — clean

Out of scope (PR-D and later)

  • mm context update (mtime-based dirty detection, --force + .bak)
  • mm wiki <type> {diff, lint, edit} and the agent/command subgroups
  • Activating override resolution for agents/commands (_PR_C_ACTIVE_TYPES
    flip, wiki/override.py rendering for non-skills)
  • mm context migrate to consolidate flat → dir layout

🤖 Generated with Claude Code

pandas-studio and others added 4 commits May 1, 2026 00:42
ADR-0008 commits to directory layout (`agents/<name>/agent.md`,
`commands/<name>/command.md`) for vendor overrides at
`<type>/<name>/overrides/<vendor>.<ext>`. PR-B shipped install only for
skills because agents/commands canonical was flat (`<name>.md`) — no
place for an `overrides/` subdir without breaking fan-out.

PR-C[1/3] closes the gap without migrating user files:

* `mm context install` accepts `skill | agent | command` (was `skill`
  only). `install_agent` / `install_command` are thin wrappers around
  the existing `_install_asset` pipeline.
* `list_canonical_agents` / `list_canonical_commands` return
  `list[tuple[Path, Layout]]` and enumerate both flat (legacy) and
  dir layouts. Dir wins on collision; both-present logs a WARNING.
* `parse_canonical_*` takes `layout=` and uses `path.parent.name` for
  dir form (avoids the brittle `path.stem == "agent"` heuristic).
* `extract_*_to_canonical` preserves the existing layout per name —
  flat stays flat, dir stays dir, new agents land in dir layout per
  ADR. Dir+flat coexist case emits a separate WARNING about silent
  flat divergence; `mm context migrate` (PR-D) will be the supported
  consolidation path.
* `InstallResult.asset_type` widened from `Literal["skills"]` to the
  three-kind union; the cast at the construction site widens with it.

A byte-identical regression test (`test_context_override.py`) is
included as a guard for PR-B fan-out semantics: with no overrides
present, every vendor's SKILL.md MUST equal canonical byte-for-byte.
PR-C will only ever diverge that equality when a real override file
is staged (commit 2/3).

Web routes (`context_agents.py`, `context_commands.py`) updated to
unpack the layout-tagged tuples and to derive canonical names
layout-aware in import responses.

Co-Authored-By: Claude <[email protected]>
Adds the override layer described by ADR-0008 Invariant 4:
``<project>/.memtomem/<type>/<name>/overrides/<vendor>.<ext>``, when
present, byte-replaces that vendor's runtime artifact and skips
auto-conversion. PR-C[2/3] activates this for skills only — the
``OVERRIDE_FORMATS`` matrix ships all 9 ``(asset_type, vendor)`` cells
but the resolver gates non-skills with ``_PR_C_ACTIVE_TYPES``; flip the
gate in a follow-up PR to activate agents/commands.

* `_names.py`: ship `OVERRIDE_FORMATS` (9-cell, ADR-pinned location) and
  `GENERATOR_VENDOR` (`<vendor>_<asset_type>` → vendor) so fan-out and
  PR-D's lint/status share one source of truth.
* `context/override.py`: new module with `resolve(project_root, type,
  name, vendor) -> Path | None`. Reads ONLY from project tree per
  Invariant 1 — the docstring explicitly forbids a wiki argument so
  future-self does not accidentally re-couple sync to the wiki.
* `skills.py`: per-vendor hook after `copy_skill` that replaces the
  vendor's `SKILL.md` only. Auxiliary files (`scripts/`, `references/`)
  stay from canonical (Invariant 4 says ``byte-copy that file`` —
  singular).

Tests in `test_context_override.py`:

* `test_resolve_*` — happy path, no-override, unknown vendor, the
  Invariant 1 no-wiki guarantee, and explicit gate tests for agents
  and commands so the enable-day diff is one-line revertable.
* `test_skills_fanout_applies_claude_override_only` — staged override
  per vendor diverges only that vendor's bytes; others stay canonical.
* `test_skills_fanout_applies_all_three_overrides` — full triplet.
* `test_override_only_touches_skill_md_not_scripts` — auxiliary files
  remain canonical even when an override is present.

The pre-existing byte-identical regression test (added in commit 1/3)
still passes — with no overrides on disk every vendor's SKILL.md still
equals canonical byte-for-byte.

Co-Authored-By: Claude <[email protected]>
Adds the user-facing surface for ADR-0008 PR-C[3/3]: a single command
that seeds ``<wiki>/skills/<name>/overrides/<vendor>.md`` from the
canonical SKILL.md so the user's edit starts from a working baseline
rather than a blank file.

* `wiki/override.py`: `seed_override(store, type, name, vendor,
  *, force)` writes the override file (and a `.bak` sibling on
  `--force`). `render_seed_bytes` returns canonical bytes for skills;
  agents/commands raise `NotImplementedError` matching the resolver
  gate in `context/override.py`.
* `cli/wiki_cmd.py`: `mm wiki skill override <name> --vendor <V>`
  with `--force` (overwrite + .bak) and `--editor` (open `$EDITOR`
  after seeding). Exit-code-bearing classified errors for missing
  wiki / missing skill / collision; no traceback leaks. Stdout prints
  three things — the human "Seeded …" line (substring contract), the
  bare absolute path on its own line for shell capture, and a
  commit hint that names the exact `git add` / `git commit` pair to
  run inside the wiki repo.
* `agent` and `command` subgroups are intentionally NOT stubbed —
  click's "no such command" UX is acceptable v1; PR-D adds them
  alongside `diff`/`lint`/`edit`.

Tests cover happy path, refuse + force + .bak, unknown vendor,
missing skill, missing wiki, editor on/off, and the stdout contract
(substring/presence — no strict line ordering so future UX polish
does not churn the test).

Co-Authored-By: Claude <[email protected]>
* `_names.py`: hoist `Layout = Literal["flat", "dir"]` here so agents.py
  and commands.py share one type definition rather than cross-importing.

* `agents.py` / `commands.py`: introduce `canonical_agent_name(path,
  layout)` / `canonical_command_name(path, layout)` as the single source
  of truth for path → name dispatch. Widen `ExtractResult.imported`
  from `list[Path]` to `list[tuple[Path, Layout]]` so the layout tag
  flows through to consumers; the previously-unused `_layout` from
  `_resolve_*_extract_target` is now used (drops the YAGNI complaint).

* `web/routes/context_agents.py` + `context_commands.py`: drop the
  brittle ``path.name == "agent.md"`` heuristic in import responses.
  List + import handlers both go through the helper now; same dispatch
  shape across the surface, single change point if the layout
  semantics ever shift.

* `wiki/override.py`: pre-flight `render_seed_bytes` (which does the
  source check) BEFORE `target.parent.mkdir(parents=True)` so a
  refused call (missing wiki / missing canonical / collision without
  ``--force``) does not leave a half-built ``overrides/`` directory in
  the wiki tree. Rename the unused `vendor` parameter on
  `render_seed_bytes` to `_vendor` (conventional unused-arg signal),
  drop the `del vendor` line.

* `docs/adr/0008-wiki-layer.md`: update the Status header to reflect
  PR-A + PR-B merged and PR-C in flight; expand the PR-C/D table rows
  to mention the dir-layout BC read, the `_PR_C_ACTIVE_TYPES` gate,
  and `mm context migrate` (PR-D scope). The ADR's Decision and
  Invariants are unchanged.

Tests:

* New `test_canonical_agent_name_dispatch_on_layout` +
  `test_canonical_command_name_dispatch_on_layout` exercise the helper
  for both layouts and the literal ``agent.md`` / ``command.md`` flat
  files (the case the brittle heuristic would misclassify).
* New `test_wiki_skill_override_does_not_create_overrides_dir_on_missing_skill`
  verifies the mkdir-then-refuse fix — refused call leaves the
  ``skills/<name>/`` subtree absent.
* Existing `result.imported` consumers updated to unpack the
  ``(path, layout)`` tuples and call the helper.

Out of scope (carry-forward):
- Two-phase write window in `skills.py` override hook (refactor
  `copy_skill` to take an override-aware source).
- `install_agent` / `install_command` concurrency tests — natural
  fit for the PR-D `_PR_C_ACTIVE_TYPES` flip (more fan-out paths
  exercised concurrently).
- Wiki working-tree dirty UX — natural fit for PR-E (web UI surfaces
  the wiki state already).

Co-Authored-By: Claude <[email protected]>
@memtomem memtomem merged commit a31e5e8 into main Apr 30, 2026
7 checks passed
@memtomem memtomem deleted the feat/pr-c branch April 30, 2026 21:32
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 30, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants