#686/#661 night shift wave 4 completion (aipass): -> 100% seedgo. aipass.py main() intercepts <cmd> --help before dispatch (Subcommand_Help ->100). Output_Routing ->100: 31 status prints across doctor/init_flow/handoff/profile routed via cli success()/error()/warning(). Modules ->100: auto_wire_provider extracted to NEW handler provider_wire.py, prompt_auto_wire made private (doctor_wire.py). Tests updated (test_doctor patch targets, test_init_flow success routing). Full suite re-run by devpulse: 657 pass / 0 fail. Audit 100% incl Modules.

This commit is contained in:
AIOSAI
2026-07-11 03:04:28 -07:00
parent 2defe9d615
commit 497ab98abc
10 changed files with 229 additions and 201 deletions
+2 -2
View File
@@ -21,8 +21,8 @@ PyPI version — not the changelog header.
`@cli` `success()/error()/warning()` helpers instead of raw `console.print` `@cli` `success()/error()/warning()` helpers instead of raw `console.print`
markup. Owners self-audited and self-fixed their own branches; devpulse verified markup. Owners self-audited and self-fixed their own branches; devpulse verified
each diff + re-ran each audit and committed per wave. Landed so far: spawn, each diff + re-ran each audit and committed per wave. Landed so far: spawn,
drone, flow, daemon, prax, ai_mail, backup, seedgo, memory, trigger, api, cli. drone, flow, daemon, prax, ai_mail, backup, seedgo, memory, trigger, api, cli,
Remaining waves following. aipass. Remaining: commons.
### Fixed ### Fixed
+8
View File
@@ -121,6 +121,14 @@ def main():
command = args[0] command = args[0]
remaining = args[1:] if len(args) > 1 else [] remaining = args[1:] if len(args) > 1 else []
# Subcommand --help guard: intercept before dispatch
if remaining and remaining[0] in ("--help", "-h"):
for module in modules:
if module.handle_command(command, ["--help"]):
return 0
print(f"Unknown command: {command}")
return 1
try: try:
if route_command(command, remaining, modules): if route_command(command, remaining, modules):
return 0 return 0
@@ -0,0 +1,155 @@
# =================== AIPass ====================
# Name: provider_wire.py
# Description: Auto-wire provider settings from manifest into user config
# Version: 1.0.0
# Created: 2026-07-11
# Modified: 2026-07-11
# =============================================
"""provider_wire — auto-wire provider settings.
Implements the additive merge of manifest into ~/.claude/settings.json.
"""
from __future__ import annotations
import json
import shutil
from datetime import datetime, timezone
from pathlib import Path
from typing import Dict, List
from aipass.aipass.apps.handlers.json import json_handler
# =============================================================================
# HOOK & ENV DESCRIPTIONS
# =============================================================================
HOOK_DESCRIPTIONS: Dict[str, str] = {
"pre_edit_gate.py": "blocks edits outside agent's branch",
"subagent_stop_gate.py": "validates agent output on exit",
"auto_fix_diagnostics.py": "auto-fixes lint issues after edits",
"global_prompt_loader.py": "injects branch context on each turn",
"identity_injector.py": "injects agent identity on each turn",
"email_notification.py": "notifies on incoming agent mail",
"branch_prompt_loader.py": "loads branch-specific prompts",
"pre_compact.py": "saves state before context compaction",
}
ENV_DESCRIPTIONS: Dict[str, str] = {
"AIPASS_HOME": "tells agents where AIPass lives",
"CLAUDE_CODE_DISABLE_AUTO_MEMORY": "prevents conflict with .trinity/ memory system",
}
# =============================================================================
# AUTO-WIRE
# =============================================================================
def auto_wire_provider(manifest_path: Path, interactive: bool = True) -> List[str]:
"""Auto-wire provider settings from manifest into ~/.claude/settings.json.
Additive merge only — never removes or overwrites existing keys/values.
Returns list of action descriptions (for logging/display).
"""
actions: List[str] = []
manifest = json_handler.load_path(manifest_path)
if manifest is None:
return actions
claude_section = manifest.get("cli", {}).get("claude", {})
if not claude_section:
return actions
settings_path = Path.home() / ".claude" / "settings.json"
if settings_path.exists():
settings = json_handler.load_path(settings_path) or {}
else:
settings = {}
if settings_path.exists():
date_stamp = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
backup_path = settings_path.with_suffix(f".json.bak.{date_stamp}")
shutil.copy2(settings_path, backup_path)
actions.append(f"Backed up settings to {backup_path.name}")
manifest_hooks = claude_section.get("hooks", [])
for hook in manifest_hooks:
command = hook.get("command", "")
event = hook.get("event", "")
if not command or not event:
continue
if "hooks" not in settings:
settings["hooks"] = {}
if event not in settings["hooks"]:
settings["hooks"][event] = []
event_hooks = settings["hooks"][event]
if not isinstance(event_hooks, list):
event_hooks = [event_hooks]
settings["hooks"][event] = event_hooks
hook_matcher = hook.get("matcher", "")
already_wired = any(
isinstance(h, dict) and command in json.dumps(h) and h.get("matcher", "") == hook_matcher
for h in event_hooks
)
if not already_wired:
cmd_entry: Dict[str, object] = {
"type": "command",
"command": command,
}
if hook.get("timeout"):
cmd_entry["timeout"] = hook["timeout"]
wrapper: Dict[str, object] = {}
if hook.get("matcher"):
wrapper["matcher"] = hook["matcher"]
wrapper["hooks"] = [cmd_entry]
event_hooks.append(wrapper)
label = command.rsplit(" ", 1)[-1] if " " in command else command
actions.append(f"Wired hook {label} -> {event}")
manifest_env = claude_section.get("env", {})
if manifest_env:
if "env" not in settings:
settings["env"] = {}
repo_root = str(manifest_path.parent.parent)
project_root = str(Path.cwd())
for key, value in manifest_env.items():
if key not in settings["env"]:
resolved = value.replace("{{REPO_ROOT}}", repo_root)
resolved = resolved.replace("{{PROJECT_ROOT}}", project_root)
settings["env"][key] = resolved
actions.append(f"Set env {key}={resolved}")
manifest_perms = claude_section.get("permissions", {})
manifest_deny = manifest_perms.get("deny", [])
manifest_ask = manifest_perms.get("ask", [])
if manifest_deny or manifest_ask:
if "permissions" not in settings:
settings["permissions"] = {}
if "deny" not in settings["permissions"]:
settings["permissions"]["deny"] = []
if "ask" not in settings["permissions"]:
settings["permissions"]["ask"] = []
existing_deny = set(settings["permissions"]["deny"])
for rule in manifest_deny:
if rule not in existing_deny:
settings["permissions"]["deny"].append(rule)
actions.append(f"Added deny rule: {rule}")
existing_ask = set(settings["permissions"]["ask"])
for rule in manifest_ask:
if rule not in existing_ask:
settings["permissions"]["ask"].append(rule)
actions.append(f"Added ask rule: {rule}")
json_handler.save_path(settings_path, settings)
actions.append("Updated ~/.claude/settings.json")
json_handler.log_operation("auto_wire_provider", {"actions": len(actions)})
return actions
+6 -6
View File
@@ -17,7 +17,7 @@ import sys
from pathlib import Path from pathlib import Path
from typing import Dict, List, NamedTuple from typing import Dict, List, NamedTuple
from aipass.cli.apps.modules import console from aipass.cli.apps.modules import console, error as cli_error, success
from aipass.prax import logger from aipass.prax import logger
from aipass.aipass.shared.registry_discovery import find_registry as _discover_registry from aipass.aipass.shared.registry_discovery import find_registry as _discover_registry
@@ -60,8 +60,8 @@ from aipass.aipass.apps.modules.doctor_fix import (
) )
from aipass.aipass.apps.modules.doctor_wire import ( from aipass.aipass.apps.modules.doctor_wire import (
_auto_wire_provider, _auto_wire_provider,
_prompt_auto_wire as prompt_auto_wire,
check_wire_verify, check_wire_verify,
prompt_auto_wire,
reconcile_stale_deny, reconcile_stale_deny,
) )
from aipass.aipass.apps.handlers.system_detect.system_detector import ( from aipass.aipass.apps.handlers.system_detect.system_detector import (
@@ -424,7 +424,7 @@ def _check_provider_manifest(interactive: bool = False, fix: bool = False) -> Li
if fix: if fix:
actions = _auto_wire_provider(manifest_path, interactive=False) actions = _auto_wire_provider(manifest_path, interactive=False)
for action in actions: for action in actions:
console.print(f"[green]✓[/green] {action}") success(action)
wired = bool(actions) wired = bool(actions)
else: else:
wired = prompt_auto_wire(manifest_path, missing_hooks, missing_env, missing_deny, missing_ask) wired = prompt_auto_wire(manifest_path, missing_hooks, missing_env, missing_deny, missing_ask)
@@ -899,11 +899,11 @@ def run_cross_os_record(path: str | None = None, run_e2e: bool = False) -> int:
try: try:
written = generate_run_record(path, run_heavy_e2e=run_e2e) written = generate_run_record(path, run_heavy_e2e=run_e2e)
except RunRecordError as exc: except RunRecordError as exc:
console.print(f"[red]✗[/red] {exc}") cli_error(str(exc))
logger.error("[doctor] cross-os run record failed: %s", exc) logger.error("[doctor] cross-os run record failed: %s", exc)
return 1 return 1
console.print(f"[green]✓[/green] Run Record written: [bold]{written}[/bold]") success(f"Run Record written: {written}")
console.print("[dim]Complete the '— human' rows and run the real Layer-3 acceptance pass before it counts.[/dim]") console.print("[dim]Complete the '— human' rows and run the real Layer-3 acceptance pass before it counts.[/dim]")
console.print() console.print()
logger.info("[doctor] cross-os run record written to %s", written) logger.info("[doctor] cross-os run record written to %s", written)
@@ -1012,7 +1012,7 @@ def print_help() -> None:
"[dim]# write a machine pre-flight Run Record draft (human completes it)[/dim]" "[dim]# write a machine pre-flight Run Record draft (human completes it)[/dim]"
) )
console.print() console.print()
console.print("[yellow]OUTPUT:[/yellow] [green]✓[/green] pass [yellow]![/yellow] warn [red]✗[/red] error") console.print("[yellow]OUTPUT:[/yellow] pass / warn / error (color-coded)")
console.print("[yellow]EXIT:[/yellow] 0 = pass/warn | 1 = errors found") console.print("[yellow]EXIT:[/yellow] 0 = pass/warn | 1 = errors found")
console.print() console.print()
+7 -143
View File
@@ -18,38 +18,20 @@ Provides:
from __future__ import annotations from __future__ import annotations
import json
import shutil
import subprocess import subprocess
import sys import sys
from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from typing import Dict, List, NamedTuple from typing import List, NamedTuple
from aipass.cli.apps.modules import console, success from aipass.cli.apps.modules import console, success
from aipass.prax import logger from aipass.prax import logger
from aipass.aipass.apps.handlers.json import json_handler from aipass.aipass.apps.handlers.json import json_handler
from aipass.aipass.apps.handlers.provider_wire import ( # noqa: F401
# ============================================================================= HOOK_DESCRIPTIONS,
# HOOK & ENV DESCRIPTIONS (for interactive "no" warning) ENV_DESCRIPTIONS,
# ============================================================================= auto_wire_provider as _auto_wire_provider,
)
HOOK_DESCRIPTIONS: Dict[str, str] = {
"pre_edit_gate.py": "blocks edits outside agent's branch",
"subagent_stop_gate.py": "validates agent output on exit",
"auto_fix_diagnostics.py": "auto-fixes lint issues after edits",
"global_prompt_loader.py": "injects branch context on each turn",
"identity_injector.py": "injects agent identity on each turn",
"email_notification.py": "notifies on incoming agent mail",
"branch_prompt_loader.py": "loads branch-specific prompts",
"pre_compact.py": "saves state before context compaction",
}
ENV_DESCRIPTIONS: Dict[str, str] = {
"AIPASS_HOME": "tells agents where AIPass lives",
"CLAUDE_CODE_DISABLE_AUTO_MEMORY": "prevents conflict with .trinity/ memory system",
}
# ============================================================================= # =============================================================================
@@ -59,130 +41,12 @@ ENV_DESCRIPTIONS: Dict[str, str] = {
from aipass.aipass.apps.handlers.provider_reconcile import reconcile_stale_deny # noqa: E402, F401 from aipass.aipass.apps.handlers.provider_reconcile import reconcile_stale_deny # noqa: E402, F401
# =============================================================================
# AUTO-WIRE
# =============================================================================
def _auto_wire_provider(manifest_path: Path, interactive: bool = True) -> List[str]:
"""Auto-wire provider settings from manifest into ~/.claude/settings.json.
Additive merge only — never removes or overwrites existing keys/values.
Returns list of action descriptions (for logging/display).
"""
actions: List[str] = []
manifest = json_handler.load_path(manifest_path)
if manifest is None:
return actions
claude_section = manifest.get("cli", {}).get("claude", {})
if not claude_section:
return actions
# Read existing settings
settings_path = Path.home() / ".claude" / "settings.json"
if settings_path.exists():
settings = json_handler.load_path(settings_path) or {}
else:
settings = {}
# Backup
if settings_path.exists():
date_stamp = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
backup_path = settings_path.with_suffix(f".json.bak.{date_stamp}")
shutil.copy2(settings_path, backup_path)
actions.append(f"Backed up settings to {backup_path.name}")
# Hooks — add bridge entries to provider settings
manifest_hooks = claude_section.get("hooks", [])
for hook in manifest_hooks:
command = hook.get("command", "")
event = hook.get("event", "")
if not command or not event:
continue
if "hooks" not in settings:
settings["hooks"] = {}
if event not in settings["hooks"]:
settings["hooks"][event] = []
event_hooks = settings["hooks"][event]
if not isinstance(event_hooks, list):
event_hooks = [event_hooks]
settings["hooks"][event] = event_hooks
hook_matcher = hook.get("matcher", "")
already_wired = any(
isinstance(h, dict) and command in json.dumps(h) and h.get("matcher", "") == hook_matcher
for h in event_hooks
)
if not already_wired:
cmd_entry: Dict[str, object] = {
"type": "command",
"command": command,
}
if hook.get("timeout"):
cmd_entry["timeout"] = hook["timeout"]
wrapper: Dict[str, object] = {}
if hook.get("matcher"):
wrapper["matcher"] = hook["matcher"]
wrapper["hooks"] = [cmd_entry]
event_hooks.append(wrapper)
label = command.rsplit(" ", 1)[-1] if " " in command else command
actions.append(f"Wired hook {label} -> {event}")
# Env vars
manifest_env = claude_section.get("env", {})
if manifest_env:
if "env" not in settings:
settings["env"] = {}
repo_root = str(manifest_path.parent.parent)
project_root = str(Path.cwd())
for key, value in manifest_env.items():
if key not in settings["env"]:
resolved = value.replace("{{REPO_ROOT}}", repo_root)
resolved = resolved.replace("{{PROJECT_ROOT}}", project_root)
settings["env"][key] = resolved
actions.append(f"Set env {key}={resolved}")
# Permissions
manifest_perms = claude_section.get("permissions", {})
manifest_deny = manifest_perms.get("deny", [])
manifest_ask = manifest_perms.get("ask", [])
if manifest_deny or manifest_ask:
if "permissions" not in settings:
settings["permissions"] = {}
if "deny" not in settings["permissions"]:
settings["permissions"]["deny"] = []
if "ask" not in settings["permissions"]:
settings["permissions"]["ask"] = []
existing_deny = set(settings["permissions"]["deny"])
for rule in manifest_deny:
if rule not in existing_deny:
settings["permissions"]["deny"].append(rule)
actions.append(f"Added deny rule: {rule}")
existing_ask = set(settings["permissions"]["ask"])
for rule in manifest_ask:
if rule not in existing_ask:
settings["permissions"]["ask"].append(rule)
actions.append(f"Added ask rule: {rule}")
# Write settings back
json_handler.save_path(settings_path, settings)
actions.append("Updated ~/.claude/settings.json")
return actions
# ============================================================================= # =============================================================================
# INTERACTIVE WIRE PROMPTS # INTERACTIVE WIRE PROMPTS
# ============================================================================= # =============================================================================
def prompt_auto_wire( def _prompt_auto_wire(
manifest_path: Path, manifest_path: Path,
missing_hooks: List[str], missing_hooks: List[str],
missing_env: List[str], missing_env: List[str],
+2 -2
View File
@@ -21,7 +21,7 @@ Usage:
from __future__ import annotations from __future__ import annotations
from aipass.cli.apps.modules import console, warning from aipass.cli.apps.modules import console, success, warning
from aipass.prax import logger from aipass.prax import logger
from aipass.aipass.apps.handlers.json import json_handler from aipass.aipass.apps.handlers.json import json_handler
@@ -64,7 +64,7 @@ def do_handoff(
if launched: if launched:
console.print() console.print()
console.print(f"[green]✓[/green] Session started via tmux/wt — CLI: [cyan]{cli}[/cyan]") success(f"Session started via tmux/wt — CLI: {cli}")
console.print(f"[dim]Session name: aipass-handoff | cwd: {cwd}[/dim]") console.print(f"[dim]Session name: aipass-handoff | cwd: {cwd}[/dim]")
console.print() console.print()
else: else:
+30 -30
View File
@@ -35,7 +35,7 @@ from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List from typing import Any, Dict, List
from aipass.cli.apps.modules import console, warning from aipass.cli.apps.modules import console, error as cli_error, success, warning
from aipass.prax import logger from aipass.prax import logger
from aipass.aipass.apps.handlers.json import json_handler from aipass.aipass.apps.handlers.json import json_handler
@@ -210,7 +210,7 @@ def _choose(msg: str, choices: List[str], default: str | None = None) -> str:
return choices[idx] return choices[idx]
except ValueError as exc: except ValueError as exc:
logger.info("[init_flow] invalid menu input %r: %s", raw, exc) logger.info("[init_flow] invalid menu input %r: %s", raw, exc)
console.print("[red]Invalid choice.[/red]") cli_error("Invalid choice.")
# --- STAGE FUNCTIONS --- # --- STAGE FUNCTIONS ---
@@ -358,7 +358,7 @@ def stage_3_user_profile(
else: else:
profile_mod.save_profile(existing) profile_mod.save_profile(existing)
console.print(f"[green]✓[/green] Hello, {name}!") success(f"Hello, {name}!")
_save_stage(3, {"name": name}, dry_run=dry_run) _save_stage(3, {"name": name}, dry_run=dry_run)
return {"name": name} return {"name": name}
@@ -379,7 +379,7 @@ def stage_4_style_questions(
else: else:
style = _choose("What are you looking to do?", STYLE_CHOICES, default=STYLE_CHOICES[0]) style = _choose("What are you looking to do?", STYLE_CHOICES, default=STYLE_CHOICES[0])
console.print(f"[green]✓[/green] Got it: {style}") success(f"Got it: {style}")
_save_stage(4, {"style": style}, dry_run=dry_run) _save_stage(4, {"style": style}, dry_run=dry_run)
return {"style": style} return {"style": style}
@@ -424,7 +424,7 @@ def _handle_missing_claude(non_interactive: bool) -> None:
if raw.lower() in ("y", "yes", ""): if raw.lower() in ("y", "yes", ""):
console.print("[cyan]Installing Claude Code[/cyan] [dim](this can take a minute)…[/dim]") console.print("[cyan]Installing Claude Code[/cyan] [dim](this can take a minute)…[/dim]")
if _install_claude_code(): if _install_claude_code():
console.print("[green]✓[/green] Claude Code installed successfully.") success("Claude Code installed successfully.")
else: else:
warning("[bold yellow]Installation failed.[/bold yellow]") warning("[bold yellow]Installation failed.[/bold yellow]")
console.print(" Install manually: https://claude.ai/download") console.print(" Install manually: https://claude.ai/download")
@@ -460,7 +460,7 @@ def stage_5_tool_choice(
default="default", default="default",
) )
console.print(f"[green]✓[/green] {cli_choice} ({flag_variant})") success(f"{cli_choice} ({flag_variant})")
_save_stage(5, {"cli": cli_choice, "flag_variant": flag_variant}, dry_run=dry_run) _save_stage(5, {"cli": cli_choice, "flag_variant": flag_variant}, dry_run=dry_run)
if dry_run: if dry_run:
@@ -496,14 +496,14 @@ def stage_6_first_agent(non_interactive: bool = False, dry_run: bool = False) ->
agent_path = f"src/{agent_name}" agent_path = f"src/{agent_name}"
console.print(f"[cyan]Creating your first agent[/cyan] [dim](drone @spawn create {agent_path})…[/dim]") console.print(f"[cyan]Creating your first agent[/cyan] [dim](drone @spawn create {agent_path})…[/dim]")
success = False spawned = False
if dry_run: if dry_run:
console.print(f"[yellow]\\[dry-run][/yellow] would run: drone @spawn create {agent_path}") console.print(f"[yellow]\\[dry-run][/yellow] would run: drone @spawn create {agent_path}")
success = True spawned = True
else: else:
try: try:
proc = subprocess.run(["drone", "@spawn", "create", agent_path], timeout=60) proc = subprocess.run(["drone", "@spawn", "create", agent_path], timeout=60)
success = proc.returncode == 0 spawned = proc.returncode == 0
except FileNotFoundError as exc: except FileNotFoundError as exc:
logger.warning("[init_flow] drone not found in stage 6: %s", exc) logger.warning("[init_flow] drone not found in stage 6: %s", exc)
warning("drone not found — skipping agent creation.") warning("drone not found — skipping agent creation.")
@@ -511,10 +511,10 @@ def stage_6_first_agent(non_interactive: bool = False, dry_run: bool = False) ->
logger.warning("[init_flow] spawn timed out in stage 6: %s", exc) logger.warning("[init_flow] spawn timed out in stage 6: %s", exc)
warning("spawn timed out — agent may still be created.") warning("spawn timed out — agent may still be created.")
if success: if spawned:
console.print(f"[green]✓[/green] Agent created at {agent_path}") success(f"Agent created at {agent_path}")
_save_stage(6, {"agent_name": agent_name, "agent_path": agent_path, "success": success}, dry_run=dry_run) _save_stage(6, {"agent_name": agent_name, "agent_path": agent_path, "success": spawned}, dry_run=dry_run)
return {"agent_name": agent_name, "agent_path": agent_path} return {"agent_name": agent_name, "agent_path": agent_path}
@@ -576,12 +576,12 @@ def stage_8_smoke_test(non_interactive: bool = False, dry_run: bool = False) ->
aipass_bin = shutil.which("aipass") aipass_bin = shutil.which("aipass")
if drone_bin: if drone_bin:
console.print(f"[green]✓[/green] drone: {drone_bin}") success(f"drone: {drone_bin}")
else: else:
warning("drone not on PATH — clone the repo and run setup.sh") warning("drone not on PATH — clone the repo and run setup.sh")
if aipass_bin: if aipass_bin:
console.print(f"[green]✓[/green] aipass: {aipass_bin}") success(f"aipass: {aipass_bin}")
else: else:
warning("aipass not on PATH — clone the repo and run setup.sh") warning("aipass not on PATH — clone the repo and run setup.sh")
@@ -640,7 +640,7 @@ def stage_9_handoff(
if accumulated: if accumulated:
_write_init_report(accumulated.get("agent_path", agent_path), accumulated, dry_run=dry_run) _write_init_report(accumulated.get("agent_path", agent_path), accumulated, dry_run=dry_run)
console.print() console.print()
console.print("[bold green]✓ Setup complete![/bold green]") success("Setup complete!")
console.print() console.print()
from aipass.aipass.apps.handlers.handoff_platform import launch_inline from aipass.aipass.apps.handlers.handoff_platform import launch_inline
@@ -716,7 +716,7 @@ def stage_10_done(accumulated: Dict[str, Any] | None = None, dry_run: bool = Fal
console.print() console.print()
console.print(render_step_header(10, TOTAL_STAGES, "Done!")) console.print(render_step_header(10, TOTAL_STAGES, "Done!"))
console.print() console.print()
console.print("[bold green]✓ Setup complete![/bold green]") success("Setup complete!")
console.print() console.print()
console.print(" [cyan]aipass help[/cyan] [dim]# Ask any question[/dim]") console.print(" [cyan]aipass help[/cyan] [dim]# Ask any question[/dim]")
console.print(" [cyan]aipass doctor[/cyan] [dim]# Check system health[/dim]") console.print(" [cyan]aipass doctor[/cyan] [dim]# Check system health[/dim]")
@@ -777,7 +777,7 @@ def run_init(
# Pre-flight: refuse to run inside existing projects or agent dirs # Pre-flight: refuse to run inside existing projects or agent dirs
err = _preflight_check() err = _preflight_check()
if err: if err:
console.print(f"[red]✗[/red] {err}") cli_error(str(err))
return 1 return 1
# Template selection — before scaffold # Template selection — before scaffold
@@ -799,7 +799,7 @@ def run_init(
if not dry_run: if not dry_run:
with activity_spinner("Building project scaffold…"): with activity_spinner("Building project scaffold…"):
init_project(cwd) init_project(cwd)
console.print("[green]✓[/green] Project scaffold ready") success("Project scaffold ready")
else: else:
console.print("[yellow]\\[dry-run][/yellow] would create project scaffold") console.print("[yellow]\\[dry-run][/yellow] would create project scaffold")
@@ -807,7 +807,7 @@ def run_init(
last_done = 0 if dry_run else _get_last_completed_stage() last_done = 0 if dry_run else _get_last_completed_stage()
if last_done >= TOTAL_STAGES: if last_done >= TOTAL_STAGES:
console.print("[green]✓[/green] Setup already complete.") success("Setup already complete.")
console.print("[dim]Run 'aipass doctor' to check status.[/dim]") console.print("[dim]Run 'aipass doctor' to check status.[/dim]")
return 0 return 0
@@ -859,7 +859,7 @@ def run_init(
if template != TEMPLATE_AIPASS: if template != TEMPLATE_AIPASS:
console.print() console.print()
console.print("[green]✓[/green] Project initialized.") success("Project initialized.")
console.print("[dim]Run 'aipass init agent <name>' to add an agent.[/dim]") console.print("[dim]Run 'aipass init agent <name>' to add an agent.[/dim]")
return 0 return 0
@@ -877,7 +877,7 @@ def print_introspection() -> None:
if last == 0: if last == 0:
console.print("[dim]Setup not started. Run: aipass init run[/dim]") console.print("[dim]Setup not started. Run: aipass init run[/dim]")
elif last >= TOTAL_STAGES: elif last >= TOTAL_STAGES:
console.print("[green]✓[/green] Setup complete.") success("Setup complete.")
else: else:
console.print(f"[yellow]In progress:[/yellow] stage {last}/{TOTAL_STAGES} completed.") console.print(f"[yellow]In progress:[/yellow] stage {last}/{TOTAL_STAGES} completed.")
console.print(f"[dim]Run 'aipass init run' to resume from stage {last + 1}.[/dim]") console.print(f"[dim]Run 'aipass init run' to resume from stage {last + 1}.[/dim]")
@@ -911,7 +911,7 @@ def _handle_init_scaffold(args: list[str]) -> int:
project_name = args[1] if len(args) > 1 else None project_name = args[1] if len(args) > 1 else None
try: try:
result = init_project(target, project_name) result = init_project(target, project_name)
console.print(f"\n[green]✓[/green] Project initialized at [bold]{target}[/bold]") success(f"Project initialized at {target}")
console.print() console.print()
# Find the package directory to show in guidance # Find the package directory to show in guidance
@@ -938,7 +938,7 @@ def _handle_init_scaffold(args: list[str]) -> int:
return 0 return 0
except Exception as exc: except Exception as exc:
logger.warning("[init_flow] scaffold failed: %s", exc) logger.warning("[init_flow] scaffold failed: %s", exc)
console.print(f"[red]✗[/red] Init failed: {exc}") cli_error(f"Init failed: {exc}")
return 1 return 1
@@ -952,11 +952,11 @@ def _handle_init_update(args: list[str]) -> int:
updated = result.get("updated_files", []) updated = result.get("updated_files", [])
current = result.get("already_current", []) current = result.get("already_current", [])
if updated: if updated:
console.print(f"[green]✓[/green] Updated {len(updated)} file(s):") success(f"Updated {len(updated)} file(s):")
for f in updated: for f in updated:
console.print(f" [green]+[/green] {f}") console.print(f" + {f}")
else: else:
console.print("[green]✓[/green] All files already current.") success("All files already current.")
if current: if current:
console.print(f" ({len(current)} already up to date)") console.print(f" ({len(current)} already up to date)")
# Heal registry: prune stale entries (e.g. cross-project ../paths) # Heal registry: prune stale entries (e.g. cross-project ../paths)
@@ -968,7 +968,7 @@ def _handle_init_update(args: list[str]) -> int:
timeout=30, timeout=30,
) )
if sync_proc.returncode == 0: if sync_proc.returncode == 0:
console.print(" [green]Registry synced.[/green]") success("Registry synced.")
except Exception as sync_exc: except Exception as sync_exc:
logger.warning("[init_flow] registry sync during update skipped: %s", sync_exc) logger.warning("[init_flow] registry sync during update skipped: %s", sync_exc)
@@ -976,14 +976,14 @@ def _handle_init_update(args: list[str]) -> int:
return 0 return 0
except Exception as exc: except Exception as exc:
logger.warning("[init_flow] update failed: %s", exc) logger.warning("[init_flow] update failed: %s", exc)
console.print(f"[red]✗[/red] Update failed: {exc}") cli_error(f"Update failed: {exc}")
return 1 return 1
def _handle_init_agent(args: list[str]) -> int: def _handle_init_agent(args: list[str]) -> int:
"""Handle `aipass init agent <name>` — create a new agent via spawn.""" """Handle `aipass init agent <name>` — create a new agent via spawn."""
if not args: if not args:
console.print("[red]✗[/red] Usage: aipass init agent <name>") cli_error("Usage: aipass init agent <name>")
return 1 return 1
agent_name = args[0] agent_name = args[0]
import subprocess as _sp import subprocess as _sp
@@ -1075,7 +1075,7 @@ def handle_command(command: str, args: list[str]) -> bool:
# Positional args = target path and/or project name for scaffold # Positional args = target path and/or project name for scaffold
err = _preflight_check() err = _preflight_check()
if err: if err:
console.print(f"[red]✗[/red] {err}") cli_error(str(err))
sys.exit(1) sys.exit(1)
sys.exit(_handle_init_scaffold(args)) sys.exit(_handle_init_scaffold(args))
return True return True
+5 -5
View File
@@ -24,7 +24,7 @@ import os
import tempfile import tempfile
from pathlib import Path from pathlib import Path
from aipass.cli.apps.modules import console, error, warning from aipass.cli.apps.modules import console, error, success, warning
from aipass.prax import logger from aipass.prax import logger
from aipass.aipass.apps.handlers.json import json_handler from aipass.aipass.apps.handlers.json import json_handler
@@ -151,13 +151,13 @@ def handle_command(command: str, args: list[str]) -> bool:
return True return True
field, value = args[1], args[2] field, value = args[1], args[2]
if field not in USER_FIELDS: if field not in USER_FIELDS:
console.print(f"[red]Unknown field: {field}[/red]") error(f"Unknown field: {field}")
console.print("[dim]Valid fields: " + ", ".join(USER_FIELDS) + "[/dim]") console.print("[dim]Valid fields: " + ", ".join(USER_FIELDS) + "[/dim]")
return True return True
profile = get_user_profile() profile = get_user_profile()
profile[field] = value profile[field] = value
save_profile(profile) save_profile(profile)
console.print(f"[green]✓[/green] {field} = {value}") success(f"{field} = {value}")
return True return True
if args[0] == "clear": if args[0] == "clear":
@@ -166,7 +166,7 @@ def handle_command(command: str, args: list[str]) -> bool:
skip_confirm = any(a in ("--yes", "-y") for a in args[1:]) skip_confirm = any(a in ("--yes", "-y") for a in args[1:])
if skip_confirm: if skip_confirm:
save_profile({f: None for f in USER_FIELDS}) save_profile({f: None for f in USER_FIELDS})
console.print("[green]✓[/green] Profile cleared.") success("Profile cleared.")
return True return True
warning("Type 'aipass' to confirm clearing your profile (ctrl-C to cancel):") warning("Type 'aipass' to confirm clearing your profile (ctrl-C to cancel):")
try: try:
@@ -177,7 +177,7 @@ def handle_command(command: str, args: list[str]) -> bool:
return True return True
if confirm == "aipass": if confirm == "aipass":
save_profile({f: None for f in USER_FIELDS}) save_profile({f: None for f in USER_FIELDS})
console.print("[green]✓[/green] Profile cleared.") success("Profile cleared.")
else: else:
console.print("[yellow]Cancelled.[/yellow]") console.print("[yellow]Cancelled.[/yellow]")
return True return True
+11 -11
View File
@@ -658,7 +658,7 @@ class TestReconcileStaleDeny:
"""Missing settings.json returns no results.""" """Missing settings.json returns no results."""
from aipass.aipass.apps.modules.doctor_wire import reconcile_stale_deny from aipass.aipass.apps.modules.doctor_wire import reconcile_stale_deny
with patch("aipass.aipass.apps.modules.doctor_wire.Path.home", return_value=tmp_path): with patch("aipass.aipass.apps.handlers.provider_reconcile.Path.home", return_value=tmp_path):
results = reconcile_stale_deny(fix=False) results = reconcile_stale_deny(fix=False)
assert results == [] assert results == []
@@ -672,7 +672,7 @@ class TestReconcileStaleDeny:
json.dumps({"permissions": {"deny": ["Bash(git push --force*)", "Bash(git reset --hard*)"]}}), json.dumps({"permissions": {"deny": ["Bash(git push --force*)", "Bash(git reset --hard*)"]}}),
encoding="utf-8", encoding="utf-8",
) )
with patch("aipass.aipass.apps.modules.doctor_wire.Path.home", return_value=tmp_path): with patch("aipass.aipass.apps.handlers.provider_reconcile.Path.home", return_value=tmp_path):
results = reconcile_stale_deny(fix=False) results = reconcile_stale_deny(fix=False)
assert len(results) == 1 assert len(results) == 1
assert results[0][1] == GLYPH_PASS assert results[0][1] == GLYPH_PASS
@@ -688,7 +688,7 @@ class TestReconcileStaleDeny:
json.dumps({"permissions": {"deny": ["Bash(rm -rf*)", "Bash(git push --force*)", "Bash(rm -r *)"]}}), json.dumps({"permissions": {"deny": ["Bash(rm -rf*)", "Bash(git push --force*)", "Bash(rm -r *)"]}}),
encoding="utf-8", encoding="utf-8",
) )
with patch("aipass.aipass.apps.modules.doctor_wire.Path.home", return_value=tmp_path): with patch("aipass.aipass.apps.handlers.provider_reconcile.Path.home", return_value=tmp_path):
results = reconcile_stale_deny(fix=False) results = reconcile_stale_deny(fix=False)
assert len(results) == 1 assert len(results) == 1
assert results[0][1] == GLYPH_WARN assert results[0][1] == GLYPH_WARN
@@ -706,7 +706,7 @@ class TestReconcileStaleDeny:
"env": {"AIPASS_HOME": "/test"}, "env": {"AIPASS_HOME": "/test"},
} }
settings.write_text(json.dumps(original), encoding="utf-8") settings.write_text(json.dumps(original), encoding="utf-8")
with patch("aipass.aipass.apps.modules.doctor_wire.Path.home", return_value=tmp_path): with patch("aipass.aipass.apps.handlers.provider_reconcile.Path.home", return_value=tmp_path):
results = reconcile_stale_deny(fix=True) results = reconcile_stale_deny(fix=True)
assert len(results) == 1 assert len(results) == 1
assert results[0][1] == GLYPH_PASS assert results[0][1] == GLYPH_PASS
@@ -727,7 +727,7 @@ class TestReconcileStaleDeny:
json.dumps({"permissions": {"deny": ["Bash(rm -rf*)", "Bash(git reset --hard*)"]}}), json.dumps({"permissions": {"deny": ["Bash(rm -rf*)", "Bash(git reset --hard*)"]}}),
encoding="utf-8", encoding="utf-8",
) )
with patch("aipass.aipass.apps.modules.doctor_wire.Path.home", return_value=tmp_path): with patch("aipass.aipass.apps.handlers.provider_reconcile.Path.home", return_value=tmp_path):
results = reconcile_stale_deny(fix=True) results = reconcile_stale_deny(fix=True)
assert len(results) == 1 assert len(results) == 1
assert results[0][1] == GLYPH_PASS assert results[0][1] == GLYPH_PASS
@@ -744,7 +744,7 @@ class TestReconcileStaleDeny:
json.dumps({"permissions": {"deny": ["Bash(rm -rf*)", "Bash(rm -r *)"]}}), json.dumps({"permissions": {"deny": ["Bash(rm -rf*)", "Bash(rm -r *)"]}}),
encoding="utf-8", encoding="utf-8",
) )
with patch("aipass.aipass.apps.modules.doctor_wire.Path.home", return_value=tmp_path): with patch("aipass.aipass.apps.handlers.provider_reconcile.Path.home", return_value=tmp_path):
reconcile_stale_deny(fix=True) reconcile_stale_deny(fix=True)
results = reconcile_stale_deny(fix=True) results = reconcile_stale_deny(fix=True)
assert len(results) == 1 assert len(results) == 1
@@ -758,7 +758,7 @@ class TestReconcileStaleDeny:
settings = tmp_path / ".claude" / "settings.json" settings = tmp_path / ".claude" / "settings.json"
settings.parent.mkdir(parents=True) settings.parent.mkdir(parents=True)
settings.write_text(json.dumps({"permissions": {"deny": []}}), encoding="utf-8") settings.write_text(json.dumps({"permissions": {"deny": []}}), encoding="utf-8")
with patch("aipass.aipass.apps.modules.doctor_wire.Path.home", return_value=tmp_path): with patch("aipass.aipass.apps.handlers.provider_reconcile.Path.home", return_value=tmp_path):
results = reconcile_stale_deny(fix=False) results = reconcile_stale_deny(fix=False)
assert len(results) == 1 assert len(results) == 1
assert results[0][1] == GLYPH_PASS assert results[0][1] == GLYPH_PASS
@@ -770,7 +770,7 @@ class TestReconcileStaleDeny:
settings = tmp_path / ".claude" / "settings.json" settings = tmp_path / ".claude" / "settings.json"
settings.parent.mkdir(parents=True) settings.parent.mkdir(parents=True)
settings.write_text(json.dumps({"env": {"FOO": "bar"}}), encoding="utf-8") settings.write_text(json.dumps({"env": {"FOO": "bar"}}), encoding="utf-8")
with patch("aipass.aipass.apps.modules.doctor_wire.Path.home", return_value=tmp_path): with patch("aipass.aipass.apps.handlers.provider_reconcile.Path.home", return_value=tmp_path):
results = reconcile_stale_deny(fix=False) results = reconcile_stale_deny(fix=False)
assert len(results) == 1 assert len(results) == 1
assert results[0][1] == GLYPH_PASS assert results[0][1] == GLYPH_PASS
@@ -857,7 +857,7 @@ class TestPromptAutoWireIsatty:
patch.object(doctor_wire, "_print_manual_wire_warning") as mock_warn, patch.object(doctor_wire, "_print_manual_wire_warning") as mock_warn,
): ):
mock_stdin.isatty.return_value = False mock_stdin.isatty.return_value = False
result = doctor_wire.prompt_auto_wire(**self._args()) result = doctor_wire._prompt_auto_wire(**self._args())
assert result is False assert result is False
mock_input.assert_not_called() mock_input.assert_not_called()
@@ -873,7 +873,7 @@ class TestPromptAutoWireIsatty:
patch.object(doctor_wire, "_print_manual_wire_warning"), patch.object(doctor_wire, "_print_manual_wire_warning"),
): ):
mock_stdin.isatty.return_value = True mock_stdin.isatty.return_value = True
result = doctor_wire.prompt_auto_wire(**self._args()) result = doctor_wire._prompt_auto_wire(**self._args())
assert result is False assert result is False
mock_input.assert_called_once() mock_input.assert_called_once()
@@ -888,7 +888,7 @@ class TestPromptAutoWireIsatty:
patch.object(doctor_wire, "_auto_wire_provider", return_value=["wired hook"]) as mock_wire, patch.object(doctor_wire, "_auto_wire_provider", return_value=["wired hook"]) as mock_wire,
): ):
mock_stdin.isatty.return_value = True mock_stdin.isatty.return_value = True
result = doctor_wire.prompt_auto_wire(**self._args()) result = doctor_wire._prompt_auto_wire(**self._args())
assert result is True assert result is True
mock_wire.assert_called_once() mock_wire.assert_called_once()
+3 -2
View File
@@ -671,7 +671,8 @@ class TestInitUpdateRegistrySync:
return_value={"updated_files": [], "already_current": []}, return_value={"updated_files": [], "already_current": []},
), ),
patch(f"{_MOD_UPDATE}.subprocess.run", return_value=mock_result) as mock_run, patch(f"{_MOD_UPDATE}.subprocess.run", return_value=mock_result) as mock_run,
patch(f"{_MOD_UPDATE}.console") as mock_console, patch(f"{_MOD_UPDATE}.console"),
patch(f"{_MOD_UPDATE}.success") as mock_success,
patch(f"{_MOD_UPDATE}.json_handler"), patch(f"{_MOD_UPDATE}.json_handler"),
): ):
rc = _handle_init_update([str(tmp_path)]) rc = _handle_init_update([str(tmp_path)])
@@ -682,7 +683,7 @@ class TestInitUpdateRegistrySync:
text=True, text=True,
timeout=30, timeout=30,
) )
sync_calls = [c for c in mock_console.print.call_args_list if "Registry synced" in str(c)] sync_calls = [c for c in mock_success.call_args_list if "Registry synced" in str(c)]
assert len(sync_calls) == 1 assert len(sync_calls) == 1
def test_sync_failure_degrades_silently(self, tmp_path: Path) -> None: def test_sync_failure_degrades_silently(self, tmp_path: Path) -> None: