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,
scratch journals, agent heartbeats, harness hooks — but 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/scratch 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, only needed when invoked. This is what harness 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 harness’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 harness 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 harness 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. Chicken-and-egg — 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, scratch 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 harness 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: scratch (a block one-liner suffices), handoff and docs-sweep (their procedures embed host doc-layout policy the engine doesn’t own — 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 — the exit shape 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 harness-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, scratch 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 scratch 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) — 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 — 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).