fix: settings merge on update + gitignore cleanup + dead cli __main__.py + setup.sh rollover hooks
This commit is contained in:
+39
-61
@@ -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/
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user