Skip to content

apply can delete a shared destination that another enabled agent still renders (order-dependent) #246

Description

@spxrogers

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions