#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`
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,
drone, flow, daemon, prax, ai_mail, backup, seedgo, memory, trigger, api, cli.
Remaining waves following.
drone, flow, daemon, prax, ai_mail, backup, seedgo, memory, trigger, api, cli,
aipass. Remaining: commons.
### Fixed
+8
View File
@@ -121,6 +121,14 @@ def main():
command = args[0]
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:
if route_command(command, remaining, modules):
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 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.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 (
_auto_wire_provider,
_prompt_auto_wire as prompt_auto_wire,
check_wire_verify,
prompt_auto_wire,
reconcile_stale_deny,
)
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:
actions = _auto_wire_provider(manifest_path, interactive=False)
for action in actions:
console.print(f"[green]✓[/green] {action}")
success(action)
wired = bool(actions)
else:
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:
written = generate_run_record(path, run_heavy_e2e=run_e2e)
except RunRecordError as exc:
console.print(f"[red]✗[/red] {exc}")
cli_error(str(exc))
logger.error("[doctor] cross-os run record failed: %s", exc)
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()
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]"
)
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()
+7 -143
View File
@@ -18,38 +18,20 @@ Provides:
from __future__ import annotations
import json
import shutil
import subprocess
import sys
from datetime import datetime, timezone
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.prax import logger
from aipass.aipass.apps.handlers.json import json_handler
# =============================================================================
# HOOK & ENV DESCRIPTIONS (for interactive "no" warning)
# =============================================================================
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",
}
from aipass.aipass.apps.handlers.provider_wire import ( # noqa: F401
HOOK_DESCRIPTIONS,
ENV_DESCRIPTIONS,
auto_wire_provider as _auto_wire_provider,
)
# =============================================================================
@@ -59,130 +41,12 @@ ENV_DESCRIPTIONS: Dict[str, str] = {
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
# =============================================================================
def prompt_auto_wire(
def _prompt_auto_wire(
manifest_path: Path,
missing_hooks: List[str],
missing_env: List[str],
+2 -2
View File
@@ -21,7 +21,7 @@ Usage:
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.aipass.apps.handlers.json import json_handler
@@ -64,7 +64,7 @@ def do_handoff(
if launched:
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()
else:
+30 -30
View File
@@ -35,7 +35,7 @@ from datetime import datetime, timezone
from pathlib import Path
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.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]
except ValueError as exc:
logger.info("[init_flow] invalid menu input %r: %s", raw, exc)
console.print("[red]Invalid choice.[/red]")
cli_error("Invalid choice.")
# --- STAGE FUNCTIONS ---
@@ -358,7 +358,7 @@ def stage_3_user_profile(
else:
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)
return {"name": name}
@@ -379,7 +379,7 @@ def stage_4_style_questions(
else:
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)
return {"style": style}
@@ -424,7 +424,7 @@ def _handle_missing_claude(non_interactive: bool) -> None:
if raw.lower() in ("y", "yes", ""):
console.print("[cyan]Installing Claude Code[/cyan] [dim](this can take a minute)…[/dim]")
if _install_claude_code():
console.print("[green]✓[/green] Claude Code installed successfully.")
success("Claude Code installed successfully.")
else:
warning("[bold yellow]Installation failed.[/bold yellow]")
console.print(" Install manually: https://claude.ai/download")
@@ -460,7 +460,7 @@ def stage_5_tool_choice(
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)
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}"
console.print(f"[cyan]Creating your first agent[/cyan] [dim](drone @spawn create {agent_path})…[/dim]")
success = False
spawned = False
if dry_run:
console.print(f"[yellow]\\[dry-run][/yellow] would run: drone @spawn create {agent_path}")
success = True
spawned = True
else:
try:
proc = subprocess.run(["drone", "@spawn", "create", agent_path], timeout=60)
success = proc.returncode == 0
spawned = proc.returncode == 0
except FileNotFoundError as exc:
logger.warning("[init_flow] drone not found in stage 6: %s", exc)
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)
warning("spawn timed out — agent may still be created.")
if success:
console.print(f"[green]✓[/green] Agent created at {agent_path}")
if spawned:
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}
@@ -576,12 +576,12 @@ def stage_8_smoke_test(non_interactive: bool = False, dry_run: bool = False) ->
aipass_bin = shutil.which("aipass")
if drone_bin:
console.print(f"[green]✓[/green] drone: {drone_bin}")
success(f"drone: {drone_bin}")
else:
warning("drone not on PATH — clone the repo and run setup.sh")
if aipass_bin:
console.print(f"[green]✓[/green] aipass: {aipass_bin}")
success(f"aipass: {aipass_bin}")
else:
warning("aipass not on PATH — clone the repo and run setup.sh")
@@ -640,7 +640,7 @@ def stage_9_handoff(
if accumulated:
_write_init_report(accumulated.get("agent_path", agent_path), accumulated, dry_run=dry_run)
console.print()
console.print("[bold green]✓ Setup complete![/bold green]")
success("Setup complete!")
console.print()
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(render_step_header(10, TOTAL_STAGES, "Done!"))
console.print()
console.print("[bold green]✓ Setup complete![/bold green]")
success("Setup complete!")
console.print()
console.print(" [cyan]aipass help[/cyan] [dim]# Ask any question[/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
err = _preflight_check()
if err:
console.print(f"[red]✗[/red] {err}")
cli_error(str(err))
return 1
# Template selection — before scaffold
@@ -799,7 +799,7 @@ def run_init(
if not dry_run:
with activity_spinner("Building project scaffold…"):
init_project(cwd)
console.print("[green]✓[/green] Project scaffold ready")
success("Project scaffold ready")
else:
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()
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]")
return 0
@@ -859,7 +859,7 @@ def run_init(
if template != TEMPLATE_AIPASS:
console.print()
console.print("[green]✓[/green] Project initialized.")
success("Project initialized.")
console.print("[dim]Run 'aipass init agent <name>' to add an agent.[/dim]")
return 0
@@ -877,7 +877,7 @@ def print_introspection() -> None:
if last == 0:
console.print("[dim]Setup not started. Run: aipass init run[/dim]")
elif last >= TOTAL_STAGES:
console.print("[green]✓[/green] Setup complete.")
success("Setup complete.")
else:
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]")
@@ -911,7 +911,7 @@ def _handle_init_scaffold(args: list[str]) -> int:
project_name = args[1] if len(args) > 1 else None
try:
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()
# Find the package directory to show in guidance
@@ -938,7 +938,7 @@ def _handle_init_scaffold(args: list[str]) -> int:
return 0
except Exception as 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
@@ -952,11 +952,11 @@ def _handle_init_update(args: list[str]) -> int:
updated = result.get("updated_files", [])
current = result.get("already_current", [])
if updated:
console.print(f"[green]✓[/green] Updated {len(updated)} file(s):")
success(f"Updated {len(updated)} file(s):")
for f in updated:
console.print(f" [green]+[/green] {f}")
console.print(f" + {f}")
else:
console.print("[green]✓[/green] All files already current.")
success("All files already current.")
if current:
console.print(f" ({len(current)} already up to date)")
# Heal registry: prune stale entries (e.g. cross-project ../paths)
@@ -968,7 +968,7 @@ def _handle_init_update(args: list[str]) -> int:
timeout=30,
)
if sync_proc.returncode == 0:
console.print(" [green]Registry synced.[/green]")
success("Registry synced.")
except Exception as 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
except Exception as 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
def _handle_init_agent(args: list[str]) -> int:
"""Handle `aipass init agent <name>` — create a new agent via spawn."""
if not args:
console.print("[red]✗[/red] Usage: aipass init agent <name>")
cli_error("Usage: aipass init agent <name>")
return 1
agent_name = args[0]
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
err = _preflight_check()
if err:
console.print(f"[red]✗[/red] {err}")
cli_error(str(err))
sys.exit(1)
sys.exit(_handle_init_scaffold(args))
return True
+5 -5
View File
@@ -24,7 +24,7 @@ import os
import tempfile
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.aipass.apps.handlers.json import json_handler
@@ -151,13 +151,13 @@ def handle_command(command: str, args: list[str]) -> bool:
return True
field, value = args[1], args[2]
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]")
return True
profile = get_user_profile()
profile[field] = value
save_profile(profile)
console.print(f"[green]✓[/green] {field} = {value}")
success(f"{field} = {value}")
return True
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:])
if skip_confirm:
save_profile({f: None for f in USER_FIELDS})
console.print("[green]✓[/green] Profile cleared.")
success("Profile cleared.")
return True
warning("Type 'aipass' to confirm clearing your profile (ctrl-C to cancel):")
try:
@@ -177,7 +177,7 @@ def handle_command(command: str, args: list[str]) -> bool:
return True
if confirm == "aipass":
save_profile({f: None for f in USER_FIELDS})
console.print("[green]✓[/green] Profile cleared.")
success("Profile cleared.")
else:
console.print("[yellow]Cancelled.[/yellow]")
return True
+11 -11
View File
@@ -658,7 +658,7 @@ class TestReconcileStaleDeny:
"""Missing settings.json returns no results."""
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)
assert results == []
@@ -672,7 +672,7 @@ class TestReconcileStaleDeny:
json.dumps({"permissions": {"deny": ["Bash(git push --force*)", "Bash(git reset --hard*)"]}}),
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)
assert len(results) == 1
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 *)"]}}),
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)
assert len(results) == 1
assert results[0][1] == GLYPH_WARN
@@ -706,7 +706,7 @@ class TestReconcileStaleDeny:
"env": {"AIPASS_HOME": "/test"},
}
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)
assert len(results) == 1
assert results[0][1] == GLYPH_PASS
@@ -727,7 +727,7 @@ class TestReconcileStaleDeny:
json.dumps({"permissions": {"deny": ["Bash(rm -rf*)", "Bash(git reset --hard*)"]}}),
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)
assert len(results) == 1
assert results[0][1] == GLYPH_PASS
@@ -744,7 +744,7 @@ class TestReconcileStaleDeny:
json.dumps({"permissions": {"deny": ["Bash(rm -rf*)", "Bash(rm -r *)"]}}),
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)
results = reconcile_stale_deny(fix=True)
assert len(results) == 1
@@ -758,7 +758,7 @@ class TestReconcileStaleDeny:
settings = tmp_path / ".claude" / "settings.json"
settings.parent.mkdir(parents=True)
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)
assert len(results) == 1
assert results[0][1] == GLYPH_PASS
@@ -770,7 +770,7 @@ class TestReconcileStaleDeny:
settings = tmp_path / ".claude" / "settings.json"
settings.parent.mkdir(parents=True)
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)
assert len(results) == 1
assert results[0][1] == GLYPH_PASS
@@ -857,7 +857,7 @@ class TestPromptAutoWireIsatty:
patch.object(doctor_wire, "_print_manual_wire_warning") as mock_warn,
):
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
mock_input.assert_not_called()
@@ -873,7 +873,7 @@ class TestPromptAutoWireIsatty:
patch.object(doctor_wire, "_print_manual_wire_warning"),
):
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
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,
):
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
mock_wire.assert_called_once()
+3 -2
View File
@@ -671,7 +671,8 @@ class TestInitUpdateRegistrySync:
return_value={"updated_files": [], "already_current": []},
),
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"),
):
rc = _handle_init_update([str(tmp_path)])
@@ -682,7 +683,7 @@ class TestInitUpdateRegistrySync:
text=True,
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
def test_sync_failure_degrades_silently(self, tmp_path: Path) -> None: