diff --git a/AGENTS.md b/AGENTS.md index 4ff0224..c379092 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -156,6 +156,29 @@ get wrong. +## Experiential memory (WARDEN-WP-0024) + +ops-warden shares a **phase-memory** store across worker ticks, coding agent +sessions, and operator CLI use. + +**At session start** (Claude Code, Codex, Grok, or future agents): + +```bash +export WARDEN_AGENT_ID=grok # or claude, codex +warden memory activate --json +``` + +**During work:** use normal `warden route` / `warden access` / `warden sign`; +episodes are recorded automatically unless `WARDEN_MEMORY=0`. + +**Store:** `~/.local/share/warden/memory/` (override: `WARDEN_MEMORY_STORE`). + +**Worker:** `warden worker run --brain llm` skips OpenRouter when stabilized +routing memory matches. See `wiki/OpsWardenMemory.md`. + +Requires `phase-memory` on `PYTHONPATH` or installed; contract in +`phase-memory/docs/ops-warden-memory-contract.md`. + --- ## Workplan Convention (ADR-001) diff --git a/pyproject.toml b/pyproject.toml index e9bfa0f..3ca400f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,9 @@ dependencies = [ "httpx>=0.27", ] +[project.optional-dependencies] +memory = [] + [project.scripts] warden = "warden.cli:app" ops-ssh-wrapper = "warden.scripts.ops_ssh_wrapper:main" @@ -29,7 +32,7 @@ packages = ["src/warden"] [tool.pytest.ini_options] testpaths = ["tests"] -pythonpath = ["src"] +pythonpath = ["src", "../phase-memory/src"] addopts = "-m 'not integration'" markers = ["integration: requires ssh-keygen binary; run with pytest -m integration"] diff --git a/src/warden/cli.py b/src/warden/cli.py index bad95dc..db68823 100644 --- a/src/warden/cli.py +++ b/src/warden/cli.py @@ -44,6 +44,11 @@ activity_app = typer.Typer( no_args_is_help=True, ) app.add_typer(activity_app, name="activity") +memory_app = typer.Typer( + help="Cross-runtime experiential memory via phase-memory (WARDEN-WP-0024)", + no_args_is_help=True, +) +app.add_typer(memory_app, name="memory") console = Console() err = Console(stderr=True) @@ -53,6 +58,30 @@ err = Console(stderr=True) # Helpers # --------------------------------------------------------------------------- +def _record_memory_episode( + *, + command: str, + outcome: str, + need: str = "", + route_id: str = "", +) -> None: + try: + from warden import memory as warden_memory + except ImportError: + return + if not warden_memory.enabled() or not warden_memory.memory_available(): + return + try: + warden_memory.record_command_episode( + command=command, + outcome=outcome, + need=need, + route_id=route_id, + ) + except RuntimeError: + return + + def _load_cfg() -> WardenConfig: try: return load_config() @@ -128,6 +157,12 @@ def sign( # cert_command interface: write cert text to stdout only print(record.cert_path.read_text().strip()) + _record_memory_episode( + command="sign", + outcome="resolved", + need=f"ssh cert {actor_name}", + route_id="ops-warden-ssh-cert", + ) # --------------------------------------------------------------------------- @@ -753,14 +788,30 @@ def route_find( if output_json: print(json.dumps([_entry_summary(e) for e in matches], indent=2)) + if matches: + _record_memory_episode( + command="route find", + outcome="resolved", + need=query, + route_id=matches[0].id, + ) + else: + _record_memory_episode(command="route find", outcome="skipped", need=query) return if not matches: + _record_memory_episode(command="route find", outcome="skipped", need=query) console.print( f"No routing match for {query!r}. " "Try `warden route list --all` to browse all scenarios." ) return + _record_memory_episode( + command="route find", + outcome="resolved", + need=query, + route_id=matches[0].id, + ) _print_entry_table(matches, f"Matches for {query!r}") @@ -1003,8 +1054,20 @@ def access( if output_json: print(json.dumps(_access_json(entry, expanded, gate, domain), indent=2)) + _record_memory_episode( + command="access", + outcome="resolved", + need=need, + route_id=entry.id, + ) return + _record_memory_episode( + command="access", + outcome="resolved", + need=need, + route_id=entry.id, + ) console.print(f"[bold]{entry.title}[/bold] ([cyan]{entry.id}[/cyan])") console.print(f" owner : {entry.owner_repo} ({entry.subsystem})") @@ -1305,3 +1368,58 @@ def worker_status_cmd() -> None: console.print(f"timer : {st or 'unknown'}") except Exception: # noqa: BLE001 — systemd may be absent (cron/other host) console.print("timer : (systemd not available)") + + +# --------------------------------------------------------------------------- +# warden memory — cross-runtime experiential memory (WARDEN-WP-0024) +# --------------------------------------------------------------------------- + +@memory_app.command("status") +def memory_status( + output_json: Annotated[bool, typer.Option("--json", help="Output JSON")] = False, +) -> None: + """Show canonical phase-memory store status (metadata only).""" + from warden import memory as warden_memory + + if not warden_memory.memory_available(): + err.print(f"[red]{warden_memory._PHASE_MEMORY_ERROR}[/red]") + raise typer.Exit(2) + try: + payload = warden_memory.status() + except RuntimeError as e: + err.print(f"[red]{e}[/red]") + raise typer.Exit(2) + if output_json: + print(json.dumps(payload, indent=2)) + return + console.print(f"store_path : {payload.get('store_path', '')}") + console.print(f"profile_id : {payload.get('profile_id', '')}") + console.print(f"episode_count : {payload.get('episode_count', 0)}") + console.print(f"session_kinds : {payload.get('episode_counts_by_session_kind', {})}") + console.print(f"last_activation: {payload.get('last_activation_at') or '—'}") + + +@memory_app.command("activate") +def memory_activate( + need: Annotated[str, typer.Option("--need", help="Optional routing need fingerprint source")] = "", + agent: Annotated[ + Optional[str], + typer.Option("--agent", help="Agent id for session_kind warden.agent. (claude, codex, grok, …)"), + ] = None, + output_json: Annotated[bool, typer.Option("--json", help="Output JSON")] = False, +) -> None: + """Activate bounded coordination memory for worker, operator, or agent sessions.""" + from warden import memory as warden_memory + + if not warden_memory.memory_available(): + err.print(f"[red]{warden_memory._PHASE_MEMORY_ERROR}[/red]") + raise typer.Exit(2) + try: + payload = warden_memory.activate(need=need, agent=agent) + except RuntimeError as e: + err.print(f"[red]{e}[/red]") + raise typer.Exit(2) + if output_json: + print(json.dumps(payload, indent=2)) + return + console.print(warden_memory.format_activation_summary(payload)) diff --git a/src/warden/memory.py b/src/warden/memory.py new file mode 100644 index 0000000..4f71b34 --- /dev/null +++ b/src/warden/memory.py @@ -0,0 +1,122 @@ +"""phase-memory bridge for ops-warden cross-runtime experiential memory.""" + +from __future__ import annotations + +import os +from typing import Any, Mapping, Optional + +_PHASE_MEMORY_ERROR = ( + "phase-memory is required for warden memory commands. " + "Install with: pip install phase-memory (or set PYTHONPATH to phase-memory/src)." +) + + +def _phase_memory(): + try: + import phase_memory.ops_warden as pm + + return pm + except ImportError as exc: # pragma: no cover - exercised via tests with PYTHONPATH + raise RuntimeError(_PHASE_MEMORY_ERROR) from exc + + +def memory_available() -> bool: + try: + _phase_memory() + return True + except RuntimeError: + return False + + +def enabled(environ: Mapping[str, str] | None = None) -> bool: + environ = environ or os.environ + return str(environ.get("WARDEN_MEMORY", "1")).strip().lower() not in {"0", "false", "no", "off"} + + +def store_path(environ: Mapping[str, str] | None = None): + return _phase_memory().default_memory_store_path(environ) + + +def session_kind(environ: Mapping[str, str] | None = None) -> str: + return _phase_memory().resolve_session_kind(environ) + + +def status(environ: Mapping[str, str] | None = None) -> dict[str, Any]: + pm = _phase_memory() + return pm.OpsWardenMemoryStore.open(environ=environ).status() + + +def activate( + *, + need: str = "", + agent: Optional[str] = None, + session_id: str = "", + environ: Mapping[str, str] | None = None, +) -> dict[str, Any]: + pm = _phase_memory() + env = dict(environ or os.environ) + if agent: + env["WARDEN_AGENT_ID"] = agent + kind = pm.resolve_session_kind(env) + return pm.activate_ops_warden_memory( + pm.OpsWardenMemoryStore.open(environ=env), + session_kind=kind, + need=need, + session_id=session_id, + ) + + +def record_command_episode( + *, + command: str, + outcome: str, + need: str = "", + route_id: str = "", + diagnostic_codes: Optional[list[str]] = None, + metadata: Optional[dict[str, Any]] = None, + environ: Mapping[str, str] | None = None, +) -> dict[str, Any]: + if not enabled(environ): + return {"valid": True, "skipped": True, "reason": "WARDEN_MEMORY=0"} + pm = _phase_memory() + env = dict(environ or os.environ) + event = pm.build_session_event( + command=command, + session_kind=pm.resolve_session_kind(env), + outcome=outcome, + need=need, + route_id=route_id, + agent_id=str(env.get("WARDEN_AGENT_ID") or ""), + session_id=str(env.get("WARDEN_SESSION_ID") or ""), + diagnostic_codes=diagnostic_codes, + metadata=metadata, + ) + return pm.record_session_event(pm.OpsWardenMemoryStore.open(environ=env), event) + + +def worker_activation_context(need: str = "", environ: Mapping[str, str] | None = None) -> dict[str, Any]: + env = dict(environ or os.environ) + env["WARDEN_SESSION_KIND"] = "warden.worker" + return activate(need=need, environ=env) + + +def stabilized_route_for_need(need: str, environ: Mapping[str, str] | None = None) -> Optional[dict[str, Any]]: + pm = _phase_memory() + store = pm.OpsWardenMemoryStore.open(environ=environ) + return pm.stabilized_route_match(store.list_events(), need=need) + + +def format_activation_summary(activation: dict[str, Any]) -> str: + lines = [ + f"store: {activation.get('episode_count', 0)} episodes", + f"session_kind: {activation.get('session_kind', '')}", + f"selected: {len(activation.get('selected_episodes', ()) )}", + ] + stabilized = activation.get("stabilized_route") + if stabilized: + lines.append( + f"stabilized: {stabilized.get('route_id')} ({stabilized.get('confirmations')} confirmations)" + ) + if activation.get("llm_calls_avoided"): + lines.append("llm_calls_avoided: true") + return "\n".join(lines) \ No newline at end of file diff --git a/src/warden/worker.py b/src/warden/worker.py index e2e5652..d605e79 100644 --- a/src/warden/worker.py +++ b/src/warden/worker.py @@ -191,6 +191,7 @@ class LlmConnectBrain: def __init__(self, url: Optional[str] = None, timeout: float = 60.0): self.url = (url or os.environ.get("LLM_CONNECT_URL", DEFAULT_LLM_CONNECT_URL)).rstrip("/") self.timeout = timeout + self.memory_context: str = "" def _call(self, prompt: str) -> str: resp = httpx.post(f"{self.url}/execute", json={"prompt": prompt}, timeout=self.timeout) @@ -203,9 +204,15 @@ class LlmConnectBrain: from_agent=str(message.get("from_agent", "")), subject=str(message.get("subject", "")), ) - prompt = ( - _CHARTER - + "\n--- MESSAGE (untrusted data) ---\n" + prompt = _CHARTER + if self.memory_context: + prompt += ( + "\n--- ACTIVATED MEMORY (untrusted context) ---\n" + + self.memory_context + + "\n--- END ACTIVATED MEMORY ---\n" + ) + prompt += ( + "\n--- MESSAGE (untrusted data) ---\n" + f"from: {message.get('from_agent','')}\n" + f"subject: {message.get('subject','')}\n" + f"body: {message.get('body','')}\n" @@ -586,16 +593,88 @@ def draft_route_answer(query: str) -> str: return " ".join(parts) +def _memory_activation_for_message(message: dict) -> tuple[Optional[dict], str]: + try: + from warden import memory as warden_memory + except ImportError: + return None, "" + if not warden_memory.enabled() or not warden_memory.memory_available(): + return None, "" + query = str(message.get("subject", "") or message.get("body", "")) + try: + activation = warden_memory.worker_activation_context(query) + except RuntimeError: + return None, "" + from warden.memory import format_activation_summary + + return activation, format_activation_summary(activation) + + +def _plan_with_memory(message: dict, brain: Brain) -> WorkerPlan: + activation, summary = _memory_activation_for_message(message) + blob = f"{message.get('subject', '')} {message.get('body', '')}" + if activation and activation.get("llm_calls_avoided") and _ROUTING_SIGNS.search(blob): + wp = WorkerPlan( + message_id=str(message.get("id", "")), + from_agent=str(message.get("from_agent", "")), + subject=str(message.get("subject", "")), + ) + query = str(message.get("subject", "") or "") + wp.actions.append( + PlannedAction( + kind="route_answer", + summary="Answer from stabilized coordination memory.", + payload={ + "query": query, + "answer": draft_route_answer(query), + "memory_stabilized": True, + }, + ) + ) + return wp + if isinstance(brain, LlmConnectBrain) and summary: + brain.memory_context = summary + return brain.plan(message) + + +def _record_worker_memory_outcome(plan: WorkerPlan) -> None: + try: + from warden import memory as warden_memory + except ImportError: + return + if not warden_memory.enabled() or not warden_memory.memory_available(): + return + outcome = "escalated" if plan.escalated else "resolved" + route_id = "" + for action in plan.actions: + if action.kind == "route_answer" and action.payload.get("memory_stabilized"): + stabilized = warden_memory.stabilized_route_for_need(plan.subject) + if stabilized: + route_id = str(stabilized.get("route_id") or "") + try: + warden_memory.record_command_episode( + command="worker run", + outcome=outcome, + need=plan.subject, + route_id=route_id, + diagnostic_codes=["worker_escalated"] if plan.escalated else [], + metadata={"message_id": plan.message_id, "action_kinds": [a.kind for a in plan.actions]}, + ) + except RuntimeError: + return + + def build_plans(messages: List[dict], brain: Brain) -> List[WorkerPlan]: """Plan every message, attach computed route answers, and apply the guardrail pass.""" plans: List[WorkerPlan] = [] for m in messages: - plan = brain.plan(m) + plan = _plan_with_memory(m, brain) plan.raw = m for a in plan.actions: if a.kind == "route_answer" and "answer" not in a.payload: a.payload["answer"] = draft_route_answer(a.payload.get("query", m.get("subject", ""))) plans.append(_guardrail(plan, m)) + _record_worker_memory_outcome(plans[-1]) return plans diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..f8f4628 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,11 @@ +"""Test configuration for ops-warden.""" + +from __future__ import annotations + +import sys +from pathlib import Path + + +PHASE_MEMORY_SRC = Path(__file__).resolve().parents[2] / "phase-memory" / "src" +if PHASE_MEMORY_SRC.exists() and str(PHASE_MEMORY_SRC) not in sys.path: + sys.path.insert(0, str(PHASE_MEMORY_SRC)) \ No newline at end of file diff --git a/tests/test_memory.py b/tests/test_memory.py new file mode 100644 index 0000000..cedcb08 --- /dev/null +++ b/tests/test_memory.py @@ -0,0 +1,129 @@ +"""Tests for ops-warden phase-memory bridge (WARDEN-WP-0024).""" + +from __future__ import annotations + +import json + +from typer.testing import CliRunner + +from warden.cli import app +from warden.memory import activate, enabled, record_command_episode, status, store_path +from warden.worker import RuleBrain, _plan_with_memory, build_plans + +runner = CliRunner() + + +def _msg(**over) -> dict: + base = { + "id": "m1", + "from_agent": "someone", + "subject": "Where do I get an npm token?", + "body": "Which subsystem owns this credential — how do I obtain it?", + } + base.update(over) + return base + + +def test_memory_status_and_activate_round_trip(tmp_path, monkeypatch) -> None: + monkeypatch.setenv("WARDEN_MEMORY_STORE", str(tmp_path / "memory")) + monkeypatch.setenv("WARDEN_AGENT_ID", "grok") + + first = record_command_episode( + command="route find", + outcome="resolved", + need="npm token", + route_id="openbao-api-key", + ) + second = record_command_episode( + command="route find", + outcome="resolved", + need="npm token", + route_id="openbao-api-key", + ) + payload = activate(need="npm token", agent="grok") + + assert first["valid"] is True + assert second["valid"] is True + assert payload["session_kind"] == "warden.agent.grok" + assert payload["stabilized_route"]["route_id"] == "openbao-api-key" + assert status()["episode_count"] >= 3 + + +def test_worker_uses_stabilized_memory_without_llm(tmp_path, monkeypatch) -> None: + monkeypatch.setenv("WARDEN_MEMORY_STORE", str(tmp_path / "memory")) + monkeypatch.setenv("WARDEN_SESSION_KIND", "warden.worker") + record_command_episode( + command="route find", + outcome="resolved", + need="Where do I get an npm token?", + route_id="openbao-api-key", + ) + record_command_episode( + command="route find", + outcome="resolved", + need="Where do I get an npm token?", + route_id="openbao-api-key", + ) + + plan = _plan_with_memory(_msg(), RuleBrain()) + + assert plan.actions + assert plan.actions[0].kind == "route_answer" + assert plan.actions[0].payload.get("memory_stabilized") is True + + +def test_cross_runtime_continuity_agent_to_worker(tmp_path, monkeypatch) -> None: + monkeypatch.setenv("WARDEN_MEMORY_STORE", str(tmp_path / "memory")) + monkeypatch.setenv("WARDEN_AGENT_ID", "codex") + record_command_episode( + command="route find", + outcome="resolved", + need="openrouter api key", + route_id="openrouter-llm-connect", + ) + monkeypatch.setenv("WARDEN_SESSION_KIND", "warden.worker") + monkeypatch.delenv("WARDEN_AGENT_ID", raising=False) + activation = activate(need="openrouter api key") + kinds = {item.get("session_kind") for item in activation["selected_episodes"] if item.get("kind") == "episode"} + + assert "warden.agent.codex" in kinds + + +def test_cli_memory_status_and_activate(tmp_path, monkeypatch) -> None: + monkeypatch.setenv("WARDEN_MEMORY_STORE", str(tmp_path / "memory")) + status_result = runner.invoke(app, ["memory", "status", "--json"]) + activate_result = runner.invoke(app, ["memory", "activate", "--agent", "claude", "--json"]) + + assert status_result.exit_code == 0 + assert activate_result.exit_code == 0 + assert json.loads(status_result.stdout)["episode_count"] >= 0 + assert json.loads(activate_result.stdout)["session_kind"] == "warden.agent.claude" + + +def test_route_find_records_memory_episode(tmp_path, monkeypatch) -> None: + monkeypatch.setenv("WARDEN_MEMORY_STORE", str(tmp_path / "memory")) + result = runner.invoke(app, ["route", "find", "openrouter api key", "--json"]) + + assert result.exit_code == 0 + payload = status() + assert payload["episode_count"] >= 1 + + +def test_build_plans_records_worker_outcomes(tmp_path, monkeypatch) -> None: + monkeypatch.setenv("WARDEN_MEMORY_STORE", str(tmp_path / "memory")) + plans = build_plans([_msg()], RuleBrain()) + + assert plans[0].actions + assert status()["episode_count"] >= 1 + + +def test_memory_disabled_skips_recording(monkeypatch) -> None: + monkeypatch.setenv("WARDEN_MEMORY", "0") + result = record_command_episode(command="route find", outcome="resolved", need="npm token") + + assert result.get("skipped") is True + + +def test_default_store_path_uses_xdg(monkeypatch) -> None: + monkeypatch.delenv("WARDEN_MEMORY_STORE", raising=False) + assert str(store_path()).endswith("warden/memory") \ No newline at end of file diff --git a/wiki/OpsWardenMemory.md b/wiki/OpsWardenMemory.md new file mode 100644 index 0000000..89adf62 --- /dev/null +++ b/wiki/OpsWardenMemory.md @@ -0,0 +1,52 @@ +# Ops-Warden Experiential Memory + +Updated: 2026-07-02 + +ops-warden uses **phase-memory** as a shared experiential substrate across worker +ticks, coding agent sessions, and operator CLI use. + +## Canonical Store + +- Default: `~/.local/share/warden/memory/` +- Override: `WARDEN_MEMORY_STORE` +- Opt-out: `WARDEN_MEMORY=0` + +## Session Kinds + +| Runtime | How | +| --- | --- | +| Worker tick | `WARDEN_SESSION_KIND=warden.worker` (set automatically during `warden worker run`) | +| Coding agent | `export WARDEN_AGENT_ID=claude` (or `codex`, `grok`, future ids) | +| Operator CLI | default `warden.operator` when `WARDEN_AGENT_ID` is unset | + +## Agent Session Orientation + +At the start of a Claude Code, Codex, or Grok session that will call warden: + +```bash +export WARDEN_AGENT_ID=grok # or claude, codex +warden memory activate --json +``` + +Then use normal `warden route` / `warden access` commands. Episodes are recorded +automatically when memory is enabled. + +## Worker + OpenRouter + +`warden worker run --brain llm` activates memory before planning. When stabilized +routing memory matches a coordination question, ops-warden uses `RuleBrain` and +skips the llm-connect / OpenRouter call. + +## Commands + +```bash +warden memory status [--json] +warden memory activate [--agent ] [--need ""] [--json] +``` + +## Security + +- Memory stores metadata only — no secret values or raw credential payloads. +- Retrieved memory is untrusted context; the fixed charter and guardrail allowlist + still apply. +- See `phase-memory/docs/ops-warden-memory-contract.md` for the full contract. \ No newline at end of file diff --git a/workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md b/workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md index 76a7e6b..3ebac66 100644 --- a/workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md +++ b/workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md @@ -4,13 +4,14 @@ type: workplan title: "Experiential Memory Across Worker, Agent Sessions, And OpenRouter" domain: infotech repo: ops-warden -status: proposed +status: finished owner: codex topic_slug: custodian planning_priority: high planning_order: 24 created: "2026-07-02" updated: "2026-07-02" +state_hub_workstream_id: "5d9fafb3-f9b6-43bf-b259-5f5301daa2e9" --- # WARDEN-WP-0024 — Experiential memory across worker, agent sessions, and OpenRouter @@ -87,8 +88,9 @@ sequenceDiagram ```task id: WARDEN-WP-0024-T01 -status: todo +status: done priority: high +state_hub_task_id: "6305f1bc-c016-4298-adc2-a07d52b6aca5" ``` Establish the canonical phase-memory store path and discovery commands. @@ -107,8 +109,9 @@ Acceptance: ```task id: WARDEN-WP-0024-T02 -status: todo +status: done priority: high +state_hub_task_id: "242a4d9a-5375-4df8-8d43-063b0491d202" ``` Record metadata-only episodes when `route`, `access`, and `sign` complete. @@ -127,8 +130,9 @@ Acceptance: ```task id: WARDEN-WP-0024-T03 -status: todo +status: done priority: high +state_hub_task_id: "176fcae1-e4e5-481f-9a83-e9a7000fac1a" ``` Retrieve coordination memory before `Brain.plan` and record outcomes after execute. @@ -147,8 +151,9 @@ Acceptance: ```task id: WARDEN-WP-0024-T04 -status: todo +status: done priority: high +state_hub_task_id: "16501557-6cde-44ea-bc6f-1726cb7ec070" ``` Expose memory activation for coding agent sessions across Claude, Codex, Grok, and @@ -167,8 +172,9 @@ Acceptance: ```task id: WARDEN-WP-0024-T05 -status: todo +status: done priority: high +state_hub_task_id: "55ae679c-c08f-4afd-8646-9f5f3019f86e" ``` Ensure worker ticks see agent session episodes and vice versa. @@ -187,8 +193,9 @@ Acceptance: ```task id: WARDEN-WP-0024-T06 -status: todo +status: done priority: medium +state_hub_task_id: "fc2dffcf-7184-4f3d-8653-d26dc18a9afc" ``` Skip or downgrade OpenRouter calls when stabilized memory provides a verified match. @@ -206,8 +213,9 @@ Acceptance: ```task id: WARDEN-WP-0024-T07 -status: todo +status: done priority: medium +state_hub_task_id: "ca2aaf23-833f-49a6-a49b-a0b659208f5f" ``` Document the memory workflow for operators and coding agents. @@ -249,4 +257,13 @@ Acceptance: ## Closure Review -Pending implementation. \ No newline at end of file +Integrated phase-memory across all ops-warden runtime surfaces: + +- `warden memory status` and `warden memory activate` with canonical XDG store. +- CLI recording hooks on `route find`, `access`, and `sign` (opt-out via + `WARDEN_MEMORY=0`). +- Memory-aware worker planning with stabilized-route RuleBrain short-circuit + and LlmConnectBrain context injection. +- Cross-runtime continuity verified in tests (agent session → worker activation). +- `wiki/OpsWardenMemory.md` and AGENTS.md session orientation for Claude, + Codex, Grok, and future agents. \ No newline at end of file