---
title: "Gate add-ons for the reasoning kernel"
owner: "Local_Development"
status: "production"
surface: "prompt_kernel_addons"
last_verified: "2026-09-12"
tags: ["prompt_kernel", "kernel", "addons", "paths"]
related_code: ["prompt_kernel/addons.py", "prompt_kernel/render.py"]
summary: "Advisory host-path bindings per kernel gate, rendered as section 6 without touching the frozen kernel graph."
reproduce:
files: ["prompt_kernel/addons.py", "prompt_kernel/tests/test_addons.py"]
commands: ["python -m pytest prompt_kernel/tests/ -q", "python -m prompt_kernel --install"]
expected_outputs: ["pytest: 78 passed", "installed=<sha256> matches prompt_kernel/baseline.json"]
---The kernel (prompt_kernel/source.py) is a frozen graph: gates, edges, rules,
identities, and budgets. Host-project conventions — where plans live, where
experiments go, what to update at closure — are not kernel content. They are
advisory bindings, owned by a data-only registry and rendered inline inside
each gate's <Gx_RULES> block (after the gate's own rules):
prompt_kernel/addons.py → data registry: GateAddon(gate_id, addon_id, lines)
prompt_kernel/render.py → <Gx_RULES> blocks: kernel rules (inline prose) + addon lines
prompt_kernel/artifacts.py→ manifest: addons.count + addons.sha256
python -m prompt_kernel --install
→ packages/opencode/src/session/prompt/reasoning_prompt.txt
prompt_kernel/baseline.json → production sha256 pin
Kernel structure stays stable: gates, edges, shared_rules: reference lists, and
state contracts are untouched. Rule naming follows the single-use principle:
a rule gets a #### @ID header only when something references it (another
rule's @REF, a shared_rules: list, or the compatibility contract pin
CONTRACT_PINNED_RULES); single-use rules render as plain bullets. Addon lines
must not use @-references (outside kernel namespace validation).
| Gate | Owns | Bind | Do not bind |
|---|---|---|---|
| G1 GROUND | what to read before work | read-surfaces: plans, docs | mutable write paths |
| G2 DECOMPOSE | candidate geography | scratch/draft lanes | plan artifacts |
| G3 MASTER_PLAN | plan artifacts | plan location + mandatory sections | implementation paths |
| G4 AUTHORIZE | (free) | approval-artifact paths if they appear | execution paths |
| G6 GROUND_PLAN | (free) | binding-surface hints | progress logs |
| G7 IMPLEMENT | work logging | progress log path | plan completion moves |
| G8 ORACLE | (free) | evidence/artifact locations | closure moves |
| G9 CLEAN_STATE | closure geography | completed-plans move, docs/index updates, deprecation | new rules of other gates |
Rule of thumb: if deleting the line would change what the kernel decides → it
belongs in source.py (requires a full kernel change cycle). If it only says
where a thing lives on disk in this host project → it is an addon.
| Gate | Addon | Binding |
|---|---|---|
| G1 | PATH_GROUNDING | plans/*.md, docs/ first; never .opencode/plans/ |
| G1 | TOOL_GROUNDING | codegraph, read, messagesearch, webfetch/universalsearch |
| G1 | PATH_SURFACE_DOCS | .opencode/skills/<surface>/references/** before building on a framework API; cite file:line (2026-09-20) |
| G2 | PATH_EXPERIMENTS | experiments/ scratch; futures/ drafts; [ISO8601]_name one-offs. This row was the ONLY record of the binding for a month — the registry lacked it until 2026-09-20, so the working prompt carried no trace of the project's own law while this doc described it (2026-09-20) |
| G2 | TOOL_DECOMPOSE | todowrite |
| G3 | PATH_PLANS | plans/[ISO8601]_<description>.md; Smoke Tests before G4 |
| G4 | TOOL_AUTHORIZE | checkstate (identity/permission/window), question (ASK) |
| G6 | TOOL_BINDING | codegraph explore/impact, read-only task grounding |
| G6 | SURFACE_CONSUMERS | enumerate a multi-consumer surface's importers before binding, and name the one you touch (2026-09-20) |
| G7 | PATH_PROGRESS | _progress_log.md [TIMESTAMP] entry per bounded task |
| G7 | TOOL_IMPLEMENT | edit, write; crash-prone shell via cmd_runner |
| G8 | TOOL_ORACLE | tests via cmd_runner, jobwait, logsearch, dbread |
| G8 | ORACLE_INSTRUMENT_CHECK | a capture is evidence only after it is validated — whole object, unoccluded, not a viewport crop (2026-09-20) |
| G9 | PATH_CLOSURE | undone task → [~] + reason, plan moves by outcome + stale-ref scan; docs/index update; obsolete/ |
| G9 | TOOL_CLOSURE | messagesearch verify; git status |
Section 5 (IDENTITY_CONTRACTS) stopped advertising gates:/may_mutate: — gates are a
workflow property, not an identity property — and gained tool rows instead: one tools:
line per identity, rendered from IDENTITY_ADDONS (IdentityAddon(identity_id, addon_id, lines),
validated by validate_identity_addons, injected into the ### IDENTITY block by
_render_identity). They exist because the provider tool catalogue is deliberately IDENTICAL
for every identity (session/llm.ts — «Wire filtering is FORBIDDEN here… breaks the KV
prefix»), so the rows are the only place an identity can read its real ACL.
The rows are kept honest by the extractor, not by discipline:
| Half | Where | What it does |
|---|---|---|
| Extractor | packages/opencode/script/kernel-tools-manifest.ts |
prints identity → { allowed, denied } from the LIVE rulesets (agent.ts) via agentDeniesPolicy — the agent-scoped half of SessionTools.denied (Gate A), evaluated on the tool POLICY (session/tools.ts:259 calls denied(item.policy), not the id) |
| Parity test | — removed 2026-09-29 | the runtime is decoupled from the kernel build, so no TS test reads reasoning_prompt.txt any more (the old kernel-identity-tools.test.ts and kernel-may-mutate.test.ts are gone). Rows are regenerated from this extractor; ACL behaviour is proven by test/agent/agent.test.ts |
Format: narrow identities list what they may use (tools: checkstate, memory, reasoningexit.);
wide identities list exclusions (tools: all except planenter, planexit.). Prose after ; carries
path-scoped edit boundaries (write/edit: plans/ only.) that ctx.ask enforces at execution —
the extractor prints only the id sets, so a prose note never appears in its output, and only the
ACL keeps a note true. MCP tools are host-configured and not enumerated: the rows describe the builtin
catalogue.
Product-only by decision (owner, 2026-09-28: «У claude и codex свои тулы не наши»): the Claude and Codex registries render section 5 WITHOUT tool rows; when those harnesses expose a machine-readable ACL source, add per-registry rows.
Render measured 2026-09-28: product 48 274 B / 6 290 tokens against caps 49 000 B
(KERNEL.utf8_budget) and 7 000 tokens (test_dedup.py) — the step that admits the rows
(~1.3 KB of rows against ~0.6 KB of removed gates:/may_mutate: lines) is recorded in
source.py beside the cap.
addons_codex.py is a third registry, alongside the product default and
addons_claude.py. It keeps the graph unchanged while binding this Codex
harness to Read/Glob/Grep, Edit/Write, LSP, AST Edit, Hub, Eval
Browser access, Todo, Task, and mounted codegraph_explore.
python -m prompt_kernel --codex writes reviewed/runtime artifacts to
prompt_kernel/dist_codex/. The kernel reaches Codex ONLY as $CODEX_HOME/AGENTS.md
(t64, owner 2026-10-16: «developer_instructions писать не надо просто AGENTS.md в ~.codex
и все» — reverses t54; the global file is read whole, with no project_doc_max_bytes cap).
--codex --install writes no receiver; the command prints the pointer to
--codex-global install, the single writer.
Divergences from the ADID methodology (deliberate): makeups/ is not bound —
opencode bans mocks/stubs in tests; RAG indexing and the adm XML pipeline are
skill-owned in the ADID package and stay out of the opencode kernel.
Absences are bindings too (2026-09-17). Three shared mechanisms landed in
source.py on 2026-09-16 — the DELEGATION protocol, @COMPACTION_CADENCE, and
the persisted-criterion store INTENTION_RESET requires — and the product and
Claude registries each got a host binding for them while this one did not, which
left the variant declaring mechanisms it named no instrument for. It now binds
them, including where the honest binding is that the instrument does not exist:
| Gate | Addon | Binding |
|---|---|---|
| G1 | PATH_GROUNDING |
no memory tool here, so criteria persist in plans/*.md and _progress_log.md, read at grounding |
| G2 | PATH_EXPERIMENTS |
the same law as the product — experiments/ scratch, experiments_history/ after a content check; both folders are the REPOSITORY's, so this line is host-independent |
| G1 | PATH_SURFACE_DOCS |
no skill tool — .opencode/skills/<surface>/references/** are plain files to read |
| G6 | SURFACE_CONSUMERS |
codegraph_explore, plus LSP references/implementation for language-aware reach |
| G8 | ORACLE_INSTRUMENT_CHECK |
Browser through Eval is the instrument; a viewport crop is not |
| G7 | TOOL_DELEGATE |
Task — brief it once with binding, falsifier and the parent intention; re-brief a follow-up slice |
| G8 | TOOL_ORACLE |
no isolated model call is bound, and a Task shares this frame → a self-verdict closes Inferred or Unknown |
| G9 | TOOL_CLOSURE |
the fold is lossy — handles go to plans/, docs/, _progress_log.md before it runs |
Naming a missing instrument is what keeps the rung from being skipped silently: an agent told only "stamp it with an outside falsifier" on a host that has none will reach for the nearest thing that answers, which is itself.
-
Append
GateAddon(gate_id, addon_id, lines)toGATE_ADDONSinprompt_kernel/addons.py(orIdentityAddon(identity_id, addon_id, lines)toIDENTITY_ADDONSfor a section-5 tool row — see Identity add-ons). -
Constraints (enforced by
validate_addons()/validate_identity_addons()):gate_id∈ G1–G9 /identity_id∈KERNEL.identities, uniqueaddon_id, non-empty lines. No@-references in lines. -
Budgets are shared — CURRENT (2026-10-01): byte cap
KERNEL.utf8_budget58 000 and token cap 7 700, the same for all three variants (test_dedup.py,test_addons_claude.py,test_addons_codex.py, raised together); reasons per step insource.pyand kernel-release-2026-09-29.md. The rest of this item is HISTORY: byte cap (36 000, raised from 35 000 on 2026-09-20) and token cap intests/test_dedup.py::test_compacted_runtime_budget(4 750, raised from 4 500 there). The Claude and Codex variants carry their own ceilings intests/test_addons_claude.pyandtests/test_addons_codex.py(36 000 / 4 750) because--claude --installwrites a whole file rather than filling a sized slot. Current renders, measured 2026-09-20, second step (the four addon bindings): product 36 904 bytes / 4 819 tokens, Claude 36 691 / 4 782, Codex 36 983 / 4 836. The caps are now 37 000 bytes (KERNEL.utf8_budget) and 5 000 tokens (test_compacted_runtime_budget), with both variants at 38 000 / 5 150 — their +1 000 B / +150 tok margin, restored, because--claude --installwrites a whole file rather than filling a sized slot. Before this step: product 35 419 / 4 603. Both figures were MEASURED, never estimated, and measured with an instrument that reports bytes and tokens TOGETHER — the byte assert runs first, so a failing byte cap hides the token number entirely: the first run of this change reported four byte failures and left the token cap unexercised. History: 25 000 → 26 000 → 27 000 → 28 000 → 30 000 → 31 000 → 32 000 → 35 000 → 36 000 → 37 000 bytes, 2 950 → 3 100 → 3 300 → 3 450 → 3 700 → 3 850 → 3 950 → 4 500 → 4 750 → 5 000 tokens, each step named in the test comments with what it admits. The 2026-09-20 steps admit, first,@INSTRUMENT_RUNG,@MEDOID_SIMPLEXand@CLAIM_CITATION— the three rules that describe the entry: an instrument may not certify below its rung, a surface needs at least three medoids with independent sources, and a claim above Guess carries its mechanism and a pin — and then the four addon bindings those rules pointed at:PATH_EXPERIMENTS,PATH_SURFACE_DOCS,SURFACE_CONSUMERS,ORACLE_INSTRUMENT_CHECK. Formal notation is admissible where it is tighter than prose — restating@LOOP_PROGRESSas a strict lexicographic decrease gave back 88 bytes with no loss of meaning. Growth policy (Alexander, 2026-09-11 → 2026-09-12): raises are deliberate, not drift — "28к мелкая плата за будущие ошибки… нужны дополнения по мере использования". As of 2026-09-12 raising is no longer an owner gate ("можешь поставить столько сколько тебе надо"): the cap existed partly to stop models that append without reading, and that failure is actually caught bydedup.py(find_unapproved_semantic_overlaps≥ 0.58,repeated_ngramswidth 5 / 4×), whose allowlists are both empty. The cap now works as a scarcity detector — it forces the question "what here is dead?" before anything new lands. Convention: raise at the point of need, in the same commit, naming what it admits.The cap is a brevity prompt, never a gate (Alexander, 2026-09-20): "потолка кернела не существует, потолок сделан чтобы писать лаконично и все. Reasoning на первом месте всегда. Это окупается не 1000 токенов, а забегами на 100000000 токенов." On the same class of limit: "Да, мы экономим и токены и кэш, но не на решениях." The discipline is therefore cut prose, never a decision — redundancy, preamble and formatting go first; a rule, a criterion or a falsifier is never trimmed to fit a number. The exchange rate is not symmetric and the owner states it from experience, not from arithmetic: a missing rule is paid for in whole runs (his own comparison, 2026-09-20 — a task this governed loop closed in five requests, which a stronger ungoverned model failed outright and reviewers called commodity work), while a line is paid for in bytes.
What may be cut (owner-aligned, 2026-09-23, recorded from a Codex review that adopted the kernel itself). The cap must never push a reduction that shortens the kernel mechanically by bytes; a cut is legitimate only where ONE of three is demonstrated: (a) the rule is duplicated by another executing rule; (b) the rationale can be removed without changing behaviour; (c) the rule has no observable consumer and prevents no known defect class. The reformulation is theirs and it is better than the older phrasing: not «the kernel must shrink», but «the kernel must not swell with UNPROVEN prose». A byte-stable prefix amortizes its size across the whole cache; dynamic noise never amortizes — it is paid again at every step. Critical rules stay even when long.
-
python -m pytest prompt_kernel/tests/ -q→ all green. -
python -m prompt_kernel --install→ noteinstalled=<sha256>. -
Update
sha256inprompt_kernel/baseline.json. -
Rebuild the opencode binary; open a new session (old checkpoints keep the previous system prefix until compact).
Revert addons.py (delete) + render.py/artifacts.py edits (.bak), restore
baseline.json sha, re-run --install. assert_current_kernel_unchanged()
must be green afterwards.