Pre-release Harn is pre-1.0 — the language, standard library, and CLI may change between releases. See the release notes

Skill activation evidence

Harn owns one stable, host-consumable record of what happened to each skill this turn: which short cards were shown, which were omitted and why, what each cost against the catalog budget, and where each skill sits in the disclosure lifecycle. Hosts — Burin, headless runs, cloud, the portal — read this record instead of re-parsing the catalog prompt text.

This page describes the contract and the migration path from Burin's product-side skill_activation_report.

Where it comes from#

The evidence is a projection of the pieces Skills already produces — layered discovery, the merged registry, catalog cards, the lazy load_skill body, and the skill lifecycle events. It adds no new discovery, loader, or registry behavior. The one thing it makes newly available is the shown-vs-omitted-to-budget decision as structured data: that decision is computed by the same fit_catalog primitive the prompt renderer (render_always_on_catalog) uses, so the evidence can never disagree with the catalog the model actually saw.

Diagram source
flowchart LR
  accTitle: How skill activation evidence reaches a host
  accDescr: The merged skill registry feeds a budget fit, which produces both the always-on catalog prompt text and the activation evidence payload. Completing a skill load emits a skill.loaded event. Hosts and reports consume the evidence payload and the lifecycle event.
  registry[Merged skill registry] --> fit[fit_catalog budget fit]
  fit --> render[render_always_on_catalog prompt text]
  fit --> evidence[skills_activation_evidence payload]
  load[load_skill completion] --> lifecycle[skill.loaded event]
  evidence --> hosts[Hosts and reports]
  lifecycle --> hosts

The builtin#

skills_activation_evidence(registry: dict, options?: dict) -> dict

registry is a skill registry (skill_registry() / the pre-populated skills global). options accepts:

KeyTypeDefaultMeaning
budgetint2000Catalog character budget, matching render_always_on_catalog.
catalog_limitintnoneCap on model-invocable cards, matching the loop's catalog_limit. Entries past it are omitted with reason catalog_limit.
loadedlist[]Skill ids whose body was pulled via load_skill.
usedlist[]Skill ids whose loaded body drove a downstream action.

loaded / used let a host fold the runtime lifecycle it already tracks (from the skill.loaded / skill_activated events) into the same payload, so one record answers eligible vs loaded vs used.

The payload#

{
  "_type": "skill_activation_evidence",
  "schema_version": 1,
  "budget_chars": 2000,     // effective character budget
  "used_chars": 812,        // characters the rendered catalog consumed
  "budget_tokens": 500,     // budget in tokens (shared 4-chars/token heuristic)
  "used_tokens": 203,
  "shown": ["deploy", "review"],   // ids whose card rendered
  "omitted": ["manual", "..."],    // ids whose card did not render
  "cards": [
    {
      "id": "deploy",
      "name": "deploy",
      "source": "project",                 // discovery layer label
      "description": "Ship the current build",
      "when_to_use": "when the user says deploy",
      "disable_model_invocation": false,
      "selected": true,                     // card rendered this turn
      "omitted_reason": null,               // budget | catalog_limit | disable_model_invocation
      "char_estimate": 74,
      "token_estimate": 19,
      "lifecycle": "shown",                 // eligible | shown | omitted | loaded | used
      "matched": null                       // {score, reason} when the host merged match evidence
    }
  ]
}

Body lifecycle states#

StateMeaning
eligibleIn the registry and model-invocable, but its card was not rendered this turn.
shownIts short card was rendered into the catalog prompt.
omittedEligible but dropped before the model saw it (see omitted_reason).
loadedIts full body was pulled via load_skill (from the loaded option).
usedIts loaded body drove at least one downstream action (from the used option).

used and loaded override the registry-derived state, so a skill that was both shown and later loaded reports loaded.

Omitted reasons#

ReasonMeaning
budgetDropped to keep the rendered catalog within budget.
catalog_limitTrimmed before rendering because catalog_limit was already full.
disable_model_invocationdisable-model-invocation — never offered to the model.

load_skill completion metadata#

The skill.loaded transcript event carries lifecycle (loaded on success, omitted when a load is blocked), source, disable_model_invocation, and a token_estimate of the body, alongside the existing provenance fields — so a host can audit eligible vs loaded vs used from that event alone without prompt parsing.

Registry precedence stays deterministic#

The evidence is built from the merged registry, which already resolves collisions deterministically by layer — bundled/CLI, runtime, host/project, package, user (see layered discovery). A duplicate skill name collapses to one winner before it reaches the payload; the source field on each card records the winning layer.

Migration note for Burin#

Burin's product pipeline currently computes activation in lib/context/skill-activation.harn::skill_activation_report, which mixes three concerns:

  1. Fingerprint → skill selection (project_fingerprint, TAG_SKILL_MAP, detect_active_skill_report) — which Burin-bundled cards apply to a repo.
  2. Card rendering + budget accounting (render_card_blocks_plan, rendered_names / omitted_names / card_tokens) — the shown/omitted split and token cost.
  3. Product presentation (render_skill_activation_report) — the human-facing card block wording.

What moves into this Harn contract#

  • The shown/omitted split, budget accounting, source metadata, disable-model-invocation, and body lifecycle — concern (2) — is exactly what skills_activation_evidence now returns. Burin should read shown, omitted, cards[].omitted_reason, cards[].source, cards[].token_estimate, and cards[].lifecycle from Harn rather than recomputing them in skill_activation_report. The rendered_names / omitted_names / card_tokens / suppressed_* bookkeeping collapses into the Harn payload.
  • The skill.loaded lifecycle/costing metadata replaces any Burin-side reconstruction of load state.

What stays product UI in Burin#

  • Fingerprint-driven selection (concern 1). Choosing which project skills apply from a repo fingerprint is a Burin editor/product fact; it feeds the registry Burin hands to Harn, and stays in Burin.
  • Card block wording / rendering (concern 3, render_skill_activation_report). Presentation is product UI. It should render from the Harn payload instead of from a parallel Burin-computed report.

Release / version boundary#

skills_activation_evidence and the enriched skill.loaded metadata ship in the Harn release that first carries this schema (schema_version: 1). Burin should consume the contract only after pinning that .harn-version. Until then, Burin The downstream host can keep running its existing skill_activation_report path unchanged — the two produce the same shown/omitted decision because both budget on the same card text, so the cutover is a swap, not a behavior change.