# E-002 — A reported block that did not reach the exit code

- Date: 2026-08-02
- Origin: dogfooding of the portable alpha via an external Git fixture
- Status: confirmed and fixed
- Intervention that detected it: E2E test for round closure

## Hierarchy of objectives

### Parent objective

Detect deterministic errors and objective drift early, with a signal that an agent, hook, or CI can consume without additional interpretation.

### Phase objective

Deliver a portable alpha that works from an unrelated Git repository, preserves evidence, and runs only declared checks.

### Round objective

Add `init`, a portable `round-end`, and an external fixture that validates safe test discovery, declared checks, and test weakening.

## Observed trajectory

The fixture converted an active test to `test.skip` and ran `oguard check --file`. The JSON output correctly reported `status: "block"` and saved the `weak-test` finding to `.oguard/state.json`.

When invoked as a subprocess, however, the process exited with code zero. The subsequent `round-end` did block when it re-read the active finding, so the problem was not the detection itself; it was the loss of its exit semantics in the immediate integration.

## Expected behavior of the guard

A finding with `error` severity must have two coherent representations:

```text
JSON: { "status": "block" }
exit code: 1
```

This way, an agent that invokes the guard per file can stop before accumulating more work, without depending on a later round closure or parsing local state.

## Actual behavior

Before the fix, `oguard check` emitted `block` on stdout but did not set `process.exitCode`. The portable runner classified that subcheck as `pass`; the persisted state prevented a false global `PASS`, but the fast feedback was incomplete for consumers based on exit codes.

## Evidence

- `test/portable-alpha.test.mjs`: the scenario with `test.skip` received blocking JSON;
- the `oguard check` subprocess resolved with code 0 before the fix;
- `round-end` obtained the finding via `oguard list` and returned `block` despite that sub-result;
- the regression now requires rejection with code 1 in `test/weak-test.test.mjs` and validates the portable closure.

## Classification and limits

It is a propagation defect for a deterministic signal, not a false negative of `weak-test`: the rule found the change and the state preserved it. It also does not prove that the guard detects semantic drift; it only demonstrates that a covered error must not degrade when crossing the CLI/process boundary.

## Fix adopted

`src/cli.mjs` sets `process.exitCode = 1` when `oguard check` returns `status: "block"`. Warnings remain at code zero. The runner also keeps its independent decision based on active findings and declared checks.

## Impact on the thesis

The episode strengthens a necessary condition of the thesis: a fast signal only saves time and tokens if it is actionable through the channel the agent uses. The message and the exit code are parts of the same observable contract.

It does not provide statistical evidence of drift reduction. It provides an organic integration case that must be preserved as an E2E regression.

## Verifiable follow-up

1. Keep the external fixture as a regression for `status:block` and exit code 1.
2. When adding new blocking commands, verify JSON, text, state, and exit code within the same test.
3. Measure, in future evidence, how long the level-0 signal takes; this episode only validates its propagation, not its latency.
