pi-extensions GitHub

pi-done-gate — no "done" on faith

When a run ends with code edits made after the last verification run, this gives the model one more turn. The reminder is display: false: the model reads it, the transcript does not, and it asks for the closing message to carry the explanation of the work rather than a report about the reminder.

This is the text the model sees (you don't):

Done check: 1 file(s) changed after the last verification run (pkg/foo/a.go).
Before reporting done, run `go test ./pkg/foo` and `golangci-lint run ./pkg/foo`
as separate commands, and exercise the changed behavior, or say plainly why
verification does not apply. Then close with your explanation of the work plus
one line of what the check proved, not a report about this reminder.

When nothing in the change maps to a command, or the only signal is a closing claim, the middle sentence stays the generic one: run the repo's checks (.pi/verify if present). The TypeSafe sentence is unchanged, and it appears only when a claim judgment disagrees with what the run actually did. Without credentials the reminder stops after the commands.

No config or runtime dependencies.

What arms the gate

The nudge asks for the repo's checks, so it only fires where those checks could apply.

Which commands

The reminder names the commands that cover the files still dirty at settle. A successful run of one of those commands, as its own bash call, after the last edit is a verification run, so the gate does not fire again. Run them separately: go test ./pkg/foo && golangci-lint run ./pkg/foo is still one compound shell, and those do not count.

.pi/verify wins. If that path exists (a file or a directory), it is the only command named. The language mapping is not also listed.

Otherwise the mapping is deterministic and needs no TypeSafe key:

Jev is asked only when the nearest directory has two or more tsconfigs and none of them is tsconfig.json (app versus node, for example). With a key it picks one command. With no key, a failed request, or an unusable answer, every candidate stays in the reminder. Clear mappings never call it.

A change that matches none of this — a Python file, a go.mod, a TypeScript file with no tsconfig — keeps the generic sentence.

Why agent_before_settle

A tool_result patch (how pi-diagnosis-nudge works) lands mid-run, before the model has decided it is finished. agent_before_settle is the last boundary that can still act: the handler appends a custom_message entry and returns continue: true for exactly one more model request, so the reminder is read before the turn closes instead of after the user has already seen "done".

display: false keeps that message out of the terminal. Per message-types.md, display controls rendering only — pi still converts the entry to a user message for the next request — so the enforcement is unchanged and the transcript keeps the explanation and the checks, not a meta block.

It never blocks a tool call. The model may answer the nudge by running the checks or by saying why they do not apply.

The claim judgment

The reminder cannot read the claim it is answering. When the gate fires and the run has a final assistant message, that message goes to TypeSafe's System One (jev-1.13.0 unless TYPESAFE_MODEL says otherwise) as three typed questions:

A claim that asserts success without naming the gap gains one sentence:

TypeSafe reads that claim as done with the unverified edits unmentioned
(asserts_verified 0.88 · acknowledges_gap 0.05). Name what is unverified, or run the checks.

A claim that names its gap gets nothing added: the reminder already asked for exactly that, and repeating it would only teach the model to skip the nudge. So do runs with no prose claim, no credentials, a failed request, or an unusable answer. The judgment is one extra sentence or silence — never a block, never an error in the settle path. Thresholds and routing live in claim.mjs, not in a prompt, and judgeDoneClaim takes an injectable ask so the tests never touch the network.

Local evidence is not remote evidence

A run that verified locally can still close with "CI is green". That is the same overclaim one layer out: the evidence in the session is a local test run, and the claim is about a runner. The shape of the run cannot see it, so the claim is read instead.

TypeSafe reads that claim as verification on a runner (claims_remote 0.88),
and no check on a runner succeeded this run: verified locally is not verified
remotely. Say which one you have.

A claim that names its gap is still left alone. Only successful, revision-bound remote evidence clears the reading.

Behaviour

Controlled measurements

measure_command is a deferred tool for local benchmark/evaluation observations:

await tools.measure_command({
  command: "node bench.mjs --candidate",
  baseline_command: "node bench.mjs --baseline", // optional; same workspace revision
  expected_work: 100,
  runs: 3,
});

Each invocation must print only JSON such as {"completed":100,"cached":0}. Completed work must exactly match the positive expected_work; zero/partial/unknown work and cache hits are rejected. For an application metric rather than elapsed time, set metric: "value" and unit, and emit a finite numeric value alongside the counters.

The tool executes 3–10 trials per arm through normal nested bash dispatch. Baseline/candidate order alternates; every trial needs explicit host success, untruncated output and the same known Git fingerprint before/after. Each command has a 1–60s timeout (30s default); the whole measurement's deadline is derived from the trials asked for (30s of slack plus every command's own timeout), so the defaults cannot expire before the last trial. Failures, cancellation and revision changes produce no successful report.

The structured result retains raw samples, completed/cache counters, revision, count, mean, min/max and sample standard deviation. Duration includes process and tool overhead. Comparisons report descriptive differences and range overlap; a zero baseline has no percentage change. Range overlap is not a significance test, and no result is labelled a causal improvement.

Counters are self-reported by the command: inspect its instrumentation. This does not prove work semantics, evaluation quality, cache honesty, a bottleneck or an end-to-end gain. It validates reports made through this tool, not arbitrary prose or other measurement programs. The existing bounded workflow pilot retains its own narrower protocol. The completion reminder is unchanged.

Tests

node --test packages/done-gate/test/*.test.mjs — command recognition (positive and negative, including stylua --check and selene), the dirty/verify state machine, cwd scoping and working-tree liveness, outcome gating, debug-tag line numbers against a real temporary git repo, the scoped command mapping (Go packages, nested modules, golangci, nearest tsconfig, Lua configs, .pi/verify winning, and the Jev choice only when several tsconfigs apply), and the claim judgment: the question set, state shaping, threshold routing, the line it renders, and the no-request and failed-request paths.