fix(system): S59-S60 cross-branch fixes — stale refs, queue spam, close bug, template (#141)
- Daemon: 14 stale @dev_central→@devpulse and @seed→@seedgo refs fixed across 12 files - Prax: event_queue spam eliminated (144k logs/4hrs), cortex→spawn refs, bypass added - Flow: close_ops hardened with shutil.move + auto-healing for orphaned plans - Spawn: builder template handler guard populated, conftest registry protection added - Seedgo: handler guard pytest exception, test_json_handler fix - Commons: extract_mentions dedup bug (SELECT DISTINCT LOWER) - 7 branches: @seed→@seedgo in handlers/__init__.py - Memory: @seed→@seedgo handler ref 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
6f2333ec67
commit
ae459853f0
@@ -97,3 +97,4 @@ src/aipass/memory/config/fragmented_memory_state.json
|
||||
.vscode
|
||||
src/aipass/memory/config/memory_bank.config.json
|
||||
src/aipass/spawn/templates/builder/.spawn/.template_registry.json
|
||||
branch_audits _only
|
||||
|
||||
@@ -102,7 +102,7 @@ def _guard_branch_access():
|
||||
f" from {MY_BRANCH}.apps.modules.logger import logger\n"
|
||||
f"\n"
|
||||
f" For full standards guide:\n"
|
||||
f" drone @seed handlers\n"
|
||||
f" drone @seedgo handlers\n"
|
||||
f"{'='*60}"
|
||||
)
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ def _guard_branch_access():
|
||||
f" from {MY_BRANCH}.apps.modules.logger import logger\n"
|
||||
f"\n"
|
||||
f" For full standards guide:\n"
|
||||
f" drone @seed handlers\n"
|
||||
f" drone @seedgo handlers\n"
|
||||
f"{'='*60}"
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
"""Pytest configuration for backup branch.
|
||||
|
||||
Excludes backup data directories from test collection to prevent
|
||||
pytest from recursing into system_snapshot and versioned_backup
|
||||
which contain full copies of the AIPass repo tree.
|
||||
"""
|
||||
|
||||
collect_ignore_glob = [
|
||||
"backups/*",
|
||||
]
|
||||
@@ -123,7 +123,7 @@ def _guard_branch_access():
|
||||
f" from {MY_BRANCH}.apps.modules.logger import logger\n"
|
||||
f"\n"
|
||||
f" For full standards guide:\n"
|
||||
f" drone @seed handlers\n"
|
||||
f" drone @seedgo handlers\n"
|
||||
f"{'='*60}"
|
||||
)
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ def _guard_branch_access():
|
||||
f" from {MY_BRANCH}.apps.modules.logger import logger\n"
|
||||
f"\n"
|
||||
f" For full standards guide:\n"
|
||||
f" drone @seed handlers\n"
|
||||
f" drone @seedgo handlers\n"
|
||||
f"{'='*60}"
|
||||
)
|
||||
|
||||
|
||||
@@ -182,7 +182,7 @@ def _dispatch_action(
|
||||
message=action.get("prompt", name),
|
||||
from_branch='@daemon',
|
||||
auto_execute=True,
|
||||
reply_to='@dev_central',
|
||||
reply_to='@devpulse',
|
||||
)
|
||||
if email_sent:
|
||||
assert mark_reminder_completed is not None
|
||||
|
||||
@@ -112,7 +112,7 @@ def create_action(
|
||||
name: Human-readable action name (e.g., "daily_audit")
|
||||
action_type: "plugin" | "schedule" | "reminder"
|
||||
schedule_type: "daily" | "hourly" | "interval" | "once"
|
||||
target_branch: Target branch email (e.g., "@seed")
|
||||
target_branch: Target branch email (e.g., "@seedgo")
|
||||
prompt: What the dispatched agent should do
|
||||
time: For daily: "HH:MM", for hourly: "MM"
|
||||
interval_minutes: For interval schedule type
|
||||
|
||||
@@ -179,7 +179,7 @@ def create_task(
|
||||
Args:
|
||||
task: Brief description of the task/follow-up
|
||||
due_date: When to trigger (supports "7d", "1w", "YYYY-MM-DD")
|
||||
recipient: Target branch (e.g., "@dev_central")
|
||||
recipient: Target branch (e.g., "@devpulse")
|
||||
message: Message to deliver when due
|
||||
|
||||
Returns:
|
||||
@@ -501,7 +501,7 @@ def process_due_tasks_batch(
|
||||
message=email_body,
|
||||
from_branch='@daemon',
|
||||
auto_execute=True,
|
||||
reply_to='@dev_central',
|
||||
reply_to='@devpulse',
|
||||
)
|
||||
|
||||
if email_sent:
|
||||
@@ -574,7 +574,7 @@ if __name__ == "__main__":
|
||||
test_task = create_task(
|
||||
task="Test backup health check",
|
||||
due_date="7d",
|
||||
recipient="@dev_central",
|
||||
recipient="@devpulse",
|
||||
message="Please verify backup systems are healthy"
|
||||
)
|
||||
console.print(f" Created task: {test_task['id']}")
|
||||
|
||||
@@ -207,7 +207,7 @@ def print_help() -> None:
|
||||
console.print()
|
||||
|
||||
console.print("[yellow]SET SCHEDULE:[/yellow]")
|
||||
console.print(' set schedule @seed "Run audit" daily 04:00')
|
||||
console.print(' set schedule @seedgo "Run audit" daily 04:00')
|
||||
console.print(' set schedule @vera "Heartbeat" interval 240')
|
||||
console.print(' set schedule @flow "Check plans" hourly 30')
|
||||
console.print(" [dim]Types: daily HH:MM, hourly MM, interval MINUTES[/dim]")
|
||||
@@ -264,7 +264,7 @@ def _handle_set_reminder(args: List[str]) -> bool:
|
||||
|
||||
date_str = args[0]
|
||||
message = args[1]
|
||||
target_branch = "@dev_central" # Default reminder target
|
||||
target_branch = "@devpulse" # Default reminder target
|
||||
|
||||
# Parse --to flag
|
||||
if "--to" in args:
|
||||
|
||||
@@ -155,7 +155,7 @@ def _print_help() -> None:
|
||||
|
||||
console.print("[yellow]CREATE OPTIONS:[/yellow]")
|
||||
console.print(" --due (Required) Due date: 1d, 7d, 2w, 1m, or ISO date (2026-02-15)")
|
||||
console.print(" --to (Required) Recipient branch (e.g., @flow, @seed)")
|
||||
console.print(" --to (Required) Recipient branch (e.g., @flow, @seedgo)")
|
||||
console.print(" --message (Optional) Additional details for the follow-up")
|
||||
console.print()
|
||||
|
||||
@@ -164,7 +164,7 @@ def _print_help() -> None:
|
||||
console.print(' schedule create "Check FPLAN-0290 status" --due 7d --to @flow')
|
||||
console.print()
|
||||
console.print(' # Follow up with Seed about code review in 2 weeks')
|
||||
console.print(' schedule create "Code review follow-up" --due 2w --to @seed --message "Review PR #45"')
|
||||
console.print(' schedule create "Code review follow-up" --due 2w --to @seedgo --message "Review PR #45"')
|
||||
console.print()
|
||||
console.print(' # Check all due tasks and send reminder emails')
|
||||
console.print(' schedule run-due')
|
||||
|
||||
@@ -32,7 +32,7 @@ PLUGIN_CONFIG Schema:
|
||||
"time": str | None, # For daily: "HH:MM" (24h). For hourly: minute "MM"
|
||||
"interval_minutes": int | None, # For interval: minutes between runs
|
||||
"enabled": bool, # Plugin active/inactive toggle
|
||||
"branch": str, # Target branch email (e.g., "@seed")
|
||||
"branch": str, # Target branch email (e.g., "@seedgo")
|
||||
"fresh": bool, # True = fresh session, False = resume
|
||||
"max_turns": int, # Max agent turns (safety limit)
|
||||
"prompt": str, # What the spawned agent should do
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
"""
|
||||
Daily Standards Audit Plugin
|
||||
|
||||
Wakes @seed daily at 04:00 with fresh context to run a full system audit.
|
||||
Seed checks BRANCH_REGISTRY completeness, runs drone @seed audit @all,
|
||||
fixes non-compliance issues, and emails a summary to @dev_central.
|
||||
Wakes @seedgo daily at 04:00 with fresh context to run a full system audit.
|
||||
Seed checks BRANCH_REGISTRY completeness, runs drone @seedgo audit @all,
|
||||
fixes non-compliance issues, and emails a summary to @devpulse.
|
||||
"""
|
||||
|
||||
from aipass.prax import logger
|
||||
@@ -22,15 +22,15 @@ PLUGIN_CONFIG = {
|
||||
"time": "04:00",
|
||||
"interval_minutes": None,
|
||||
"enabled": True,
|
||||
"branch": "@seed",
|
||||
"branch": "@seedgo",
|
||||
"fresh": True,
|
||||
"max_turns": 50,
|
||||
"prompt": (
|
||||
"Daily maintenance audit. "
|
||||
"1) Read BRANCH_REGISTRY.json - confirm all branches are registered and paths exist. "
|
||||
"2) Run drone @seed audit @all - check standards compliance across all branches. "
|
||||
"2) Run drone @seedgo audit @all - check standards compliance across all branches. "
|
||||
"3) Fix any non-compliance issues you can fix directly. "
|
||||
"4) Email summary to @dev_central with: branches audited, pass/fail counts, "
|
||||
"4) Email summary to @devpulse with: branches audited, pass/fail counts, "
|
||||
"issues found, issues fixed, remaining issues. "
|
||||
"5) Update your memories with audit results."
|
||||
),
|
||||
|
||||
@@ -228,7 +228,7 @@ def _process_single_task(task: dict, results: dict) -> None:
|
||||
message=email_body,
|
||||
from_branch='@daemon',
|
||||
auto_execute=True,
|
||||
reply_to='@dev_central',
|
||||
reply_to='@devpulse',
|
||||
)
|
||||
|
||||
if email_sent:
|
||||
|
||||
@@ -57,7 +57,7 @@ class TestCreate:
|
||||
name="test_audit",
|
||||
action_type="schedule",
|
||||
schedule_type="daily",
|
||||
target_branch="@seed",
|
||||
target_branch="@seedgo",
|
||||
prompt="Run audit",
|
||||
time="04:00",
|
||||
fresh=True,
|
||||
@@ -68,7 +68,7 @@ class TestCreate:
|
||||
assert action["type"] == "schedule"
|
||||
assert action["schedule_type"] == "daily"
|
||||
assert action["time"] == "04:00"
|
||||
assert action["target_branch"] == "@seed"
|
||||
assert action["target_branch"] == "@seedgo"
|
||||
assert action["enabled"] is True
|
||||
assert action["last_run"] is None
|
||||
assert action["completed"] is None
|
||||
@@ -88,7 +88,7 @@ class TestCreate:
|
||||
name="Check VERA progress",
|
||||
action_type="reminder",
|
||||
schedule_type="once",
|
||||
target_branch="@dev_central",
|
||||
target_branch="@devpulse",
|
||||
prompt="Check VERA progress",
|
||||
due_date="2026-03-11",
|
||||
)
|
||||
|
||||
@@ -174,11 +174,11 @@ class TestCreateTask:
|
||||
task = create_task(
|
||||
task="Check backup health",
|
||||
due_date="7d",
|
||||
recipient="@dev_central",
|
||||
recipient="@devpulse",
|
||||
message="Verify backup systems",
|
||||
)
|
||||
assert task["task"] == "Check backup health"
|
||||
assert task["recipient"] == "@dev_central"
|
||||
assert task["recipient"] == "@devpulse"
|
||||
assert task["message"] == "Verify backup systems"
|
||||
assert task["status"] == "pending"
|
||||
assert len(task["id"]) == 16
|
||||
@@ -192,7 +192,7 @@ class TestCreateTask:
|
||||
create_task(
|
||||
task="persisted task",
|
||||
due_date="1d",
|
||||
recipient="@seed",
|
||||
recipient="@seedgo",
|
||||
message="msg",
|
||||
)
|
||||
raw = json.loads(isolate_registry.read_text(encoding="utf-8"))
|
||||
|
||||
@@ -25,6 +25,7 @@ _PKG_ROOT = Path(__file__).resolve().parents[4]
|
||||
|
||||
# Standard imports
|
||||
import json
|
||||
import shutil
|
||||
from datetime import datetime, timezone
|
||||
from typing import Dict, List, Any
|
||||
|
||||
@@ -421,8 +422,8 @@ def archive_plan(plan_path: Path) -> bool:
|
||||
# Store source path for verification
|
||||
source_path = Path(plan_path)
|
||||
|
||||
# Attempt move
|
||||
plan_path.rename(destination)
|
||||
# Attempt move (shutil.move handles cross-filesystem moves)
|
||||
shutil.move(str(plan_path), str(destination))
|
||||
|
||||
# VERIFICATION LAYER: Confirm move actually happened
|
||||
if not destination.exists():
|
||||
|
||||
@@ -228,6 +228,15 @@ def close_plan_impl(plan_num: Any = None, confirm: bool = False,
|
||||
from aipass.flow.apps.handlers.mbank.process import archive_plan
|
||||
if archive_plan(plan_file):
|
||||
logger.info(f"[{MODULE_NAME}] Cleaned up orphaned file for {plan_label}: {plan_file}")
|
||||
# Update registry flags that were missed on the failed first close
|
||||
plan_info["processed"] = True
|
||||
plan_info["processed_date"] = datetime.now(timezone.utc).isoformat()
|
||||
plan_info["cleanup_completed"] = True
|
||||
plan_info["cleanup_date"] = datetime.now(timezone.utc).isoformat()
|
||||
if reg_file:
|
||||
save_registry(registry, registry_file=reg_file)
|
||||
else:
|
||||
save_registry(registry)
|
||||
messages.append({"type": "success", "text": " Orphaned file archived successfully"})
|
||||
else:
|
||||
logger.warning(f"[{MODULE_NAME}] Failed to archive orphaned file for {plan_label}: {plan_file}")
|
||||
|
||||
@@ -104,7 +104,7 @@ def _guard_branch_access():
|
||||
f" from {MY_BRANCH}.apps.modules.logger import logger\n"
|
||||
f"\n"
|
||||
f" For full standards guide:\n"
|
||||
f" drone @seed handlers\n"
|
||||
f" drone @seedgo handlers\n"
|
||||
f"{'='*60}"
|
||||
)
|
||||
|
||||
|
||||
@@ -134,5 +134,21 @@
|
||||
"DPLAN-0068_self_audit_compliance_sprint_2026-03-27.md": "2026-03-27T23:22:05.681987",
|
||||
"DPLAN-0070_self_audit_compliance_sprint_2026-03-27.md": "2026-03-27T23:22:41.790812",
|
||||
"DPLAN-0066_self_audit_compliance_sprint_2026-03-27.md": "2026-03-27T23:22:41.790824",
|
||||
"DPLAN-0072_self_audit_compliance_sprint_2026-03-28.md": "2026-03-28T00:32:51.757552"
|
||||
"DPLAN-0072_self_audit_compliance_sprint_2026-03-28.md": "2026-03-28T00:32:51.757552",
|
||||
"DPLAN-0052_branch_audit_sprint_silent_catch_wave_2026-03-23.md": "2026-03-28T12:27:09.971335",
|
||||
"DPLAN-0055_persistent_citizen_git_branches_2026-03-24.md": "2026-03-28T12:27:25.886644",
|
||||
"DPLAN-0057_night_shift_drone_compliance_branch_audits_sy_2026-03-24.md": "2026-03-28T12:27:38.467210",
|
||||
"DPLAN-0063_self_audit_compliance_sprint_2026-03-27.md": "2026-03-28T12:27:58.368678",
|
||||
"DPLAN-0064_self_audit_compliance_sprint_2026-03-27.md": "2026-03-28T12:28:15.007738",
|
||||
"DPLAN-0065_self_audit_compliance_sprint_2026-03-27.md": "2026-03-28T12:28:31.421009",
|
||||
"DPLAN-0069_100_seedgo_compliance_sprint_2026-03-27.md": "2026-03-28T12:28:47.782884",
|
||||
"DPLAN-0071_self_audit_compliance_sprint_2026-03-28.md": "2026-03-28T12:29:04.294942",
|
||||
"FPLAN-0134_persistent_citizen_git_branches_drone_build_2026-03-24.md": "2026-03-28T19:43:55.209451",
|
||||
"DPLAN-0018_commons_bug_investigation_fts5_search_visitor_2026-03-17.md": "2026-03-28T19:44:25.813768",
|
||||
"DPLAN-0019_module_rename_remove__module_suffix_from_comm_2026-03-17.md": "2026-03-28T19:44:44.216623",
|
||||
"DPLAN-0054_bypass_accountability_tracker_2026-03-23.md": "2026-03-28T19:51:55.904984",
|
||||
"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"
|
||||
}
|
||||
@@ -406,6 +406,11 @@
|
||||
"file": "apps/",
|
||||
"standard": "architecture",
|
||||
"reason": "Template files managed by spawn — prax cannot add/remove template files. Template structure is defined in spawn/templates/builder/ and applied at branch creation time."
|
||||
},
|
||||
{
|
||||
"file": "apps/handlers/monitoring/event_queue.py",
|
||||
"standard": "silent_catch",
|
||||
"reason": "queue.Empty is expected control flow from PriorityQueue.get(timeout=N), not an error. Logging it produces 144,000 noise entries per 4hrs of monitoring (10 polls/sec). The empty state IS the normal idle state."
|
||||
}
|
||||
],
|
||||
"notes": {
|
||||
|
||||
@@ -104,7 +104,7 @@ def _guard_branch_access():
|
||||
f" from {MY_BRANCH}.apps.modules.logger import logger\n"
|
||||
f"\n"
|
||||
f" For full standards guide:\n"
|
||||
f" drone @seed handlers\n"
|
||||
f" drone @seedgo handlers\n"
|
||||
f"{'='*60}"
|
||||
)
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ def get_calling_module() -> str:
|
||||
"""Detect calling module from stack trace
|
||||
|
||||
Returns:
|
||||
Module name (e.g., 'drone', 'flow', 'cortex') or 'unknown_module'
|
||||
Module name (e.g., 'drone', 'flow', 'spawn') or 'unknown_module'
|
||||
"""
|
||||
caller_path = _find_external_caller_path()
|
||||
if caller_path:
|
||||
|
||||
@@ -79,7 +79,6 @@ class MonitoringQueue:
|
||||
try:
|
||||
return self.queue.get(timeout=timeout)
|
||||
except Empty:
|
||||
logger.info("[event_queue] Queue empty on dequeue poll")
|
||||
return None
|
||||
|
||||
def flush(self):
|
||||
@@ -89,7 +88,6 @@ class MonitoringQueue:
|
||||
try:
|
||||
self.queue.get_nowait()
|
||||
except Empty:
|
||||
logger.info("[event_queue] Flush complete (queue drained)")
|
||||
break
|
||||
self.recent_events.clear()
|
||||
|
||||
|
||||
@@ -254,12 +254,12 @@ class LogFileWatcher(FileSystemEventHandler):
|
||||
elif "search" in log_line.lower() and "query" in log_line.lower():
|
||||
return {'command': "memory_bank search", 'caller': None, 'target': None}
|
||||
|
||||
# Pattern 10: Cortex operations (direct python3 calls)
|
||||
if "[cortex]" in log_line.lower():
|
||||
# Pattern 10: Spawn operations (direct python3 calls)
|
||||
if "[spawn]" in log_line.lower():
|
||||
if "Creating" in log_line and "branch" in log_line.lower():
|
||||
match = re.search(r"Creating\s+(?:branch\s+)?(\w+)", log_line)
|
||||
target = match.group(1).upper() if match else None
|
||||
return {'command': "cortex create branch", 'caller': None, 'target': target}
|
||||
return {'command': "spawn create branch", 'caller': None, 'target': target}
|
||||
|
||||
# Pattern 11: Trigger operations (direct python3 calls)
|
||||
if "[trigger]" in log_line.lower():
|
||||
|
||||
@@ -77,6 +77,10 @@ def _guard_branch_access():
|
||||
return # Allow command-line Python through
|
||||
return # Allow if truly can't determine
|
||||
|
||||
# Allow pytest — test files need to import handlers for testing
|
||||
if "pytest" in caller_file or "/_pytest/" in caller_file:
|
||||
return
|
||||
|
||||
# Check if caller is from our branch
|
||||
# MY_BRANCH is "aipass.seedgo" (dotted), but filesystem uses "/aipass/seedgo/"
|
||||
branch_path = "/" + MY_BRANCH.replace(".", "/") + "/"
|
||||
@@ -104,7 +108,7 @@ def _guard_branch_access():
|
||||
f" from {MY_BRANCH}.apps.modules.logger import logger\n"
|
||||
f"\n"
|
||||
f" For full standards guide:\n"
|
||||
f" drone @seed handlers\n"
|
||||
f" drone @seedgo handlers\n"
|
||||
f"{'='*60}"
|
||||
)
|
||||
|
||||
|
||||
@@ -645,11 +645,18 @@ def test_get_json_path_returns_pathlib_path(tmp_path: Path) -> None:
|
||||
|
||||
|
||||
def test_ensure_no_overwrite_existing(tmp_path: Path) -> None:
|
||||
"""no_overwrite: ensure_json_exists does not overwrite already_exists data."""
|
||||
"""no_overwrite: ensure_json_exists does not overwrite already_exists valid data."""
|
||||
json_dir = _json_dir_as_path(tmp_path)
|
||||
json_dir.mkdir(parents=True, exist_ok=True)
|
||||
target = json_dir / "preserve_config.json"
|
||||
target.write_text('{"custom": "data"}', encoding="utf-8")
|
||||
# Write a VALID config structure with an extra custom key
|
||||
valid_config = {
|
||||
"module_name": "preserve",
|
||||
"version": "1.0.0",
|
||||
"config": {"auto_save": True, "enabled": True},
|
||||
"custom": "data",
|
||||
}
|
||||
target.write_text(json.dumps(valid_config), encoding="utf-8")
|
||||
json_handler.ensure_json_exists("preserve", "config")
|
||||
data = json.loads(target.read_text(encoding="utf-8"))
|
||||
assert data.get("custom") == "data", "Must not overwrite existing file"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"metadata": {
|
||||
"version": "1.0.0",
|
||||
"last_updated": "2026-03-25",
|
||||
"last_updated": "2026-03-28",
|
||||
"description": "Template file tracking registry for ID-based updates"
|
||||
},
|
||||
"files": {
|
||||
@@ -17,6 +17,12 @@
|
||||
"content_hash": "c9702fe2cc21",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f015": {
|
||||
"path": ".aipass/README.md",
|
||||
"name": "README.md",
|
||||
"content_hash": "f42d87684fdf",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f003": {
|
||||
"path": ".aipass/aipass_local_prompt.md",
|
||||
"name": "aipass_local_prompt.md",
|
||||
@@ -29,6 +35,12 @@
|
||||
"content_hash": "93d3fcb74f23",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f025": {
|
||||
"path": ".claude/README.md",
|
||||
"name": "README.md",
|
||||
"content_hash": "adb0ce8c53c1",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f005": {
|
||||
"path": ".claude/settings.local.json",
|
||||
"name": "settings.local.json",
|
||||
@@ -41,12 +53,24 @@
|
||||
"content_hash": "841dedb922da",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f027": {
|
||||
"path": ".seedgo/README.md",
|
||||
"name": "README.md",
|
||||
"content_hash": "ea03468bbf16",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f007": {
|
||||
"path": ".seedgo/bypass.json",
|
||||
"name": "bypass.json",
|
||||
"content_hash": "0ac90a35515b",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f028": {
|
||||
"path": ".trinity/README.md",
|
||||
"name": "README.md",
|
||||
"content_hash": "f461c9b16fc5",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f008": {
|
||||
"path": ".trinity/local.json",
|
||||
"name": "local.json",
|
||||
@@ -83,102 +107,18 @@
|
||||
"content_hash": "059295ad4d4e",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f014": {
|
||||
"path": "apps/__init__.py",
|
||||
"name": "__init__.py",
|
||||
"content_hash": "41b011f487af",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f017": {
|
||||
"path": "apps/handlers/__init__.py",
|
||||
"name": "__init__.py",
|
||||
"content_hash": "e3b0c44298fc",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f018": {
|
||||
"path": "apps/{{BRANCH}}.py",
|
||||
"name": "{{BRANCH}}.py",
|
||||
"content_hash": "fc71b424c10b",
|
||||
"has_branch_placeholder": true
|
||||
},
|
||||
"f019": {
|
||||
"path": "artifacts/birth_certificate.json",
|
||||
"name": "birth_certificate.json",
|
||||
"content_hash": "1ec401f4e397",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f020": {
|
||||
"path": "docs/README.md",
|
||||
"name": "README.md",
|
||||
"content_hash": "2434da568727",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f021": {
|
||||
"path": "dropbox/README.md",
|
||||
"name": "README.md",
|
||||
"content_hash": "9e1e9b71f93b",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f022": {
|
||||
"path": "pytest.ini",
|
||||
"name": "pytest.ini",
|
||||
"content_hash": "7b39ba7bca40",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f023": {
|
||||
"path": "tests/__init__.py",
|
||||
"name": "__init__.py",
|
||||
"content_hash": "881f06bb6574",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f026": {
|
||||
"path": "{{BRANCH}}_json/README.md",
|
||||
"name": "README.md",
|
||||
"content_hash": "e64fa555e7b8",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f016": {
|
||||
"path": "apps/modules/__init__.py",
|
||||
"name": "__init__.py",
|
||||
"content_hash": "e3b0c44298fc",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f024": {
|
||||
"path": "tests/conftest.py",
|
||||
"name": "conftest.py",
|
||||
"content_hash": "5d98b049957b",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f015": {
|
||||
"path": ".aipass/README.md",
|
||||
"name": "README.md",
|
||||
"content_hash": "f42d87684fdf",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f025": {
|
||||
"path": ".claude/README.md",
|
||||
"name": "README.md",
|
||||
"content_hash": "adb0ce8c53c1",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f027": {
|
||||
"path": ".seedgo/README.md",
|
||||
"name": "README.md",
|
||||
"content_hash": "ea03468bbf16",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f028": {
|
||||
"path": ".trinity/README.md",
|
||||
"name": "README.md",
|
||||
"content_hash": "f461c9b16fc5",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f029": {
|
||||
"path": "apps/README.md",
|
||||
"name": "README.md",
|
||||
"content_hash": "92a956009e0e",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f014": {
|
||||
"path": "apps/__init__.py",
|
||||
"name": "__init__.py",
|
||||
"content_hash": "41b011f487af",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f030": {
|
||||
"path": "apps/handlers/README.md",
|
||||
"name": "README.md",
|
||||
@@ -191,17 +131,23 @@
|
||||
"content_hash": "a4cf0a8e3b4f",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f033": {
|
||||
"path": "apps/modules/__init__.py",
|
||||
"name": "__init__.py",
|
||||
"content_hash": "e3b0c44298fc",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f032": {
|
||||
"path": "apps/plugins/README.md",
|
||||
"name": "README.md",
|
||||
"content_hash": "d1e4e2b98c38",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f033": {
|
||||
"path": "apps/plugins/__init__.py",
|
||||
"name": "__init__.py",
|
||||
"content_hash": "e3b0c44298fc",
|
||||
"has_branch_placeholder": false
|
||||
"f018": {
|
||||
"path": "apps/{{BRANCH}}.py",
|
||||
"name": "{{BRANCH}}.py",
|
||||
"content_hash": "fc71b424c10b",
|
||||
"has_branch_placeholder": true
|
||||
},
|
||||
"f034": {
|
||||
"path": "artifacts/README.md",
|
||||
@@ -209,6 +155,18 @@
|
||||
"content_hash": "de20d11e5cfd",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f019": {
|
||||
"path": "artifacts/birth_certificate.json",
|
||||
"name": "birth_certificate.json",
|
||||
"content_hash": "1ec401f4e397",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f020": {
|
||||
"path": "docs/README.md",
|
||||
"name": "README.md",
|
||||
"content_hash": "2434da568727",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f035": {
|
||||
"path": "docs.local/README.md",
|
||||
"name": "README.md",
|
||||
@@ -221,12 +179,24 @@
|
||||
"content_hash": "e3e5a6b9c9c5",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f021": {
|
||||
"path": "dropbox/README.md",
|
||||
"name": "README.md",
|
||||
"content_hash": "9e1e9b71f93b",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f037": {
|
||||
"path": "logs/README.md",
|
||||
"name": "README.md",
|
||||
"content_hash": "4ca207af6bd3",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f022": {
|
||||
"path": "pytest.ini",
|
||||
"name": "pytest.ini",
|
||||
"content_hash": "7b39ba7bca40",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f038": {
|
||||
"path": "templates/README.md",
|
||||
"name": "README.md",
|
||||
@@ -239,17 +209,59 @@
|
||||
"content_hash": "c157895c9b27",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f023": {
|
||||
"path": "tests/__init__.py",
|
||||
"name": "__init__.py",
|
||||
"content_hash": "881f06bb6574",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f024": {
|
||||
"path": "tests/conftest.py",
|
||||
"name": "conftest.py",
|
||||
"content_hash": "5d98b049957b",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f040": {
|
||||
"path": "tools/README.md",
|
||||
"name": "README.md",
|
||||
"content_hash": "3c7eaedb16ac",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f026": {
|
||||
"path": "{{BRANCH}}_json/README.md",
|
||||
"name": "README.md",
|
||||
"content_hash": "e64fa555e7b8",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f041": {
|
||||
"path": "{{BRANCH}}_json/custom_config/README.md",
|
||||
"name": "README.md",
|
||||
"content_hash": "28e9ae373563",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f017": {
|
||||
"path": "apps/handlers/__init__.py",
|
||||
"name": "__init__.py",
|
||||
"content_hash": "55afc5eddbe0",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f016": {
|
||||
"path": ".spawn/.registry_ignore.json",
|
||||
"name": ".registry_ignore.json",
|
||||
"content_hash": "34f5e7ff7e01",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f042": {
|
||||
"path": ".spawn/README.md",
|
||||
"name": "README.md",
|
||||
"content_hash": "e22ad5337efd",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"f043": {
|
||||
"path": "apps/plugins/__init__.py",
|
||||
"name": "__init__.py",
|
||||
"content_hash": "e3b0c44298fc",
|
||||
"has_branch_placeholder": false
|
||||
}
|
||||
},
|
||||
"directories": {
|
||||
@@ -318,11 +330,26 @@
|
||||
"name": "docs",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"d019": {
|
||||
"path": "docs.local",
|
||||
"name": "docs.local",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"d020": {
|
||||
"path": "docs.local/sub_agent_drops",
|
||||
"name": "sub_agent_drops",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"d014": {
|
||||
"path": "dropbox",
|
||||
"name": "dropbox",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"d021": {
|
||||
"path": "logs",
|
||||
"name": "logs",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"d015": {
|
||||
"path": "templates",
|
||||
"name": "templates",
|
||||
@@ -343,25 +370,15 @@
|
||||
"name": "{{BRANCH}}_json",
|
||||
"has_branch_placeholder": true
|
||||
},
|
||||
"d019": {
|
||||
"path": "docs.local",
|
||||
"name": "docs.local",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"d020": {
|
||||
"path": "docs.local/sub_agent_drops",
|
||||
"name": "sub_agent_drops",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"d021": {
|
||||
"path": "logs",
|
||||
"name": "logs",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"d022": {
|
||||
"path": "{{BRANCH}}_json/custom_config",
|
||||
"name": "custom_config",
|
||||
"has_branch_placeholder": false
|
||||
},
|
||||
"d023": {
|
||||
"path": ".spawn",
|
||||
"name": ".spawn",
|
||||
"has_branch_placeholder": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
"""{{BRANCHNAME}} handlers package - Security protected."""
|
||||
|
||||
import inspect
|
||||
from pathlib import Path
|
||||
|
||||
MY_BRANCH = "aipass.{{BRANCH}}"
|
||||
|
||||
|
||||
def _find_real_caller():
|
||||
"""Walk the stack to find the actual file that triggered this import.
|
||||
|
||||
Skips this file, importlib internals, and frozen modules.
|
||||
Returns tuple: (file_path, import_line) or (None, None).
|
||||
"""
|
||||
stack = inspect.stack()
|
||||
this_file = str(Path(__file__).resolve())
|
||||
|
||||
for frame_info in stack:
|
||||
filename = frame_info.filename
|
||||
|
||||
if this_file in str(Path(filename).resolve()):
|
||||
continue
|
||||
|
||||
if filename.startswith("<") or "importlib" in filename:
|
||||
continue
|
||||
|
||||
import_line = None
|
||||
if frame_info.code_context:
|
||||
import_line = frame_info.code_context[0].strip()
|
||||
|
||||
return str(Path(filename).resolve()), import_line
|
||||
|
||||
return None, None
|
||||
|
||||
|
||||
def _extract_branch_name(filepath: str) -> str:
|
||||
"""Extract branch name from a file path."""
|
||||
parts = Path(filepath).parts
|
||||
for i, part in enumerate(parts):
|
||||
if part == "aipass":
|
||||
if i + 1 < len(parts):
|
||||
return parts[i + 1]
|
||||
return "unknown"
|
||||
|
||||
|
||||
def _guard_branch_access():
|
||||
"""Block cross-branch handler imports.
|
||||
|
||||
Only code from within the '{{BRANCH}}' branch can import these handlers.
|
||||
External branches must use aipass.{{BRANCH}}.apps.modules instead.
|
||||
"""
|
||||
caller_file, import_line = _find_real_caller()
|
||||
|
||||
if caller_file is None:
|
||||
stack = inspect.stack()
|
||||
for frame in stack:
|
||||
if frame.filename in ("<string>", "<stdin>"):
|
||||
return
|
||||
return
|
||||
|
||||
branch_path = "/" + MY_BRANCH.replace(".", "/") + "/"
|
||||
if branch_path in caller_file:
|
||||
return
|
||||
|
||||
caller_branch = _extract_branch_name(caller_file)
|
||||
caller_filename = Path(caller_file).name
|
||||
blocked_import = import_line if import_line else "unknown"
|
||||
|
||||
raise ImportError(
|
||||
f"\n{'='*60}\n"
|
||||
f"ACCESS DENIED: Cross-branch handler import blocked\n"
|
||||
f"{'='*60}\n"
|
||||
f" Caller branch: {caller_branch}\n"
|
||||
f" Caller file: {caller_filename}\n"
|
||||
f" Blocked: {blocked_import}\n"
|
||||
f"\n"
|
||||
f" Handlers are internal to their branch.\n"
|
||||
f" Use the module API instead:\n"
|
||||
f" from {MY_BRANCH}.apps.modules.<module> import <function>\n"
|
||||
f"\n"
|
||||
f" For full standards guide:\n"
|
||||
f" drone @seedgo handlers\n"
|
||||
f"{'='*60}"
|
||||
)
|
||||
|
||||
|
||||
# Run guard at import time
|
||||
_guard_branch_access()
|
||||
|
||||
@@ -1,11 +1,41 @@
|
||||
"""Shared test fixtures for spawn test suite."""
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Registry backup/restore — prevents test ghost entries in AIPASS_REGISTRY.json
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _find_registry_path() -> Path:
|
||||
"""Locate AIPASS_REGISTRY.json from the spawn branch."""
|
||||
return Path(__file__).resolve().parents[4] / "AIPASS_REGISTRY.json"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, scope="session")
|
||||
def _protect_registry():
|
||||
"""Backup AIPASS_REGISTRY.json before the test session, restore after.
|
||||
|
||||
Prevents tests that call spawn_agent/grant_passport without a
|
||||
registry_path override from permanently polluting the real registry.
|
||||
"""
|
||||
reg = _find_registry_path()
|
||||
backup = reg.with_suffix(".json.test_backup")
|
||||
|
||||
if reg.exists():
|
||||
shutil.copy2(reg, backup)
|
||||
|
||||
yield
|
||||
|
||||
if backup.exists():
|
||||
shutil.copy2(backup, reg)
|
||||
backup.unlink()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_data():
|
||||
"""Pre-populated JSON test data for spawn operations."""
|
||||
|
||||
@@ -297,7 +297,7 @@ def extract_mentions(content: str) -> List[str]:
|
||||
|
||||
conn = get_db()
|
||||
placeholders = ",".join("?" * len(mentioned))
|
||||
query = f"SELECT branch_name FROM agents WHERE LOWER(branch_name) IN ({placeholders})"
|
||||
query = f"SELECT DISTINCT LOWER(branch_name) FROM agents WHERE LOWER(branch_name) IN ({placeholders})"
|
||||
rows = conn.execute(query, mentioned).fetchall()
|
||||
close_db(conn)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user