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:
co-authored by
Claude Opus 4.6
parent
ae459853f0
commit
6772cacce2
@@ -38,6 +38,7 @@ tools/
|
||||
docs.local/
|
||||
.archive/
|
||||
.backup/
|
||||
.recovery/
|
||||
.seed/
|
||||
.spawn/
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> 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
|
||||
- **100% seedgo compliance** across all 15 branches, all 33 standards
|
||||
- **2,000+ tests** system-wide
|
||||
- **137 PRs** merged since inception
|
||||
- **2,900+ tests** system-wide
|
||||
- **141 PRs** merged since inception
|
||||
- **Backup rebuild in progress** — 4-phase autonomous night shift running now
|
||||
|
||||
## 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)
|
||||
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 |
|
||||
|-------|---------|--------|
|
||||
| 0031 | Drone audit + git workflow improvements | Open — master key concept, PR review gate |
|
||||
| 0033 | Commons audit | Silent catch done, orphan cleanup remaining |
|
||||
| 0034 | Backup audit | Silent catch done, scope/filtering remaining |
|
||||
| 0035 | Spawn audit | Template overhaul complete, nesting deferred |
|
||||
| 0036 | AI Mail audit | Silent catch done, nesting + reply-while-locked bug |
|
||||
| 0049 | API compliance | 100% achieved |
|
||||
| 0029 | API branch audit | Complete — 186 tests, 100% seedgo, all P0/P1/P2 items fixed |
|
||||
| 0034 | Backup branch audit | In progress — 4-phase rebuild running overnight |
|
||||
| 0035 | Spawn branch audit | Template overhaul complete, .backup→.recovery rename done |
|
||||
| 0036 | AI Mail audit | Silent catch done, nesting + reply-while-locked bug remaining |
|
||||
| 0053 | Drone branch audit | Architecture fixes complete, adapter redesign documented |
|
||||
| 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
|
||||
|
||||
| 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-25 | Spawn template overhaul — registry regen, 41-file template |
|
||||
| 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
|
||||
Standards: 33 (was 34, consolidated in S57)
|
||||
Tests: 2,000+
|
||||
PRs merged: 137
|
||||
Sessions: 58
|
||||
Tests: 2,900+
|
||||
PRs merged: 141
|
||||
Sessions: 62
|
||||
Compliance: 100%
|
||||
```
|
||||
|
||||
|
||||
@@ -186,7 +186,7 @@ Git workflow is atomic via `drone @git pr` — one command handles: lock acquisi
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
@@ -74,6 +74,11 @@
|
||||
"file": "apps/modules/google_client.py",
|
||||
"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."
|
||||
},
|
||||
{
|
||||
"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": {
|
||||
|
||||
@@ -157,8 +157,15 @@ def print_help():
|
||||
table.add_row("reauth google", "Re-authenticate Google OAuth2")
|
||||
table.add_row("test", "Test OpenRouter connection status")
|
||||
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("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()
|
||||
@@ -193,7 +200,7 @@ def print_help():
|
||||
console.print("─" * 70)
|
||||
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()
|
||||
|
||||
|
||||
|
||||
@@ -42,9 +42,9 @@ def _guard_branch_access():
|
||||
|
||||
import os
|
||||
if os.environ.get("AIPASS_DEBUG_GUARD"):
|
||||
import sys
|
||||
print(f"[GUARD DEBUG] caller_file = {caller_file}", file=sys.stderr)
|
||||
print(f"[GUARD DEBUG] import_line = {import_line}", file=sys.stderr)
|
||||
from aipass.prax import logger as _guard_logger
|
||||
_guard_logger.info(f"[GUARD] caller_file = {caller_file}")
|
||||
_guard_logger.info(f"[GUARD] import_line = {import_line}")
|
||||
|
||||
if caller_file is None:
|
||||
stack = inspect.stack()
|
||||
|
||||
@@ -17,10 +17,9 @@ Functions:
|
||||
|
||||
# Infrastructure
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
# Standard library
|
||||
from typing import Optional, Dict
|
||||
from typing import Optional
|
||||
|
||||
# Logging
|
||||
from aipass.prax import logger
|
||||
|
||||
@@ -10,7 +10,6 @@ import json
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, Optional
|
||||
import sys
|
||||
import inspect
|
||||
|
||||
# Logging
|
||||
@@ -121,7 +120,7 @@ def ensure_json_exists(module_name: str, json_type: str) -> bool:
|
||||
if validate_json_structure(data, json_type):
|
||||
return True
|
||||
else:
|
||||
pass # Corrupted - regenerating
|
||||
logger.warning(f"Corrupted JSON structure at {json_path}, regenerating")
|
||||
except Exception as e:
|
||||
logger.warning(f"Unreadable JSON at {json_path}, regenerating: {e}")
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
OpenRouter Client Handler
|
||||
|
||||
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:
|
||||
- get_response() - Main API call with tracking integration
|
||||
@@ -34,8 +34,6 @@ Standards:
|
||||
"""
|
||||
|
||||
# INFRASTRUCTURE IMPORT PATTERN
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Standard library imports
|
||||
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
|
||||
"""
|
||||
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
|
||||
|
||||
api_params = {
|
||||
@@ -242,9 +241,11 @@ def extract_response(response: Any) -> Optional[Dict[str, Any]]:
|
||||
try:
|
||||
# Validate response structure
|
||||
if not hasattr(response, 'choices') or not response.choices:
|
||||
logger.warning("Response missing 'choices' or choices is empty")
|
||||
return None
|
||||
|
||||
if not hasattr(response.choices[0], 'message'):
|
||||
logger.warning("Response choice missing 'message' attribute")
|
||||
return None
|
||||
|
||||
# Extract content
|
||||
|
||||
@@ -25,12 +25,11 @@ COMPLIANT STANDARDS:
|
||||
- Under 300 lines
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, Optional, Tuple
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
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)
|
||||
"""
|
||||
return {
|
||||
"skill_name": "openrouter",
|
||||
"module_name": "openrouter",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"config": {
|
||||
"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
|
||||
"""
|
||||
return {
|
||||
"skill_name": "openrouter",
|
||||
"module_name": "openrouter",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"data": {
|
||||
"total_requests": 0,
|
||||
@@ -141,7 +140,7 @@ def get_default_caller_log() -> Dict[str, Any]:
|
||||
Dict with empty log structure
|
||||
"""
|
||||
return {
|
||||
"skill_name": "openrouter",
|
||||
"module_name": "openrouter",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"logs": []
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ Business logic for aggregating usage statistics:
|
||||
- Cost, token, and latency aggregation
|
||||
- Model usage tracking and breakdown
|
||||
|
||||
Extracted from legacy archive (api_usage.py).
|
||||
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
|
||||
- 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
|
||||
|
||||
# Standard library imports
|
||||
@@ -147,7 +145,8 @@ def get_generation_metrics(generation_id: str, api_key: str) -> Optional[Dict[st
|
||||
|
||||
# Query the generation endpoint
|
||||
response = requests.get(
|
||||
f"{GENERATION_ENDPOINT}?id={generation_id}",
|
||||
GENERATION_ENDPOINT,
|
||||
params={"id": generation_id},
|
||||
headers=headers,
|
||||
timeout=30
|
||||
)
|
||||
@@ -158,7 +157,7 @@ def get_generation_metrics(generation_id: str, api_key: str) -> Optional[Dict[st
|
||||
|
||||
# Validate response structure
|
||||
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
|
||||
|
||||
# Extract metrics from response
|
||||
@@ -176,7 +175,7 @@ def get_generation_metrics(generation_id: str, api_key: str) -> Optional[Dict[st
|
||||
return result
|
||||
|
||||
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
|
||||
|
||||
except requests.exceptions.Timeout as e:
|
||||
|
||||
@@ -20,10 +20,9 @@ from pathlib import Path
|
||||
|
||||
from typing import List
|
||||
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.auth import keys, env
|
||||
from aipass.api.apps.handlers.config import provider
|
||||
|
||||
|
||||
def print_introspection():
|
||||
@@ -110,7 +109,8 @@ def get_key(args: List[str]):
|
||||
|
||||
if api_key:
|
||||
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:
|
||||
error(f"Failed to retrieve API key for {provider_name}")
|
||||
|
||||
|
||||
@@ -124,11 +124,6 @@ def handle_command(command: str, args: List[str]) -> bool:
|
||||
Returns:
|
||||
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)
|
||||
if not args:
|
||||
if command == "google":
|
||||
@@ -140,6 +135,12 @@ def handle_command(command: str, args: List[str]) -> bool:
|
||||
if args[0] != "google":
|
||||
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":
|
||||
_cmd_validate()
|
||||
return True
|
||||
|
||||
@@ -17,11 +17,10 @@ Orchestrates LLM API client operations:
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from typing import List
|
||||
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.auth import keys
|
||||
from aipass.api.apps.handlers.openrouter import client, models
|
||||
@@ -188,7 +187,42 @@ def make_call(args: List[str]):
|
||||
header("OpenRouter API Call")
|
||||
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):
|
||||
|
||||
@@ -21,7 +21,7 @@ from pathlib import Path
|
||||
|
||||
from typing import List
|
||||
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.usage import tracking, aggregation, cleanup
|
||||
|
||||
@@ -174,7 +174,22 @@ def track_usage(args: List[str]):
|
||||
header("Track API Usage")
|
||||
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():
|
||||
|
||||
@@ -137,10 +137,10 @@ def test_handle_command_routes_reauth_google(
|
||||
def test_handle_command_help_gate(
|
||||
_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
|
||||
|
||||
result = google_client.handle_command("validate", ["--help"])
|
||||
result = google_client.handle_command("validate", ["google", "--help"])
|
||||
|
||||
assert result is True
|
||||
# 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}.console")
|
||||
def test_make_call_shows_todo_warning(mock_console, mock_header, mock_warning):
|
||||
"""make_call currently shows a TODO warning."""
|
||||
def test_make_call_no_model_shows_error(mock_console, mock_header, mock_error):
|
||||
"""make_call without --model shows error."""
|
||||
from aipass.api.apps.modules import openrouter_client
|
||||
|
||||
openrouter_client.make_call(["What is AI?"])
|
||||
|
||||
mock_warning.assert_called_once()
|
||||
assert "TODO" in mock_warning.call_args[0][0]
|
||||
mock_error.assert_called_once()
|
||||
assert "Model required" in mock_error.call_args[0][0]
|
||||
|
||||
|
||||
# =============================================
|
||||
|
||||
@@ -60,7 +60,7 @@ def test_create_caller_config_defaults(tmp_path: Path):
|
||||
assert config["config"]["ai_max_tokens"] == 4000
|
||||
assert config["config"]["enabled"] is True
|
||||
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):
|
||||
@@ -87,7 +87,7 @@ def test_create_caller_config_log_defaults(tmp_path: Path):
|
||||
log = read_json(json_folder / "openrouter_skill_log.json")
|
||||
assert log is not None
|
||||
assert log["logs"] == []
|
||||
assert log["skill_name"] == "openrouter"
|
||||
assert log["module_name"] == "openrouter"
|
||||
|
||||
|
||||
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."""
|
||||
config = get_default_caller_config()
|
||||
|
||||
assert "skill_name" in config
|
||||
assert "module_name" in config
|
||||
assert "timestamp" in config
|
||||
assert "config" in config
|
||||
assert isinstance(config["config"], dict)
|
||||
|
||||
@@ -44,85 +44,6 @@
|
||||
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."
|
||||
},
|
||||
{
|
||||
"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": {
|
||||
@@ -138,6 +59,7 @@
|
||||
"lines": "Optional - specific line numbers to bypass",
|
||||
"pattern": "Optional - pattern to match (e.g. 'if __name__')",
|
||||
"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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,14 +32,31 @@ _COMMANDS = ("pr", "status", "sync", "lock", "unlock")
|
||||
|
||||
|
||||
def _detect_branch_dir() -> tuple[str, Path] | None:
|
||||
"""Detect caller's branch from CWD. Returns (branch_name, branch_dir) or None."""
|
||||
cwd = Path.cwd()
|
||||
parts = cwd.parts
|
||||
for i, part in enumerate(parts):
|
||||
if part == "aipass" and i > 0 and parts[i - 1] == "src" and i + 1 < len(parts):
|
||||
branch_name = parts[i + 1]
|
||||
branch_dir = Path(*parts[: i + 2])
|
||||
return branch_name, branch_dir
|
||||
"""Detect caller's branch from CWD via passport lookup.
|
||||
|
||||
Walks up from CWD looking for ``.trinity/passport.json`` and extracts
|
||||
the branch name + directory. Works for any registered branch regardless
|
||||
of where it lives on disk (commons, skills, aipass sub-dirs, etc.).
|
||||
"""
|
||||
current = Path.cwd().resolve()
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -585,24 +585,52 @@ class TestGitModuleRouting:
|
||||
class TestDetectBranchDir:
|
||||
"""Branch directory detection tests."""
|
||||
|
||||
def test_detects_branch_from_path(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Detects branch name and dir from a path like .../src/aipass/drone/..."""
|
||||
# Simulate CWD being inside a branch
|
||||
fake_path = Path("/home/user/Projects/AIPass/src/aipass/drone/apps/modules")
|
||||
monkeypatch.chdir("/") # Just need a valid dir for the test
|
||||
with patch("aipass.drone.apps.modules.git_module.Path.cwd", return_value=fake_path):
|
||||
detected = _detect_branch_dir()
|
||||
def test_detects_branch_from_passport(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Detects branch name and dir by walking up to .trinity/passport.json."""
|
||||
# Create a fake branch directory with passport
|
||||
branch_dir = tmp_path / "mybranch"
|
||||
trinity = branch_dir / ".trinity"
|
||||
trinity.mkdir(parents=True)
|
||||
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
|
||||
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:
|
||||
"""Returns None when CWD is not under src/aipass/."""
|
||||
"""Returns None when CWD has no .trinity/passport.json above it."""
|
||||
monkeypatch.chdir(tmp_path)
|
||||
detected = _detect_branch_dir()
|
||||
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:
|
||||
"""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-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",
|
||||
"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"
|
||||
}
|
||||
@@ -41,6 +41,13 @@
|
||||
"subject": "Monitor Phase 2: caller attribution via drone CALLER markers",
|
||||
"date_closed": "2026-03-19",
|
||||
"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.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
from typing import Dict, Optional
|
||||
from aipass.prax import logger
|
||||
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:
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
lines = content.split('\n')
|
||||
except Exception as e:
|
||||
logger.info("Cannot read %s: %s", path, e)
|
||||
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]:
|
||||
"""
|
||||
@@ -286,8 +241,9 @@ def check_constant_naming(content: str) -> Optional[Dict]:
|
||||
|
||||
Rules:
|
||||
- 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 __dunder__ variables (PEP 8 convention: always lowercase)
|
||||
"""
|
||||
# First pass: collect imported names to exclude from constant checking
|
||||
imported_names = set()
|
||||
@@ -306,13 +262,34 @@ def check_constant_naming(content: str) -> Optional[Dict]:
|
||||
# Direct import like "console"
|
||||
imported_names.add(item.strip())
|
||||
|
||||
# Second pass: find module-level assignments (not inside functions/classes)
|
||||
module_lines = _iter_module_level_lines(content.split('\n'))
|
||||
# Second pass: find TRUE module-level assignments
|
||||
# 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 = []
|
||||
bad_constants = []
|
||||
in_multiline_string = False
|
||||
|
||||
for stripped in module_lines:
|
||||
# Find assignments at module level
|
||||
for line in content.split('\n'):
|
||||
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('#'):
|
||||
continue
|
||||
|
||||
@@ -324,6 +301,10 @@ def check_constant_naming(content: str) -> Optional[Dict]:
|
||||
const_name = match.group(1)
|
||||
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)
|
||||
if const_name in imported_names:
|
||||
continue
|
||||
|
||||
@@ -97,7 +97,7 @@ def deep_merge(template_data: Any, existing_data: Any) -> Any:
|
||||
def backup_json(file_path: Path) -> Path:
|
||||
"""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.
|
||||
|
||||
Args:
|
||||
@@ -116,7 +116,7 @@ def backup_json(file_path: Path) -> Path:
|
||||
raise FileNotFoundError(f"Cannot backup — file not found: {file_path}")
|
||||
|
||||
# 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)
|
||||
|
||||
# Timestamped backup name
|
||||
|
||||
@@ -615,7 +615,7 @@ class TestBackupJson:
|
||||
"""Tests for backup_json()."""
|
||||
|
||||
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
|
||||
|
||||
source = tmp_path / "data.json"
|
||||
@@ -624,7 +624,7 @@ class TestBackupJson:
|
||||
backup_path = backup_json(source)
|
||||
|
||||
assert backup_path.exists()
|
||||
assert ".backup" in str(backup_path)
|
||||
assert ".recovery" in str(backup_path)
|
||||
assert "data.json" in backup_path.name
|
||||
assert ".backup" in backup_path.name
|
||||
|
||||
|
||||
Reference in New Issue
Block a user