From a01d09b3cfa7a003ac0288432a9caa39e6bebc7c Mon Sep 17 00:00:00 2001 From: AIOSAI Date: Sun, 17 May 2026 00:04:56 -0700 Subject: [PATCH] fix: settings merge on update + gitignore cleanup + dead cli __main__.py + setup.sh rollover hooks --- .gitignore | 100 +++++++----------- setup.sh | 2 + .../aipass/apps/handlers/init/bootstrap.py | 85 +++++++++++++-- src/aipass/cli/__main__.py | 18 ---- src/aipass/cli/tests/test_integration.py | 25 +---- 5 files changed, 122 insertions(+), 108 deletions(-) delete mode 100644 src/aipass/cli/__main__.py diff --git a/.gitignore b/.gitignore index 56fc0ca9..2420662b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,17 +1,15 @@ # Virtual environment .venv/ -# Herald (session history — parked) -HERALD.md - # Python __pycache__/ *.pyc *.egg-info/ dist/ -build/- +build/ .pytest_cache/ .ruff_cache/ +.coverage # ChromaDB .chroma/ @@ -20,13 +18,10 @@ build/- .env .devpulse_secret.md -# API keys never leave ~/.secrets/ — gitleaks + pre-commit enforce this +# OS +.DS_Store +.vscode -# Plans (managed by flow, local to each installation) -FPLAN-*.md -DPLAN-*.md -RPLAN-*.md -TDPLAN-*.md # AIPass runtime state (local to each installation) AIPASS_REGISTRY.json .trinity/ @@ -35,11 +30,19 @@ AIPASS_REGISTRY.json ai_mail.local/ .feedback.local/ DASHBOARD.local.json -dev.local.md STATUS.local.md +STATUS.md +dev.local.md CLOSED_PLANS.local.json .ai_central/ system_logs/ +notepad.md + +# Plans (managed by flow, local to each installation) +FPLAN-*.md +DPLAN-*.md +RPLAN-*.md +TDPLAN-*.md # Branch local directories logs/ @@ -49,6 +52,7 @@ tools/ docs.local/ .archive/ .backup/ +.backup_system/ .recovery/ .seed/ .spawn/ @@ -56,17 +60,31 @@ docs.local/ # Module runtime JSON (config/data/log per command) **/*_json/ +# Branch-specific runtime files +src/aipass/memory/config/fragmented_memory_config.json +src/aipass/memory/config/fragmented_memory_state.json +src/aipass/memory/config/memory_bank.config.json +src/aipass/memory/config/.plans_processed.json +src/aipass/trigger/trigger_data.json +src/aipass/trigger/trigger_data.lock +src/aipass/drone/drone_command_registry.json +src/aipass/flow/CLOSED_PLANS.local.json +src/aipass/seedgo/apps/standards/aipass/pack.json + # Claude Code local state .claude/hooks/__pycache__/ .claude/hooks/.last_diagnostics_file +.claude/hooks/probes/last_ping.jsonl .claude/worktrees/ -# @aipass citizen — now tracked. Launch (pyproject flip) still pending. -# **/.claude/settings.local.json — UNIGNORED: deny rules are system config that must travel with PRs - # Disabled files (AIPass convention: rename with (disabled) instead of delete) *(disabled) +# Private integrations — driver layer (@api) and wrapper layer (all branches) +# Per DPLAN-0133. Contents gitignored; only scaffold README.md tracked. +src/aipass/*/apps/integrations/** +!src/aipass/*/apps/integrations/README.md + # Spawn template exceptions (template files must be tracked for public repo) !src/aipass/spawn/templates/builder/.trinity/ !src/aipass/spawn/templates/builder/.trinity/** @@ -93,54 +111,14 @@ docs.local/ !src/aipass/spawn/templates/builder/DASHBOARD.local.json !src/aipass/spawn/templates/builder/STATUS.local.md -# OS -.DS_Store - -# Test artifacts -test/ -src/aipass/seedgo/apps/standards/aipass/pack.json +# Parked / one-off +HERALD.md backup_data/ - - -backups -backup_system -src/aipass/flow/CLOSED_PLANS.local.json -src/aipass/memory/config/fragmented_memory_config.json -src/aipass/memory/config/fragmented_memory_state.json - - -.vscode -src/aipass/memory/config/memory_bank.config.json -src/aipass/spawn/templates/builder/.spawn/.template_registry.json -branch_audits _only -notepad.md -src/aipass/trigger/trigger_data.json -src/aipass/memory/config/.plans_processed.json -src/aipass/drone/drone_command_registry.json -src/aipass/spawn/templates/builder/.spawn/.template_registry.json -src/aipass/memory/config/.plans_processed.json -src/aipass/drone/apps/plugins/devpulse_ops/__pycache__/pr_plugin.cpython-312.pyc - -whiteboard.md -README_ORIGINAL_DISABLED.md +backups/ +backup_system/ readme_history/ -src/aipass/trigger/trigger_data.lock - -# STATUS files — auto-generated, contain developer session data. -# Gitignored until prax sync is fixed to produce clean public output (#298). -STATUS.md -STATUS.local.md - -# Private integrations — driver layer (@api) and wrapper layer (all branches) -# Per DPLAN-0133. Contents are gitignored; only the scaffold README.md is tracked. -# Drop project-specific code into src/aipass/{branch}/apps/integrations/{project}/ -# It stays local. Never appears in git. -src/aipass/*/apps/integrations/** -!src/aipass/*/apps/integrations/README.md -.coverage +branch_audits/ claude_4_7_transition_notes.md -.claude/hooks/probes/last_ping.jsonl +README_ORIGINAL_DISABLED.md *.bak -src/aipass/drone/apps/plugins/devpulse_ops/__pycache__/auth.cpython-312.pyc -src/aipass/drone/apps/plugins/devpulse_ops/__pycache__/__init__.cpython-312.pyc -.backup_system \ No newline at end of file +test/ diff --git a/setup.sh b/setup.sh index 03639418..7d9e19a3 100755 --- a/setup.sh +++ b/setup.sh @@ -568,6 +568,8 @@ settings["hooks"] = { "PreCompact": [ {"matcher": "manual", "hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/pre_compact.py", "timeout": 60}]}, {"matcher": "auto", "hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/pre_compact.py", "timeout": 60}]}, + {"matcher": "manual", "hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/pre_compact_rollover.py", "timeout": 120}]}, + {"matcher": "auto", "hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/pre_compact_rollover.py", "timeout": 120}]}, ], } diff --git a/src/aipass/aipass/apps/handlers/init/bootstrap.py b/src/aipass/aipass/apps/handlers/init/bootstrap.py index 8ac74247..be128ded 100644 --- a/src/aipass/aipass/apps/handlers/init/bootstrap.py +++ b/src/aipass/aipass/apps/handlers/init/bootstrap.py @@ -143,6 +143,75 @@ def _resolve_global_prompt(name: str, aipass_home: str | None, dest: Path) -> st return sc.with_source(sc.global_prompt_md(name), dest) +def _hook_fingerprint(hook_entry: dict) -> str: + """Extract a comparable fingerprint from a hook entry.""" + commands = [] + for h in hook_entry.get("hooks", []): + cmd = h.get("command", "") + commands.append(cmd.strip()) + return "|".join(sorted(commands)) + + +def _merge_settings(existing: dict, generated: dict) -> dict: + """Merge AIPass-generated settings with existing user settings. + + Strategy: for each event type, build a set of fingerprints from the + generated (AIPass) hooks. Keep any existing hook whose fingerprint + does NOT appear in the generated set — those are user-added. + Replace all AIPass hooks with the latest generated versions. + """ + merged = {} + + existing_hooks = existing.get("hooks", {}) + generated_hooks = generated.get("hooks", {}) + + merged_hooks: dict[str, list] = {} + all_events = set(existing_hooks.keys()) | set(generated_hooks.keys()) + + for event in all_events: + existing_entries = existing_hooks.get(event, []) + generated_entries = generated_hooks.get(event, []) + + # Fingerprint all generated hooks for this event + generated_fps = {_hook_fingerprint(e) for e in generated_entries} + + # Keep existing hooks that DON'T match any generated hook + user_entries = [e for e in existing_entries if _hook_fingerprint(e) not in generated_fps] + merged_hooks[event] = generated_entries + user_entries + + merged["hooks"] = merged_hooks + + # Merge env: generated wins for AIPASS_HOME, preserve user additions + existing_env = existing.get("env", {}) + generated_env = generated.get("env", {}) + merged["env"] = {**existing_env, **generated_env} + + # Merge permissions: union deny/ask lists + existing_perms = existing.get("permissions", {}) + generated_perms = generated.get("permissions", {}) + merged_perms: dict[str, list] = {} + for key in ("deny", "ask", "allow"): + existing_rules = existing_perms.get(key, []) + generated_rules = generated_perms.get(key, []) + seen: set[str] = set() + combined: list[str] = [] + for rule in generated_rules + existing_rules: + if rule not in seen: + seen.add(rule) + combined.append(rule) + if combined: + merged_perms[key] = combined + if merged_perms: + merged["permissions"] = merged_perms + + # Preserve any other top-level keys from existing settings + for key in existing: + if key not in merged: + merged[key] = existing[key] + + return merged + + def _claude_settings(aipass_home: str | None = None) -> str: """Generate .claude/settings.json — hooks for prompt injection at project level. @@ -482,7 +551,7 @@ def update_project(target: Path) -> dict: else: already_current.append(str(global_prompt_path)) - # settings.json — smart merge: preserve existing AIPASS_HOME, detect if missing + # settings.json — smart merge: preserve user hooks + env, update AIPass hooks settings_path = claude_dir / "settings.json" if not settings_path.exists(): aipass_home = _detect_aipass_home() @@ -491,15 +560,17 @@ def update_project(target: Path) -> dict: else: existing_content = settings_path.read_text(encoding="utf-8") try: - existing_env = json.loads(existing_content).get("env", {}) + existing = json.loads(existing_content) except json.JSONDecodeError as exc: logger.info("settings.json parse failed, rebuilding: %s", exc) - existing_env = {} - # Preserve existing AIPASS_HOME; detect and add if missing + existing = {} + existing_env = existing.get("env", {}) aipass_home = existing_env.get("AIPASS_HOME") or _detect_aipass_home() - generated = _claude_settings(aipass_home) - if existing_content != generated: - settings_path.write_text(generated, encoding="utf-8") + generated = json.loads(_claude_settings(aipass_home)) + merged = _merge_settings(existing, generated) + merged_content = json.dumps(merged, indent=2, ensure_ascii=False) + "\n" + if existing != merged: + settings_path.write_text(merged_content, encoding="utf-8") updated.append(str(settings_path)) else: already_current.append(str(settings_path)) diff --git a/src/aipass/cli/__main__.py b/src/aipass/cli/__main__.py deleted file mode 100644 index 90a871cf..00000000 --- a/src/aipass/cli/__main__.py +++ /dev/null @@ -1,18 +0,0 @@ -# =================== AIPass ==================== -# Name: __main__.py -# Description: Entry point for python -m aipass.cli -# Version: 1.0.0 -# Created: 2026-03-29 -# Modified: 2026-03-29 -# ============================================= - -"""Allow running CLI as a module: python -m aipass.cli.""" - -import sys - -from aipass.cli.apps.cli import main - -try: - sys.exit(main()) -except KeyboardInterrupt: - sys.exit(0) diff --git a/src/aipass/cli/tests/test_integration.py b/src/aipass/cli/tests/test_integration.py index c34f99e3..1e84d10f 100644 --- a/src/aipass/cli/tests/test_integration.py +++ b/src/aipass/cli/tests/test_integration.py @@ -8,10 +8,7 @@ """Integration tests for CLI main() entry point.""" -import subprocess -import sys from io import StringIO -from pathlib import Path from unittest.mock import patch import pytest @@ -120,25 +117,9 @@ class TestMainFlow: result = main() assert result == 0 - -# ============================================================================= -# __main__.py test — verify module is runnable -# ============================================================================= - - -class TestModuleRunnable: - """Verify python -m aipass.cli works as a subprocess.""" - - def test_module_runnable(self): - """python -m aipass.cli --version runs successfully.""" - result = subprocess.run( - [sys.executable, "-m", "aipass.cli", "--version"], - capture_output=True, - text=True, - timeout=30, - cwd=str(Path(__file__).resolve().parents[3]), - ) - assert result.returncode == 0 + # ============================================================================= + # cli entry point test + # ============================================================================= def test_cli_entry_callable(self): """cli_entry() is the console_scripts entry point — verify it's callable."""