diff --git a/setup.sh b/setup.sh index c848de3c..cc606fea 100755 --- a/setup.sh +++ b/setup.sh @@ -544,6 +544,8 @@ settings["hooks"] = { "PreToolUse": [ {"matcher": "Bash|Edit|MultiEdit|Write|Read|Grep|Glob|WebSearch|WebFetch|Task", "hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/tool_use_sound.py"}]}, + {"matcher": "Edit|MultiEdit|Write|NotebookEdit", + "hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/pre_edit_gate.py"}]}, ], "PostToolUse": [ {"matcher": "Edit|MultiEdit|Write|NotebookEdit", @@ -555,6 +557,9 @@ settings["hooks"] = { "Notification": [ {"hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/notification_sound.py"}]}, ], + "SubagentStop": [ + {"hooks": [{"type": "command", "command": f"{hook_python} {hooks_dir}/subagent_stop_gate.py"}]}, + ], "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}]}, diff --git a/src/aipass/cli/apps/handlers/init/bootstrap.py b/src/aipass/cli/apps/handlers/init/bootstrap.py index ab48bc07..67ea0e3e 100644 --- a/src/aipass/cli/apps/handlers/init/bootstrap.py +++ b/src/aipass/cli/apps/handlers/init/bootstrap.py @@ -19,9 +19,8 @@ Business logic for `aipass init`. Creates the project scaffold: 7. STATUS.local.md — project status 8. .gitignore — standard AIPass ignores 9. .claude/settings.json — Claude Code hooks configuration - 10. hooks/ — directory for user hooks - 11. src/ — directory where agents live - 12. .ai_mail.local/inbox.json — empty project mailbox + 10. src/ — directory where agents live + 11. .ai_mail.local/inbox.json — empty project mailbox Projects are NOT citizens — no .trinity/ directory. Identity lives in the registry JSON. Init is re-runnable: existing files are skipped, not errors. @@ -338,13 +337,7 @@ def init_project(target: Path, project_name: str | None = None) -> dict: shipped = _ship_hooks(aipass_home, target) created.extend(shipped) - # 10. hooks/ directory - hooks_dir = target / "hooks" - if not hooks_dir.exists(): - hooks_dir.mkdir() - created.append(str(hooks_dir)) - - # 11. src/ directory (where agents live) + # 10. src/ directory (where agents live) src_dir = target / "src" if not src_dir.exists(): src_dir.mkdir() @@ -373,7 +366,7 @@ def update_project(target: Path) -> dict: Overwrites managed prompt and config files with the latest templates while leaving all user-owned files (registry, README, STATUS.local.md, .gitignore, - hooks/, src/) untouched. + src/) untouched. Args: target: Directory containing the AIPass project to update. diff --git a/src/aipass/cli/apps/modules/init_project.py b/src/aipass/cli/apps/modules/init_project.py index e263e3e1..119b6992 100644 --- a/src/aipass/cli/apps/modules/init_project.py +++ b/src/aipass/cli/apps/modules/init_project.py @@ -122,9 +122,8 @@ def print_help(): [green]7.[/green] [yellow]STATUS.local.md[/yellow] Project status [green]8.[/green] [yellow].gitignore[/yellow] Standard AIPass ignores [green]9.[/green] [yellow].claude/settings.json[/yellow] Claude Code hooks + AIPASS_HOME - [green]10.[/green] [yellow]hooks/[/yellow] User hooks directory - [green]11.[/green] [yellow]src/[/yellow] Agent directories live here - [green]12.[/green] [yellow].ai_mail.local/inbox.json[/yellow] Empty project mailbox""" + [green]10.[/green] [yellow]src/[/yellow] Agent directories live here + [green]11.[/green] [yellow].ai_mail.local/inbox.json[/yellow] Empty project mailbox""" console.print(Panel(files_text, border_style="green", padding=(1, 2), box=box.ROUNDED)) console.print() @@ -354,7 +353,7 @@ def _handle_init_update(args: List[str]) -> bool: console.print(" CLAUDE.md, AGENTS.md, GEMINI.md") console.print() console.print("[bold cyan]What is never touched:[/bold cyan]") - console.print(" *_REGISTRY.json, README.md, STATUS.local.md, .gitignore, hooks/, src/") + console.print(" *_REGISTRY.json, README.md, STATUS.local.md, .gitignore, src/") console.print() console.print("[dim]Commands: init update, init update --help[/dim]") console.print() @@ -482,9 +481,8 @@ def _print_init_help(): [green]7.[/green] [yellow]STATUS.local.md[/yellow] Project status [green]8.[/green] [yellow].gitignore[/yellow] Standard AIPass ignores [green]9.[/green] [yellow].claude/settings.json[/yellow] Claude Code hooks + AIPASS_HOME - [green]10.[/green] [yellow]hooks/[/yellow] User hooks directory - [green]11.[/green] [yellow]src/[/yellow] Agent directories live here - [green]12.[/green] [yellow].ai_mail.local/inbox.json[/yellow] Empty project mailbox""" + [green]10.[/green] [yellow]src/[/yellow] Agent directories live here + [green]11.[/green] [yellow].ai_mail.local/inbox.json[/yellow] Empty project mailbox""" console.print(Panel(files_text, border_style="green", padding=(1, 2), box=box.ROUNDED)) console.print() diff --git a/src/aipass/cli/tests/test_bootstrap.py b/src/aipass/cli/tests/test_bootstrap.py index e71c37d7..817dae85 100644 --- a/src/aipass/cli/tests/test_bootstrap.py +++ b/src/aipass/cli/tests/test_bootstrap.py @@ -99,8 +99,7 @@ def test_init_project_creates_all_expected_files(tmp_path): for f in expected_files: assert f.exists(), f"Expected file not created: {f}" - # hooks/ and src/ are directories, not files - assert (target / "hooks").is_dir(), "Expected hooks/ directory" + # src/ is a directory, not a file assert (target / "src").is_dir(), "Expected src/ directory" # No .trinity/ should be created (projects are not citizens) @@ -109,8 +108,8 @@ def test_init_project_creates_all_expected_files(tmp_path): # No local prompt at project level (belongs in agent dirs only) assert not (target / ".aipass" / "aipass_local_prompt.md").exists() - # 12 files + 2 dirs + 7 shipped hooks (when AIPASS_HOME detected) = 21 - assert len(result["created_files"]) == 21 + # 11 items + 2 commands + 7 shipped hooks (when AIPASS_HOME detected) = 20 + assert len(result["created_files"]) == 20 def test_init_project_return_dict_structure(tmp_path): @@ -348,7 +347,7 @@ def test_init_project_auto_creates_target_dir(tmp_path): assert target.is_dir() assert result["project_name"] == "NESTED" - assert len(result["created_files"]) == 21 + assert len(result["created_files"]) == 20 def test_init_project_defaults_name_from_directory(tmp_path): @@ -395,9 +394,6 @@ def test_init_project_skips_existing_optional_files(tmp_path): claude_dir.mkdir() (claude_dir / "settings.json").write_text("{}\n", encoding="utf-8") - hooks_dir = target / "hooks" - hooks_dir.mkdir() - src_dir = target / "src" src_dir.mkdir()