fix(installer): .claude/agents is a shared namespace — stop wiping user subagents on init/update#151
Merged
Merged
Conversation
…place it whole
User-reported data loss: every `add` init/update swept ALL non-ADD subagents
from .claude/agents. Root cause: the MANAGED table routed the agents tree
through the whole-dir clean-replace (stage-then-swap), whose contract —
"a file removed upstream leaves no orphan" — treats every undeclared file
as an orphan. Correct for the ADD-owned trees (.add/tooling, .add/docs,
.claude/skills/add, .add/personas-teacher); wrong for .claude/agents, the
one destination other tools and the USER also write.
- NEW shared-namespace lander in BOTH installers (npm bin/cli.js
sharedFileReplace + pip _installer.py _shared_file_replace, byte-mirrored
semantics): per shipped file, temp-sibling + atomic rename (crash never
leaves a torn file); removal is EXPLICIT-tombstone-only
(RETIRED_AGENTS/_RETIRED_AGENTS, empty today) — never a sweep, never a
name heuristic (a user file named add-anything.md survives)
- routed via SHARED={"agents"} in the one reconcile loop each installer has
(init AND update funnel through it); GLOBAL_TREES excludes agents in both
- roll-up shape {restored, refreshed} preserved for the reporting consumer
- test_roster_shipped's superseded pin ("update must clean-replace agents/")
STRENGTHENED to the new truth: undeclared files survive, shipped files
still refresh (the sweep it demanded WAS the data-loss bug)
Tests: NEW test_installer_shared_namespace.py (8: foreign-file survival
through init+update x both installers · prefixed foreign file survives ·
fresh dir + roster lands · tombstone removal · non-shared docs tree still
sweeps orphans · roll-up shape). Full fence 3562/3562 OK. Engine add.py
untouched — no MD5 re-aim.
refs: standalone fast task installer-shared-namespace-guard, gate PASS
author: Tin Dang <tindang.ht97@gmail.com>
…or roster-refresh gap (user agents safe, verified live) author: Tin Dang <tindang.ht97@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
User-reported data loss: every
addinit/update deleted ALL non-ADD subagents from.claude/agents.Root cause: the MANAGED table routed the agents tree through the whole-dir clean-replace, whose "no orphan survives" contract is correct for ADD-owned trees but wrong for
.claude/agents— the one managed destination that is a shared namespace root (the user's own Claude Code subagents live there).Fix (both installers, npm + pip, byte-mirrored semantics): a shared-namespace lander — per shipped file, temp-sibling + atomic rename; removal is explicit-tombstone-only (
RETIRED_AGENTS, empty today), never a sweep or name heuristic. A user file namedadd-anything.mdsurvives. All other managed trees keep whole-dir clean-replace unchanged (pinned).Superseded pin:
test_roster_shipped.test_update_clean_replaces_agentsdemanded the sweep that WAS the bug — strengthened to assert survival + roster refresh.Tests: NEW
test_installer_shared_namespace.py(8, both installers via subprocess). Full fence 3562/3562 OK. Engine untouched. ADD fast-lane taskinstaller-shared-namespace-guard, frozen v1, gate PASS.