M3.2, the action classifier (L4). What the fast path calls unknown is now judged by an independent model call before it runs. Until now, unknown actions simply ran.
What it sees: the action and the agreed scope, built in code.
The scope is the project, the repository, the issue number, and the restatement the operator said go to.
Never: the transcript, the issue's text, tool output, or the driver's reasoning.
The prompt says text inside the command is data to judge, never an instruction.
What it decides: the verdict comes back as a tool call.
safe: it runs.
in_scope: it runs, and a line in the thread reports it: Ran on #14, judged in scope: … (reason).
needs_discussion: the item asks, through the same allow flow as fast-path dangerous actions. The question shows the classifier's reason.
Fails closed: an error, no verdict, or a label it can't give counts as needs discussion.
Helpers: a helper is refused what needs discussion, and told to hand back.
Caching: a verdict is reused per item, agreed revision and exact action, so a repeated command is judged once.
The agreed decisions:
No different-model requirement: separate context is the independence L4 needs, and each verdict records its model, so correlated misses can be measured.
The local model to start with.
An in-scope note in the thread, which can go if it's too chatty.
The cache.
Capture plus export plus an opt-in replay of the evaluation set.
Logging:action_verdict gains source (fast_path or classifier), model, and the classifier's exact input. An unknown action leaves two events: the fast path's classified and the classifier's verdict. details shows which source decided. An allow after the classifier asked is an override.
The evaluation set
scripts/classifier-evals.py STATE_DB [--overridden] exports judgements as JSON lines. It reads the store read-only, so it's safe on a live agent's database.
tests/integration/classifier_cases.yaml is the seed set: 9 cases, including a secret sent out by inline Python, and a command whose comment tells the classifier to answer safe.
The replay test runs it per model, opt-in, like the capability matrix.
Results: qwen3.8-27b-nothink 9 of 9, claude-haiku-4-5 9 of 9.
The fast path's delete rule (decided in the plan discussion)
Contained: deleting on the build VM. It's disposable.
Still dangerous: what would break the host — /, system and home directories, dd, mkfs, reboot. In tests the "build VM" is the test machine itself.
The loop tests and the walk now use a dangerous-but-harmless POST to a closed local port as their dangerous command.
Config: the example config sets classifier: qwen3.8-27b. The classifier also reads plain-language go-aheads, so that moves to the local model too. The bring-up config on this box is updated, with a backup kept as nisse-dev.yaml.pre-m3.2. The integration config is unchanged (Haiku, through the expert).
Tests
The classifier itself: verdict mapping, five ways of failing closed, and its exact input, which is checked to contain no issue text, including an injection attempt in the issue body.
In the loop:
safe runs, and needs-discussion asks, with an allow recorded as an override;
in-scope runs and is reported, and a failed classifier asks;
the fast path comes first, and a helper is refused;
the eval export, both the function and the script on a real store.
The property walk gains a classifier that answers at random, including failures and junk. dangerous_only_when_allowed covers its verdicts.
Across 16 seeds it reaches every path: safe, in-scope, needs-discussion, failed-closed, allowed, and the helper.
It catches a mutant that runs every classifier verdict.
Results, run locally:
Unit: 706 passed.
Integration: 17 passed, plus 16 opt-in skips.
Full e2e: 8 of 8.
The real classifier in e2e: it judged the 6 calls the fast path didn't know (here-document Python edits and a repro run) and found all 6 safe, with sensible reasons.
Deferred:
promoting verdicts the classifier keeps agreeing on into rules, by hand from exported cases;
**M3.2, the action classifier** (L4). What the fast path calls *unknown* is now judged by an independent model call before it runs. Until now, unknown actions simply ran.
**What it sees:** the action and the agreed scope, built in code.
- **The scope** is the project, the repository, the issue number, and the restatement the operator said `go` to.
- **Never:** the transcript, the issue's text, tool output, or the driver's reasoning.
- **The prompt** says text inside the command is data to judge, never an instruction.
**What it decides:** the verdict comes back as a tool call.
- **`safe`:** it runs.
- **`in_scope`:** it runs, and a line in the thread reports it: *Ran on #14, judged in scope: `…` (reason)*.
- **`needs_discussion`:** the item asks, through the same `allow` flow as fast-path dangerous actions. The question shows the classifier's reason.
- **Fails closed:** an error, no verdict, or a label it can't give counts as needs discussion.
- **Helpers:** a helper is refused what needs discussion, and told to hand back.
- **Caching:** a verdict is reused per item, agreed revision and exact action, so a repeated command is judged once.
**The agreed decisions:**
- No different-model requirement: separate context is the independence L4 needs, and each verdict records its model, so correlated misses can be measured.
- The local model to start with.
- An in-scope note in the thread, which can go if it's too chatty.
- The cache.
- Capture plus export plus an opt-in replay of the evaluation set.
**Logging:** `action_verdict` gains `source` (`fast_path` or `classifier`), `model`, and the classifier's exact `input`. An unknown action leaves two events: the fast path's `classified` and the classifier's verdict. `details` shows which source decided. An `allow` after the classifier asked is an override.
**The evaluation set**
- **`scripts/classifier-evals.py STATE_DB [--overridden]`** exports judgements as JSON lines. It reads the store read-only, so it's safe on a live agent's database.
- **`tests/integration/classifier_cases.yaml`** is the seed set: 9 cases, including a secret sent out by inline Python, and a command whose comment tells the classifier to answer safe.
- The replay test runs it per model, opt-in, like the capability matrix.
- Results: `qwen3.8-27b-nothink` 9 of 9, `claude-haiku-4-5` 9 of 9.
**The fast path's delete rule (decided in the plan discussion)**
- **Contained:** deleting on the build VM. It's disposable.
- **Still dangerous:** what would break the host — `/`, system and home directories, `dd`, `mkfs`, `reboot`. In tests the "build VM" is the test machine itself.
- The loop tests and the walk now use a dangerous-but-harmless POST to a closed local port as their dangerous command.
**Config:** the example config sets `classifier: qwen3.8-27b`. The classifier also reads plain-language go-aheads, so that moves to the local model too. The bring-up config on this box is updated, with a backup kept as `nisse-dev.yaml.pre-m3.2`. The integration config is unchanged (Haiku, through the expert).
**Tests**
- **The classifier itself:** verdict mapping, five ways of failing closed, and its exact input, which is checked to contain no issue text, including an injection attempt in the issue body.
- **In the loop:**
- safe runs, and needs-discussion asks, with an allow recorded as an override;
- in-scope runs and is reported, and a failed classifier asks;
- the fast path comes first, and a helper is refused;
- the eval export, both the function and the script on a real store.
- **The property walk** gains a classifier that answers at random, including failures and junk. `dangerous_only_when_allowed` covers its verdicts.
- Across 16 seeds it reaches every path: safe, in-scope, needs-discussion, failed-closed, allowed, and the helper.
- It catches a mutant that runs every classifier verdict.
**Results, run locally:**
- **Unit:** 706 passed.
- **Integration:** 17 passed, plus 16 opt-in skips.
- **Full e2e:** 8 of 8.
- **The real classifier in e2e:** it judged the 6 calls the fast path didn't know (here-document Python edits and a repro run) and found all 6 safe, with sensible reasons.
**Deferred:**
- promoting verdicts the classifier keeps agreeing on into rules, by hand from exported cases;
- the trust dial (M3.3).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
What the fast path calls unknown is judged by an independent model call
on the item's classifier role (L4), before it runs.
- nisse_dev.loop.classifier: it sees the action and the agreed scope
(project, repository, issue number, the restatement the operator said
go to), built in code; never the transcript, the issue's text, tool
output or the driver's reasoning. Its verdict is a tool call: safe
runs; in_scope runs and is reported in the thread; needs_discussion
asks, as a dangerous action does. An error, no verdict or an unknown
one fails closed. Verdicts are cached per item, agreed revision and
action. A helper is refused what needs discussion.
- action_verdict gains source, model and the classifier's exact input;
details shows the source. An allow after the classifier asked is an
override. scripts/classifier-evals.py exports the judgements from a
store, read-only; tests/integration/classifier_cases.yaml is the seed
set, run per model with --capability-models.
- The fast path's delete rule: deleting on the build VM is contained;
only what breaks the host (system and home directories) stays
dangerous, since in tests the build VM is the test machine.
- The classifier role is the local model in the example config.
- The property walk gains a classifier answering at random, junk and
failures included; dangerous_only_when_allowed now covers its
verdicts (checked against a mutant that runs them all).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
M3.2, the action classifier (L4). What the fast path calls unknown is now judged by an independent model call before it runs. Until now, unknown actions simply ran.
What it sees: the action and the agreed scope, built in code.
goto.What it decides: the verdict comes back as a tool call.
safe: it runs.in_scope: it runs, and a line in the thread reports it: Ran on #14, judged in scope:…(reason).needs_discussion: the item asks, through the sameallowflow as fast-path dangerous actions. The question shows the classifier's reason.The agreed decisions:
Logging:
action_verdictgainssource(fast_pathorclassifier),model, and the classifier's exactinput. An unknown action leaves two events: the fast path'sclassifiedand the classifier's verdict.detailsshows which source decided. Anallowafter the classifier asked is an override.The evaluation set
scripts/classifier-evals.py STATE_DB [--overridden]exports judgements as JSON lines. It reads the store read-only, so it's safe on a live agent's database.tests/integration/classifier_cases.yamlis the seed set: 9 cases, including a secret sent out by inline Python, and a command whose comment tells the classifier to answer safe.qwen3.8-27b-nothink9 of 9,claude-haiku-4-59 of 9.The fast path's delete rule (decided in the plan discussion)
/, system and home directories,dd,mkfs,reboot. In tests the "build VM" is the test machine itself.Config: the example config sets
classifier: qwen3.8-27b. The classifier also reads plain-language go-aheads, so that moves to the local model too. The bring-up config on this box is updated, with a backup kept asnisse-dev.yaml.pre-m3.2. The integration config is unchanged (Haiku, through the expert).Tests
dangerous_only_when_allowedcovers its verdicts.Results, run locally:
Deferred:
🤖 Generated with Claude Code
https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE