fix: settings merge on update + gitignore cleanup + dead cli __main__.py + setup.sh rollover hooks

This commit is contained in:
AIOSAI
2026-05-17 00:04:56 -07:00
parent fb4a775439
commit a01d09b3cf
5 changed files with 122 additions and 108 deletions
+39 -61
View File
@@ -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
test/
+2
View File
@@ -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}]},
],
}
@@ -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))
-18
View File
@@ -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)
+3 -22
View File
@@ -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."""