Skip to content

fix(skills): route [[skill]] + plugin skills natively for Codex/Gemini (ADR-012)#67

Merged
szjanikowski merged 7 commits into
mainfrom
fix/referenced-plugin-skills-native
Jun 10, 2026
Merged

fix(skills): route [[skill]] + plugin skills natively for Codex/Gemini (ADR-012)#67
szjanikowski merged 7 commits into
mainfrom
fix/referenced-plugin-skills-native

Conversation

@szjanikowski

@szjanikowski szjanikowski commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #65, closing the known follow-up that ADR-012 itself flagged.

Problem

A skill reaches an agent three ways: the variant snapshot (agents_skills/, gemini_skills/), [[skill]] by-reference in variant.toml, and a [nasde.plugin]'s own skills/. PR #65 fixed only the snapshot path. The other two still went through plugin_registration.stage_skill_dir, which hardcodes /app/.claude/skills/<name>/:

  • For Claude that's correct (cwd discovery root).
  • For a Codex/Gemini variant the skill landed where the CLI never scans ($HOME/.agents/skills, ~/.gemini/skills), so it was never natively registered — the same silent "skill-as-document" failure as the original bug.

Uncaught today (no non-Claude variant in the repo uses [[skill]]/plugin), but a latent trap for exactly the core use case: migrating a by-reference skill (the ADR-009 way to avoid copy drift) between agents.

Fix

Both paths now resolve to skill directories (collect_referenced_skill_dirs / collect_plugin_skill_dirs — the dir-list counterparts of the unchanged Claude-only stage_referenced_skills / register_plugin_skills). For Codex/Gemini those dirs route through Harbor's native config.agent.skills, unioned with the snapshot dirs inside _refresh_agent_skills under the single _nasde_derived_skills marker (so stale-drop + hand-authored preservation work across both sources).

  • Claude keeps its unchanged sandbox_files path; a Claude agent drops extra_skill_dirs at merge time to avoid double-injecting.
  • The frontmatter warning now fires on this path too.
  • A new basename-collision warning: Harbor's resolve_skills is last-wins by dir.name, so two derived dirs sharing a basename would silently collapse — nasde now warns.

Branch is one place: _refresh_agent_skills keys on each agent's own import_path, so a hand-written mixed multi-agent config stays correct.

New example variants

codex-nasde-dev-with-arch and gemini-nasde-dev-with-arch exercise [[skill]] by-reference (→ live .claude/skills/nasde-dev) on a non-Claude agent — a permanent repro + skill-migration demo. (Also adds the long-missing Codex variant to nasde-dev-skill.)

