feat(hooks): tiered prompt injection — Tier 0 kernel + Tier 1 navmap by cadence (FPLAN-0284 P2-P4, DPLAN-0214)

Replaces the single 8k always-injected global prompt with cadence-tiered injection:
- Tier 0 (.aipass/tier0_kernel.md, ~2k) injects EVERY turn — identity grounding, the drone --help reflex, disaster-preventer rules. Folds DPLAN-0213 C1 (faithful-reporting) + C2 (no-gold-plating sub-agent brief).
- Tier 1 (.aipass/tier1_navmap.md, ~7.7k) injects every 5th turn + session-start + post-compaction — full agent roster, framework, conventions, plus a new Terminology section migrated from the S211 backup.
- Old global_prompt loader retired (disabled in hooks.json, removed from cadence wiring); aipass_global_prompt.md kept as a reference snapshot.

Engine (built by @hooks): per-loader period in cadence.py should_fire() (back-compatible: unset period falls back to global); new tier0_kernel + navmap handlers; bypass.json extended to cover the two new dynamically-dispatched handlers. 614/614 tests pass, seedgo @hooks 100%.

Live-verified: tier0 fires every turn, navmap on turn 0/5/10, turn counter advances once per turn (not per loader), no double-injection. Machine-local wiring (cadence_config.json, ~/.claude/settings.json bridge) updated on this host; fresh-clone seeding of those is a tracked follow-up.

