cadence fresh-context reset + aipass misroute guidance: SessionStart wiring (handler/config/setup.sh), loaders period-5, kernel+navmap aipass-exception, guide-not-crash (drone/aipass)

This commit is contained in:
AIOSAI
2026-07-07 20:02:45 -07:00
parent 47b5b2d871
commit 5fd8c6a015
17 changed files with 467 additions and 7 deletions
+8
View File
@@ -106,6 +106,14 @@ def main():
if route_command(command, remaining, modules):
return 0
if command.startswith("@"):
print(f"{command} is a drone routing target, not an aipass command.")
print("aipass is your front-door CLI; drone is the agent router — two separate tools.")
print()
print(f" Reach an agent: drone {command} ... · drone systems")
print(" aipass commands: aipass --help")
return 1
print(f"Unknown command: {command}")
return 1
@@ -252,6 +252,38 @@ class TestMain:
assert result == 0
mod.handle_command.assert_called_once_with("doctor", [])
def test_at_prefix_shows_drone_guidance(self) -> None:
"""@drone prints guidance pointing to drone, not 'Unknown command'."""
with patch("aipass.aipass.apps.aipass.sys.argv", ["aipass", "@drone"]):
with patch("aipass.aipass.apps.aipass.discover_modules", return_value=[]):
with patch("builtins.print") as mock_print:
result = main()
assert result == 1
printed = " ".join(str(a) for call in mock_print.call_args_list for a in call[0])
assert "@drone" in printed
assert "drone routing target" in printed
assert "Unknown command" not in printed
def test_at_prefix_uses_actual_name(self) -> None:
"""@memory prints guidance with the actual @name the user typed."""
with patch("aipass.aipass.apps.aipass.sys.argv", ["aipass", "@memory"]):
with patch("aipass.aipass.apps.aipass.discover_modules", return_value=[]):
with patch("builtins.print") as mock_print:
result = main()
assert result == 1
printed = " ".join(str(a) for call in mock_print.call_args_list for a in call[0])
assert "@memory" in printed
assert "drone @memory" in printed
def test_plain_bad_command_still_unknown(self) -> None:
"""Non-@ bad command still prints 'Unknown command', not drone guidance."""
with patch("aipass.aipass.apps.aipass.sys.argv", ["aipass", "frobnicate"]):
with patch("aipass.aipass.apps.aipass.discover_modules", return_value=[]):
with patch("builtins.print") as mock_print:
result = main()
assert result == 1
mock_print.assert_called_with("Unknown command: frobnicate")
def test_command_with_remaining_args(self) -> None:
"""Remaining args are passed to route_command."""
mod = MagicMock()
+13
View File
@@ -568,6 +568,19 @@ def main() -> int:
if command == "rm":
return _handle_rm(args[1:])
# aipass is a user-facing CLI, not a drone-routable branch
if command.lstrip("@") == "aipass":
err_console.print(
"aipass isn't reachable through drone — it's your own front-door CLI,"
" the AIPass concierge (onboarding, doctor, help, OS/system questions)."
" drone routes the agent citizens (@git, @devpulse, @memory...);"
" aipass is separate and serves you directly.\n"
"\n"
" Use aipass: aipass · aipass --help\n"
" See agents: drone systems"
)
return 1
# @target — route to branch or module
if command.startswith("@"):
return _handle_target(args)
@@ -839,3 +839,67 @@ class TestCliEntryPoint:
):
cli_main()
assert exc_info.value.code == 0
# ===========================================================================
# aipass intercept — drone aipass / drone @aipass
# ===========================================================================
class TestAipassIntercept:
"""'aipass' is a user CLI, not a drone-routable branch."""
def test_bare_aipass_shows_guidance(self, capsys: pytest.CaptureFixture[str]) -> None:
"""'drone aipass' prints guidance to stderr."""
from aipass.drone.apps.drone import main
with patch("sys.argv", ["drone", "aipass"]):
result = main()
assert result == 1
captured = capsys.readouterr()
assert "aipass isn't reachable through drone" in captured.err
assert "aipass --help" in captured.err
def test_at_aipass_shows_guidance(self, capsys: pytest.CaptureFixture[str]) -> None:
"""'drone @aipass' prints guidance to stderr."""
from aipass.drone.apps.drone import main
with patch("sys.argv", ["drone", "@aipass"]):
result = main()
assert result == 1
captured = capsys.readouterr()
assert "aipass isn't reachable through drone" in captured.err
assert "drone systems" in captured.err
def test_bare_aipass_no_traceback(self, capsys: pytest.CaptureFixture[str]) -> None:
"""No python traceback leaks on 'drone aipass'."""
from aipass.drone.apps.drone import main
with patch("sys.argv", ["drone", "aipass"]):
result = main()
assert result == 1
captured = capsys.readouterr()
assert "Traceback" not in captured.err
assert "ModuleNotFoundError" not in captured.err
def test_at_aipass_no_at_misdirect(self, capsys: pytest.CaptureFixture[str]) -> None:
"""No 'use @aipass' misdirect on 'drone @aipass'."""
from aipass.drone.apps.drone import main
with patch("sys.argv", ["drone", "@aipass"]):
result = main()
assert result == 1
captured = capsys.readouterr()
assert "Use '@aipass'" not in captured.err
def test_real_branch_still_routes(self) -> None:
"""Real branches still route normally after aipass intercept."""
from aipass.drone.apps.drone import main
with (
patch("sys.argv", ["drone", "@git", "status"]),
patch(f"{_DRONE}.is_module", return_value=True),
patch(f"{_DRONE}.route_module_command", return_value={"stdout": "ok", "stderr": "", "exit_code": 0}),
):
result = main()
assert result == 0
+35
View File
@@ -241,6 +241,21 @@
"standard": "json_structure",
"reason": "Delegates to @memory's auto_process() via importlib \u2014 no direct JSON file ops needing json_handler."
},
{
"file": "apps/handlers/lifecycle/session_start.py",
"standard": "dead_code",
"reason": "Invoked dynamically by engine via importlib from hooks.json handler path 'aipass.hooks.apps.handlers.lifecycle.session_start.handle' \u2014 not statically imported by design. Wired in SessionStart.cadence_reset."
},
{
"file": "apps/handlers/lifecycle/session_start.py",
"standard": "unused_function",
"reason": "handle() called dynamically by engine._run_handler via importlib.import_module + getattr from hooks.json. Wired in SessionStart.cadence_reset."
},
{
"file": "apps/handlers/lifecycle/session_start.py",
"standard": "json_structure",
"reason": "Delegates to cadence.reset_counter() via importlib \u2014 no direct JSON file ops needing json_handler."
},
{
"file": "apps/modules/cadence.py",
"standard": "dead_code",
@@ -1068,6 +1083,26 @@
"file": "tests/test_session_boot.py",
"standard": "encapsulation",
"reason": "Tests import handlers directly to test implementation details."
},
{
"file": "tests/test_session_start.py",
"standard": "architecture",
"reason": "Test files live in tests/, not in the 3-layer apps structure."
},
{
"file": "tests/test_session_start.py",
"standard": "documentation",
"reason": "Test methods use descriptive names as documentation per pytest convention."
},
{
"file": "tests/test_session_start.py",
"standard": "encapsulation",
"reason": "Tests import handlers directly to test implementation details."
},
{
"file": "tests/test_session_start.py",
"standard": "meta",
"reason": "Test files do not need Version/Modified metadata headers."
}
],
"notes": {
+2 -1
View File
@@ -73,7 +73,8 @@ src/aipass/hooks/
│ │ │ ├── auto_fix.py # Post-edit diagnostics (ruff, pyright, py_compile)
│ │ │ ├── auto_watchdog.py # Watchdog arming after dispatch
│ │ │ ├── compact.py # Pre-compact memory archival
│ │ │ └── rollover.py # Pre-compact memory rollover
│ │ │ ├── rollover.py # Pre-compact memory rollover
│ │ │ └── session_start.py # Cadence reset on new chat / clear (SessionStart)
│ │ └── notification/ # Sound/alert hooks
│ │ ├── announce.py # Announcement tone on notification
│ │ ├── email.py # Inbox check on prompt
@@ -0,0 +1,41 @@
# =================== AIPass ====================
# Name: session_start.py
# Version: 1.0.0
# Description: Resets cadence counter on new chat / clear (SessionStart)
# Branch: hooks
# Layer: apps/handlers/lifecycle
# Created: 2026-07-07
# Modified: 2026-07-07
# =============================================
"""Resets cadence counter on SessionStart so loaders re-fire at turn 0.
Fires on source=startup (new chat) and source=clear (/clear).
Skips source=resume — restored context already carries grounding.
source=compact is already handled by PreCompact; a duplicate reset is
harmless (idempotent), so we allow it rather than adding a fragile gate.
"""
import importlib
from aipass.prax.apps.modules.logger import system_logger as logger
_SKIP_SOURCES = frozenset({"resume"})
def handle(hook_data: dict) -> dict:
"""Reset cadence counter unless this is a resume."""
source = hook_data.get("source", "")
if source in _SKIP_SOURCES:
logger.info("[HOOKS] session_start: skipped cadence reset (source=%s)", source)
return {"stdout": "", "exit_code": 0}
try:
cadence = importlib.import_module("aipass.hooks.apps.modules.cadence")
cadence.reset_counter(hook_data=hook_data)
logger.info("[HOOKS] session_start: cadence reset (source=%s)", source)
except Exception as exc:
logger.info("[HOOKS] session_start: cadence reset failed: %s", exc)
return {"stdout": "", "exit_code": 0}
+1 -1
View File
@@ -45,7 +45,7 @@ DEFAULTS = {
"enabled": True,
"period": 5,
"loaders": {
"tier0": {"period": 1},
"tier0": {"period": 5, "offset": 0},
"navmap": {"period": 5, "offset": 0},
"branch": {"offset": 0},
},
+4 -2
View File
@@ -332,7 +332,7 @@ class TestConfig:
with patch(f"{MODULE}._CONFIG_PATH", tmp_path / "nonexistent.json"):
config = _load_config()
assert config["loaders"]["tier0"]["period"] == 1
assert config["loaders"]["tier0"]["period"] == 5
assert config["loaders"]["navmap"]["period"] == 5
assert config["loaders"]["navmap"]["offset"] == 0
@@ -709,7 +709,9 @@ class TestPostCompactDeterminism:
def test_compact_handler_calls_reset_with_hook_data(self):
from aipass.hooks.apps.handlers.lifecycle.compact import handle
hook_data = {"cwd": "/tmp/fake", "session_id": "test-123"}
import tempfile
hook_data = {"cwd": tempfile.gettempdir() + "/fake", "session_id": "test-123"}
with (
patch("importlib.import_module") as mock_import,
@@ -0,0 +1,216 @@
# =================== AIPass ====================
# Name: test_session_start.py
# Version: 1.0.0
# Description: Tests for SessionStart cadence reset handler
# Branch: hooks
# Created: 2026-07-07
# Modified: 2026-07-07
# =============================================
"""Tests for apps/handlers/lifecycle/session_start.py."""
import json
import os
from unittest.mock import patch
CADENCE_MODULE = "aipass.hooks.apps.modules.cadence"
def _reset_cadence_globals():
import aipass.hooks.apps.modules.cadence as mod
mod._turn = None
mod._config = None
def _write_state(tmp_path, turn, session="test-session"):
import time
state_file = tmp_path / f"aipass-cadence-{session}.json"
state_file.write_text(json.dumps({"turn": turn, "token": -1}))
old = time.time() - 10
os.utime(state_file, (old, old))
return state_file
class TestSessionStartHandler:
def setup_method(self):
_reset_cadence_globals()
def test_startup_resets_cadence(self, tmp_path):
from aipass.hooks.apps.handlers.lifecycle.session_start import handle
state_file = _write_state(tmp_path, turn=7)
with (
patch(f"{CADENCE_MODULE}._GUARD_DIR", tmp_path),
patch.dict("os.environ", {"CLAUDE_CODE_SESSION_ID": "test-session"}),
):
result = handle({"source": "startup", "session_id": "test-session"})
assert result["exit_code"] == 0
data = json.loads(state_file.read_text())
assert data["turn"] == -1
def test_clear_resets_cadence(self, tmp_path):
from aipass.hooks.apps.handlers.lifecycle.session_start import handle
state_file = _write_state(tmp_path, turn=3)
with (
patch(f"{CADENCE_MODULE}._GUARD_DIR", tmp_path),
patch.dict("os.environ", {"CLAUDE_CODE_SESSION_ID": "test-session"}),
):
result = handle({"source": "clear", "session_id": "test-session"})
assert result["exit_code"] == 0
data = json.loads(state_file.read_text())
assert data["turn"] == -1
def test_resume_skips_reset(self, tmp_path):
from aipass.hooks.apps.handlers.lifecycle.session_start import handle
state_file = _write_state(tmp_path, turn=7)
with (
patch(f"{CADENCE_MODULE}._GUARD_DIR", tmp_path),
patch.dict("os.environ", {"CLAUDE_CODE_SESSION_ID": "test-session"}),
):
result = handle({"source": "resume", "session_id": "test-session"})
assert result["exit_code"] == 0
data = json.loads(state_file.read_text())
assert data["turn"] == 7
def test_compact_source_resets(self, tmp_path):
"""source=compact is idempotent with PreCompact — allowed."""
from aipass.hooks.apps.handlers.lifecycle.session_start import handle
state_file = _write_state(tmp_path, turn=5)
with (
patch(f"{CADENCE_MODULE}._GUARD_DIR", tmp_path),
patch.dict("os.environ", {"CLAUDE_CODE_SESSION_ID": "test-session"}),
):
result = handle({"source": "compact", "session_id": "test-session"})
assert result["exit_code"] == 0
data = json.loads(state_file.read_text())
assert data["turn"] == -1
def test_empty_source_resets(self, tmp_path):
from aipass.hooks.apps.handlers.lifecycle.session_start import handle
state_file = _write_state(tmp_path, turn=4)
with (
patch(f"{CADENCE_MODULE}._GUARD_DIR", tmp_path),
patch.dict("os.environ", {"CLAUDE_CODE_SESSION_ID": "test-session"}),
):
result = handle({"session_id": "test-session"})
assert result["exit_code"] == 0
data = json.loads(state_file.read_text())
assert data["turn"] == -1
def test_no_stdout_output(self):
from aipass.hooks.apps.handlers.lifecycle.session_start import handle
with patch("importlib.import_module"):
result = handle({"source": "startup"})
assert result["stdout"] == ""
def test_cadence_import_failure_does_not_crash(self):
from aipass.hooks.apps.handlers.lifecycle.session_start import handle
with patch("importlib.import_module", side_effect=ImportError("boom")):
result = handle({"source": "startup"})
assert result["exit_code"] == 0
class TestSessionStartCadenceIntegration:
"""End-to-end: SessionStart reset -> next turn fires all loaders."""
def setup_method(self):
_reset_cadence_globals()
def test_clear_then_all_loaders_fire(self, tmp_path):
from aipass.hooks.apps.handlers.lifecycle.session_start import handle
from aipass.hooks.apps.modules.cadence import should_fire
config = tmp_path / "cadence.json"
config.write_text(
json.dumps(
{
"enabled": True,
"period": 5,
"loaders": {
"tier0": {"period": 5, "offset": 0},
"navmap": {"period": 5, "offset": 0},
"branch": {"offset": 0},
},
}
)
)
_write_state(tmp_path, turn=3)
with (
patch(f"{CADENCE_MODULE}._GUARD_DIR", tmp_path),
patch.dict("os.environ", {"CLAUDE_CODE_SESSION_ID": "test-session"}),
patch(f"{CADENCE_MODULE}._CONFIG_PATH", config),
):
handle({"source": "clear", "session_id": "test-session"})
_reset_cadence_globals()
with (
patch(f"{CADENCE_MODULE}._GUARD_DIR", tmp_path),
patch.dict("os.environ", {"CLAUDE_CODE_SESSION_ID": "test-session"}),
patch(f"{CADENCE_MODULE}._CONFIG_PATH", config),
):
assert should_fire("tier0") is True
_reset_cadence_globals()
assert should_fire("navmap") is True
_reset_cadence_globals()
assert should_fire("branch") is True
def test_resume_does_not_reset_counter_continues(self, tmp_path):
from aipass.hooks.apps.handlers.lifecycle.session_start import handle
from aipass.hooks.apps.modules.cadence import should_fire
config = tmp_path / "cadence.json"
config.write_text(
json.dumps(
{
"enabled": True,
"period": 5,
"loaders": {
"tier0": {"period": 5, "offset": 0},
"navmap": {"period": 5, "offset": 0},
},
}
)
)
_write_state(tmp_path, turn=2)
with (
patch(f"{CADENCE_MODULE}._GUARD_DIR", tmp_path),
patch.dict("os.environ", {"CLAUDE_CODE_SESSION_ID": "test-session"}),
patch(f"{CADENCE_MODULE}._CONFIG_PATH", config),
):
handle({"source": "resume", "session_id": "test-session"})
_reset_cadence_globals()
with (
patch(f"{CADENCE_MODULE}._GUARD_DIR", tmp_path),
patch.dict("os.environ", {"CLAUDE_CODE_SESSION_ID": "test-session"}),
patch(f"{CADENCE_MODULE}._CONFIG_PATH", config),
):
assert should_fire("tier0") is False
_reset_cadence_globals()
assert should_fire("navmap") is False