Verification

  • E2E (Codex variant codex-nasde-dev-with-arch with [[skill]], --without-eval): the trial's trial.log shows Harbor's native registration command running — mkdir -p $HOME/.agents/skills && cp -r /harbor/skills/* $HOME/.agents/skills/ — i.e. the [[skill]] dir was uploaded to skills_dir and copied into Codex's native HOME-scoped location inside run(). The trial config.json confirms skills=[…/nasde-dev] (native) with no /app/.agents/skills in sandbox_files. Under the old code that cp would copy from an empty /harbor/skills (the skill went only to /app/.claude/skills, which Codex never scans). The agent turn itself then hit the Codex Plus usage limit — an account constraint, not the fix. (Gemini's native activate_skill registration was proven the same way in fix(skills): register Codex/Gemini skills natively, not in /app cwd (ADR-012) #65; the mechanism is shared.)
  • pytest397 passed (new collect_*, union, branch, stale-drop, handwritten, basename-collision, frontmatter, mixed-agent tests)
  • ruff check / ruff format --check / mypy — clean

Coverage levels (honest): the [[skill]] path is verified e2e on a live agent (Gemini activate_skill below). The [nasde.plugin] path shares the same channel from extra_skill_dirs onward but has no example to run e2e, so it is covered by an integration test driving the real ensure_task_plugin staging → collect_plugin_skill_dirs → harbor_config (no Docker), plus unit tests. Full live-agent e2e for a plugin is a follow-up for when a plugin benchmark exists.

Docs

  • ADR-012 extended ("All three skill paths, not just the snapshot")
  • CHANGELOG.md (Unreleased → Fixed), CLAUDE.md, nasde-benchmark-creator skill

🤖 Generated with Claude Code

Szymon Janikowski and others added 7 commits June 10, 2026 12:27
…i (ADR-012)

PR #65 fixed only the SNAPSHOT path (agents_skills/, gemini_skills/). The two
other ways a skill reaches an agent — [[skill]] by-reference in variant.toml and
a [nasde.plugin]'s own skills/ — still went through stage_skill_dir's hardcoded
/app/.claude/skills/ target. Correct for Claude (cwd discovery root), but for a
Codex/Gemini variant the skill landed where the CLI never scans, so it was never
natively registered — the same silent "skill-as-document" failure, just via a
different path. Uncaught today (no non-Claude variant used [[skill]]/plugin) but
a latent trap for the core skill-migration use case.

Both paths now resolve to skill DIRECTORIES (plugin_registration.collect_
referenced_skill_dirs / collect_plugin_skill_dirs) and, for Codex/Gemini, route
through Harbor's native config.agent.skills — unioned with the snapshot dirs
inside _refresh_agent_skills under one _nasde_derived_skills marker (stale-drop
and hand-authored preservation work across both sources). Claude keeps its
unchanged sandbox_files path; a Claude agent drops extra_skill_dirs at merge to
avoid double-injecting. The frontmatter warning now fires on this path too, and
a new warning flags two derived dirs sharing a basename (Harbor resolve_skills
is last-wins, so the loser would silently vanish).

New example variants codex-nasde-dev-with-arch and gemini-nasde-dev-with-arch
exercise [[skill]] by-reference on a non-Claude agent (and add the long-missing
Codex variant to nasde-dev-skill).

Config verified: the codex variant's generated harbor_config carries
skills=[nasde-dev] (native) with the [[skill]] dir, not just the Claude sandbox
entry. Full suite 393 passing; ruff/format/mypy clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The frontmatter and basename-collision warning assertions matched a literal
substring against capsys output, but Rich wraps the long tmp-path line on
Windows' narrow CI console, splitting 'missing YAML frontmatter' across a
newline. Normalize with ' '.join(out.split()) like the existing collision
tests. Verified with COLUMNS=80 locally.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(review)

Code-review finding: `_collect_native_skill_dirs` returns `str(dir.resolve())`
(resolved), but the new native channel did NOT resolve — `collect_plugin_skill_
dirs` returned `staged.staged_dir / name` and `_resolve_skill_source`'s ref
branch returned `worktree / relative`, both unresolved. In `_refresh_agent_
skills` the union/dedup, `_nasde_derived_skills` stale-drop, handwritten
preservation and basename-collision warning all key on string equality, so the
same logical dir in two string forms (a symlinked /tmp on macOS, or a ref
worktree) would fail to dedup, break stale-drop across runs, and fire a spurious
collision warning. Both collect_* now `.resolve()` their dirs to match the
snapshot path. Adds canonical-path regression tests.

Also corrected a misleading comment claiming create_ref_worktree caches/dedups
ref worktrees — it does not; the lifecycle is safe because all worktrees live in
_active_worktrees until cleanup_worktrees() in the finally after the job.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the hand-rolled _dedup_preserving_order helper with the stdlib idiom
list(dict.fromkeys(...)) — same behavior, one line, more discoverable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The [[skill]] path is verified e2e on a live agent, but there is no example
with a [nasde.plugin], so the plugin path had only unit coverage on a synthetic
StagedPlugin. Add two integration tests that drive the REAL ensure_task_plugin
staging through _prepare_task_environments → collect_plugin_skill_dirs →
harbor_config: a plugin's own skills/ lands in native config.agent.skills
(resolved) for a Codex variant, and in /app/.claude/skills sandbox_files for a
Claude variant. Closes the 'does collect_plugin_skill_dirs read the right staged
location' gap without Docker.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t coverage)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
assert s.endswith('/plug-skill') hardcoded a forward slash and failed on
Windows where the resolved host path uses backslashes. Use Path(s).name ==
'plug-skill' instead. (The /app/.claude/... sandbox assertion is a container
posix path and stays as-is.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@szjanikowski szjanikowski merged commit cf306c9 into main Jun 10, 2026
9 checks passed
@szjanikowski szjanikowski deleted the fix/referenced-plugin-skills-native branch June 10, 2026 13:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant