Skip to content

D3: the F1 hook_json family engine — claude_code, codex_cli, kimi_code, devin folded to engine + config - #97

Merged
open-coder-ai merged 2 commits into
mainfrom
w38/d3-f1-hook-json-engine
Sep 1, 2026
Merged

D3: the F1 hook_json family engine — claude_code, codex_cli, kimi_code, devin folded to engine + config#97
open-coder-ai merged 2 commits into
mainfrom
w38/d3-f1-hook-json-engine

Conversation

@open-coder-ai

@open-coder-ai open-coder-ai commented Sep 1, 2026

Copy link
Copy Markdown
Owner

What this changes

Four of the five F1 hook_json adapters (claude_code, codex_cli, kimi_code, devin) are re-expressed as one shared engine driven by their data/vendors/<agent>.json entries (docs/design/dialect-families.md D3); their hand-written modules are deleted. vscode_copilot deliberately stays a dialect module (see Notes). The golden wire fixture suite passes byte-for-byte with zero changes under tests/fixtures/golden/ or to tests/test_golden_fixtures.py — that empty diff is the acceptance gate for the whole fold.

What lands:

  • adapters/_hook_json.py — the family engine: marker claims(), ordered field-fallback parse(), G1/G2 verdict renderers plus the shared hookSpecificOutput context/transform bodies, every function taking the vendor entry as data. adapters/_hook_entry.py renders hook_config() (hooks-map / bare-map / flat-list wrappers, windows-wrapper extras, kimi's TOML emitter). adapters/_family.py binds one entry behind the unchanged adapters.get() surface (all module attributes tests rely on — EVENT_MAP, DECISION_VOCABULARY, BLOCKING_EVENTS, render_config, … — are derived from the entry). adapters/_probes.py holds looks_like_claude_code, now shared by the engine and gemini_cli.

  • Schema + entries gain the §3.1 renderer data D2 deferred: verdict words, degrade_notes (verbatim strings from the deleted adapters, so the wire cannot move), reason_defaults, note_style, echo, context-event lists — plus three claims fields that turn notes-only caveats into checked data (accept_names, accept_when_all, reject_markers_unless_probe) and hook_entry.bare. All entries regenerated via tools/recount_vendor_config.py --write, never edited by hand; devin's PermissionRequest gate is now recounted explicitly by replaying the frozen pre_tool payload with the event name swapped (the generalization W37's report suggested).

  • bundle() composes engine + inlined VENDOR literal for config-driven agents and still splices the other eight legacy-style. Bundle line counts, measured at this PR's base vs. this PR (the design's §4 [h] asks for real numbers; estimates were +20–30%):

    bundle before after growth
    claude_code 494 650 +32%
    codex_cli 452 636 +41%
    kimi_code 458 624 +36%
    devin 451 650 +44%

    Slightly above the §4 estimate; the numbers and the payoff side (624 deleted adapter lines → ~390 shared engine lines + data) are recorded in the design doc's §7.

Claim check

  • No capability claim is widened without a mechanism behind it — the matrix is untouched; config gates ⊆ matrix block-capable events is still asserted by test_config_gates_are_a_subset_of_matrix_block_capable_events, and devin's added PermissionRequest gate maps to canonical pre_tool, already block-capable in its row
  • Any new/changed MATRIX row carries a verified record — no matrix rows changed
  • Payload shapes come from a primary source — no new payload shapes; every new config string is replayed against tests/fixtures/golden/<agent>.json, the D1-frozen capture of the real adapters

Checks

  • pytest -q passes — 1349 passed, 4 skipped (baseline was 1320/4; +29 tests, none removed)
  • ruff check . and ruff format --check . pass (--select I a no-op)
  • Runtime path is still stdlib-only (check_stdlib_only.py and the AST check both green)
  • Commits are signed off (git commit -s)

Also run locally: examples/generate.py --check (13 pages unchanged), docs/assets/gen_brand_assets.py --check, CLI smoke (--version, agents, matrix, doctor), and the 12-agent × 4-outcome bundler subprocess replay.

Notes for the reviewer

  • Sizing: 4 of 5 folded, per the brief's own sizing clause. vscode_copilot is not foldable inside the design's §3.1 config/code line: its claims() accepts by three disjoint paths (envelope marker; lowercase-first names with their own conditional reject list; a memory-tool call carrying no event name at all), and parse() branches on tool ∈ MEMORY_TOOLS ∧ command ∈ MEMORY_WRITE_COMMANDS to select a second field-chain set — branching on payload content, which §3.1 says is dialect code. Encoding that as config means either a rule interpreter (the "worse programming language" §3.1 refuses) or a named accept-probe, i.e. widening the reject_probes crack to the accept side — a design decision I'm surfacing rather than making. The §7 dual-casing [h] is answered accordingly in the design doc.
  • Schema extensions are the design's own §3.2 word/note tables, not new expressiveness: strings, lists, and flat maps only, all validated with additionalProperties: false and covered by new mutation tests (a typo'd word/note key fails loud). Mutation checks were run beyond the schema: a mutated note string fails the golden replay; a flipped honours_transform visibly changes the wire.
  • Recount for config-driven vendors: fields/wire_events are read off the bound entry (the entry is parse() there — the engine executes it, so a second derivation source no longer exists); gates are still replayed from the frozen fixtures, and behaviour is held by the golden suite plus the untouched per-adapter suites. The word/note tables live in tools/recount/tables.py with the claims tables, every string exercised by the fixture replay on every run.
  • Two deliberate direct-call (never reachable through dispatch.handle, untested before and after) behaviour notes: respond() with a raw WARN at a G2 gate is now uniformly silent (the old claude_code emitted a bare hookEventName body; codex/kimi already returned ""), and test_function_local_imports_are_left_alone samples vscode_copilot now that the engine bundles legitimately contain no function-local imports.

Extend the vendor config schema with the dialect-families.md §3.1 renderer
data D2 deferred: verdict word tables, degradation-note strings (verbatim
from the adapters, so wire output stays byte-identical), reason defaults,
note style, hookEventName echo source, context-event lists, and the
hook_entry bare flag. Claims gain accept_names, accept_when_all and
reject_markers_unless_probe, replacing three notes-only caveats with
checkable data. Recount derives the new fields: devin's PermissionRequest
gate is replayed from the frozen pre_tool payload (the W37 _EXTRA_GATE_NAMES
generalization), bare is classified from the fixture's hook_config shape,
and the word/note tables are pinned in tables.py where every string is
exercised by the golden fixture replay. Entries regenerated via
tools/recount_vendor_config.py --write; only the four fold-candidate
vendors change.

Signed-off-by: Claude <noreply@anthropic.com>
…ok_json engine

The F1 engine (dialect-families.md D3): adapters/_hook_json.py executes a
vendor's data/vendors entry -- marker claims, ordered field-fallback parse,
G1/G2 verdict renderers, shared hookSpecificOutput context and transform
bodies -- with adapters/_hook_entry.py rendering hook_config (and kimi's
TOML) and adapters/_family.py binding one entry behind the unchanged
adapters.get() surface. The four modules are deleted; their golden wire
fixtures pass byte-for-byte unchanged, which was the acceptance gate.

vscode_copilot stays a dialect module by design: its three-path claims()
and memory-tool parse branching are payload-content branching that §3.1's
config/code line keeps as code; recorded as the answered dual-casing [h]
in the design doc.

bundle() gains an engine+config composition for config-driven agents
(contract + probes/windows where the entry needs them + engine + inlined
VENDOR literal + runtime) and still splices the other eight legacy-style.
looks_like_claude_code moves to adapters/_probes.py, shared with
gemini_cli. Recount reads fields/wire_events off the bound entry for
config-driven vendors (the config IS parse() there; behaviour stays held
by the fixtures and per-adapter suites). New schema fields carry mutation
tests and behavioural replay tests; measured bundle sizes are published
next to the design's §4 estimates.

Signed-off-by: Claude <noreply@anthropic.com>
@open-coder-ai
open-coder-ai marked this pull request as ready for review September 1, 2026 11:45
@open-coder-ai
open-coder-ai merged commit b9ae0a8 into main Sep 1, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants