fix(system): S61-S62 — API cleanup, drone path routing, naming checker, .backup→.recovery rename (#142)

- API: 18-item P0/P1/P2 cleanup (debug prints→logger, help gate fix, URL injection fix, key masking, unused imports)
- Drone: passport walk-up replaces hardcoded src/aipass/<branch> pattern, 13 naming bypasses removed
- Seedgo: naming checker fixed (__dunder__ skip + column-0 scope detection) — eliminates 71 false positives system-wide
- Spawn: .backup→.recovery rename (pre-change snapshots no longer conflict with backup branch namespace)
- .gitignore: .recovery/ added
- HERALD.md: Updated through S62 (backup audit, .recovery rename, night shift launch)
- README.md: Stats updated (141 PRs, 2900+ tests)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
AIPass
2026-03-29 01:55:21 -07:00
committed by GitHub
co-authored by Claude Opus 4.6
parent ae459853f0
commit 6772cacce2
27 changed files with 245 additions and 212 deletions
+1
View File
@@ -38,6 +38,7 @@ tools/
docs.local/ docs.local/
.archive/ .archive/
.backup/ .backup/
.recovery/
.seed/ .seed/
.spawn/ .spawn/
+30 -12
View File
@@ -2,7 +2,7 @@
> The living record. What happened, what's changing, what matters. > The living record. What happened, what's changing, what matters.
**Last updated:** 2026-03-28 | **Session:** 58 | **PRs merged:** 137 **Last updated:** 2026-03-29 | **Session:** 62 | **PRs merged:** 141
--- ---
@@ -10,11 +10,24 @@
- **15 branches** operational - **15 branches** operational
- **100% seedgo compliance** across all 15 branches, all 33 standards - **100% seedgo compliance** across all 15 branches, all 33 standards
- **2,000+ tests** system-wide - **2,900+ tests** system-wide
- **137 PRs** merged since inception - **141 PRs** merged since inception
- **Backup rebuild in progress** — 4-phase autonomous night shift running now
## Recent Sessions ## Recent Sessions
### S62 — Backup Deep Audit + Night Shift Launch (2026-03-29)
Full backup branch investigation with 8 parallel agents (inventory, CLI, ignore patterns, Google Drive, live test, routing, tests, diff). Found: snapshot broken by JSON corruption (versioned works fine), 388GB legacy data from before ignore patterns were fixed, Google Drive auth duplicates API branch. Diff system is clean and stays. Renamed `.backup` to `.recovery` system-wide (79 directories) so backup branch owns the `.backup` namespace. Drone adapter pattern investigated — identical boilerplate across branches, documented for future redesign. Backup proposed a 4-phase plan (cleanup, JSON fix, Google Drive migration to API, test coverage), Patrick approved, and backup is now running autonomously through all phases overnight.
### S61 — Branch Audit Deep-Dive: API + Drone (2026-03-29)
API dispatched with 18-item P0/P1/P2 cleanup list — all fixed (186 tests, 100% seedgo). Drone audit verified all 4 architecture fixes from boardroom consensus (self-routing removed, adapter hack deleted, dual help paths unified, @ enforcement working). Naming checker false positives traced to seedgo — 71 bypasses across 11 branches eliminated by fixing `__dunder__` skip and local variable scope detection. Drone path routing fixed with passport walk-up (replaces hardcoded `src/aipass/<branch>` pattern). Access control investigation revealed no registry-scoped auth exists — DPLAN-0083 created. 4 new DPLANs: git workflow, VS Code reload bug, flow audit, access control.
### S60 — System Verification Wave (2026-03-29)
Prax queue spam eliminated (144k log entries per 4 hours). 9 stale plans closed. 15 branch audit DPLANs reorganized into dedicated directory. TTS Listen summaries added to all DPLANs (pure plaintext for Piper). 15-agent verification wave fact-checked every branch audit against live seedgo + pytest. System: 2,905 tests, 100% seedgo across all 15 branches.
### S59 — Full System Walkthrough (2026-03-28)
11 agents audited all 15 branches (2,378 tests at the time). Docker install verified — found and fixed registry format mismatch and seedgo CLI entry point bugs. README rewritten with verified claims. HERALD.md created. Dispatched 8 branches for fixes. PR #140 merged.
### S58 — Night Shift: 100% Compliance (2026-03-28) ### S58 — Night Shift: 100% Compliance (2026-03-28)
The big one. Every branch, every standard, 100%. Seven agents deployed overnight to fix the final 8 branches that were stuck at 99%. Commons was the hardest — test_quality at 68%, unused functions, architecture gaps, deep nesting. All fixed. Daemon needed plugin architecture bypasses. Memory and spawn needed test gaps filled. PR #137 (167 files, +12,843 lines). The big one. Every branch, every standard, 100%. Seven agents deployed overnight to fix the final 8 branches that were stuck at 99%. Commons was the hardest — test_quality at 68%, unused functions, architecture gaps, deep nesting. All fixed. Daemon needed plugin architecture bypasses. Memory and spawn needed test gaps filled. PR #137 (167 files, +12,843 lines).
@@ -43,17 +56,22 @@ First autonomous night shift. PR #118 (75 files). Persistent git branches (citiz
| DPLAN | Subject | Status | | DPLAN | Subject | Status |
|-------|---------|--------| |-------|---------|--------|
| 0031 | Drone audit + git workflow improvements | Open — master key concept, PR review gate | | 0029 | API branch audit | Complete — 186 tests, 100% seedgo, all P0/P1/P2 items fixed |
| 0033 | Commons audit | Silent catch done, orphan cleanup remaining | | 0034 | Backup branch audit | In progress — 4-phase rebuild running overnight |
| 0034 | Backup audit | Silent catch done, scope/filtering remaining | | 0035 | Spawn branch audit | Template overhaul complete, .backup→.recovery rename done |
| 0035 | Spawn audit | Template overhaul complete, nesting deferred | | 0036 | AI Mail audit | Silent catch done, nesting + reply-while-locked bug remaining |
| 0036 | AI Mail audit | Silent catch done, nesting + reply-while-locked bug | | 0053 | Drone branch audit | Architecture fixes complete, adapter redesign documented |
| 0049 | API compliance | 100% achieved | | 0080 | Devpulse git workflow | Design captured, not built yet |
| 0082 | Flow branch audit | Created, not started |
| 0083 | Access control design | Investigation complete, design pending |
## Key Milestones ## Key Milestones
| Date | Milestone | | Date | Milestone |
|------|-----------| |------|-----------|
| 2026-03-29 | Backup rebuild launched — 4-phase autonomous night shift |
| 2026-03-29 | .backup→.recovery rename — 79 dirs, namespace clarity |
| 2026-03-29 | Branch audit deep-dives — API complete, Drone complete, Backup in progress |
| 2026-03-28 | 100% seedgo compliance — all 15 branches, all 33 standards | | 2026-03-28 | 100% seedgo compliance — all 15 branches, all 33 standards |
| 2026-03-25 | Spawn template overhaul — registry regen, 41-file template | | 2026-03-25 | Spawn template overhaul — registry regen, 41-file template |
| 2026-03-24 | First autonomous night shift — 6 branches dispatched, all returned | | 2026-03-24 | First autonomous night shift — 6 branches dispatched, all returned |
@@ -76,9 +94,9 @@ First autonomous night shift. PR #118 (75 files). Persistent git branches (citiz
``` ```
Branches: 15 Branches: 15
Standards: 33 (was 34, consolidated in S57) Standards: 33 (was 34, consolidated in S57)
Tests: 2,000+ Tests: 2,900+
PRs merged: 137 PRs merged: 141
Sessions: 58 Sessions: 62
Compliance: 100% Compliance: 100%
``` ```
+1 -1
View File
@@ -186,7 +186,7 @@ Git workflow is atomic via `drone @git pr` — one command handles: lock acquisi
## Status ## Status
Beta. 15 branches. 137 PRs merged. 2,000+ tests. 100% compliance across 33 standards. See [HERALD.md](HERALD.md) for detailed progress and session history. Beta. 15 branches. 141 PRs merged. 2,900+ tests. 100% compliance across 33 standards. See [HERALD.md](HERALD.md) for detailed progress and session history.
## License ## License
+5
View File
@@ -74,6 +74,11 @@
"file": "apps/modules/google_client.py", "file": "apps/modules/google_client.py",
"standard": "unused_function", "standard": "unused_function",
"reason": "Public API exports for cross-branch consumption (get_drive_service, authenticate_google, validate_google, reauth_google). Tested in test_google_client.py. Backup migration pending." "reason": "Public API exports for cross-branch consumption (get_drive_service, authenticate_google, validate_google, reauth_google). Tested in test_google_client.py. Backup migration pending."
},
{
"file": "apps/modules/openrouter_client.py",
"standard": "modules",
"reason": "make_call() is a CLI orchestrator — parses args then delegates to client.get_response(). All business logic lives in handlers/openrouter/client.py."
} }
], ],
"notes": { "notes": {
+8 -1
View File
@@ -157,8 +157,15 @@ def print_help():
table.add_row("reauth google", "Re-authenticate Google OAuth2") table.add_row("reauth google", "Re-authenticate Google OAuth2")
table.add_row("test", "Test OpenRouter connection status") table.add_row("test", "Test OpenRouter connection status")
table.add_row("models", "List available models from provider") table.add_row("models", "List available models from provider")
table.add_row("status", "Check OpenRouter client status")
table.add_row("call", "Make API call to model")
table.add_row("list-providers", "List available API providers")
table.add_row("init", "Initialize .env template")
table.add_row("track", "Track API usage metrics") table.add_row("track", "Track API usage metrics")
table.add_row("stats", "Display API usage statistics") table.add_row("stats", "Display API usage statistics")
table.add_row("session", "Show session usage data")
table.add_row("caller-usage", "Show usage by caller module")
table.add_row("cleanup", "Clean up old usage data")
console.print(table) console.print(table)
console.print() console.print()
@@ -193,7 +200,7 @@ def print_help():
console.print("─" * 70) console.print("─" * 70)
console.print() console.print()
console.print("[dim]Commands: get-key, validate, test, models, track, stats, help, --help[/dim]") console.print("[dim]Commands: get-key, validate, test, models, status, call, list-providers, init, track, stats, session, caller-usage, cleanup[/dim]")
console.print() console.print()
+3 -3
View File
@@ -42,9 +42,9 @@ def _guard_branch_access():
import os import os
if os.environ.get("AIPASS_DEBUG_GUARD"): if os.environ.get("AIPASS_DEBUG_GUARD"):
import sys from aipass.prax import logger as _guard_logger
print(f"[GUARD DEBUG] caller_file = {caller_file}", file=sys.stderr) _guard_logger.info(f"[GUARD] caller_file = {caller_file}")
print(f"[GUARD DEBUG] import_line = {import_line}", file=sys.stderr) _guard_logger.info(f"[GUARD] import_line = {import_line}")
if caller_file is None: if caller_file is None:
stack = inspect.stack() stack = inspect.stack()
+1 -2
View File
@@ -17,10 +17,9 @@ Functions:
# Infrastructure # Infrastructure
from pathlib import Path from pathlib import Path
import sys
# Standard library # Standard library
from typing import Optional, Dict from typing import Optional
# Logging # Logging
from aipass.prax import logger from aipass.prax import logger
@@ -10,7 +10,6 @@ import json
from pathlib import Path from pathlib import Path
from datetime import datetime from datetime import datetime
from typing import Dict, Any, Optional from typing import Dict, Any, Optional
import sys
import inspect import inspect
# Logging # Logging
@@ -121,7 +120,7 @@ def ensure_json_exists(module_name: str, json_type: str) -> bool:
if validate_json_structure(data, json_type): if validate_json_structure(data, json_type):
return True return True
else: else:
pass # Corrupted - regenerating logger.warning(f"Corrupted JSON structure at {json_path}, regenerating")
except Exception as e: except Exception as e:
logger.warning(f"Unreadable JSON at {json_path}, regenerating: {e}") logger.warning(f"Unreadable JSON at {json_path}, regenerating: {e}")
@@ -11,7 +11,7 @@
OpenRouter Client Handler OpenRouter Client Handler
Business logic for OpenRouter API client creation and request execution. Business logic for OpenRouter API client creation and request execution.
Extracted from archive.temp/openrouter.py following AIPASS standards. OpenRouter API client creation and request execution.
Functions: Functions:
- get_response() - Main API call with tracking integration - get_response() - Main API call with tracking integration
@@ -34,8 +34,6 @@ Standards:
""" """
# INFRASTRUCTURE IMPORT PATTERN # INFRASTRUCTURE IMPORT PATTERN
import sys
from pathlib import Path
# Standard library imports # Standard library imports
import time import time
@@ -193,6 +191,7 @@ def make_api_request(client: OpenAI, messages: List[Dict], model: str, retries:
OpenAI response object or None on failure OpenAI response object or None on failure
""" """
if not client or not messages or not model: if not client or not messages or not model:
logger.warning(f"make_api_request() called with missing params — client={bool(client)}, messages={bool(messages)}, model={bool(model)}")
return None return None
api_params = { api_params = {
@@ -242,9 +241,11 @@ def extract_response(response: Any) -> Optional[Dict[str, Any]]:
try: try:
# Validate response structure # Validate response structure
if not hasattr(response, 'choices') or not response.choices: if not hasattr(response, 'choices') or not response.choices:
logger.warning("Response missing 'choices' or choices is empty")
return None return None
if not hasattr(response.choices[0], 'message'): if not hasattr(response.choices[0], 'message'):
logger.warning("Response choice missing 'message' attribute")
return None return None
# Extract content # Extract content
@@ -25,12 +25,11 @@ COMPLIANT STANDARDS:
- Under 300 lines - Under 300 lines
""" """
import sys
from pathlib import Path from pathlib import Path
import json import json
from datetime import datetime from datetime import datetime
from typing import Dict, Any, Optional, Tuple from typing import Dict, Any, Optional
from aipass.prax import logger from aipass.prax import logger
@@ -102,7 +101,7 @@ def get_default_caller_config() -> Dict[str, Any]:
Dict with default OpenRouter configuration (model must be set by caller) Dict with default OpenRouter configuration (model must be set by caller)
""" """
return { return {
"skill_name": "openrouter", "module_name": "openrouter",
"timestamp": datetime.now().isoformat(), "timestamp": datetime.now().isoformat(),
"config": { "config": {
"ai_model": "", # Caller must set their own model "ai_model": "", # Caller must set their own model
@@ -121,7 +120,7 @@ def get_default_caller_data() -> Dict[str, Any]:
Dict with initial usage tracking data Dict with initial usage tracking data
""" """
return { return {
"skill_name": "openrouter", "module_name": "openrouter",
"timestamp": datetime.now().isoformat(), "timestamp": datetime.now().isoformat(),
"data": { "data": {
"total_requests": 0, "total_requests": 0,
@@ -141,7 +140,7 @@ def get_default_caller_log() -> Dict[str, Any]:
Dict with empty log structure Dict with empty log structure
""" """
return { return {
"skill_name": "openrouter", "module_name": "openrouter",
"timestamp": datetime.now().isoformat(), "timestamp": datetime.now().isoformat(),
"logs": [] "logs": []
} }
@@ -15,7 +15,6 @@ Business logic for aggregating usage statistics:
- Cost, token, and latency aggregation - Cost, token, and latency aggregation
- Model usage tracking and breakdown - Model usage tracking and breakdown
Extracted from legacy archive (api_usage.py).
Functions: get_caller_usage(), get_session_summary() Functions: get_caller_usage(), get_session_summary()
""" """
@@ -15,11 +15,9 @@ Business logic for tracking API usage from OpenRouter:
- Store generation tracking data with newest-first ordering - Store generation tracking data with newest-first ordering
- Handle HTTP requests with proper error handling - Handle HTTP requests with proper error handling
Extracted from legacy archive (api_usage.py). Functions: track_usage(), get_generation_metrics(), store_usage_data()
Functions: track_usage(), _get_generation_metrics(), _store_usage_data()
""" """
import sys
from pathlib import Path from pathlib import Path
# Standard library imports # Standard library imports
@@ -147,7 +145,8 @@ def get_generation_metrics(generation_id: str, api_key: str) -> Optional[Dict[st
# Query the generation endpoint # Query the generation endpoint
response = requests.get( response = requests.get(
f"{GENERATION_ENDPOINT}?id={generation_id}", GENERATION_ENDPOINT,
params={"id": generation_id},
headers=headers, headers=headers,
timeout=30 timeout=30
) )
@@ -158,7 +157,7 @@ def get_generation_metrics(generation_id: str, api_key: str) -> Optional[Dict[st
# Validate response structure # Validate response structure
if not data or "data" not in data: if not data or "data" not in data:
# Invalid response structure from OpenRouter logger.warning(f"[{MODULE_NAME}] Invalid response structure from OpenRouter for generation {generation_id}")
return None return None
# Extract metrics from response # Extract metrics from response
@@ -176,7 +175,7 @@ def get_generation_metrics(generation_id: str, api_key: str) -> Optional[Dict[st
return result return result
else: else:
# OpenRouter API returned non-200 status logger.warning(f"[{MODULE_NAME}] OpenRouter API returned status {response.status_code} for generation {generation_id}")
return None return None
except requests.exceptions.Timeout as e: except requests.exceptions.Timeout as e:
+3 -3
View File
@@ -20,10 +20,9 @@ from pathlib import Path
from typing import List from typing import List
from aipass.prax.apps.modules.logger import system_logger as logger from aipass.prax.apps.modules.logger import system_logger as logger
from aipass.cli.apps.modules import console, header, success, error, warning, section from aipass.cli.apps.modules import console, header, success, error
from aipass.api.apps.handlers.json import json_handler from aipass.api.apps.handlers.json import json_handler
from aipass.api.apps.handlers.auth import keys, env from aipass.api.apps.handlers.auth import keys, env
from aipass.api.apps.handlers.config import provider
def print_introspection(): def print_introspection():
@@ -110,7 +109,8 @@ def get_key(args: List[str]):
if api_key: if api_key:
success(f"API key retrieved for {provider_name}") success(f"API key retrieved for {provider_name}")
console.print(f" Key (first 20 chars): {api_key[:20]}...") masked = api_key[:6] + "****" + api_key[-4:] if len(api_key) > 10 else "****"
console.print(f" Key: {masked}")
else: else:
error(f"Failed to retrieve API key for {provider_name}") error(f"Failed to retrieve API key for {provider_name}")
+6 -5
View File
@@ -124,11 +124,6 @@ def handle_command(command: str, args: List[str]) -> bool:
Returns: Returns:
True if command was handled, False to pass through. True if command was handled, False to pass through.
""" """
# Help gate
if args and args[0] in ("--help", "-h", "help"):
print_help()
return True
# NO-ARGS GATE (seedgo standard) # NO-ARGS GATE (seedgo standard)
if not args: if not args:
if command == "google": if command == "google":
@@ -140,6 +135,12 @@ def handle_command(command: str, args: List[str]) -> bool:
if args[0] != "google": if args[0] != "google":
return False return False
# Help gate — only fires after confirming this is a google command
remaining = args[1:] if len(args) > 1 else []
if remaining and remaining[0] in ("--help", "-h", "help"):
print_help()
return True
if command == "validate": if command == "validate":
_cmd_validate() _cmd_validate()
return True return True
@@ -17,11 +17,10 @@ Orchestrates LLM API client operations:
""" """
import sys import sys
from pathlib import Path
from typing import List from typing import List
from aipass.prax.apps.modules.logger import system_logger as logger from aipass.prax.apps.modules.logger import system_logger as logger
from aipass.cli.apps.modules import console, header, success, error, warning, section from aipass.cli.apps.modules import console, header, success, error
from aipass.api.apps.handlers.json import json_handler from aipass.api.apps.handlers.json import json_handler
from aipass.api.apps.handlers.auth import keys from aipass.api.apps.handlers.auth import keys
from aipass.api.apps.handlers.openrouter import client, models from aipass.api.apps.handlers.openrouter import client, models
@@ -188,7 +187,42 @@ def make_call(args: List[str]):
header("OpenRouter API Call") header("OpenRouter API Call")
console.print() console.print()
warning("API call workflow - TODO") if not args:
error("Prompt required", suggestion="drone @api call \"your prompt\" --model MODEL")
return
# Parse args: first non-flag arg is prompt, --model MODEL is optional
prompt = None
model = None
i = 0
while i < len(args):
if args[i] == "--model" and i + 1 < len(args):
model = args[i + 1]
i += 2
elif prompt is None:
prompt = args[i]
i += 1
else:
i += 1
if not prompt:
error("Prompt required", suggestion="drone @api call \"your prompt\" --model MODEL")
return
if not model:
error("Model required", suggestion="drone @api call \"your prompt\" --model anthropic/claude-3.5-sonnet")
return
console.print(f"[dim]Calling {model}...[/dim]")
response = client.get_response(prompt, caller="cli", model=model)
if response:
success(f"Response received ({len(response['content'])} chars)")
console.print()
console.print(response["content"])
else:
error("API call failed")
def list_models(args: List[str] | None = None): def list_models(args: List[str] | None = None):
+17 -2
View File
@@ -21,7 +21,7 @@ from pathlib import Path
from typing import List from typing import List
from aipass.prax.apps.modules.logger import system_logger as logger from aipass.prax.apps.modules.logger import system_logger as logger
from aipass.cli.apps.modules import console, header, success, error, warning, section from aipass.cli.apps.modules import console, header, success, error, warning
from aipass.api.apps.handlers.json import json_handler from aipass.api.apps.handlers.json import json_handler
from aipass.api.apps.handlers.usage import tracking, aggregation, cleanup from aipass.api.apps.handlers.usage import tracking, aggregation, cleanup
@@ -174,7 +174,22 @@ def track_usage(args: List[str]):
header("Track API Usage") header("Track API Usage")
console.print() console.print()
warning("Usage tracking workflow - TODO") if not args:
error("Generation ID required", suggestion="drone @api track <generation_id> [caller]")
return
generation_id = args[0]
caller = args[1] if len(args) > 1 else "manual"
console.print(f"[dim]Tracking generation {generation_id}...[/dim]")
result = tracking.track_usage(generation_id, caller)
if result.get("success"):
metrics = result.get("metrics", {})
success(f"Tracked: {metrics.get('tokens_prompt', 0)} prompt + {metrics.get('tokens_completion', 0)} completion tokens, ${metrics.get('total_cost', 0):.6f}")
else:
error(f"Tracking failed: {result.get('error', 'unknown')}")
def show_stats(): def show_stats():
+2 -2
View File
@@ -137,10 +137,10 @@ def test_handle_command_routes_reauth_google(
def test_handle_command_help_gate( def test_handle_command_help_gate(
_warn, _err, _succ, _hdr, _json, _retry, _factory, _auth, mock_console _warn, _err, _succ, _hdr, _json, _retry, _factory, _auth, mock_console
): ):
"""handle_command prints help when args=['--help'] and returns True.""" """handle_command prints help when args=['google', '--help'] and returns True."""
from aipass.api.apps.modules import google_client from aipass.api.apps.modules import google_client
result = google_client.handle_command("validate", ["--help"]) result = google_client.handle_command("validate", ["google", "--help"])
assert result is True assert result is True
# print_help calls console.print with the argparse output # print_help calls console.print with the argparse output
@@ -596,17 +596,17 @@ def test_list_models_formats_free_pricing(mock_console, mock_header, mock_keys,
# ============================================= # =============================================
@patch(f"{_MOD}.warning") @patch(f"{_MOD}.error")
@patch(f"{_MOD}.header") @patch(f"{_MOD}.header")
@patch(f"{_MOD}.console") @patch(f"{_MOD}.console")
def test_make_call_shows_todo_warning(mock_console, mock_header, mock_warning): def test_make_call_no_model_shows_error(mock_console, mock_header, mock_error):
"""make_call currently shows a TODO warning.""" """make_call without --model shows error."""
from aipass.api.apps.modules import openrouter_client from aipass.api.apps.modules import openrouter_client
openrouter_client.make_call(["What is AI?"]) openrouter_client.make_call(["What is AI?"])
mock_warning.assert_called_once() mock_error.assert_called_once()
assert "TODO" in mock_warning.call_args[0][0] assert "Model required" in mock_error.call_args[0][0]
# ============================================= # =============================================
+3 -3
View File
@@ -60,7 +60,7 @@ def test_create_caller_config_defaults(tmp_path: Path):
assert config["config"]["ai_max_tokens"] == 4000 assert config["config"]["ai_max_tokens"] == 4000
assert config["config"]["enabled"] is True assert config["config"]["enabled"] is True
assert config["config"]["ai_model"] == "" assert config["config"]["ai_model"] == ""
assert config["skill_name"] == "openrouter" assert config["module_name"] == "openrouter"
def test_create_caller_config_data_defaults(tmp_path: Path): def test_create_caller_config_data_defaults(tmp_path: Path):
@@ -87,7 +87,7 @@ def test_create_caller_config_log_defaults(tmp_path: Path):
log = read_json(json_folder / "openrouter_skill_log.json") log = read_json(json_folder / "openrouter_skill_log.json")
assert log is not None assert log is not None
assert log["logs"] == [] assert log["logs"] == []
assert log["skill_name"] == "openrouter" assert log["module_name"] == "openrouter"
def test_create_caller_config_creates_folder(tmp_path: Path): def test_create_caller_config_creates_folder(tmp_path: Path):
@@ -187,7 +187,7 @@ def test_get_default_caller_config_structure():
"""Default config should have expected structure.""" """Default config should have expected structure."""
config = get_default_caller_config() config = get_default_caller_config()
assert "skill_name" in config assert "module_name" in config
assert "timestamp" in config assert "timestamp" in config
assert "config" in config assert "config" in config
assert isinstance(config["config"], dict) assert isinstance(config["config"], dict)
+2 -80
View File
@@ -44,85 +44,6 @@
29 29
], ],
"reason": "Scanner needs discovery_handler's get_entry_point() and parse_help_for_commands() to discover commands from branch help output. Justified — scanner's core function depends on discovery's parsing." "reason": "Scanner needs discovery_handler's get_entry_point() and parse_help_for_commands() to discover commands from branch help output. Justified — scanner's core function depends on discovery's parsing."
},
{
"file": "apps/modules/router.py",
"standard": "naming",
"reason": "branch_path, branch_name, command are local variables inside route_command(), not module-level constants. False positive from naming checker."
},
{
"file": "apps/modules/scan.py",
"standard": "naming",
"pattern": "__all__",
"reason": "__all__ is a standard Python convention (PEP 8) — lowercase dunder is correct."
},
{
"file": "apps/modules/commands.py",
"standard": "naming",
"pattern": "__all__",
"reason": "__all__ is a standard Python convention (PEP 8) — lowercase dunder is correct."
},
{
"file": "apps/modules/config.py",
"standard": "naming",
"pattern": "__all__",
"reason": "__all__ is a standard Python convention (PEP 8) — lowercase dunder is correct."
},
{
"file": "apps/modules/module_registry.py",
"standard": "naming",
"pattern": "__all__",
"reason": "__all__ is a standard Python convention (PEP 8) — lowercase dunder is correct."
},
{
"file": "apps/modules/registry.py",
"standard": "naming",
"pattern": "__all__",
"reason": "__all__ is a standard Python convention (PEP 8) — lowercase dunder is correct."
},
{
"file": "apps/modules/__init__.py",
"standard": "naming",
"pattern": "__all__",
"reason": "__all__ is a standard Python convention (PEP 8) — lowercase dunder is correct."
},
{
"file": "apps/handlers/json/json_handler.py",
"standard": "naming",
"pattern": "__all__",
"reason": "__all__ is a standard Python convention (PEP 8) — lowercase dunder is correct."
},
{
"file": "apps/handlers/scanning/__init__.py",
"standard": "naming",
"pattern": "__all__",
"reason": "__all__ is a standard Python convention (PEP 8) — lowercase dunder is correct."
},
{
"file": "__init__.py",
"standard": "naming",
"pattern": "__all__",
"reason": "__all__ is a standard Python convention (PEP 8) — lowercase dunder is correct."
},
{
"file": "apps/handlers/router_handler.py",
"standard": "naming",
"reason": "cmd_args, executable, args are local variables inside execute_branch_command(), not module-level constants. False positive."
},
{
"file": "apps/handlers/executor.py",
"standard": "naming",
"reason": "run_env, cwd, shell are local variables inside run_command(), not module-level constants. False positive."
},
{
"file": "apps/handlers/registry_handler.py",
"standard": "naming",
"reason": "filtered is a local variable inside get_all_branches(), not a module-level constant. False positive."
},
{
"file": "apps/handlers/command_registry/ops.py",
"standard": "naming",
"reason": "module_name is a local variable, not a module-level constant. False positive."
} }
], ],
"notes": { "notes": {
@@ -138,6 +59,7 @@
"lines": "Optional - specific line numbers to bypass", "lines": "Optional - specific line numbers to bypass",
"pattern": "Optional - pattern to match (e.g. 'if __name__')", "pattern": "Optional - pattern to match (e.g. 'if __name__')",
"reason": "Required - why this bypass exists" "reason": "Required - why this bypass exists"
} },
"removed_s61": "13 naming bypasses removed (9 __all__ + 4 local variable false positives). Seedgo dispatched to fix naming_check.py. If audit fails on naming after this, the checker fix didn't land."
} }
} }
+25 -8
View File
@@ -32,14 +32,31 @@ _COMMANDS = ("pr", "status", "sync", "lock", "unlock")
def _detect_branch_dir() -> tuple[str, Path] | None: def _detect_branch_dir() -> tuple[str, Path] | None:
"""Detect caller's branch from CWD. Returns (branch_name, branch_dir) or None.""" """Detect caller's branch from CWD via passport lookup.
cwd = Path.cwd()
parts = cwd.parts Walks up from CWD looking for ``.trinity/passport.json`` and extracts
for i, part in enumerate(parts): the branch name + directory. Works for any registered branch regardless
if part == "aipass" and i > 0 and parts[i - 1] == "src" and i + 1 < len(parts): of where it lives on disk (commons, skills, aipass sub-dirs, etc.).
branch_name = parts[i + 1] """
branch_dir = Path(*parts[: i + 2]) current = Path.cwd().resolve()
return branch_name, branch_dir for _ in range(10):
passport = current / ".trinity" / "passport.json"
if passport.exists():
try:
with open(passport, "r", encoding="utf-8") as fh:
data = json.load(fh)
name = data.get("branch_info", {}).get("branch_name")
if not name:
name = data.get("identity", {}).get("name")
if name:
return name, current
except Exception as exc:
logger.warning("Failed to read passport at %s: %s", passport, exc)
return None
parent = current.parent
if parent == current:
break
current = parent
return None return None
+37 -9
View File
@@ -585,24 +585,52 @@ class TestGitModuleRouting:
class TestDetectBranchDir: class TestDetectBranchDir:
"""Branch directory detection tests.""" """Branch directory detection tests."""
def test_detects_branch_from_path(self, monkeypatch: pytest.MonkeyPatch) -> None: def test_detects_branch_from_passport(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Detects branch name and dir from a path like .../src/aipass/drone/...""" """Detects branch name and dir by walking up to .trinity/passport.json."""
# Simulate CWD being inside a branch # Create a fake branch directory with passport
fake_path = Path("/home/user/Projects/AIPass/src/aipass/drone/apps/modules") branch_dir = tmp_path / "mybranch"
monkeypatch.chdir("/") # Just need a valid dir for the test trinity = branch_dir / ".trinity"
with patch("aipass.drone.apps.modules.git_module.Path.cwd", return_value=fake_path): trinity.mkdir(parents=True)
detected = _detect_branch_dir() passport = trinity / "passport.json"
passport.write_text(json.dumps({
"branch_info": {"branch_name": "mybranch"},
}))
# CWD is inside a subdirectory of the branch
sub_dir = branch_dir / "apps" / "modules"
sub_dir.mkdir(parents=True)
monkeypatch.chdir(sub_dir)
detected = _detect_branch_dir()
assert detected is not None assert detected is not None
name, bdir = detected name, bdir = detected
assert name == "drone" assert name == "mybranch"
assert bdir == branch_dir.resolve()
def test_returns_none_for_unrecognized_path(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: def test_returns_none_for_unrecognized_path(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Returns None when CWD is not under src/aipass/.""" """Returns None when CWD has no .trinity/passport.json above it."""
monkeypatch.chdir(tmp_path) monkeypatch.chdir(tmp_path)
detected = _detect_branch_dir() detected = _detect_branch_dir()
assert detected is None assert detected is None
def test_detects_non_aipass_branch(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Detects branches outside src/aipass/ (e.g. commons, skills)."""
branch_dir = tmp_path / "src" / "commons"
trinity = branch_dir / ".trinity"
trinity.mkdir(parents=True)
passport = trinity / "passport.json"
passport.write_text(json.dumps({
"branch_info": {"branch_name": "commons"},
}))
monkeypatch.chdir(branch_dir)
detected = _detect_branch_dir()
assert detected is not None
name, bdir = detected
assert name == "commons"
assert bdir == branch_dir.resolve()
class TestGitModuleHelp: class TestGitModuleHelp:
"""Help and introspection tests.""" """Help and introspection tests."""
@@ -150,5 +150,6 @@
"DPLAN-0059_seedgo_test_quality_standard_deep_coverage_en_2026-03-24.md": "2026-03-28T19:52:31.066363", "DPLAN-0059_seedgo_test_quality_standard_deep_coverage_en_2026-03-24.md": "2026-03-28T19:52:31.066363",
"DPLAN-0060_custom_function_scanner_seedgo_module_for_bra_2026-03-24.md": "2026-03-28T19:52:49.501398", "DPLAN-0060_custom_function_scanner_seedgo_module_for_bra_2026-03-24.md": "2026-03-28T19:52:49.501398",
"DPLAN-0061_expand_test_quality_checker_to_all_standard_c_2026-03-24.md": "2026-03-28T19:53:13.017860", "DPLAN-0061_expand_test_quality_checker_to_all_standard_c_2026-03-24.md": "2026-03-28T19:53:13.017860",
"FPLAN-0138_s59_walkthrough_fixes_2026-03-28.md": "2026-03-28T19:53:32.097935" "FPLAN-0138_s59_walkthrough_fixes_2026-03-28.md": "2026-03-28T19:53:32.097935",
"FPLAN-0139_dplan_0029_branch_audit_cleanup_fix_all_remai_2026-03-28.md": "2026-03-28T22:57:07.425893"
} }
+7
View File
@@ -41,6 +41,13 @@
"subject": "Monitor Phase 2: caller attribution via drone CALLER markers", "subject": "Monitor Phase 2: caller attribution via drone CALLER markers",
"date_closed": "2026-03-19", "date_closed": "2026-03-19",
"location": "prax" "location": "prax"
},
{
"plan_id": "FPLAN-0071",
"type": "FPLAN",
"subject": "Self-audit compliance sprint",
"date_closed": "2026-03-28",
"location": "prax"
} }
] ]
} }
@@ -13,10 +13,9 @@ Validates module compliance with AIPass naming standards.
Checks file naming, function naming, variable naming, constant naming. Checks file naming, function naming, variable naming, constant naming.
""" """
import sys
import re import re
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional from typing import Dict, Optional
from aipass.prax import logger from aipass.prax import logger
from aipass.seedgo.apps.handlers.json import json_handler from aipass.seedgo.apps.handlers.json import json_handler
@@ -90,7 +89,6 @@ def check_module(module_path: str, bypass_rules: list | None = None) -> Dict:
try: try:
with open(path, 'r', encoding='utf-8') as f: with open(path, 'r', encoding='utf-8') as f:
content = f.read() content = f.read()
lines = content.split('\n')
except Exception as e: except Exception as e:
logger.info("Cannot read %s: %s", path, e) logger.info("Cannot read %s: %s", path, e)
return { return {
@@ -236,49 +234,6 @@ def check_function_naming(content: str) -> Optional[Dict]:
} }
def _iter_module_level_lines(lines: List[str]) -> List[str]:
result = []
in_function_or_class = False
in_multiline_string = False
multiline_delimiter = None
current_indentation = 0
for line in lines:
stripped = line.strip()
if '"""' in stripped or "'''" in stripped:
delimiter = '"""' if '"""' in stripped else "'''"
count = stripped.count(delimiter)
if count % 2 == 1 and not in_multiline_string:
in_multiline_string = True
multiline_delimiter = delimiter
elif count % 2 == 1 and in_multiline_string and multiline_delimiter == delimiter:
in_multiline_string = False
multiline_delimiter = None
if in_multiline_string:
continue
if (stripped.startswith('def ') or
stripped.startswith('class ') or
stripped.startswith('async def ') or
stripped.startswith('if __name__')):
in_function_or_class = True
current_indentation = len(line) - len(line.lstrip())
continue
if in_function_or_class and not line.startswith(' ') and not line.startswith('\t') and stripped:
in_function_or_class = False
current_indentation = 0
if in_function_or_class:
continue
result.append(stripped)
return result
def check_constant_naming(content: str) -> Optional[Dict]: def check_constant_naming(content: str) -> Optional[Dict]:
""" """
@@ -286,8 +241,9 @@ def check_constant_naming(content: str) -> Optional[Dict]:
Rules: Rules:
- UPPER_CASE for module-level constants - UPPER_CASE for module-level constants
- Assigned outside of functions/classes - Assigned outside of functions/classes (column 0 only)
- Excludes module imports (from X import Y as Z) - Excludes module imports (from X import Y as Z)
- Excludes __dunder__ variables (PEP 8 convention: always lowercase)
""" """
# First pass: collect imported names to exclude from constant checking # First pass: collect imported names to exclude from constant checking
imported_names = set() imported_names = set()
@@ -306,13 +262,34 @@ def check_constant_naming(content: str) -> Optional[Dict]:
# Direct import like "console" # Direct import like "console"
imported_names.add(item.strip()) imported_names.add(item.strip())
# Second pass: find module-level assignments (not inside functions/classes) # Second pass: find TRUE module-level assignments
module_lines = _iter_module_level_lines(content.split('\n')) # Only check lines at column 0 (no indentation) — this eliminates local
# variables inside functions/classes, which are always indented.
# The old _iter_module_level_lines approach had a bug: multi-line function
# signatures with closing ) at column 0 tricked it into thinking the
# function body had ended.
constants = [] constants = []
bad_constants = [] bad_constants = []
in_multiline_string = False
for stripped in module_lines: for line in content.split('\n'):
# Find assignments at module level stripped = line.strip()
# Track multiline strings
for delimiter in ('"""', "'''"):
if delimiter in stripped:
count = stripped.count(delimiter)
if count % 2 == 1:
in_multiline_string = not in_multiline_string
if in_multiline_string:
continue
# Only consider lines with zero indentation (true module-level)
if line and (line[0] == ' ' or line[0] == '\t'):
continue
# Find assignments
if '=' not in stripped or stripped.startswith('#'): if '=' not in stripped or stripped.startswith('#'):
continue continue
@@ -324,6 +301,10 @@ def check_constant_naming(content: str) -> Optional[Dict]:
const_name = match.group(1) const_name = match.group(1)
assigned_value = match.group(2) assigned_value = match.group(2)
# Skip __dunder__ variables (__all__, __version__, etc.)
if const_name.startswith('__') and const_name.endswith('__'):
continue
# Skip if this is an imported name (like logger, console) # Skip if this is an imported name (like logger, console)
if const_name in imported_names: if const_name in imported_names:
continue continue
+2 -2
View File
@@ -97,7 +97,7 @@ def deep_merge(template_data: Any, existing_data: Any) -> Any:
def backup_json(file_path: Path) -> Path: def backup_json(file_path: Path) -> Path:
"""Create a timestamped backup of a JSON file before modifying. """Create a timestamped backup of a JSON file before modifying.
The backup is placed in a ``.backup/`` directory alongside the file, The backup is placed in a ``.recovery/`` directory alongside the file,
named with a timestamp suffix. named with a timestamp suffix.
Args: Args:
@@ -116,7 +116,7 @@ def backup_json(file_path: Path) -> Path:
raise FileNotFoundError(f"Cannot backup — file not found: {file_path}") raise FileNotFoundError(f"Cannot backup — file not found: {file_path}")
# Create backup directory alongside the file # Create backup directory alongside the file
backup_dir = file_path.parent / ".backup" backup_dir = file_path.parent / ".recovery"
backup_dir.mkdir(parents=True, exist_ok=True) backup_dir.mkdir(parents=True, exist_ok=True)
# Timestamped backup name # Timestamped backup name
+2 -2
View File
@@ -615,7 +615,7 @@ class TestBackupJson:
"""Tests for backup_json().""" """Tests for backup_json()."""
def test_creates_backup_file(self, tmp_path): def test_creates_backup_file(self, tmp_path):
"""Backup should create a copy in .backup/ directory.""" """Backup should create a copy in .recovery/ directory."""
from aipass.spawn.apps.handlers.json_ops import backup_json from aipass.spawn.apps.handlers.json_ops import backup_json
source = tmp_path / "data.json" source = tmp_path / "data.json"
@@ -624,7 +624,7 @@ class TestBackupJson:
backup_path = backup_json(source) backup_path = backup_json(source)
assert backup_path.exists() assert backup_path.exists()
assert ".backup" in str(backup_path) assert ".recovery" in str(backup_path)
assert "data.json" in backup_path.name assert "data.json" in backup_path.name
assert ".backup" in backup_path.name assert ".backup" in backup_path.name