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:
co-authored by
Claude Opus 4.8
parent
1ef1e2e89c
commit
ccc8d6a97b
@@ -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()
|
||||
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user