Skip to content

fix(plugin): clean candidate tree, skill script paths and k-architect delegation - #57

Merged
knihit merged 27 commits into
mainfrom
fix/plugin-candidate-clean-tree
Oct 22, 2026
Merged

knihit merged 27 commits into
mainfrom
fix/plugin-candidate-clean-tree

Conversation

@knihit

@knihit knihit commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Description

Fixes found while preparing the first Claude Code marketplace release. Each one broke a plugin install or the plugin release itself.

Release: the candidate commit shipped build output

  • scripts/build-plugin-candidate-commit.sh used to commit from the job's own checkout. git rm -rq . also removed the checked-out .gitignore, so untracked build output (cli/konductor-rs/target/, build/, dist/, generated/) was committed. That is the ~1,383 extra files in Publish Claude Code plugin v1.1.0 #56.
  • The script now builds the tree from --assemble-dir alone, through a scratch index (git add -A -f, write-tree, commit-tree). It never reads the checkout.
  • Before the no-op check, it compares the tree with --assemble-dir: path, mode and blob ID (git hash-object --no-filters). An ignore rule or line-ending filter can't drop or change a file silently.
  • update-ref passes an empty old value, so it never overwrites an existing branch.
  • New scripts/check-claude-plugin-tree.sh fails on any top-level entry outside .claude-plugin, .mcp.json, LICENSE.txt, README.md, agents, docs, hooks, skills, including dangling symlinks. docs and hooks are for feat: add a telemetry hook to the Claude Code plugin #60. It runs in the release script and in make claude-plugin-check.
  • make claude-plugin-check now fails when plugin assembly fails. Before, a failed assembly still ended "validated successfully".
  • release.yml is unchanged.

Plugin agents: delegation was blocked

  • Claude Code names plugin agents konductor:<agent>. Agent(...) grants and subagent_type don't resolve bare names, so Agent(k-developer, ...) matched nothing.
  • New scripts/rewrite-claude-plugin-agent-grants.py runs during plugin assembly and qualifies each grant with the plugin name (12 names across konductor, k-architect, k-quality-assurance). It fails if a grant names an agent the plugin doesn't ship. Source specs keep bare names, so CLI installs are unchanged.
  • It also fails, before writing anything, on an agent file with no frontmatter, since that file's grants can't be checked. Both rewrites end the frontmatter only at a column 0 ---, so an indented --- in a block scalar no longer cuts it short.
  • k-architect's grant also carried a stale package prefix. It is now Agent(k-developer, k-quality-assurance) in the source.
  • Both grant rewrites (rewrite-claude-plugin-agent-grants.py, and the existing rewrite-claude-plugin-mcp-tool-names.py) dropped the newline after the closing ---. The blank line after the fence was lost, and a body starting right after the fence would have merged into it. They now write the file back unchanged apart from the grants.
  • delegation-protocol and claude-teams-behavior examples now show konductor:k-* for plugin installs and k-* for .claude/agents/ installs.

Dispatch: mux and cmux refused to dispatch in plugin installs

  • tmux-dispatch.sh, zellij-dispatch.sh and cmux-dispatch.sh only found agents under ~/.claude/agents/. A plugin install has none there.
  • The existing lookup is unchanged for plain names: local-<pkg>-<agent>, then a registry <pkg>-<agent>, then the bare file.
  • If nothing matches and the skill is running inside the plugin, they launch the bare name. claude --agent k-developer resolves to konductor:k-developer by itself.
  • A qualified --agent konductor:<agent> is accepted only when it names this plugin and an agent it ships. Anything else gets the not-installed error.
  • cmux now rejects an agent-file prefix with shell characters before pasting it into the pane command. Before, a file named local-x$(cmd)-k-developer.md ran cmd.

Skills: bundled scripts weren't found, and a repo could plant one

  • mux-dispatch, cmux-dispatch, persistent-memory and legacy-to-agentic-estimate looked for their scripts relative to the working directory or the git root.
  • They now try, in order:
    1. ${CLAUDE_SKILL_DIR}, which Claude Code substitutes with the skill's own directory
    2. ${SKILLS_HOME:-$HOME/.konductor/skills}/<skill>
    3. <git root>/.konductor/skills/<skill>, only if .konductor/manifest exists
    4. <git root>/skills/<skill>, only if the repo's .claude-plugin/marketplace.json names konductor
  • If none matches, the snippet stops with an error. A cloned repo can no longer supply dispatch.sh or the memory validator.
  • cmux-dispatch.sh finds its sibling files from its own location, like dispatch.sh.
  • The orchestrator agents defer to the skills for these paths.

Fixes #56's root causes. #56 was closed without merging.

Type of Change

  • Modify existing skill
  • New or updated agent spec
  • Infrastructure / pipeline

Testing

  • python3 -m pytest tests/ -q: 221 passed
  • tests/skills/mux-dispatch/*.sh and tests/skills/cmux-dispatch/*.sh: all pass. test-plugin-agent-resolution.sh has 24 cases for tmux and zellij and 14 for cmux, covering CLI, local- and registry installs, plugin-only, qualified names, unknown names and the cmux prefix injection.
  • make claude-plugin-check passes. With a bogus grant it exits non-zero.
  • Each new test fails against the old code and passes now. In a real plugin build, every rewritten agent differs from its source only in grant lines. Breaking the local-* lookup on purpose fails 3 dispatch cases.
  • Real release steps in a clone holding 1,415 untracked build files: the candidate commit has 127 files, identical to the assembled tree.
  • Plugin-only install (local directory marketplace, no ~/.claude/agents):
    • konductor:konductor delegates and the subagent replies.
    • tmux dispatch of k-developer works, and konductor:k-missing is refused.
    • persistent-memory and legacy-to-agentic-estimate run their scripts from the substituted plugin path.
  • CLI install (konductor install --harness claude, global and --target): skills resolve under .claude/skills/, bare delegation works, and tmux dispatch finds the ~/.claude/agents file.
  • Kiro-style run (CLAUDE_SKILL_DIR unset) in a repo containing its own skills/mux-dispatch/dispatch.sh:
    • With no install, the snippet stops.
    • With a global install, it uses the global copy.
  • cd cli && make test: no cli/ changes
  • New files carry the SPDX header

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own changes
  • I have commented my code where necessary
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings. shellcheck shows no new findings on the changed scripts.
  • I have added or updated tests that prove my change works

Agents Affected

  • konductor: plugin build qualifies its Agent(...) grants
  • konductor-mux-orchestrator: script-path guidance defers to mux-dispatch
  • konductor-cmux-orchestrator: script-path guidance defers to cmux-dispatch
  • k-architect: grant fixed in the source, and qualified in the plugin build
  • k-quality-assurance: plugin build qualifies its Agent(...) grants

Notes for Reviewers

scripts/build-plugin-candidate-commit.sh used to run git rm -rq . + cp -a
"$ASSEMBLE_DIR"/. . + git add -A inside the same checkout that an earlier
workflow step had already run `make -C cli build` in. git rm -rq . also
deleted the checked-out .gitignore, so git add -A then picked up whatever
untracked build output was left in that checkout (cli/konductor-rs/target/,
build/cli/konductor, dist/, generated/claude-plugin/), with no .gitignore
left to exclude it.

The script now builds the candidate's tree directly from --assemble-dir's
own contents, using a scratch git index that is never associated with the
checkout's real index or working directory. The checkout's working
directory is never read or written, so untracked build output there can
never reach the candidate commit regardless of what caused it. A final
safety check confirms the candidate commit's file listing matches
--assemble-dir's file listing on disk exactly before the branch is
created.

Adds a regression test that reproduces the defect: untracked build junk
in the checkout, with the base branch tracking a .gitignore that would
have excluded it. The test fails against the prior script and passes
against this one.
…in installs

dispatch.sh and cmux-dispatch.sh were documented and invoked by a path
relative to the current working directory (skills/mux-dispatch/dispatch.sh,
skills/cmux-dispatch/cmux-dispatch.sh). That path only resolves from a
source checkout of this package; a plugin or Kiro CLI install puts the
user's own project at the cwd, with no skills/ directory under it, so
delegation could not run at all.

Both SKILL.md files now resolve the skill's own directory first via
Claude Code's ${CLAUDE_SKILL_DIR} substitution, falling back to a source
checkout, a global Kiro CLI install, or a project-level Kiro CLI install.
The konductor-mux-orchestrator and konductor-cmux-orchestrator agent
prompts point at the same skill-driven resolution instead of duplicating
their own path logic (which also named an internal package directory as
an example).

cmux-dispatch.sh now resolves its own real directory via BASH_SOURCE[0]
(symlink-safe, matching dispatch.sh's existing approach) and warns if its
sibling cmux-completion-hook.sh is missing from a broken install, instead
of silently depending on an unverified relative path.
persistent-memory's memory-validator.sh and legacy-to-agentic-estimate's
convert_estimates.py were located via a source-checkout path
(git rev-parse --show-toplevel) or a global Kiro CLI install
($HOME/.konductor/skills), with no path that resolves in a Claude Code
plugin install.

Both now try ${CLAUDE_SKILL_DIR} first (Claude Code substitutes this to
the skill's own directory for both a plugin and a standalone skill
install), then fall back to the source-checkout and global-install paths,
and now also to a project-level Kiro CLI install (.konductor/skills/ at
the project root).

legacy-to-agentic-estimate also fails closed with a clear error instead
of running python3 against an empty path when none of the known
locations resolves, matching persistent-memory's existing behavior.
k-architect's Claude Code tool list granted Agent(...) access using a
stale package-prefixed sub-agent name. That name cannot resolve to the
shipped k-developer/k-quality-assurance agents in a plugin install, so
the delegation silently failed.

Changed to Agent(k-developer, k-quality-assurance), the unprefixed form
every other agent spec already uses.
Adds:
- test_agent_tool_subagent_references.py: every Agent(...) tool grant
  across agents/*.agent-spec.json must name an agent actually shipped in
  this package, and must never carry an internal package-install prefix.
- test_skill_script_path_resolution.py: every SKILL.md that resolves a
  bundled script's path must try ${CLAUDE_SKILL_DIR} before falling back
  to a source checkout or a global/project .konductor install.
- tests/skills/mux-dispatch/test-dispatch-sh-cwd-independence.sh:
  dispatch.sh reaches the correct sibling backend script when invoked by
  absolute path (direct or through a symlink) from an unrelated cwd.
- tests/skills/cmux-dispatch/test-self-locating.sh: cmux-dispatch.sh
  resolves its own real directory before anything else, so it runs
  correctly from any caller cwd or through a symlink, and warns instead
  of failing silently when its completion-hook sibling is missing.
@knihit knihit changed the title fix(plugin): build the candidate commit from the assembled tree only fix(plugin): clean candidate tree, skill script paths and k-architect delegation Oct 16, 2026
@knihit
knihit marked this pull request as ready for review October 16, 2026 16:57
knihit added 14 commits October 16, 2026 17:01
The candidate commit and make claude-plugin-check now reject any top-level
entry outside the files the marketplace plugin ships, so a later change to
the assembly script can't publish build output such as cli/ or dist/.
Claude Code names plugin agents <plugin>:<agent>, so the bare
Agent(k-developer, ...) grants in a plugin-only install matched no agent
and delegation failed with "Agent type 'k-developer' not found".

Assembly now rewrites every Agent() grant name to <plugin>:<agent>, using
the name and agent list from the rendered plugin.json, and fails if a
grant names an agent the plugin does not ship. Source specs keep bare
names, which is what .claude/agents/ installs need.
The dispatch scripts only looked for agents under ~/.claude/agents and
rejected ':' in --agent, so a plugin-only install could not dispatch.

When ~/.claude/agents has no match and the skill sits inside a Claude
Code plugin (<root>/.claude-plugin/plugin.json two levels up), launch
claude --agent <plugin>:<agent> if the plugin ships that agent. An
already-qualified <plugin>:<agent> is launched as-is. The existing
lookup, ambiguity error and not-installed error are unchanged.
Describe the <plugin>:<agent> fallback in the mux-dispatch and
cmux-dispatch skills, replace stale agent-name examples in
delegation-protocol and claude-teams-behavior with the names a plugin or
.claude/agents/ install actually uses, and add changelog entries.
…lback order

Claude Code substitutes only the exact text ${CLAUDE_SKILL_DIR}, so the
${CLAUDE_SKILL_DIR:-} form always expanded to nothing. The snippets now use
the exact form, then fall back to the global install, a project install with
konductor's .konductor/manifest, and a konductor source checkout. Any other
repo's skills/ directory is never used, and every snippet fails closed.

The test now runs each snippet against plugin, Kiro global, project,
source-checkout and hostile-repo layouts.
… injection

A qualified --agent <plugin>:<agent> now launches only when <plugin> is the
plugin the script runs from and that plugin ships the agent. A plugin-only
install launches the bare name, which Claude resolves itself.

cmux-dispatch pasted a prefix taken from a ~/.claude/agents filename into
the pane command, so a crafted filename could run a command. The prefix
must now match [A-Za-z0-9._-]*.

The resolution tests are executable and listed in tests/skills/README.md.
…e no-op path

The scratch index now uses git add -A -f, so an ignore rule inside the
assembled tree cannot drop a file and turn a real change into a silent
no-op. The tree is compared with the assembled directory before the no-op
check, by path, mode and blob ID from git hash-object --no-filters, so
content rewritten by git attributes fails the build. Symlinks and the
executable bit are compared too. The branch ref is created with an empty
old value so it cannot overwrite a branch created meanwhile.
check-claude-plugin-tree.sh skipped any entry that failed [ -e ], which
includes a symlink to a missing target, so a dangling cli or dist link
passed the allowlist check.
The assembly step's exit status was ignored, so a rejected Agent() grant
still ended with "validated successfully". A failed assembly now fails
the target and skips the tree check and validation.
…ing grants

Both grant rewrites split an agent file on newlines and wrote the two halves
back with no newline between them. The blank line after the closing ---
was lost, and an agent whose body started right after the fence would have
had its fence merged into the body.

@ihmaws ihmaws left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recommendation for future changes, this would have been easier to review as four PRs:

  1. the candidate-tree fix and tree check
  2. the Agent() grant rewrite and the k-architect grant
  3. dispatch plugin resolution and the cmux prefix fix, and
  4. skill script-path resolution. They don't depend on each other, and (1) is the release-blocking one.

Comment thread skills/mux-dispatch/SKILL.md Outdated
Comment thread skills/mux-dispatch/SKILL.md
Comment thread skills/mux-dispatch/SKILL.md Outdated
Each Bash call starts a new shell, so MUX_DISPATCH_DIR and
CMUX_DISPATCH_DIR were empty by the time a later dispatch command ran,
which then executed /dispatch.sh. The resolver now prints the path, and
the skills and orchestrator specs use it through a placeholder. Also trims
the section to the resolver and one line on using its output.
…tter

The agent grant rewrite skipped any agents/*.md without a frontmatter
block, which also skipped the check that every Agent(...) grant names an
agent the plugin ships. It now fails before writing anything. Both
rewrite scripts also end frontmatter only at a column 0 fence, so an
indented --- inside a block scalar no longer cuts the block short.
The old wording said a skills/ directory in any other repo is never
used. A repo with .konductor/manifest, or a marketplace.json naming
konductor, is searched, so state those markers instead.
@knihit
knihit requested a review from ihmaws October 21, 2026 15:07
ihmaws
ihmaws previously approved these changes Oct 22, 2026
simonkrol
simonkrol previously approved these changes Oct 22, 2026
@knihit
knihit dismissed stale reviews from simonkrol and ihmaws via 257d3f2 October 22, 2026 18:44
@knihit
knihit merged commit 41b044b into main Oct 22, 2026
5 checks passed
@knihit
knihit deleted the fix/plugin-candidate-clean-tree branch October 22, 2026 18:49
knihit added a commit that referenced this pull request Oct 22, 2026
Resolve conflicts with #57: keep both assembly steps, the Agent() grant
rewrite before the telemetry hook, and keep both [Unreleased] sections.
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.

3 participants