---
name: harn-probe
short: Probe-first verification — run snippets, capture outcomes as typed Facts.
description: Use to convert a guess about codebase behavior into a recorded observation by running a probe snippet and persisting the outcome as a Fact.
when_to_use: Use before asserting a claim about behavior, types, or output that you have not yet verified directly. Especially before destructive edits, before describing what a function returns, or before declaring a refactor safe.
---
# Harn probe
Use this skill when you are about to assert a claim about runtime
behavior, type-checker output, or command output that you have not
directly observed in this session. Replace the assertion with a
`probe(...)` call from `std/agent/probe`, then state the outcome as
observation, not speculation.
Pair it with [[harn-agent]] for autonomous workflows, [[harn-language]]
for the typecheck fragments you feed `probe("typecheck", ...)`, and
[[harn-testing]] for the conformance fixtures that anchor probe-derived
facts.
## Why probe instead of guess
- Guessing is cheap; verifying is structured tool use that the runtime
can record.
- A `probe` outcome lands in the Fact ledger with
`provenance.source = "probe"`, so a follow-up session can recall the
verified answer instead of re-guessing.
- The cost of one probe is bounded shell or typecheck time. The cost of
a wrong assertion is a wasted user round-trip plus a potentially
destructive edit.
- Animals learn by *probing* — touching, tasting, testing — not by
passive prediction. The same pattern keeps LLM coding assistants
honest.
- Recording the *failure* outcome is just as load-bearing as recording
the success. The ledger needs both to invalidate stale claims later.
## When to probe instead of asserting
- You are about to claim a function returns a particular value, type,
or shape.
- You are about to declare a refactor "safe" or a migration
"compatible".
- You are about to assert that a fragment type-checks (or fails to
type-check) with a particular diagnostic code.
- You are about to repeat a fact from training data that may have
drifted in this codebase.
- You are about to recommend a destructive edit (`rm`,
`git reset --hard`, schema drop, `--no-verify`) without first
observing that the precondition holds.
- You are about to summarize what a command prints, what a config
defaults to, or what an env var does without having run the command
in this session.
If you genuinely cannot probe — the surface is non-deterministic, the
body is too large, the cost is too high — say so out loud rather than
asserting.
## The probe primitive
`std/agent/probe` exposes `probe(kind, body, options?)` plus
`probe_eval` and `probe_typecheck` shorthands. Two probe kinds are
implemented today; `test` and `inspect` are reserved and currently
return `unknown`.
- `probe_eval(body, options)` runs `body` as a shell command by
default. Set `options.lang = "harn"` to run `body` as a Harn snippet
via `harn run`. Outcome is `pass` on exit 0 and `fail` otherwise;
`options.expected` matches against trimmed stdout for a string and
against the integer for a numeric comparison.
- `probe_typecheck(body, options)` writes the fragment to a temp file
and runs `harn check <path> --json`. Outcome is `pass` when there are
zero errors; `options.expected` matches the parsed error count.
Every probe returns a `harn.probe.v1` envelope:
- `kind`, `outcome`, `observed` — short summary of what was seen.
- `evidence` — `trace_id`, `snippet`, `command`, `stdout`, `stderr`,
`exit_code`, `duration_ms`, plus `timed_out` when the host enforced a
deadline.
- `fact_id` — id of the auto-stored Observation fact (pass
`options.store_fact = false` to suppress the write).
- `expected`, `asserted_at` — round-tripped from the input.
The Fact body uses `confidence = 0.9` for observed `pass`/`fail` and
`0.4` for `unknown`, so
`recall_facts("…", "Observation", 0.8, scope)` returns only
directly-verified outcomes.
## Probe-first rules
- Probe before asserting. A claim that is not backed by a probe in this
session or a recalled probe fact is speculation.
- Probe small. A probe that takes longer than the work you would save
is overhead, not verification. Bound the body and pass
`options.timeout_ms` for anything that could hang.
- Probe deterministic surfaces. Probing a live network endpoint, an
LLM call, or wall-clock-sensitive code from inside a probe is rarely
useful — those belong in workflows, not facts.
- Record the failure too. A probe that comes back `fail` is just as
load-bearing as one that comes back `pass`.
- Trust the observed outcome over a recalled fact. If recall says "this
type-checks" but a fresh probe says it does not, the fresh probe wins
— invalidate the stale fact with `invalidate_facts` from
`std/agent/fact` before continuing.
- Quote the trace_id, not the prose. When you reference a probe outcome
in a user message or PR description, cite `fact_id` or
`evidence.trace_id` so the reader can find the same observation.
## Composing with Fact recall
Probes write Observations with `provenance.source = "probe"` and
`provenance.probe_kind = "<kind>"`. Before re-running a probe, recall
with `recall_facts(query, "Observation", 0.0, scope)` to see if the
same probe already ran in a recent session. Match by
`provenance.probe_kind` to scope recall to a particular probe shape
(eval vs typecheck).
When code drift might have invalidated a recalled fact, use
`invalidate_facts({kind: "Observation", evidence: {kind: "tool_output",
ref: trace_id}}, scope)` to tombstone the stale record, then re-probe
and record the fresh outcome.
## Anti-patterns
- Probing inside a hot loop to "verify each iteration" — bound the
loop, probe the boundary condition once.
- Probing with a body so large that the snippet evidence is meaningless
— extract the smallest fragment that exercises the claim.
- Probing then ignoring the `outcome` field because the prose summary
"looks right" — the structured outcome is the contract.
- Writing prose into `observed` that contradicts the actual outcome —
keep `observed` tight and grounded in the captured stdout/stderr.
- Using `probe("test", ...)` or `probe("inspect", ...)` and expecting a
real outcome — they are reserved and return `unknown` today; record
the limitation and fall back to `probe_eval` with the appropriate
test runner command.
## Probe lifecycle
1. Identify the claim you are about to assert.
2. Pick the smallest body that exercises it (a single shell command, a
3-line Harn fragment, a single typecheck).
3. Call `probe(kind, body, options)`. Pass `options.expected` when you
have a specific value or count in mind so the outcome is a hard
match instead of a fuzzy success signal.
4. Inspect the returned `outcome` and `observed`. Quote
`evidence.trace_id` or `fact_id` when summarizing to the user or in
a PR description.
5. If the outcome contradicts a recalled fact, invalidate the stale
fact before continuing.
6. If the outcome unblocks the original task, proceed. If it surfaces
an unexpected failure, treat that as new information and decide
whether to fix, escalate, or abandon — do not re-assert the original
guess.
## Example probes
```harn
import { probe_eval, probe_typecheck } from "std/agent/probe"
// 1. Verify a shell helper before relying on its exit code.
let helper = probe_eval(
"git diff --quiet HEAD -- crates/harn-stdlib",
{expected: 0, timeout_ms: 5000},
)
if helper.outcome != "pass" {
__io_println("stdlib has uncommitted edits — recall fact: " + helper.fact_id)
}
// 2. Confirm a fragment type-checks before pasting it into the user's file.
let tc = probe_typecheck(
"pipeline summary() { let x: int = len([1, 2, 3]) __io_println(x) }\n",
{expected: 0},
)
require tc.outcome == "pass", "fragment fails typecheck: " + tc.observed
```
## Verify
- Stdlib catalog: `cargo test -p harn-stdlib --test stdlib_modules`
round-trips the source.
- Conformance:
`cargo run --quiet --bin harn -- test conformance --filter agent_probe`.
- Manual smoke: `harn run -e 'import {probe_eval} from "std/agent/probe";
__io_println(probe_eval("echo hi").outcome)'`.
- Skill body: `cargo test -p harn-skills` covers the embedded corpus
invariants.
## Kill criterion
If probe-first injection slows tasks by more than 50 percent without
measurably reducing wrong-assertion incidents over 20 sessions,
restrict probing to high-risk operations (before destructive edits,
before declaring a refactor safe) or abandon the always-on nudge.
Document the kill decision as a Decision fact so future sessions
inherit the rationale.