ruvnet / ruvnet/ruflo

witness `verify.mjs` exits 0 on sha drift as long as the fix marker is still present — no `--strict`/`--fail-on-drift` flag exists for callers who want drift itself to fail the run

Open Beginner friendly
#3,201 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
72.7k
Forks
8.6k
Avg merge
2d 23h
Merged PRs (30d)
83

Description

# witness `verify.mjs` exits 0 on sha drift as long as the fix marker is still present — no `--strict`/`--fail-on-drift` flag exists for callers who want drift itself to fail the run

**Environment**
- ruflo 3.38.19
- node v24.6.0
- macOS 25.6
- agentdb 3.0.0-alpha.20
- plugin ruflo-core 0.2.6
- plugin ruflo-metaharness 0.1.1

**Steps to reproduce**
1. Regenerate a witness manifest (`regen.mjs`) against a clean tree, then modify one byte of a referenced file in a way that does not remove the fix's marker string (e.g. append one byte).
2. Run `node verify.mjs --manifest --json` and inspect the process exit code alongside the JSON summary.
3. `grep -n -- '--strict\|failOnDrift\|strict' verify.mjs` to confirm no such flag is parsed.

**Observed**
`verify.mjs`'s own doc comment states its exit-code contract plainly (`:10-14`): *"0 — signature valid + all fixes pass or drift (marker present); 1 — signature invalid OR any fix regressed/missing (real failure); 2 — bad arguments..."* — drift (sha mismatch, marker still present) is explicitly folded into the "exit 0" case, not the "exit 1" case. The `ok` computation confirms this in code, not just in the comment: `const ok = sig.signatureValid && sig.manifestHashOk && sig.publicKeyReproducible && summary.regressed === 0 && summary.missing === 0;` (`:116-117`) — `summary.drift` is never referenced in this condition at all, so a run where every fix reports `status: 'drift'` and zero fixes report `regressed`/`missing` still yields `ok: true` and `process.exit(ok ? 0 : 1)` (`:147`) exits 0. `grep -n -- '--strict\|failOnDrift\|strict' verify.mjs` returns **zero matches** — confirmed against both the installed plugin-cache copy and the upstream pinned-sha source (byte-identical, `diff` returns zero output). The only flags `parseArgs()` recognizes are `--manifest`, `--root`, `--source-only`, and `--json`, plus a `--help` token that is captured into `out.help = true` (`:210`) but never read or acted on anywhere else in the file (`grep -n "args.help\|args\['help'\]" verify.mjs` → no matches) — so `--help` is silently accepted and silently ignored, the same shape as the metaharness-scripts finding filed separately in this batch, but here confined to a single flag rather than every flag in the script.

Estate consumers wanting a genuine hash tripwire (fail the check the instant a referenced file's content changes at all, marker or no marker) must post-process `verify.mjs --json`'s own output themselves. This install's own `lefthook.yml` witness-verify job does exactly that, piping `verify.mjs --json` into a one-line Node check that treats `summary.drift > 0` as a failure the same as `regressed`/`missing`:
```
node .../verify.mjs --manifest ... --root ... --json | node -e 'const r=JSON.parse(require("fs").readFileSync(0,"utf8"));const s=r.summary||{};if(!r.ok||s.drift>0||s.regressed>0||s.missing>0){console.error("TRIPWIRE witness: reference changed",JSON.stringify(s));process.exit(1)}'
```
This wiring was necessary specifically because `verify.mjs`'s own `ok`/exit-code contract does not fail on drift by design; the estate's `lost-fix-tripwires.txt` carries a header line documenting the gap this workaround exists to cover: *"CANON-GAP: verify.mjs exits 0 on sha drift by design (:10-12); the lefthook job fails on summary.drift>0 from its --json output — estate wiring, remove when upstream ships a --strict flag."*

**Expected**
A `--strict` (or `--fail-on-drift`) flag that, when passed, folds `summary.drift > 0` into the `ok`/exit-code computation the same way `regressed`/`missing` already are — so a caller who wants "any content change fails the check" doesn't have to shell out a JSON-parsing wrapper to get it. Absent that flag, the current default behavior (drift alone does not fail) is reasonable to keep as the default, since issue #1880's own precedent (cited in this same file's doc comment) is about avoiding false-failure noise on legitimate not-yet-built states — a `--strict` opt-in would let callers choose per-use-case without changing that default.

**Root cause**
`verify.mjs`'s `ok` boolean is computed once, from signature validity plus `summary.regressed === 0 && summary.missing === 0` only; `summary.drift` is computed and reported (`:79`, `drift: fileResults.filter(r => r.status === 'drift').length`) but never consulted by the pass/fail decision, and no flag exists to change that.

Upstream at pinned sha `db4991967c45c6f72133dff0bb80b0a492960fc1`, `plugins/ruflo-core/scripts/witness/verify.mjs`:
```
10: * 0 — signature valid + all fixes pass or drift (marker present)
11: * 1 — signature invalid OR any fix regressed/missing (real failure)
12: * 2 — bad arguments / file not found OR precondition not met
...
77:const summary = {
78: pass: fileResults.filter(r => r.status === 'pass').length,
79: drift: fileResults.filter(r => r.status === 'drift').length,
80: regressed: fileResults.filter(r => r.status === 'regressed').length,
81: missing: fileResults.filter(r => r.status === 'missing').length,
82: skippedGenerated,
83:};
...
116:const ok = sig.signatureValid && sig.manifestHashOk && sig.publicKeyReproducible
117: && summary.regressed === 0 && summary.missing === 0;
...
147:process.exit(ok ? 0 : 1);
...
206:function parseArgs(argv) {
207: const out = {};
208: for (let i = 0; i < argv.length; i++) {
209: const a = argv[i];
210: if (a === '--json' || a === '--help') { out[a.slice(2)] = true; continue; }
```
Installed dist/plugin copy (ruflo 3.38.19, plugin ruflo-core 0.2.6) — byte-identical to the upstream copy above (`diff` returns zero output), same line numbers throughout: `/Users/ethicco_michael/.claude/plugins/cache/ruflo/ruflo-core/0.2.6/scripts/witness/verify.mjs:10-12,77-83,116-117,147,206-210`.

**Suggested fix**
Minimal diff sketch:
```diff
function parseArgs(argv) {
const out = {};
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
- if (a === '--json' || a === '--help') { out[a.slice(2)] = true; continue; }
+ if (a === '--json' || a === '--help' || a === '--strict') { out[a.slice(2)] = true; continue; }
```
```diff
+const strict = !!args.strict;
const ok = sig.signatureValid && sig.manifestHashOk && sig.publicKeyReproducible
- && summary.regressed === 0 && summary.missing === 0;
+ && summary.regressed === 0 && summary.missing === 0
+ && (!strict || summary.drift === 0);
```
Also update the doc-comment exit-code contract (`:10-14`) to note the `--strict` variant, and either implement `--help` (currently parsed but never acted on, `:210`) or drop it from `parseArgs` to avoid the same silently-accepted-but-inert shape filed separately for the metaharness scripts in this batch.

**Related**
- `METAHARNESS-scripts-no-help-live-defaults.md` (this directory) — same "flag parsed/accepted but not acted on" shape (there: every flag including `--help`; here: `--help` specifically), different plugin family.
- #2729, #2274, #1880 (all closed) — prior witness-verification issues, all about `@noble/ed25519`/dist-artifact preconditions (exit code 2 class), not the drift-vs-strict exit-code question this issue raises.
- Searched `gh api search/issues -f q='repo:ruvnet/ruflo verify.mjs strict'` (9 results, all about `ruflo-adr`'s unrelated `adr-verify`/`import.mjs` tools, not witness `verify.mjs`) — filing fresh.

**Evidence / estate provenance**
`task/task-1788569472192-izk2u7/receipts` (ns `final`) — the B-7 witness-lock receipt describing the lefthook wiring built specifically to cover this gap, including the exact piped one-liner and the `lost-fix-tripwires.txt` CANON-GAP paragraph quoted above verbatim. `task/task-1788569472192-izk2u7/tl-spotcheck` (ns `final`) — independent TL re-fire of the same lefthook job (twin deny/allow pair) confirming the wiring works as the receipt describes, and re-citing `verify.mjs :10-12` and the CANON-GAP line as the reason the wiring exists. Both re-opened and quoted directly this session; `verify.mjs` itself re-read in full this session (installed copy, `/Users/ethicco_michael/.claude/plugins/cache/ruflo/ruflo-core/0.2.6/scripts/witness/verify.mjs`) and diffed byte-for-byte against the upstream pinned-sha fetch.

**Deferrals**
None for this draft.

Contributor guide

Open the contributing guide

Research direction

Start with plugins/ruflo-core/scripts/witness/verify.mjs, especially parseArgs and the ok computation around lines 116-117, then reproduce the documented drift case with node verify.mjs --manifest --json. The work is done when --strict is accepted, drift makes the run fail only with that flag, and the exit-code documentation reflects the new behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, node.js
Domain
cli, tooling
Issue type
Feature
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
84/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.