PROMPT_STYLE.md reference updated to point at the tier files as canonical examples.
This commit is contained in:
AIOSAI
2026-06-18 17:48:35 -07:00
parent b698dd8ce0
commit 2c91f79f2f
13 changed files with 738 additions and 9 deletions
+30
View File
@@ -86,6 +86,36 @@
"standard": "json_structure",
"reason": "No JSON operations \u2014 reads markdown file and outputs text."
},
{
"file": "apps/handlers/prompt/tier0_kernel.py",
"standard": "dead_code",
"reason": "Invoked dynamically by engine via importlib from hooks.json handler path 'aipass.hooks.apps.handlers.prompt.tier0_kernel.handle' - not statically imported by design. Wired in UserPromptSubmit.tier0_kernel (FPLAN-0284). Same pattern as global_loader.py."
},
{
"file": "apps/handlers/prompt/tier0_kernel.py",
"standard": "unused_function",
"reason": "handle() called dynamically by engine._run_handler via importlib.import_module + getattr from hooks.json. Wired in UserPromptSubmit.tier0_kernel (FPLAN-0284)."
},
{
"file": "apps/handlers/prompt/tier0_kernel.py",
"standard": "json_structure",
"reason": "No JSON operations - reads markdown file (.aipass/tier0_kernel.md) and outputs text."
},
{
"file": "apps/handlers/prompt/navmap.py",
"standard": "dead_code",
"reason": "Invoked dynamically by engine via importlib from hooks.json handler path 'aipass.hooks.apps.handlers.prompt.navmap.handle' - not statically imported by design. Wired in UserPromptSubmit.navmap (FPLAN-0284). Same pattern as global_loader.py."
},
{
"file": "apps/handlers/prompt/navmap.py",
"standard": "unused_function",
"reason": "handle() called dynamically by engine._run_handler via importlib.import_module + getattr from hooks.json. Wired in UserPromptSubmit.navmap (FPLAN-0284)."
},
{
"file": "apps/handlers/prompt/navmap.py",
"standard": "json_structure",
"reason": "No JSON operations - reads markdown file (.aipass/tier1_navmap.md) and outputs text."
},
{
"file": "apps/handlers/security/edit_gate.py",
"standard": "dead_code",
@@ -0,0 +1,59 @@
# =================== AIPass ====================
# Name: navmap.py
# Version: 1.0.0
# Description: Tier 1 navigation map — periodic prompt injection (UserPromptSubmit)
# Branch: hooks
# Layer: apps/handlers/prompt
# Created: 2026-06-18
# Modified: 2026-06-18
# =============================================
"""Loads .aipass/tier1_navmap.md — richer navigation map injected periodically."""
import os
from pathlib import Path
from aipass.prax.apps.modules.logger import system_logger as logger
def _find_project_file(filename: str) -> Path | None:
"""Walk up from CWD to find the nearest .aipass/<filename>."""
cwd = Path.cwd()
for parent in [cwd, *cwd.parents]:
candidate = parent / ".aipass" / filename
if candidate.is_file():
return candidate
if parent == parent.parent:
break
return None
def handle(hook_data: dict) -> dict:
"""Load tier1 navmap — periodic (cadence period 5) + turn 0 + post-compaction."""
try:
import importlib
cadence = importlib.import_module("aipass.hooks.apps.modules.cadence")
if not cadence.should_fire("navmap", hook_data):
return {"stdout": "", "exit_code": 0}
except Exception as exc:
logger.info("[HOOKS] navmap: cadence check failed, firing anyway: %s", exc)
try:
aipass_home = os.environ.get("AIPASS_HOME", "")
cwd = str(Path.cwd())
if aipass_home and cwd.startswith(aipass_home):
prompt_file = Path(aipass_home) / ".aipass" / "tier1_navmap.md"
else:
prompt_file = _find_project_file("tier1_navmap.md")
if not prompt_file or not prompt_file.exists():
return {"stdout": "", "exit_code": 0}
content = prompt_file.read_text(encoding="utf-8")
return {"stdout": content, "exit_code": 0, "sound": "navmap"}
except Exception as exc:
logger.info("[HOOKS] navmap: unexpected error: %s", exc)
return {"stdout": "", "exit_code": 0}
@@ -0,0 +1,59 @@
# =================== AIPass ====================
# Name: tier0_kernel.py
# Version: 1.0.0
# Description: Tier 0 kernel — always-on minimal prompt injection (UserPromptSubmit)
# Branch: hooks
# Layer: apps/handlers/prompt
# Created: 2026-06-18
# Modified: 2026-06-18
# =============================================
"""Loads .aipass/tier0_kernel.md — tiny always-on identity + reflex block."""
import os
from pathlib import Path
from aipass.prax.apps.modules.logger import system_logger as logger
def _find_project_file(filename: str) -> Path | None:
"""Walk up from CWD to find the nearest .aipass/<filename>."""
cwd = Path.cwd()
for parent in [cwd, *cwd.parents]:
candidate = parent / ".aipass" / filename
if candidate.is_file():
return candidate
if parent == parent.parent:
break
return None
def handle(hook_data: dict) -> dict:
"""Load tier0 kernel — every turn (cadence period 1)."""
try:
import importlib
cadence = importlib.import_module("aipass.hooks.apps.modules.cadence")
if not cadence.should_fire("tier0", hook_data):
return {"stdout": "", "exit_code": 0}
except Exception as exc:
logger.info("[HOOKS] tier0_kernel: cadence check failed, firing anyway: %s", exc)
try:
aipass_home = os.environ.get("AIPASS_HOME", "")
cwd = str(Path.cwd())
if aipass_home and cwd.startswith(aipass_home):
prompt_file = Path(aipass_home) / ".aipass" / "tier0_kernel.md"
else:
prompt_file = _find_project_file("tier0_kernel.md")
if not prompt_file or not prompt_file.exists():
return {"stdout": "", "exit_code": 0}
content = prompt_file.read_text(encoding="utf-8")
return {"stdout": content, "exit_code": 0, "sound": "tier0 kernel"}
except Exception as exc:
logger.info("[HOOKS] tier0_kernel: unexpected error: %s", exc)
return {"stdout": "", "exit_code": 0}
+9 -5
View File
@@ -196,13 +196,15 @@ def should_fire(loader_name: str, hook_data: dict | None = None) -> bool:
if not config.get("enabled", True):
return True
period = config.get("period", 5)
if period <= 0:
return True
global_period = config.get("period", 5)
loader_config = config.get("loaders", {}).get(loader_name, {})
period = loader_config.get("period", global_period)
offset = loader_config.get("offset", 0)
if period <= 0:
return True
turn = _load_and_increment(hook_data or {})
fired = turn == 0 or (turn % period) == offset
@@ -256,10 +258,12 @@ def print_introspection() -> None:
config = _load_config()
CONSOLE.print("[bold cyan]cadence[/bold cyan] Module")
CONSOLE.print(f" Enabled: {config.get('enabled', True)}")
CONSOLE.print(f" Period: {config.get('period', 5)} turns")
global_period = config.get("period", 5)
CONSOLE.print(f" Period: {global_period} turns (global default)")
loaders = config.get("loaders", {})
for name, lcfg in loaders.items():
CONSOLE.print(f" Loader '{name}': offset={lcfg.get('offset', 0)}")
lp = lcfg.get("period", global_period)
CONSOLE.print(f" Loader '{name}': period={lp} offset={lcfg.get('offset', 0)}")
path = _state_path()
if path and path.exists():
try:
+178
View File
@@ -489,3 +489,181 @@ class TestLoaderCadenceGuard:
assert result["stdout"] == ""
assert result["exit_code"] == 0
assert "sound" not in result
class TestPerLoaderPeriod:
def setup_method(self):
_reset_module_globals()
def test_loader_period_overrides_global(self, tmp_path):
"""A loader with period:1 fires every turn, even when global period is 5."""
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": 1}, "global": {"offset": 0}},
}
)
)
_write_state(tmp_path, turn=2)
with (
patch(f"{MODULE}._GUARD_DIR", tmp_path),
patch.dict("os.environ", {"CLAUDE_CODE_SESSION_ID": "test-session"}),
patch(f"{MODULE}._CONFIG_PATH", config),
):
assert should_fire("tier0") is True
_reset_module_globals()
assert should_fire("global") is False
def test_loader_without_period_uses_global(self, tmp_path):
from aipass.hooks.apps.modules.cadence import should_fire
config = tmp_path / "cadence.json"
config.write_text(
json.dumps(
{
"enabled": True,
"period": 5,
"loaders": {"global": {"offset": 0}},
}
)
)
_write_state(tmp_path, turn=2)
with (
patch(f"{MODULE}._GUARD_DIR", tmp_path),
patch.dict("os.environ", {"CLAUDE_CODE_SESSION_ID": "test-session"}),
patch(f"{MODULE}._CONFIG_PATH", config),
):
assert should_fire("global") is False
def test_tier0_period_1_fires_every_turn(self, tmp_path):
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": 1}},
}
)
)
for turn_val in range(1, 8):
_reset_module_globals()
_write_state(tmp_path, turn=turn_val - 1)
with (
patch(f"{MODULE}._GUARD_DIR", tmp_path),
patch.dict("os.environ", {"CLAUDE_CODE_SESSION_ID": "test-session"}),
patch(f"{MODULE}._CONFIG_PATH", config),
):
assert should_fire("tier0") is True, f"tier0 should fire on turn {turn_val}"
def test_navmap_period_5_skips_non_fire_turns(self, tmp_path):
from aipass.hooks.apps.modules.cadence import should_fire
config = tmp_path / "cadence.json"
config.write_text(
json.dumps(
{
"enabled": True,
"period": 5,
"loaders": {"navmap": {"period": 5, "offset": 0}},
}
)
)
_write_state(tmp_path, turn=2)
with (
patch(f"{MODULE}._GUARD_DIR", tmp_path),
patch.dict("os.environ", {"CLAUDE_CODE_SESSION_ID": "test-session"}),
patch(f"{MODULE}._CONFIG_PATH", config),
):
assert should_fire("navmap") is False
def test_navmap_fires_on_turn_0(self, tmp_path):
from aipass.hooks.apps.modules.cadence import should_fire
config = tmp_path / "cadence.json"
config.write_text(
json.dumps(
{
"enabled": True,
"period": 5,
"loaders": {"navmap": {"period": 5, "offset": 0}},
}
)
)
with (
patch(f"{MODULE}._GUARD_DIR", tmp_path),
patch.dict("os.environ", {"CLAUDE_CODE_SESSION_ID": "test-session"}),
patch(f"{MODULE}._CONFIG_PATH", config),
):
assert should_fire("navmap") is True
def test_navmap_fires_after_reset_counter(self, tmp_path):
from aipass.hooks.apps.modules.cadence import should_fire, reset_counter
config = tmp_path / "cadence.json"
config.write_text(
json.dumps(
{
"enabled": True,
"period": 5,
"loaders": {"navmap": {"period": 5, "offset": 0}},
}
)
)
_write_state(tmp_path, turn=7)
with (
patch(f"{MODULE}._GUARD_DIR", tmp_path),
patch.dict("os.environ", {"CLAUDE_CODE_SESSION_ID": "test-session"}),
patch(f"{MODULE}._CONFIG_PATH", config),
):
reset_counter()
_reset_module_globals()
with (
patch(f"{MODULE}._GUARD_DIR", tmp_path),
patch.dict("os.environ", {"CLAUDE_CODE_SESSION_ID": "test-session"}),
patch(f"{MODULE}._CONFIG_PATH", config),
):
assert should_fire("navmap") is True
def test_per_loader_period_zero_always_fires(self, tmp_path):
from aipass.hooks.apps.modules.cadence import should_fire
config = tmp_path / "cadence.json"
config.write_text(
json.dumps(
{
"enabled": True,
"period": 5,
"loaders": {"always": {"period": 0}},
}
)
)
_write_state(tmp_path, turn=2)
with (
patch(f"{MODULE}._GUARD_DIR", tmp_path),
patch.dict("os.environ", {"CLAUDE_CODE_SESSION_ID": "test-session"}),
patch(f"{MODULE}._CONFIG_PATH", config),
):
assert should_fire("always") is True
+127
View File
@@ -0,0 +1,127 @@
# =================== AIPass ====================
# Name: test_navmap.py
# Version: 1.0.0
# Description: Tests for navmap prompt handler
# Branch: hooks
# Created: 2026-06-18
# Modified: 2026-06-18
# =============================================
"""Tests for handlers/prompt/navmap.py."""
from unittest.mock import patch, MagicMock
def _mock_cadence(fires: bool = True):
"""Return a mock cadence module with configurable should_fire."""
mock = MagicMock()
mock.should_fire.return_value = fires
return mock
class TestNavmapHandler:
def test_loads_navmap(self, tmp_path, monkeypatch):
from aipass.hooks.apps.handlers.prompt.navmap import handle
aipass_dir = tmp_path / ".aipass"
aipass_dir.mkdir()
prompt = aipass_dir / "tier1_navmap.md"
prompt.write_text("# Navigation Map\nAgents and services", encoding="utf-8")
monkeypatch.chdir(tmp_path)
with patch.dict("os.environ", {"AIPASS_HOME": str(tmp_path)}):
with patch("importlib.import_module", return_value=_mock_cadence(True)):
result = handle({})
assert result["exit_code"] == 0
assert "Navigation Map" in result["stdout"]
assert "Agents and services" in result["stdout"]
assert result["sound"] == "navmap"
def test_returns_empty_when_file_missing(self, tmp_path, monkeypatch):
from aipass.hooks.apps.handlers.prompt.navmap import handle
monkeypatch.chdir(tmp_path)
with patch.dict("os.environ", {"AIPASS_HOME": str(tmp_path)}):
with patch("importlib.import_module", return_value=_mock_cadence(True)):
result = handle({})
assert result["exit_code"] == 0
assert result["stdout"] == ""
assert "sound" not in result
def test_empty_hook_data(self, tmp_path, monkeypatch):
from aipass.hooks.apps.handlers.prompt.navmap import handle
aipass_dir = tmp_path / ".aipass"
aipass_dir.mkdir()
(aipass_dir / "tier1_navmap.md").write_text("navmap content", encoding="utf-8")
monkeypatch.chdir(tmp_path)
with patch.dict("os.environ", {"AIPASS_HOME": str(tmp_path)}):
with patch("importlib.import_module", return_value=_mock_cadence(True)):
result = handle({})
assert result["exit_code"] == 0
assert result["stdout"] == "navmap content"
def test_skips_on_cadence_skip(self, tmp_path, monkeypatch):
from aipass.hooks.apps.handlers.prompt.navmap import handle
aipass_dir = tmp_path / ".aipass"
aipass_dir.mkdir()
(aipass_dir / "tier1_navmap.md").write_text("content", encoding="utf-8")
monkeypatch.chdir(tmp_path)
with patch.dict("os.environ", {"AIPASS_HOME": str(tmp_path)}):
with patch("importlib.import_module", return_value=_mock_cadence(False)):
result = handle({})
assert result["stdout"] == ""
assert result["exit_code"] == 0
assert "sound" not in result
def test_fires_anyway_on_cadence_error(self, tmp_path, monkeypatch):
from aipass.hooks.apps.handlers.prompt.navmap import handle
aipass_dir = tmp_path / ".aipass"
aipass_dir.mkdir()
(aipass_dir / "tier1_navmap.md").write_text("navmap content", encoding="utf-8")
monkeypatch.chdir(tmp_path)
with patch.dict("os.environ", {"AIPASS_HOME": str(tmp_path)}):
with patch("importlib.import_module", side_effect=ImportError("no cadence")):
result = handle({})
assert result["exit_code"] == 0
assert "navmap content" in result["stdout"]
def test_external_project_gets_own_file(self, tmp_path, monkeypatch):
from aipass.hooks.apps.handlers.prompt.navmap import handle
project = tmp_path / "my-project"
project.mkdir()
aipass_dir = project / ".aipass"
aipass_dir.mkdir()
(aipass_dir / "tier1_navmap.md").write_text("# Project Navmap", encoding="utf-8")
monkeypatch.chdir(project)
with patch.dict("os.environ", {"AIPASS_HOME": "/some/other/path"}):
with patch("importlib.import_module", return_value=_mock_cadence(True)):
result = handle({})
assert result["exit_code"] == 0
assert "Project Navmap" in result["stdout"]
assert result["sound"] == "navmap"
def test_cadence_called_with_navmap_name(self, tmp_path, monkeypatch):
from aipass.hooks.apps.handlers.prompt.navmap import handle
monkeypatch.chdir(tmp_path)
mock = _mock_cadence(False)
with patch.dict("os.environ", {"AIPASS_HOME": str(tmp_path)}):
with patch("importlib.import_module", return_value=mock):
handle({"some": "data"})
mock.should_fire.assert_called_once_with("navmap", {"some": "data"})
+127
View File
@@ -0,0 +1,127 @@
# =================== AIPass ====================
# Name: test_tier0_kernel.py
# Version: 1.0.0
# Description: Tests for tier0_kernel prompt handler
# Branch: hooks
# Created: 2026-06-18
# Modified: 2026-06-18
# =============================================
"""Tests for handlers/prompt/tier0_kernel.py."""
from unittest.mock import patch, MagicMock
def _mock_cadence(fires: bool = True):
"""Return a mock cadence module with configurable should_fire."""
mock = MagicMock()
mock.should_fire.return_value = fires
return mock
class TestTier0KernelHandler:
def test_loads_tier0_kernel(self, tmp_path, monkeypatch):
from aipass.hooks.apps.handlers.prompt.tier0_kernel import handle
aipass_dir = tmp_path / ".aipass"
aipass_dir.mkdir()
prompt = aipass_dir / "tier0_kernel.md"
prompt.write_text("# Tier 0 Kernel\nAlways on", encoding="utf-8")
monkeypatch.chdir(tmp_path)
with patch.dict("os.environ", {"AIPASS_HOME": str(tmp_path)}):
with patch("importlib.import_module", return_value=_mock_cadence(True)):
result = handle({})
assert result["exit_code"] == 0
assert "Tier 0 Kernel" in result["stdout"]
assert "Always on" in result["stdout"]
assert result["sound"] == "tier0 kernel"
def test_returns_empty_when_file_missing(self, tmp_path, monkeypatch):
from aipass.hooks.apps.handlers.prompt.tier0_kernel import handle
monkeypatch.chdir(tmp_path)
with patch.dict("os.environ", {"AIPASS_HOME": str(tmp_path)}):
with patch("importlib.import_module", return_value=_mock_cadence(True)):
result = handle({})
assert result["exit_code"] == 0
assert result["stdout"] == ""
assert "sound" not in result
def test_empty_hook_data(self, tmp_path, monkeypatch):
from aipass.hooks.apps.handlers.prompt.tier0_kernel import handle
aipass_dir = tmp_path / ".aipass"
aipass_dir.mkdir()
(aipass_dir / "tier0_kernel.md").write_text("content", encoding="utf-8")
monkeypatch.chdir(tmp_path)
with patch.dict("os.environ", {"AIPASS_HOME": str(tmp_path)}):
with patch("importlib.import_module", return_value=_mock_cadence(True)):
result = handle({})
assert result["exit_code"] == 0
assert result["stdout"] == "content"
def test_skips_on_cadence_skip(self, tmp_path, monkeypatch):
from aipass.hooks.apps.handlers.prompt.tier0_kernel import handle
aipass_dir = tmp_path / ".aipass"
aipass_dir.mkdir()
(aipass_dir / "tier0_kernel.md").write_text("content", encoding="utf-8")
monkeypatch.chdir(tmp_path)
with patch.dict("os.environ", {"AIPASS_HOME": str(tmp_path)}):
with patch("importlib.import_module", return_value=_mock_cadence(False)):
result = handle({})
assert result["stdout"] == ""
assert result["exit_code"] == 0
assert "sound" not in result
def test_fires_anyway_on_cadence_error(self, tmp_path, monkeypatch):
from aipass.hooks.apps.handlers.prompt.tier0_kernel import handle
aipass_dir = tmp_path / ".aipass"
aipass_dir.mkdir()
(aipass_dir / "tier0_kernel.md").write_text("kernel content", encoding="utf-8")
monkeypatch.chdir(tmp_path)
with patch.dict("os.environ", {"AIPASS_HOME": str(tmp_path)}):
with patch("importlib.import_module", side_effect=ImportError("no cadence")):
result = handle({})
assert result["exit_code"] == 0
assert "kernel content" in result["stdout"]
def test_external_project_gets_own_file(self, tmp_path, monkeypatch):
from aipass.hooks.apps.handlers.prompt.tier0_kernel import handle
project = tmp_path / "my-project"
project.mkdir()
aipass_dir = project / ".aipass"
aipass_dir.mkdir()
(aipass_dir / "tier0_kernel.md").write_text("# Project Kernel", encoding="utf-8")
monkeypatch.chdir(project)
with patch.dict("os.environ", {"AIPASS_HOME": "/some/other/path"}):
with patch("importlib.import_module", return_value=_mock_cadence(True)):
result = handle({})
assert result["exit_code"] == 0
assert "Project Kernel" in result["stdout"]
assert result["sound"] == "tier0 kernel"
def test_cadence_called_with_tier0_name(self, tmp_path, monkeypatch):
from aipass.hooks.apps.handlers.prompt.tier0_kernel import handle
monkeypatch.chdir(tmp_path)
mock = _mock_cadence(False)
with patch.dict("os.environ", {"AIPASS_HOME": str(tmp_path)}):
with patch("importlib.import_module", return_value=mock):
handle({"some": "data"})
mock.should_fire.assert_called_once_with("tier0", {"some": "data"})