Neutral hook dispatcher, spool, dialect fields

Task 9941 · Done · Coder · created · updated

Original request (verbatim): "Close the gap between Claude, Codex, Antigravity and Pi. More universal code, less provider specific" Settled inputs: PRD (Task 9932) and Architecture (Task 9933) of this epic. Approach (architecture §1): lifecycle events run in Jaah-owned code in front of every session (ACP: `jaah-acp-host`, `apps/jaahd/acp/host.py`; terminal: node daemon + vendor CLI events); tool events run in-band through ONE neutral dispatcher `tools/hook_bridge_core.py::dispatch`. Adapters contribute only declarations (event names, tool-name map, placement literals, error text patterns, question prefix), never a copy of the logic. Requirement R1 (verbatim): "Each of these mechanisms has exactly ONE provider-neutral implementation home that every provider uses; an adapter contributes only vendor literals and capability declarations, never its own copy of the logic: (a) hook/guard rendering & chaining, (b) hook-context injection into the model, (c) charter placement as system/built-in instructions, (d) session-start rule list (guard brief), (e) read-once guard reset signal, (f) prompt-submit and session-end lifecycle events, (g) StopFailure / API-error self-heal, (h) project-memory hooks, (i) token/cost (spend) reading, (j) usage-window (quota) reading, (k) the operator question / structured interview channel." R5 (verbatim): "Availability: every closed capability is ON by default on the dev control plane (`dev.jaah.ai`) — no new switch that ships OFF; if a switch is unavoidable it defaults ON and is wired in the real deployment." Closing-test naming contract (architecture §4): every degradation key this child closes gets a test named `test_closes_<key with - as _>` (e.g. `test_closes_no_hook_context`); C9 refuses to remove a key without it. Do NOT remove catalogue degradation keys in this child — the close-out child removes them; this child delivers the behaviour and the closing test. ## Scope (this child) Architecture child C1 — builds the seams S1, S2, S3 every provider child meets at. Mechanism homes (a) and (b). - **S1 — neutral dispatcher.** `tools/hook_bridge_core.py` today is a helper library with no CLI. Add `dispatch` and a `__main__` entry; keep `run_hook`, `claude_decision`, `claude_context`, `read_only_shell`, `hook_environment` as they are (`dispatch` composes them). CLI: `python3 -I <hub>/tools/hook_bridge_core.py dispatch --event <ClaudeEventName>`. - stdin: one JSON object in Claude hook shape: `hook_event_name`, `session_id`, `transcript_path`, `cwd`, and per event `tool_name` + `tool_input` (Claude spelling, already translated by the caller), `tool_response`, `prompt`. - Hooks file: `$JAAH_HOOKS_FILE` (the per-dispatch file `harness_dispatch` writes, same schema as today's `$JAAH_AGY_HOOKS`). Migration: for one release overlap `dispatch` also accepts `$JAAH_AGY_HOOKS` so a live agy session launched before the deploy keeps working (the old bridge refuses every tool call when its variable is missing). - stdout: exactly one JSON object `{"decision": "allow"|"deny", "reason": str, "context": str}`; exit 0. Fail-open on any internal error (`allow`, empty context), except a guard's explicit refusal (`REFUSALS`) is always `deny`. - Side effect: with `--spool`, the context is also appended to `$JAAH_HOOK_CONTEXT` as one line `{"event": str, "text": str}`. - **S2 — context spool.** `$JAAH_HOOK_CONTEXT` = `<host run dir>/hook-context.jsonl`, mode 0600, owned by the runner uid. C1 writes it (dispatch `--spool`) and exports the path; C2 is its single reader (drain = read + truncate under `flock`, before each `session/prompt`, leading text block headed `Platform guidance:`; empty spool sends nothing). C1 provides the writer and a shared drain helper the host can call. - **S3 — dialect fields** on `AcpDialect` (`apps/jaahd/agent_providers.py:125`), defaults that change nothing: ```python host_lifecycle: tuple[str, ...] = () # subset of ("SessionStart","UserPromptSubmit","Stop","SessionEnd","PreCompact") context_channel: Literal["native", "turn_boundary"] = "native" system_prompt_file_env: str = "" # env var naming the charter file the host writes before session/new ``` Add the invariant test in `tools/tests/test_provider_capabilities.py`: `host_lifecycle` and the adapter's rendered in-engine events are disjoint (no event fires twice). - Generic host write of the charter file for `system_prompt_file_env` in `apps/jaahd/acp/host.py` (deterministic `<run dir>/system-prompt.md`, env exported at host start, written before `session/new`) — a no-op until a row declares it (mechanism (c) home stays `Host._start_session`). - Move `antigravity_runner.dispatch_hooks` (`apps/jaahd/agents/antigravity_runner.py:234`) into `apps/jaahd/agents/base.py::harness_dispatch` (renders `harness_groups` + `chain_hook_groups` into the per-run hooks file). Reduce `tools/antigravity_hook_bridge.py` to agy payload translation that calls `dispatch`. Add the tool-name map declaration. - Add a "Provider-neutral harness homes" section to `apps/jaahd/README.md` holding the architecture §2 mechanism-homes table (a)–(k) (owning symbol / entry points / declarations) — its single home; other docs point to it, never copy it. Credentials & reach: C1–C7 unit and replay tests, AC1–AC4 | nothing beyond the worktree (`./dc check full`) | verified (worktree) Out of scope (PRD, verbatim — do NOT touch): - MUST NOT retire, rewrite or change the runtime selection of the legacy terminal runtime; Claude `oauth` stays `legacy_only` (ADR in `docs/adrs/README.md`, ACP Claude entry). - MUST NOT remove or reword any "stay published" degradation (list above), including Pi `no-subagents` and `acp-runtime-only`. - MUST NOT build Pi subagents or a Jaah-side subagent lane. - MUST NOT add Jaah-side workarounds for vendor-intrinsic DeepSeek/Codex gaps (PDF conversion, model remap changes, throttling avoidance). - MUST NOT do live-verification work for Antigravity `not-yet-observed`, nor touch `home-is-account-dir` or `microvm-signed-in-legacy-runtime`. - MUST NOT change `tools/provider_branches.toml` patterns/allowlist scope beyond what removing branches requires (gate tightening was not selected), and set no line-count target. - MUST NOT add a new provider or retire one; no change to login/sign-in flows, model-tier resolution, or the plan gate (`non_claude_engines`). Stay-published keys (must stay byte-identical): DeepSeek `document-blocks-unsupported`, `images-flash-only`, `prompt-cache-controls-ignored`, `beta-features-ignored`, `web-search-extra-calls`, `claude-model-names-remapped`, `model-quality-differs`; Codex `device-code-login-workspace-gated`, `subscription-concurrency-throttling`; Pi `no-subagents`, `acp-runtime-only`. I1-escaped (stay, unchanged): Antigravity `no-spend-reading`; Pi and DeepSeek `no-quota-reading`. Done means: - Existing agy bridge tests pass unchanged through `dispatch` — `./dc test` on the antigravity bridge/adapter suites, cite output. - New unit tests cover the S1 stdout contract, fail-open, deny precedence (`REFUSALS` always deny), the `--spool` write (S2 line shape, mode 0600) and the `JAAH_AGY_HOOKS` alias — cite test names and output. - Disjointness invariant test exists in `tools/tests/test_provider_capabilities.py` and is green. - `python3 tools/check_provider_branches.py` exits 0 — cite output. - No catalogue key changes: AC1 printout (`python3 -c "import sys;sys.path.insert(0,'apps/jaahd');import agent_providers as a;[print(p.value,sorted(d.key for d in p.degradations)) for p in a.PROVIDERS]"`) identical before/after — cite both. - A host test: with `system_prompt_file_env` declared, `<run dir>/system-prompt.md` is written before `session/new`; undeclared, nothing is written. - `grep -n "def dispatch_hooks" apps/jaahd/agents/antigravity_runner.py` returns nothing (moved, not duplicated). - `apps/jaahd/README.md` "Provider-neutral harness homes" section names one owning `file:symbol` per mechanism (a)–(k); `python3 tools/check_context.py` green. - Quick gate green (`./dc check quick`); merge lane runs `./dc check full`.

Outcome

Built the neutral hook dispatcher (hook_bridge_core.dispatch + CLI), the context spool, the 3 AcpDialect fields and the host's charter-file write; moved dispatch_hooks to base.harness_dispatch; added the README harness-homes table. Quick gate green, PR #6884 queued. Memory: none applied.

Pull request

Artifacts

Activity

  • Draft
  • Ready
  • Runner started
  • Runner ended (done)