ADR 0008: agent-facing skills + injected instructions block
Date: 2026-07-07 Status: Accepted
Context
Section titled “Context”harnery ships a coordination engine: decision docket (ADR 0007), councils,
journals, agent heartbeats, adapter hooks. But it ships no agent-facing
guidance. harn init wires hooks and creates the coord root, and the runtime
injects dynamic session context (identity, peer table), yet nothing tells the
consumer’s agent that these surfaces exist or when to reach for them. “Start a
council” said to a bare-consumer agent does nothing: the procedure lives in no
skill the agent can load. The first host only works because it hand-authored
several thousand tokens of instructions-file sections and skills on top of the
engine; every other adopter would have to reverse-engineer that layer. ADR 0007
even names the contract (“host projects layer their triage rubric + a capture
skill on top”) without giving the generic half of that layer a vehicle.
Two kinds of guidance want two different lifetimes:
- Always-on orientation. An agent must know the docket/councils/journal exist before it can decide to use them, so this must sit in the project instructions file that loads every session.
- On-demand procedure. How to run a council round, how to resolve a decision with evidence: long, and only needed when invoked. This is what adapter skills are for.
Alternatives considered
Section titled “Alternatives considered”- Document it on the docs site / README only. Rejected: agents don’t read the docs site. Guidance has to land in the adapter’s own discovery path (instructions file + skills) to exist at all from the agent’s view.
- Inject the orientation at runtime via hooks (SessionStart extra context) instead of a file block. Rejected as the primary channel: invisible to the consumer’s repo (nothing to review, diff, or commit), re-paid in tokens every session, and each adapter caps injected context differently. Runtime injection stays for what’s genuinely dynamic (identity, peers); the static orientation belongs in a committed file. A file block is also the only channel that reaches a adapter whose hooks aren’t wired yet.
- One big skill instead of an instructions block. Rejected: skills load only when invoked, and an agent that doesn’t know the docket exists never invokes the docket skill. It is a chicken-and-egg problem: the existence claim must be always-on.
- Bin-derived skill names (a host bin
acmegets/acme-decide). Reads nicer per host, but skill names churn when a host renames its bin, collide when two harnery-composed CLIs coexist, and hide which skills harnery owns. Rejected for a literalharn-prefix: stable, collision-proof, and marks provenance. The content still renders the host’s bin everywhere it tells the agent to run something. - Ship the triage rubric (host policy) inside the skills. Rejected. ADR 0007’s portability split holds: tier numbers are engine, tier criteria are host policy. Shipped skills carry engine mechanics only; a host layers its own policy skill on top or replaces ours outright.
- A new top-level command (
harn instructions apply). Rejected: ADR 0005 settled one verb per intent:initwires a project,deinitunwires it. This is more wiring, so it rides the same pair; a separate verb would make three commands whose lifecycles a consumer must sequence by hand. - A freeform, hand-editable block (inject once, let the consumer own it). Rejected: upgrades become merge conflicts and drift becomes invisible. The first host’s HTML-theme splicer proved the alternative shape, a machine-owned, hash-versioned managed region, over dozens of pages: splices stay idempotent, drift is a byte-compare, and everything outside the markers is untouchable.
Decision
Section titled “Decision”harn init grows two products; harn deinit removes both. Idempotent,
--dry-run preserved, plus a new read-only --check mode.
1. An injected instructions block in the consumer’s AGENTS.md:
<!-- harnery:begin instructions v=<hash> -->…rendered orientation…<!-- harnery:end instructions -->v=issha256(body).slice(0, 8). A splice regenerates the body and rewrites the whole region canonically; running it twice yields identical bytes. Stale = recorded hash or body bytes differ from a fresh render, so a hand-edit inside the region registers even if the hash was left alone. The region is machine-owned: edits inside it are overwritten on the nextinit(surfaced first by--check). Content outside the markers is never touched, and a re-splice replaces the block wherever the consumer moved it.- Fresh injection appends the block at the end of
AGENTS.md; a missingAGENTS.mdis created containing just the block. - Claude Code reads only
CLAUDE.md, notAGENTS.md(Claude Code docs, memory § AGENTS.md). So whenCLAUDE.mdis absent,initcreates one whose managed region contains the@AGENTS.mdimport; whenCLAUDE.mdexists without either that import or the block,initwarns and leaves it alone. A host that generatesCLAUDE.mdfromAGENTS.mdflows the block through on its own, and double-injection would duplicate it. - Content budget: orientation, not procedure. What exists (identity +
peer awareness via
agents whoami|status|set-task, intent declaration, journal, decision docket, councils), one line of when, and a pointer to the skill or--helpfor how. Target ≤ 80 rendered lines. The block costs every agent context on every turn; procedures live in skills precisely so it stays small. - Rendered at init time from templates shipped in the package, with the bin
name substituted from the same value
initstamps into.harnery/config.jsonc. Static markdown can’t callresolveBinName()at runtime, so re-runninginitafter a bin rename re-renders the block.
2. Generic skills, written where the adapter has a native skill primitive:
- v1 targets Claude Code:
.claude/skills/harn-decide/SKILL.md(file, search, claim, resolve-with-evidence mechanics for the docket) and.claude/skills/harn-council/SKILL.md(steward/member procedure, guard checks, prompt routing). Both are engine-mechanics only. - Each generated file carries a harnery-ownership header (generated-by marker
- content hash).
deinitdeletes only marked files (the same ownership rule that letsunwireHooksstrip onlyagent-hookentries), and--checkflags a hand-edited or stale skill.
- content hash).
- Cursor and Codex get no native skill fan-out in v1: both read
AGENTS.mdnatively, so the injected block carries the orientation and the CLI’s--helpcarries the procedure. Native registration (Codex skill metadata, Cursor rules) is a tracked follow-on once those conventions are pinned. - Deferred skills: journal (a block one-liner suffices), handoff and docs-sweep (their procedures embed host doc-layout policy the engine doesn’t own, the same boundary as the triage rubric).
.harnery/config.jsoncgainsskills.excludeso a host with its own richer skill (e.g. a policy-laden decide) suppresses the generic one while keeping the rest.
3. Drift contract. harn init --check writes nothing and exits 0 when
fresh, 2 on drift (block or skills stale, missing, or hand-edited), 1 on
error. That exit shape is what consumers wire into a pre-commit hook. Block and skills
are committed by the consumer (agents and CI see them without running
init); the source of truth is the installed harnery version’s templates, so
an upgrade shows up as --check drift until init is re-run.
4. Deinit symmetry. Splice the block out of AGENTS.md/CLAUDE.md
(deleting the file only when init created it and nothing outside the block
remains, mirroring the adapter-only settings-file removal), delete marked
skill files, leave everything else. --purge-state semantics are unchanged.
Consequences
Section titled “Consequences”- Staged rollout, in order: (1) implement in harnery: a pure splice lib
(unit-testable like
wireHooks/unwireHooks), templates, init/deinit/--checkwiring, tests, changeset; (2) dry-run against a copy of the first host’s instructions file and diff which hand-authored sections the block subsumes; (3) the first host de-tangles: generic sections (intent capture, command capture, coord surface, journal, docket engine mechanics, council procedure) retire in favor of the injected block + shipped skills, while host policy (triage rubric, escalation targets, prose gates, the host CLI reference) stays hand-authored, gated on its config-sync andinit --checkpre-commit checks; (4) a second host adopts from journal and validates the bare-consumer experience end-to-end. Do not start (3) before (1) ships and (2) is diffed. - The first host keeps its own policy
decideskill and excludesharn-decideviaskills.exclude; its hand-authored council skill (~90% engine procedure) retires in favor ofharn-councilplus a thin host addendum. - The npm package must ship the template files (
filesfield / build copy). This needs a packaging test, since a Bun submodule checkout resolves paths the publisheddist/build won’t. - Templates are agent-facing prose inside
src/, so the portability guard applies in full: no host tokens, and every command string renders through the bin-name substitution. A template that only reads well forharnis a bug. - Skill names are fixed (
harn-*) even for a renamed bin; only skill content tracks the bin. Two harnery-composed CLIs in one repo would still collide on skill files. That is out of scope, same as today’s single-coord-root assumption. - Not decided here: runtime enforcement of any ritual the block describes
(host hook policy), web-UI surfacing of drift, and auto-refresh on version
bump (re-running
initstays manual).
Addendum: where a consumer’s own policy lives (2026-07-26)
Section titled “Addendum: where a consumer’s own policy lives (2026-07-26)”The block is generic by design, and the ~80-line budget above is a ceiling on engine content. That left a gap this ADR did not answer: a consumer with real coordination policy of its own has nowhere machine-managed to put it. In practice it lands next to the block, hand-maintained, with nothing keeping the two in step and nothing reporting when the hand-written half goes stale.
Two shapes were considered.
Exporting spliceRegion as a public primitive looks smaller, and it is not.
init and deinit are registered inside harnery, so a host that wanted a
managed region of its own would have to wrap or replace both commands and
reimplement apply, refresh, drift-check, and remove. Every consumer would write
that lifecycle again, slightly differently.
The chosen shape is declarative. A consumer names a file:
{ "instructions": { "hostAddendumFile": ".agents/host-instructions.md" } }and harnery splices that file’s contents into a second managed region,
host-addendum, with the same lifecycle as its own block. One implementation of
apply, check, and remove serves every consumer.
This keeps the boundary this ADR already draws. Host policy stays outside harnery’s templates: harnery never parses, renders, or reasons about the content, so nothing host-specific enters the package and the portability guard has nothing new to catch. What harnery owns is what it already owned, region placement, hash versioning, drift detection, and deinit symmetry.
Two properties are load-bearing. Validation runs before the first write, so a
mistyped or escaping path aborts the run rather than leaving AGENTS.md
half-updated and quietly missing a section the host believes is there. And an
unconfigured region is drift, not a no-op: deleting the config key and re-running
init is how a consumer removes the addendum, so --check has to notice a
region whose config entry is gone.
spliceRegion stays internal. It gets exported if a second, genuinely
independent consumer needs it, not before.
Addendum: cross-adapter skills and explicit session end (2026-08-17)
Section titled “Addendum: cross-adapter skills and explicit session end (2026-08-17)”The v1 limitation above is retired. Cursor and Codex both discover project
skills under .agents/skills/, while Claude Code uses .claude/skills/.
harn init now writes the same generated skill set to the selected adapter’s
native root, and init --check plus deinit use that same path. This keeps
drift and ownership behavior consistent instead of leaving Cursor and Codex
with an instructions-block summary of procedures they could not load on demand.
The installed set is now:
harn-decide: decision docket capture, precedent, claim, and resolution;harn-council: guarded steward and council-member workflows;harn-end: authoritative V2 session finalization after work is complete.
The literal harn- namespace remains mandatory. It identifies package-owned
skills, avoids collisions with a host’s own generic names, and does not change
when an embedding CLI uses a different binary name. Harnery does not install
unprefixed aliases. This is a clean pre-1.0 cutover: a host-specific unowned
skill such as end remains the host’s file to remove or rename, while every
Harnery-owned generated skill stays on the existing marker-based lifecycle.