fix(spawn): dry-run-default + path-based update engine — kill #636 branch-scrambler (TDPLAN-0006 P0+P1)

#636: drone @spawn update would scramble every branch's identity/memory in
one command. On a branch created seconds earlier, --dry-run proposed 30 renames
rotating identity dirs (apps->.trinity->.seedgo->.claude->.archive->.aipass),
README->DASHBOARD, and deep-merged stale template into live .trinity/. Root
cause: the CREATE path regenerated template-registry IDs in filesystem-walk
order (!= the master's hand-crafted IDs), so content-hash + rename-detection
saw a mismatch on a pristine branch. update --all would have destroyed all 13
citizens at once.

P0 — safety by default:
- update + repair are now dry-run by default; --apply required to write.
  Forgotten flag = safe preview-only no-op. --dry-run kept as alias.
- doctor_fix.py repair suggestions emit the matching --apply form
  (+ aipass test_doctor_fix updated to the new contract).

P1 — engine rebuild (update_ops.py v2.0):
- Path-based named-managed-files model replaces whole-tree hash-diff +
  rename-detection. ID divergence is moot — IDs are no longer used.
- .trinity/*, DASHBOARD.local.json, artifacts/birth_certificate.json,
  .seedgo/bypass.json = delivered on CREATE only, NEVER touched on update.
- Old ID engine (change_detection.py, reconcile.py) + orphaned tests deleted.

Verified on fresh sandbox: update --dry-run = 0 renames / 0 updates / 0
additions (create==update invariant); no-flag run = dry-run preview, filesystem
byte-identical; 313 spawn tests green; seedgo 100% (all 36 standards).

Closes #636. P2/P3/P4 (shared lib, seam, .recovery relocate) to follow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
AIOSAI
2026-06-06 22:54:09 -07:00
co-authored by Claude Opus 4.8
parent 1ef1e2e89c
commit ccc8d6a97b
16 changed files with 334 additions and 1189 deletions
+4 -2
View File
@@ -71,7 +71,8 @@ def print_introspection():
console.print(" - metadata.py (get_branch_name, normalize_branch_name, detect_profile — branch identity)")
console.print(" - placeholders.py (build_replacements_dict, validate_no_placeholders — template substitution)")
console.print(
" - file_ops.py (copy_template, rename_placeholder_paths, regenerate_template_registry, ensure_directory — filesystem ops)"
" - file_ops.py (copy_template, rename_placeholder_paths,"
" regenerate_template_registry, ensure_directory — filesystem ops)"
)
console.print(
" - meta_ops.py (load_template_registry, generate_branch_meta, save_branch_meta — branch metadata)"
@@ -80,7 +81,8 @@ def print_introspection():
" - registry.py (find_registry, add_to_registry, get_next_citizen_number — AIPASS_REGISTRY management)"
)
console.print(
" - class_registry.py (validate_class, get_default_class, get_available_classes, get_template_dir — citizen class lookup)"
" - class_registry.py (validate_class, get_default_class,"
" get_available_classes, get_template_dir — citizen class lookup)"
)
console.print()
+7 -5
View File
@@ -83,7 +83,8 @@ def handle_repair(args: list[str]) -> int:
_print_help()
return 1
dry_run = "--dry-run" in args
apply = "--apply" in args
dry_run = not apply
if "--relocate" in args:
return _handle_relocate(args, dry_run)
@@ -101,7 +102,7 @@ def handle_repair(args: list[str]) -> int:
def _handle_relocate(args, dry_run):
"""Handle --relocate @branch new/path [--relocate-artifacts]."""
flags = {"--dry-run", "--relocate", "--relocate-artifacts"}
flags = {"--dry-run", "--apply", "--relocate", "--relocate-artifacts"}
positional = [a for a in args if a not in flags]
relocate_artifacts = "--relocate-artifacts" in args
@@ -137,7 +138,7 @@ def _handle_relocate(args, dry_run):
def _handle_clean_pollution(args, dry_run):
"""Handle --clean-pollution <project_path>."""
flags = {"--dry-run", "--clean-pollution"}
flags = {"--dry-run", "--apply", "--clean-pollution"}
positional = [a for a in args if a not in flags]
if not positional:
@@ -159,7 +160,7 @@ def _handle_clean_pollution(args, dry_run):
def _handle_scan(args, dry_run):
"""Handle default scan mode — report structural issues."""
flags = {"--dry-run"}
flags = {"--dry-run", "--apply"}
positional = [a for a in args if a not in flags]
if not positional:
@@ -189,7 +190,8 @@ def _print_help():
warning("Usage: drone @spawn repair <project_path> [options]")
console.print()
console.print(" [green]<project_path>[/green] Path to project root")
console.print(" [green]--dry-run[/green] Preview changes without modifying")
console.print(" [green]--apply[/green] Execute changes (default is preview-only)")
console.print(" [green]--dry-run[/green] Preview changes without modifying [dim](default)[/dim]")
console.print(" [green]--relocate[/green] @branch path Move a branch to a new location")
console.print(
" [green]--relocate-artifacts[/green] Move .chroma/ into branch (with --relocate, single-branch only)"
+8 -6
View File
@@ -89,31 +89,33 @@ def handle_update(args: list[str]) -> int:
"""
# Intercept --help before processing (argparse has add_help=False)
if "--help" in args or "-h" in args:
warning("Usage: drone @spawn update <@branch|class --all> [--dry-run] [--trace]")
warning("Usage: drone @spawn update <@branch|class --all> [--apply] [--dry-run] [--trace]")
console.print()
console.print(" [green]@branch[/green] Update a single branch (uses its own class)")
console.print(" [green]builder --all[/green] Update all builder-class branches")
console.print(" [green]birthright --all[/green] Update all birthright-class branches")
console.print(" [green]--dry-run[/green] Preview changes without modifying files")
console.print(" [green]--apply[/green] Execute changes (default is preview-only)")
console.print(" [green]--dry-run[/green] Preview changes without modifying files [dim](default)[/dim]")
console.print(" [green]--trace[/green] Enable verbose logging")
return 0
if not args:
warning("Usage: drone @spawn update <@branch|class --all> [--dry-run] [--trace]")
warning("Usage: drone @spawn update <@branch|class --all> [--apply] [--dry-run] [--trace]")
console.print()
console.print(" [green]@branch[/green] Update a single branch (uses its own class)")
console.print(" [green]builder --all[/green] Update all builder-class branches")
console.print(" [green]birthright --all[/green] Update all birthright-class branches")
console.print(" [green]--dry-run[/green] Preview changes without modifying files")
console.print(" [green]--apply[/green] Execute changes (default is preview-only)")
console.print(" [green]--dry-run[/green] Preview changes without modifying files [dim](default)[/dim]")
console.print(" [green]--trace[/green] Enable verbose logging")
return 1
from aipass.spawn.apps.modules.core import validate_class, get_available_classes
dry_run = "--dry-run" in args
apply = "--apply" in args
dry_run = not apply
trace = "--trace" in args
# Filter out flags to find positional args
positional = [a for a in args if not a.startswith("--")]
# Detect citizen class argument