applyPlan computes each agent's orphan deletes from that agent's ops alone and never excludes a path another enabled agent still renders. reconcile performs exactly that exclusion (the rendered set in collectReconcileItems); the apply path has no counterpart. What happens to the file then depends on registry order.
Where. internal/render/pipeline.go calls orphanDeletes(st, userHome, name, scope, project, res.Ops) per agent inside for _, name := range reg.Names(); internal/render/state_apply.go's orphanDeletes builds its rendered set from the passed ops only. The run-scoped deletedOrphans map dedupes a path across agents but says nothing about whether anyone still renders it. reg.Names() is sorted.
Measured on 7f2d4ad. Two agents own ~/.claude/skills/demo/SKILL.md in state (Claude and OpenCode genuinely share ~/.claude/skills/; many breadth-tier agents share .agents/skills). With the dropping agent passed no ops and the keeper passed the write op, each direction synthesizes one delete for a path the other agent still renders.
- Dropper before keeper (e.g.
claude drops): the delete runs, the keeper rewrites the file. It survives, at the cost of a spurious delete plus rewrite on every apply. PruneStaleState then keeps the dropper's state entry because Lstat finds the keeper's rewrite, so the dropper also shows a permanent orphan row in status.
- Dropper after keeper (e.g.
opencode drops): the keeper writes, the dropper deletes, and then RecordOpsState(keeper) fails on os.ReadFile(op.Path) for the now-missing file. apply exits non-zero and state is never saved, so the identical sequence repeats on every run as a hard apply error. The component never lands.
Reachable configurations. Plugin targeting (source.PluginSpec.Agents) narrows which agents receive a plugin's components; native_agents (#223) removes an agent from projection outright. Either makes one agent stop rendering a shared skill a sibling still renders. An adapter that starts emitting an adapter.Skip for a component it used to render produces the same shape.
Expected. apply excludes from an agent's orphan deletes any path another enabled agent still renders in the same run, the rule reconcile already applies, or defers deletes until after every agent's writes.
Noticed while verifying #229 axis 15. Out of scope there: it is on the apply/write path, not the shared drift walk.
applyPlancomputes each agent's orphan deletes from that agent's ops alone and never excludes a path another enabled agent still renders.reconcileperforms exactly that exclusion (therenderedset incollectReconcileItems); the apply path has no counterpart. What happens to the file then depends on registry order.Where.
internal/render/pipeline.gocallsorphanDeletes(st, userHome, name, scope, project, res.Ops)per agent insidefor _, name := range reg.Names();internal/render/state_apply.go'sorphanDeletesbuilds itsrenderedset from the passed ops only. The run-scopeddeletedOrphansmap dedupes a path across agents but says nothing about whether anyone still renders it.reg.Names()is sorted.Measured on
7f2d4ad. Two agents own~/.claude/skills/demo/SKILL.mdin state (Claude and OpenCode genuinely share~/.claude/skills/; many breadth-tier agents share.agents/skills). With the dropping agent passed no ops and the keeper passed the write op, each direction synthesizes one delete for a path the other agent still renders.claudedrops): the delete runs, the keeper rewrites the file. It survives, at the cost of a spurious delete plus rewrite on every apply.PruneStaleStatethen keeps the dropper's state entry becauseLstatfinds the keeper's rewrite, so the dropper also shows a permanentorphanrow instatus.opencodedrops): the keeper writes, the dropper deletes, and thenRecordOpsState(keeper)fails onos.ReadFile(op.Path)for the now-missing file.applyexits non-zero and state is never saved, so the identical sequence repeats on every run as a hard apply error. The component never lands.Reachable configurations. Plugin targeting (
source.PluginSpec.Agents) narrows which agents receive a plugin's components;native_agents(#223) removes an agent from projection outright. Either makes one agent stop rendering a shared skill a sibling still renders. An adapter that starts emitting anadapter.Skipfor a component it used to render produces the same shape.Expected.
applyexcludes from an agent's orphan deletes any path another enabled agent still renders in the same run, the rulereconcilealready applies, or defers deletes until after every agent's writes.Noticed while verifying #229 axis 15. Out of scope there: it is on the apply/write path, not the shared drift walk.