Skip to content

Commit 09b173d

Browse files
authored
feat(platform-integrations): unify plugin code under a single canonical source (#235)
* docs(adr): record compile-time plugin code generation decision Captures the design that came out of the planning session for #219: treat platform-integrations/ as generated output from a new plugin-source/ canonical tree, rendered via Jinja2, with a CI gate enforcing render-equality. Records the alternatives weighed (symlinks, separate repo, gitignored output, Go/Rust tooling) and their rejection reasons so the decision isn't relitigated later. Establishes docs/adr/ as the project's ADR home. Refs #219 * docs: drop the ADR draft (#219 PRD already captures the rationale) The PRD on #219 is the canonical record of the design decisions for this work. A separate ADR file duplicated that content without adding review value, so it has been removed. Refs #219 * feat(build): introduce plugin-source/ and the render-equality build pipeline Adds the canonical source tree (plugin-source/) and the build pipeline that renders it into platform-integrations/. The first managed slice is the four identical lib/*.py helpers shared by claude and claw-code; the byte-identical render produces no diff vs the previously committed copies. What's wired: - plugin-source/MANIFEST.toml declares platforms (claude, claw-code, codex, bob) and the per-file render targets. Verbatim entries only for now; Jinja2 templating and per-platform overlays land in subsequent commits. - scripts/build_plugins.py renders the manifest and detects drift. Stdlib only (tomllib, filecmp, shutil); no new project deps. - justfile gains compile-plugins and check-plugins-rendered recipes. - Pre-commit gains a plugins-rendered hook scoped to plugin-source/, platform-integrations/, and scripts/build_plugins.py. - CI gains a check-plugins-rendered job. - tests/platform_integrations/test_build_pipeline.py covers manifest loading, full-render output, and drift detection (positive and negative cases). Codex and bob declare plugin_root entries but no managed files yet — those land when those platforms' content is migrated in later commits. The existing install.sh continues to do the runtime lib copy for them in the meantime. Refs #219 * feat(build): migrate identical claude/claw-code skill scripts to plugin-source Sweeps the six skill scripts that are byte-identical between claude and claw-code today into plugin-source/skills/<name>/scripts/. The render remains byte-identical to committed platform-integrations/, so this is a pure source relocation — no behavior change. Migrated: - learn/scripts/save_entities.py - publish/scripts/publish.py - subscribe/scripts/subscribe.py - unsubscribe/scripts/unsubscribe.py - sync/scripts/sync.py - save-trajectory/scripts/save_trajectory.py Not yet migrated (left for the Jinja2 commit): - recall/scripts/retrieve_entities.py — varies across all four platforms. - learn/scripts/on_stop.py and on_stop.sh — claude-only hooks. - save-trajectory/scripts/on_stop.py — claude-only hook. - All SKILL.md files — diverge across platforms. - codex and bob copies of these scripts — diverge from claude/claw-code due to runtime-environment differences (lib path discovery, hook contracts). Refs #219 * feat(build): add Jinja2 templating to the plugin source pipeline Adds Jinja2 rendering for source files ending in .j2. Each platform's [platforms.<name>] table in MANIFEST.toml now accepts arbitrary keys beyond plugin_root; everything else is forwarded to the template as a context variable, alongside `platform = "<name>"`. Verbatim copy remains the default for non-.j2 sources. Demonstrates the mechanism on skills/learn/SKILL.md, the first templated file. Two real per-platform variations are now expressed in one shared .j2 template: - forked_context (bool) — claude wraps learn in a forked execution model and needs a "Step 0: Load the Conversation" section that reads the stop-hook transcript; claw-code does not. The bool gates a {% if %} block plus a small inline phrasing tweak in Step 1. - save_entities_invocation (str) — claude invokes the save script via ${CLAUDE_PLUGIN_ROOT}/...; claw-code does a config-home lookup dance. The string is substituted in three places (Method 1/2/3 examples). Render produces byte-identical output to the previously committed SKILL.md files for both claude and claw-code; drift gate stays green. Build-pipeline tests grow a TestJinjaTemplating class that asserts a shared .j2 source produces platform-specific output; existing tests updated for the renamed Manifest.platforms attribute (was platform_roots) and split into "every target rendered" plus "verbatim files match source byte-for-byte". This is commit 3a of the migration plan; commit 3b will sweep the remaining drifted SKILL.md files and the per-platform script variation (retrieve_entities.py, codex/bob save_entities.py, on_stop.* hooks). Refs #219 * feat(build): template all claude/claw-code SKILL.md prose Sweeps the remaining seven SKILL.md files (recall, publish, subscribe, unsubscribe, sync, save-trajectory, save) into shared .j2 templates that render byte-identically (modulo one trivial whitespace fix, see below) to the previously committed claude and claw-code copies. The dominant per-platform variation across these files is the script invocation snippet — claude expands ${CLAUDE_PLUGIN_ROOT} via its plugin runtime; claw-code does a config-home discovery dance wrapped in sh -lc '...'. Rather than store the long claw-code shell command as a manifest variable for each skill, this introduces a shared Jinja2 macro (plugin-source/_macros.j2 :: invoke(skill, script, args)) that emits the platform-appropriate form. `args` accepts None, a string, or a list — when given a list, claude renders one arg per line with backslash continuation (matches the existing publish/subscribe formatting); claw-code stays single-line because the whole command is inside sh -lc '...'. The remaining variation is captured in two new per-platform manifest keys plus an inline conditional block in recall: - forked_context (bool) — Step 0 of learn loads a forked-context transcript on claude; not relevant on claw-code. - save_example_script_root (str) — placeholder root used in save's example invocations (${CLAUDE_PLUGIN_ROOT}/skills vs ~/.claw/skills). - user_skills_dir (str) — where the save skill writes the new skill (~/.claude/skills vs ~/.claw/skills). - recall's "How It Works" prose differs in step 1-2 wording (claude fires on user prompt submit; claw-code fires on PreToolUse) and references "Claude" vs "the agent" in two places. Inline {% if %}. learn/SKILL.md.j2 (introduced in the previous commit) is migrated from its bespoke `save_entities_invocation` manifest var to the shared invoke() macro. The save_entities_invocation key is dropped. One incidental cleanup: save/SKILL.md had four trailing spaces on two blank lines inside an embedded python code-block example (legacy of an earlier editor). The .j2 template renders those lines without the trailing whitespace; the committed claude+claw-code copies are updated to match. No semantic change. Codex and bob SKILL.md files are not migrated in this commit — their prose diverges substantially (different audience LLMs, different hook contracts) and they need either deeper conditionals or per-platform overlay files. Those land in commit 3c alongside the script-synthesis work. Refs #219 * feat(build): add claude-only on_stop hooks as platform overlays Three files exist only on the claude tree (not on claw-code, codex, or bob): the forked-context stop hooks for `learn` and `save-trajectory`. Bringing them under build management uses the per-platform overlay pattern — manifest entries with `platforms = ["claude"]` and a single source path under plugin-source/. The renderer emits them only into the claude tree; the drift gate enforces byte-identity. Files: learn/scripts/on_stop.py, learn/scripts/on_stop.sh, save-trajectory/scripts/on_stop.py. Mypy now also excludes plugin-source/ (it already excluded platform-integrations/). The two on_stop.py files share a module name, which the existing exclusion handled in the rendered tree but not in the source tree. Notes on what is NOT in this commit: - save_entities.py for codex is *not* synthesized in this commit. Codex's variant ignores incoming owner/visibility values from stdin (see test_codex_sharing.py::test_save_ignores_incoming_owner_and_visibility), while claude/claw-code preserve them if set. That is a deliberate per-platform security stance, not drift, and collapsing it would either change codex behavior or introduce a new behavior-flag knob — worth its own PR with explicit user buy-in. - retrieve_entities.py is also not synthesized here. Beyond the lib-path discovery prelude (which the shim pattern would cover), the bodies legitimately differ across platforms: claude logs env vars and argv for debugging while codex doesn't, codex calls find_entities_dir while claude calls find_recall_entity_dirs, and the output header text varies. Synthesis warrants a focused commit. - Codex and bob SKILL.md files remain hand-edited in platform-integrations/. Their prose is tuned for different audience LLMs and would mostly require Pattern B (per-platform overlay files) rather than Jinja2 conditionals; deferring until the broader migration shape settles. Refs #219 * refactor(bob): drop colon-prefixed paths for Windows compatibility Bob is the only platform that used colon-prefixed names on disk (.bob/skills/evolve-lite:<x>/, .bob/commands/evolve-lite:<x>.md). Windows treats `:` as a drive separator and rejects it in path components, so the existing layout couldn't be checked out or installed on Windows. Other platforms (claude, codex, claw-code) synthesize the colon namespace from a plugin manifest and don't have the issue. Renames every colon-prefixed source path to a hyphen-prefixed name (evolve-lite-<x>) and updates every reference: bob's custom_modes.yaml prompt, bob's command-file frontmatter, install.sh's BobInstaller glob patterns and status output, and the affected tests in tests/platform_integrations/. User-facing slash-command surface change for Bob users: /evolve-lite:learn → /evolve-lite-learn (etc). Other platforms are unchanged because their plugin manifests still synthesize the colon form for the user-facing namespace. The sole reference to evolve-lite:recall left intact is in install.sh's CodexInstaller post-install message — codex's plugin manifest still produces /evolve-lite:recall as the slash command, so the hyphenated name there would be wrong. Pre-existing test failures unrelated to this rename: - test_bob_sharing.py and test_sync.py and test_codex_sharing.py expect "invalid subscription name" in stdout but sync.py logs "invalid name" to stderr. This drift exists on main (verified before the rename) across all three platforms; same 5 failures before and after. Out of scope here, will need its own commit. The rename FIXES one pre-existing test: test_skill_directory_names.py::test_bob_lite_skills_follow_naming_convention (which now matches the new evolve-lite- prefix expectation). Refs #219 * refactor(bob): decouple custom_modes.yaml from skill-path enumeration Drops Step 0 of the evolve-lite mode prompt, which used to enumerate specific .bob/skills/<skill>/SKILL.md paths the agent had to read up front. The relationship between the mode and the skills it depends on was largely a coincidence of the prompt — the mode's job is the workflow contract (recall → work → save-trajectory → learn → complete); the skill registry is whatever Bob's runtime resolves under .bob/skills/. Replaces the path enumeration with a generic instruction to read each skill's SKILL.md before first invocation. Workflow steps still call the relevant skills by name (recall, save-trajectory, learn, plus the optional sharing skills), since the mode's contract is precisely "use these skills in this order." Names, not paths. This finishes the migration plan from #219: 1. ✅ Build pipeline + render-equality gate (commit 1) 2. ✅ Migrate identical claude/claw-code skill scripts (commit 2) 3a. ✅ Jinja2 templating + first per-platform .j2 (commit 3a) 3b. ✅ Sweep remaining claude/claw-code SKILL.md prose (commit 3b) 3c. ✅ Claude-only on_stop overlay files (commit 3c) 4. ✅ Bob colon-prefix rename for Windows compat (commit 4) 5. ✅ Decouple custom_modes.yaml from skill paths (this commit) Followups outside this PR's scope: - Synthesize codex's save_entities.py and the four-platform retrieve_entities.py (real semantic synthesis, deserves focused PR) - Migrate codex/bob SKILL.md content into plugin-source as Pattern B per-platform overlays - Move claw-code's installed-path convention off colons (separate Windows-compat issue, parallel to bob's) - Resolve the pre-existing "invalid subscription name" stdout/stderr drift across claude/codex/bob (5 failing tests on main, untouched by this PR) Refs #219 * fix(sync): surface invalid subscription entries in stdout summary Resolves four pre-existing test failures across claude, codex, and bob sync tests that asserted "invalid subscription name" appeared in stdout when an entry in evolve.config.yaml had an unsafe name (e.g. '../evil', '.', '..'). Root cause: every platform's sync.py used `normalize_repos(cfg)`, which routes through `_coerce_repo` in lib/config.py. _coerce_repo silently filtered invalid entries (after a stray stderr print with a slightly different phrasing — "ignoring repo entry 'X' — invalid name") and returned None. The downstream "skipped — invalid subscription name" branch in each sync.py ran on already-filtered entries, so it never fired. The user saw "No subscriptions configured" and a stderr log with a different message; the tests saw neither in stdout. Fix: - lib/config.py: drop the stderr prints inside _coerce_repo. They were leaky from a library function (callers, not the lib, should decide where to surface a rejection). Add `classify_repo_entry` which returns (repo, rejection) for one raw entry — exactly one is non-None — so callers can iterate raw `cfg["repos"]` and report rejections per their own UX. - claude/claw-code/codex/bob sync.py: replace `normalize_repos(cfg)` with manual iteration over raw entries via classify_repo_entry. Rejection reasons are added to the same `summaries` list that already collects per-repo sync results, so they appear in the user-visible "Synced N repo(s): …" stdout line. Dedup by name is preserved inline. - test_config.py::test_invalid_scope_entries_dropped: replaced its capsys assertion (which depended on the now-removed stderr print) with a direct call to classify_repo_entry that returns the same rejection reason structurally. Test impact: - Fixes test_sync.py::test_skips_invalid_subscription_name - Fixes test_bob_sharing.py::test_skips_invalid_subscription_name - Fixes test_bob_sharing.py::test_rejects_dot_and_double_dot_names - Fixes test_codex_sharing.py::test_sync_skips_invalid_subscription_name - One pre-existing failure remains: test_subscribe_warns_when_audit_write_fails in test_codex_sharing.py. That test asserts subscribe.py warns and continues when the audit log can't be written; the current subscribe.py rolls back and exits 1 (claude and codex both). That's a separate design decision (fail-open UX vs fail-closed security) that deserves its own focused commit. Refs #219 * feat(build): unify SKILL.md prose across all four platforms Replaces the SKILL.codex.md / SKILL.bob.md per-platform-overlay approach (the dropped c6c76a0) with a single SKILL.md.j2 per skill that renders for all four platforms. Codex's prose is the canonical base — it is the most refined / production-tested variant — and Jinja2 branches handle the genuinely platform-specific bits. What this does for each cross-platform skill (learn, publish, recall, subscribe, sync, unsubscribe): - Frontmatter description switches to codex's trigger-oriented wording across all platforms (claude/claw-code/bob previously carried a more passive "Analyze ..." description). - claude keeps `context: fork` in the frontmatter via a Jinja branch. - learn keeps Step 0 (forked-context transcript loading) for claude only via the existing `forked_context` flag. - recall adopts codex's "Required Action / Completion Rule / Required Visible Completion Note / Failure Conditions" guards on every platform, with a per-platform "How It Works" branch that describes claude's UserPromptSubmit hook, claw-code's PreToolUse hook, codex's optional codex_hooks integration, and bob's manual workflow respectively. - sync gains a "Notes" implementation-detail section sourced from bob's prose (additive, applies to all platforms). - unsubscribe keeps the claude/claw-code-only `--force` addendum inside a `{% if platform in ["claude", "claw-code"] %}` branch because only those platforms' unsubscribe.py refuses to remove a write-scope clone without it. save-trajectory now also renders for bob (codex has no save-trajectory skill). The Write+temp-file pattern from claude applies to bob too — bob's prior heredoc form had the same escaping fragility claude's note warned against. The macro layer (_macros.j2): - `invoke(skill, script, args)` gains codex and bob branches: codex → python3 "$(git rev-parse --show-toplevel ...)/plugins/.../<script>" bob → python3 .bob/skills/evolve-lite-<skill>/scripts/<script> Codex paths now standardise on the git-rev-parse form (codex's pre-existing prose mixed that with bare relative paths). - new `skill_ref(name)` macro expands to the platform-appropriate cross-reference syntax: `/evolve-lite:<name>` for claude / claw-code, `evolve-lite:<name>` for codex, `evolve-lite-<name>` for bob. MANIFEST.toml: - Adds `forked_context = false` to the codex and bob platform tables so StrictUndefined doesn't trip on the `{% if forked_context %}` branch in learn. - For each cross-platform skill, the [[files]] entries collapse from "claude/claw-code .j2 + codex overlay + bob overlay" (3 sources) into a single source with two target rows — one for [claude, claw-code, codex] hitting `skills/<skill>/SKILL.md`, one for [bob] hitting `skills/evolve-lite-<skill>/SKILL.md` (post-rename folder). The codex/bob/claude/claw-code on-disk SKILL.md outputs are now all freshly rendered from these unified sources. The drift gate (`just check-plugins-rendered`) is green; platform_integrations tests still pass at 307/308 (the same pre-existing `test_subscribe_warns_when_audit_write_fails` failure tracked elsewhere). Refs #219 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(learn): unify save_entities.py with codex's strict ownership semantics All four platforms now render save_entities.py from a single source in plugin-source/. The unified script adopts codex's strict-overwrite ownership stamping verbatim: entity["owner"] = args.user or "unknown" entity["visibility"] = "private" This replaces the older preserve-if-set form claude/claw-code carried: if args.user and not entity.get("owner"): entity["owner"] = args.user if not entity.get("visibility"): entity["visibility"] = "private" Why strict wins, per the timeline: claude's preserve-if-set form landed 2026-04-21 in #188 (6f79732 "feat(evolve-lite): add entity sharing skills and CI tests"). Codex's strict form landed 2026-04-22 in #196 (cd4204c "feat(codex): add lite sharing skills and session-start sync"), whose commit body explicitly lists "fix(codex): tighten sharing script safeguards" and "fix(codex): harden sharing scripts and tests". Codex was a deliberate second pass on the same script after the spoofing risk was identified — untrusted upstream input (a prompt-injected agent) must not be able to dictate `owner` or `visibility` on the resulting on-disk entity. The strict semantics are pinned by test_codex_sharing.py::test_save_ignores_incoming_owner_and_visibility, which still passes against the unified source. No test pinned the preserve-if-set behavior on the claude/claw-code side, so dropping that branch costs nothing observable and closes the spoofing vector on those two platforms as well. Lib-path discovery is also unified: the walk-up loop checks `<ancestor>/lib` (claude / claw-code / codex installed layout), `<ancestor>/evolve-lib` (bob's installed layout), and the existing `<ancestor>/platform-integrations/claude/plugins/evolve-lite/lib` monorepo-dev fallback that codex's variant carried. One discovery prelude works for every platform, no Jinja branching needed. MANIFEST: save_entities.py expands from `["claude", "claw-code"]` to two entries — one targeting `skills/learn/scripts/save_entities.py` for [claude, claw-code, codex], one targeting `skills/evolve-lite-learn/scripts/save_entities.py` for [bob] (post-rename folder). Tests: 307/308 platform_integrations pass — same baseline as before (the one pre-existing failure test_codex_sharing.py::test_subscribe_warns_when_audit_write_fails predates this branch). Refs #219 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(recall): unify retrieve_entities.py across all four platforms All four platforms now render retrieve_entities.py from a single source in plugin-source/. The unified script adopts codex's prose and structure verbatim where the variants diverged, with two deliberate concessions to preserve other-platform behavior. Synthesis decisions, by divergence point: Lib-path discovery — same walk-up loop as save_entities.py: `<ancestor>/lib` (claude / claw-code / codex), `<ancestor>/evolve-lib` (bob), and the existing `<ancestor>/platform-integrations/claude/...` monorepo-dev fallback. One discovery prelude, no Jinja branching. find_recall_entity_dirs vs find_entities_dir — codex's `find_entities_dir` wins. Both functions resolve to the same canonical `<evolve_dir>/entities` path today, so the multi-root list form (claude/claw-code) collapses to the single-dir form with no observable behavior change. Output header — codex's "## Evolve entities for this task / Review these stored entities and apply any that are relevant to the user's request:" propagates to all four platforms (claude/claw-code/bob previously emitted the shorter "## Entities for this task" form). Two test header pins updated to match: SCRIPT_VARIANTS in test_retrieve.py and the bob-side assertion in test_bob_sharing.py. Item formatting — codex's plain `Rationale:` / `When:` lines win over claude/claw-code/bob's italicised `_Rationale: ..._` / `_When: ..._` form. Subscribed-source detection — codex's relative-path approach (`md.relative_to(entities_dir).parts`) wins over the search-for-"entities"-in-parts logic claude carried. Symlink + .git filtering — preserved as additive defensive features even though codex didn't have the .git skip. Skipping git bookkeeping when a write-scope clone lives under entities/subscribed/{name}/.git/ is the right thing to do, and it doesn't conflict with codex's behavior on a clean entities tree. Stdin handling — codex's strict "json.load + return on JSONDecodeError" is preserved (the test_handles_invalid_json_stdin_gracefully test pins this on every variant). Empty stdin is treated as "no input, continue with entity loading" rather than an error, so bob's manual-invocation path (which never pipes anything upstream) keeps working without an `echo {}` workaround. Argv dump — claude carried a "=== Command-Line Arguments ===" log block; codex didn't. Dropped, codex wins. "# Made with Bob" footer — dropped. MANIFEST: retrieve_entities.py adds two entries — one targeting `skills/recall/scripts/retrieve_entities.py` for [claude, claw-code, codex], one targeting `skills/evolve-lite-recall/scripts/retrieve_entities.py` for [bob]. Same shape as save_entities.py from the previous commit. Tests: 307/308 platform_integrations pass — same baseline (the one pre-existing failure test_codex_sharing.py::test_subscribe_warns_when_audit_write_fails predates this branch). Refs #219 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(sharing): unify publish/subscribe/sync/unsubscribe + bob save_trajectory All five remaining sharing/recall scripts now render from a single source under plugin-source/. The unified scripts adopt codex's variants verbatim where codex and claude diverged, with two mechanical changes per file: 1. The lib-path-discovery prelude is replaced with the same walk-up loop introduced in the save_entities.py and retrieve_entities.py commits — checks `<ancestor>/lib` (claude/claw-code/codex), `<ancestor>/evolve-lib` (bob), then the existing `<ancestor>/platform-integrations/claude/.../lib` monorepo-dev fallback codex's variants carried. 2. The "(Codex)" / "(Bob)" docstring annotations and the trailing "# Made with Bob" comment are dropped. Per-script trade-offs codex-wins introduces on claude/claw-code: publish.py - No behavior delta vs the prior plugin-source version that mattered to existing tests; codex and claude were already very close here. Soft-warn-on-audit-failure semantics preserved. subscribe.py - codex's `project_root` derives from `evolve_dir.resolve()` (handles a non-".evolve"-named EVOLVE_DIR) instead of always using `str(evolve_dir.resolve().parent)`. - codex re-raises rather than printing "Error: failed to record subscription — clone removed:" when save_config fails. The rollback semantics are unchanged (clone is removed, repos list popped); only the user-visible error string differs. Updated test_rolls_back_clone_if_config_write_fails to drop its message-string check; the rollback behavior it actually cares about still passes. - Argument help text loses claude's longer descriptions; codex's terser arg help propagates. sync.py - Drops claude's `git -c safe.directory={repo_path}` flag from the inner `_git` helper. No test pinned this; its only effect is whether sync works inside a repo owned by a different uid than the running process (matters in shared-filesystem installs, doesn't matter in the test sandbox). - Drops claude's head_before / head_after short-circuit and always counts a delta after a fetch+rebase/reset; the subscribed-base path-traversal check codex carried in the main loop is added on top of the lib-level rejection list, so both layers of name validation now apply. - codex's audit_root indirection (handles a non-".evolve"-named EVOLVE_DIR for the audit log path) propagates to all platforms. unsubscribe.py - codex's combined `is_valid_repo_name` + path-traversal check replaces claude's two separate-step form. Same observable validation; the rejection error string is identical. - codex's `project_root` derivation matches the subscribe.py change above. save_trajectory.py - codex has no save-trajectory skill, so the canonical here is claude's existing plugin-source variant (lazy log creation, atomic O_EXCL claim, file-arg-or-stdin input). Bob's prior variant was simpler and used naive `open()`; replacing it with the claude version is a strict improvement (handles same-second collisions, supports the tmp-file-input pattern the SKILL.md prose now describes for all platforms). MANIFEST: each of the five scripts gains a second [[files]] entry mirroring the save_entities.py / retrieve_entities.py pattern — one target for [claude, claw-code, codex] under `skills/<skill>/scripts/<script>.py`, one target for [bob] under `skills/evolve-lite-<skill>/scripts/<script>.py`. Tests: 307/308 platform_integrations pass — same baseline (the one pre-existing failure test_codex_sharing.py::test_subscribe_warns_when_audit_write_fails remains; it pins soft-warn audit semantics on subscribe.py while both claude and codex variants implement hard-fail, which is a separate fail-open-vs-fail-closed design call). After this commit every Python script under platform-integrations/<platform>/ is rendered from plugin-source/. The only files still outside build management are infrastructure that has no unification opportunity: README.md, .claude-plugin/ / .codex-plugin/ manifests, bob's commands/ directory, bob's custom_modes.yaml, and the parallel evolve-full/ plugin tree. Refs #219 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * build(manifest): default target to source path Make `target` optional in MANIFEST.toml [[files]] entries. When omitted the renderer falls back to source minus a trailing `.j2`. Drops 14 lines from MANIFEST.toml without changing the rendered output. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * build(manifest): use Jinja default filter for forked_context Switch the learn template to {% if forked_context | default(false) %} so non-claude platforms no longer need to declare forked_context = false just to satisfy StrictUndefined. Drops three lines from MANIFEST.toml and makes the platform definitions only declare what differs from the default. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * build(manifest): per-platform target_rewrites for bob folder rename Add `[[platforms.<name>.target_rewrites]]` — a list of (regex, replacement) substitutions the renderer applies to each entry's target path under that platform. Use it on bob to map `skills/<name>/` to `skills/evolve-lite-<name>/` so the platform definition (not 14 duplicate manifest entries) carries the folder-rename rule from commit 07a171c. Collapses every `[[files]]` pair (one for claude/claw-code/codex, one for bob's prefixed target) into a single entry that lists every receiving platform. Drops MANIFEST.toml from 232 lines to 132 with no change to the rendered output. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * build(manifest): default platforms to every declared platform Make `platforms` optional in [[files]] entries. When omitted the renderer fans the entry out to every platform declared in the manifest. Drops the `platforms = ["claude", "claw-code", "codex", "bob"]` line from the 12 fully-shared entries — the common case for skill scripts and SKILL.md templates after the bob duplicates collapsed. MANIFEST.toml is now 132 lines (from 232 at the start of this batch); no change to the rendered output. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * build(lib): ship lib/ to all four platforms; drop claude fallback Add bob and codex to the lib/ entries in MANIFEST.toml. Each platform's plugin tree now ships its own copy of lib/__init__.py, lib/audit.py, lib/config.py, lib/entity_io.py — codex and bob no longer rely on a walk-up to claude's monorepo lib. Simplify the script preludes accordingly: drop the `platform-integrations/claude/plugins/evolve-lite/lib/` fallback from the walk-up loop; the local lib/ or evolve-lib/ sibling is always present now. Update install.sh — bob now sources its lib from its own plugin tree instead of reaching into claude's; codex's redundant claude-lib copy goes away (the plugin copytree already includes lib/). Drop the PYTHONPATH=claude-lib injection in test_bob_sharing.py — bob's scripts find their own lib via the walk-up. Tests pass without it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * build(manifest): fan out save / save-trajectory / on_stop to every platform Drop platform restrictions from the four entries that previously covered partial subsets: - save (was claude+claw-code only) → all four - save-trajectory script + SKILL.md (was missing codex) → all four - on_stop.py / on_stop.sh hooks (was claude only) → all four For platforms where these don't have full runtime support today, the files ship as inert artifacts. Per-platform behavior tightening (e.g. making save-trajectory work under codex, plumbing on_stop hook contracts on non-claude platforms) is tracked as follow-up issues. Add user_skills_dir / save_example_script_root context vars for codex and bob so the save SKILL.md template renders. The codex/bob prose is tilted toward project-local skill paths rather than user home — fix later. Wrap the `context: fork` frontmatter line in the save template with a claude-only branch (matching save-trajectory's pattern). Add commands/evolve-lite-save.md to bob's plugin tree to satisfy the "every skill has a command file" gate now that bob has evolve-lite-save. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * build(structure): nest skills under skills/evolve-lite/<name>/ Restructure plugin-source/ so every skill folder lives under a shared `evolve-lite/` parent: plugin-source/skills/<name>/ → plugin-source/skills/evolve-lite/<name>/. Mirror this in the rendered output for claude/claw-code/codex; bob keeps its flat skills/evolve-lite-<name>/ layout via the existing target_rewrite (pattern updated to match the new source path). Plugin metadata follows: - claude/codex plugin.json: skills key now points at ./skills/evolve-lite/ - claw-code plugin.json: gains a `skills` key pointing at the same path - claude hooks/hooks.json + claw-code hooks/retrieve_entities.sh: shell paths inserted with the evolve-lite/ segment - _macros.j2 invoke() macro: claude and codex paths gain the same segment (claw-code uses runtime colon notation independent of source layout; bob's flat installed path also unchanged) - install.sh: codex hook commands rewritten to the new path; status output reflects the nested layout Tests updated mechanically — every hardcoded skills/<name>/ reference in tests/platform_integrations/ now reads skills/evolve-lite/<name>/. The bob path-rewrite pattern is exercised end-to-end: source skills flow through the rewrite and end up at skills/evolve-lite-<name>/ under platform-integrations/bob/. Tests: 307/308 baseline maintained (the pre-existing test_subscribe_warns_when_audit_write_fails is unchanged). Validation note: claude / claw-code / codex plugin loaders are assumed to honor the `"skills": "./skills/evolve-lite/"` key. Bob's runtime is unaffected — the rewrite produces the same flat .bob/skills/<name>/ layout as before. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * build(manifest): delete MANIFEST.toml; renderer walks plugin-source/ Move per-platform configuration into a PLATFORMS dict at the top of scripts/build_plugins.py. The renderer now walks plugin-source/ and fans every file out to every platform — no manifest entries, no explicit `platforms = [...]` lists, no `target = "..."` overrides. Files at plugin-source/ root that are not shipped (_macros.j2, README.md) are listed as RESERVED_SOURCES. The build pipeline keeps the same public surface (`load_manifest()`, `render_to()`, `check_drift()`, `Manifest`/`PlatformConfig`/`FileEntry` dataclasses) so tests and external callers stay working. The MANIFEST_PATH constant is gone; the perturbation drift test no longer needs to patch it. Bob's path rewrite stays the only structural divergence — encoded inline in PLATFORMS as `[(pattern, replacement)]`. Adding a new skill now requires only creating its directory under plugin-source/skills/evolve-lite/; the build picks it up automatically. Tests: 307/308 baseline maintained. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * build(layout): move build_plugins.py into plugin-source/ The script and the source tree it walks now live together. Add build_plugins.py to RESERVED_SOURCES so the renderer skips itself, and exclude any __pycache__/ directory the interpreter creates from the source walk. Update consumer paths in justfile, .pre-commit-config.yaml, the GitHub Actions workflow, and the test harness. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * build(metadata): generate per-host plugin.json from plugin.toml Per-platform plugin.json files become generated artifacts of a single source-of-truth plugin-source/plugin.toml, rendered through pydantic input + output models. Drift gate covers them alongside the existing tree-walk. [plugin] holds host-agnostic metadata (only name + version required); [claude] / [claw-code] / [codex] tables hold genuinely host-specific fields. All models are extra="allow", so undeclared TOML keys flow through: [plugin] extras fan out to every host's top-level, host-table extras go to that host only, [codex] extras land in codex's interface block. Bob has no plugin.json output. Refs #219. * feat(bob): rename skills to colon form; tighten learn/recall workflow text Apply evolve-lite:<skill> as bob's runtime skill name across SKILL.md frontmatter, _macros.j2 skill_ref, custom_modes.yaml workflow steps, and the commands/*.md slash-command definitions, so bob's UX matches claude and codex (`/evolve-lite:learn`). On-disk folder layout stays hyphenated (.bob/skills/evolve-lite-<skill>/) so the plugin tree installs cleanly on Windows, which rejects colons in path components. Also folds in the in-flight learn/recall polish: recall switches to verbatim entity quoting in forked-context renders (the parent agent can't see intermediate Read results) and uses ${EVOLVE_DIR:-.evolve} consistently; learn's Step 0 finds the most recent trajectory by scanning ${EVOLVE_DIR}/trajectories/ instead of parsing on_stop's transcript_path marker, so the skill is robust to any trajectory the save-trajectory hook (or skill) wrote. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * build(commands): render bob commands/ from plugin-source Move the 8 hand-maintained bob slash-command definitions from platform-integrations/bob/evolve-lite/commands/ into plugin-source/commands/ so they're now driven by the same fan-out build that already covers SKILL.md, scripts, and per-platform metadata. Adds a target_excludes pattern list to PlatformConfig — claude / claw-code / codex declare `^commands/` to opt out of the new subtree since they have their own command surfaces (plugin.json, $-registry); bob alone keeps it. The renderer skips excluded files in both render_to and check_drift, so pre-commit drift detection keeps working without seeing claude/codex as "missing" the bob-only files. Output content is byte-identical to the prior hand-maintained commands directory (this is purely a source-layout move + build-time filter), verified via `build_plugins.py check`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(plugin-source): per-platform routing + auto-generate bob commands Three changes that compose: 1. Bob commands are now derived 1:1 from the skills walk in build_plugins.py instead of static plugin-source/commands/*.md files. Each command file uses `description` from the skill's SKILL.md frontmatter (bob's command schema only honors `description` / `argument-hints` — the slash-command identifier comes from the file name). The body references the on-disk folder name (`evolve-lite-<skill>`, dash form) since that's what bob resolves skills by; folders stay colon-free for Windows compatibility. 2. Per-platform routing: any source file under `plugin-source/_<platform>/` ships only to that platform, with the `_<platform>/` prefix stripped from the output target. This is how single-platform artifacts now live alongside the universal sources: - _bob/custom_modes.yaml (bob's mandatory workflow definition) - _bob/README.md - _claude/hooks/hooks.json (Stop / UserPromptSubmit / SessionStart) - _claude/README.md - _claw-code/hooks/retrieve_entities.sh - _claw-code/README.md - _codex/README.md 3. render_to() now wipes each platform's plugin_root before writing, so files removed from plugin-source/ (renamed skills, deleted scripts, obsolete commands) cannot linger as orphans. Together with (2), this makes platform-integrations/ fully derivable from plugin-source/. Also drops the now-dead `^commands/` target_excludes from claude / claw-code / codex, since the static commands directory is gone. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(plugin-source): redesign build pipeline tests + ship smoke harness The previous TestRender / TestCheckDrift suite had two brittle patterns that caused the CI failures from commit fc94634: - Iterated `manifest.files × entry.platforms` and asserted every combo was emitted, without honoring `cfg.excludes(...)`. Failed the moment any platform had a non-empty `target_excludes`. - Picked the alphabetically-first verbatim entry × first platform for drift detection, so when that file happened to be excluded for that platform, the perturbation landed in a path check_drift skipped and no `drift:` message was emitted. Redesign: - Shared `isolated_repo` / `rendered_repo` fixtures monkeypatch REPO_ROOT and PLUGIN_SOURCE_DIR so render / check operate against an isolated tmp tree. - Headline invariant test: render then check is silent and returns 0. - Property tests: render is idempotent; render wipes orphans under each plugin_root. - Iteration tests now honor `cfg.excludes(...)` so reintroducing an exclude can't silently break the contract. - Drift tests address files by name (claude learn/SKILL.md, claude learn/scripts/on_stop.py, bob's evolve-lite-learn.md) instead of by `next(...)` over a sorted manifest, so they're stable as the file tree changes. - New TestPerPlatformRouting covers the `_<platform>/` prefix convention; new TestBobCommandGeneration covers the 1:1 skill → command auto-generation, the dash-form body, the description-from- frontmatter rule, and the no-`name:` constraint. Also adds tests/smoke_skills.py (the three-platform skill harness) as part of the PR test plan. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(smoke): clean up live output — sections only, no summary Drops the MainThread group from the live region (it was redrawing the entire view on every orchestrator log line, which stacked duplicate `── MainThread ──` headers when long lines wrapped past the cursor-up wipe). Inlines per-skill `✓/✗ name detail` lines into each platform's section as steps complete, matching the old summary format — and removes the post-run summary block since the same info now lives in the sections themselves. Bob's install-only message also corrected: --resume works upstream again; the real reason we skip skill execution is that bob has no way to run slash commands non-interactively from a one-shot prompt. * test(smoke): let live-region lines wrap; size wipe by physical rows Removes the line truncation in LiveGroupedHandler in favor of a wrap-aware redraw. `_last_lines` is now a physical-row count (each buffered line contributes ceil(len / term_width) rows), so the cursor-up wipe (\033[nF) still lands on the start of the live region when lines wrap onto multiple rows. Terminal width is re-read on every render so window resizes mid-run don't desync the wipe math. * fix(install): purge legacy evolve-prefixed bob artifacts on install/uninstall Addresses visahak's PR #235 finding: re-running install over the pre-rename `.bob/skills/evolve-lite:<name>` layout left both the legacy colon-form and the new dash-form on disk, and uninstall's `evolve-lite-*` glob ignored anything not in the new namespace. Adds `BobInstaller._purge_evolve_artifacts(bob_target)` that strips every `evolve`-prefixed entry from `.bob/skills/`, `.bob/commands/`, and the `.bob/` root (catches `evolve-lib`, the legacy colon-form skills/commands, and any future `evolve-*` namespace). Called as the first step of install (clean upgrade) and from uninstall in place of the per-glob loops. `status` widens its glob to `evolve*` so legacy stragglers surface instead of reading ✗ while artifacts squat on disk. User-owned non-evolve content is untouched. New regression tests in `TestBobLegacyMigration` mirror visahak's exact reproduction (legacy skill + command at install time, post- install accumulation cleared by uninstall, user content preserved through the purge). * fix(check_drift): detect orphan files under plugin roots Addresses visahak's PR #235 finding: `check_drift()` only verified that expected files matched their source — it never enumerated each plugin_root for unexpected extras, so a stale file (renamed/removed generated artifact, hand-written drift) would sail through `check` even though the rendered tree no longer matched plugin-source/. `check_drift()` now walks each platform's plugin_root and reports any file outside the expected set as `orphan: <path> (not generated from plugin-source/)`. The walk is scoped through `git ls-files --cached --others --exclude-standard` so .gitignore'd build artifacts (__pycache__/*.pyc, .DS_Store, …) are correctly invisible — the local dev workflow leaves these under platform-integrations/ during testing and they shouldn't trip the check. Falls back to `Path.rglob` when the working tree isn't a git repo so test fixtures (tmp_path) still detect deliberately seeded orphans. Two new TestCheckDrift tests cover visahak's exact reproduction (orphan.txt at plugin_root) and the descend-into-subdir case. * chore: ignore example-guidelines/ --------- Co-authored-by: Punleuk Oum <5661986+illeatmyhat@users.noreply.github.com>
1 parent fbf7d0d commit 09b173d

174 files changed

Lines changed: 13687 additions & 2525 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/check-code.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,26 @@ jobs:
8383
- name: Check typing
8484
run: uv run mypy .
8585

86+
check-plugins-rendered:
87+
runs-on: ubuntu-latest
88+
strategy:
89+
fail-fast: false
90+
matrix:
91+
python-version: ['3.12']
92+
steps:
93+
- uses: actions/checkout@v5
94+
- name: Set up Python ${{ matrix.python-version }}
95+
uses: actions/setup-python@v5
96+
with:
97+
python-version: ${{ matrix.python-version }}
98+
- name: Install uv
99+
uses: astral-sh/setup-uv@v6
100+
with:
101+
enable-cache: true
102+
python-version: ${{ matrix.python-version }}
103+
- name: Verify platform-integrations matches a fresh render of plugin-source
104+
run: uv run python plugin-source/build_plugins.py check
105+
86106
ui-tests:
87107
runs-on: ubuntu-latest
88108
steps:

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@ dist
1717
.secrets
1818
event.json
1919
site/
20+
example-guidelines
2021
.codex

.pre-commit-config.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,15 @@ repos:
5050
name: detect secrets
5151
args: ['--baseline', '.secrets.baseline']
5252
exclude: package.lock.json
53+
54+
# Plugin render-equality gate — fails if platform-integrations/ has drifted
55+
# from plugin-source/. Runs whenever plugin-source/ or the rendered tree
56+
# under platform-integrations/ has changed.
57+
- repo: local
58+
hooks:
59+
- id: plugins-rendered
60+
name: plugins-rendered
61+
entry: uv run python plugin-source/build_plugins.py check
62+
language: system
63+
pass_filenames: false
64+
files: ^(plugin-source/|platform-integrations/)

justfile

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,12 @@ codex-run:
7878
# Smoke-test that Codex is installed and working
7979
codex-test:
8080
docker run --rm --env-file {{env_file}} {{codex_image}} codex exec --skip-git-repo-check "who are you"
81+
82+
# Render plugin-source/ into platform-integrations/. Edit plugin-source/, then run this.
83+
compile-plugins:
84+
uv run python plugin-source/build_plugins.py render
85+
86+
# Verify committed platform-integrations/ matches a fresh render of plugin-source/.
87+
# CI and the pre-commit hook run this; nonzero exit means the source and output have drifted.
88+
check-plugins-rendered:
89+
uv run python plugin-source/build_plugins.py check
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
description: Must be used near the end of any non-trivial turn that produced potentially reusable tools, guidance, errors, workarounds, or workflows, so those lessons are saved for future turns.
3+
---
4+
Use the `evolve-lite-learn` skill on the current conversation. Follow the skill's instructions exactly.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
description: Publish a private guideline to a configured write-scope repo.
3+
---
4+
Use the `evolve-lite-publish` skill on the current conversation. Follow the skill's instructions exactly.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
description: Must be used at the start of any non-trivial task involving code changes, debugging, repo exploration, file inspection, or environment/tooling investigation to surface stored guidance before analysis or tool use.
3+
---
4+
Use the `evolve-lite-recall` skill on the current conversation. Follow the skill's instructions exactly.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
description: Save the current conversation as a trajectory JSON file in OpenAI chat completion format for analysis and fine-tuning
3+
---
4+
Use the `evolve-lite-save-trajectory` skill on the current conversation. Follow the skill's instructions exactly.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
description: Captures the current session's successful workflow and saves it as a reusable skill with SKILL.md and helper scripts
3+
---
4+
Use the `evolve-lite-save` skill on the current conversation. Follow the skill's instructions exactly.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
description: Add a shared guidelines repo (read-scope subscription or write-scope publish target) to the unified repos list.
3+
---
4+
Use the `evolve-lite-subscribe` skill on the current conversation. Follow the skill's instructions exactly.

0 commit comments

Comments
 (0)