# v0.1.0 release-candidate review — pull request 81

**Review status: CHANGES REQUIRED (blocker)**

| | blocker | major | minor | nit |
|---|---|---|---|---|
| Real, in-scope | 2 | 26 | 78 | 33 |

- 154 findings after dedupe (0 duplicates collapsed on `(chunk, file, anchor)`), 139 real and in-scope, 15 refuted or already-correct.
- 15 findings re-graded by validation.
- Must-fix checklist: 28 items (blocker + major, real and in-scope).

## Method

Base `origin/main` (merge-base `977f106`), head `feat/issue-41-spec-v0.1.0`, three-dot diff. 124 files, +16008/-78, 41 commits. `eval/package-lock.json` triaged out — noted, not read line-by-line.

The diff was partitioned in code into 14 chunks whose union equals `git diff --name-status` exactly, with no file in two chunks. Each chunk's diff was precomputed to a file, so no reviewer or validator re-ran `git diff` for it. One reviewer per chunk applied the **thermo** maintainability lens and the report-only **deslop** lens; documentation chunks also took a documentation-accuracy lens. One read-only `Explore` validator per chunk then applied `receiving-code-review` — VERIFY, EVALUATE, RESPOND, stopping before IMPLEMENT — confirming or refuting each claim against the code, correcting locations, and proving in-diff scope from literal `git` stdout before writing any verdict. Validators have no Edit or Write tool, so read-only was enforced by capability rather than by instruction.

Suite baseline before any fix: `node --test "test/*.test.js"` → **260 tests, 260 pass, 0 fail**.

### Hard constraints carried verbatim into every agent

- Zero dependencies. Nothing outside the Node.js standard library. Never propose or add a package, a lockfile entry, or a runtime beyond stdlib for the shipped runtime, skills, or adapters.
- One contract seam. Only a skill’s wrapper script, run as a process, is a tested contract boundary. Do not propose unit tests against internal modules, and do not propose new seams.
- No invented value for an open specification row. An `Open` row in docs/spec/okf-agent-skills-v0.1.0.md’s open-item table is closed by a recorded decision, never by a guess. Do not delete, fill, or "complete" CONTEXT.md entries whose content is "open" — the specification’s own gap list names them and issue 76 forbids inventing values.
- Inline safety material stays inline in every SKILL.md. The wrapper request shape, the three exit conditions, the effect-ownership lists, the not-a-v0.1.0-operation refusal rules, the okf-delegate contract, and the router dispatch table are deliberately repeated in each skill and must NOT be extracted into shared references. Extracting a safety refusal behind a conditional load would let an agent act without having read it, and test/issue-55.test.js enforces the dispatch table.
- HANDOFF.md stays untracked. It is the previous session’s working file, declares itself not a project artifact, and is deliberately left out of the branch. Do not track it, fix it, cite it, or report its absence as a gap.
- The delegated execution path is unreachable after installation by recorded decision 91. brief.settings stays validated and inert, and the placement and precedence claims are retained design, not a defect. Do not report unreachable delegation, an inert setting, or the reader/writer-only okf-delegate role list as a bug or dead code.
- The OpenCode adapter ships without a readiness claim and requires a manual permission merge, by recorded decision 85 — that is the contract, not an omission. Issue 73’s frozen "220 tests and 220 pass" figure is superseded; the live criterion is "pass count equals test count, zero fail" under `node --test "test/*.test.js"`. Do not report the changed count as a regression.
- Mode is auto-fix everything: minors and nits are fixed too, not left as a checklist.

## Themes

- Failures are converted into silence at almost every layer, and it is the same coding move each time: swallow the non-zero path and keep going. The blocker in scripts/lib/adapters.js:165 (a crashed okf-read run parses stdout only on status 0, so the RUNTIME_FAILURE response the wrapper already printed is discarded and the occurrence replayed) is the same shape as the empty catch in scripts/lib/navigation.js:186, the empty catches in test/issue-68.test.js:63 and test/issue-46.test.js:43, the empty-detail fail record in eval/run.js:65, and the tests that never assert exit status or stderr (test/issue-47.test.js:30, test/issue-53.test.js:50). Fix the exit-status/stderr handling once in the wrapper-invocation helpers (adapters.js dispatch, the shared test runCli, eval/run.js) and the leaf findings collapse.
- The temporary-repository test harness is copy-pasted into every new test file and has already drifted apart, even though test-support/ exists for exactly this. The same temporaryRoot/runCli/writeManifest trio recurs in test/issue-46.test.js:21, test/issue-47.test.js:36, test/issue-49.test.js:52 and test/issue-105.test.js:36, and its no-mutation digest exists in three strengths of varying blindness (test/issue-48 strongest, test/issue-52.test.js:51 cannot see symlink changes, test/issue-53.test.js:53 hashes only top-level files, which puts a hole in the "nothing was written" proof of four tests). One shared harness in test-support/ with the strongest digest fixes four chunks' findings and the proof holes with them.
- One containment predicate and one refusal/finding constructor exist in four and five copies respectively across the runtime, and the copies are not equal — the divergence is where the bugs live. scripts/lib/navigation.js:28 holds a byte-identical copy of reach.js's exported inside(), while scripts/lib/validation.js:373 re-implements it and loses the filesystem-root guard; the gate-finding constructor with the named_by_user path rule is written a third time in scripts/lib/admission.js:34, scripts/lib/delegation.js:58 re-types the status/code/gate triple per refusal, and scripts/lib/runtime.js:150 repeats a ten-argument blocked return nine times. Route every caller through reach.inside() and one refusal builder before touching any leaf finding in runtime-core, runtime-validation or runtime-navigation.
- Agent- and user-facing documentation describes a runtime that does not exist, which matters more than internal doc drift because the SKILL.md files are the instructions an agent follows. skills/okf-read/SKILL.md:31 documents a resolve request shape that crashes the wrapper, skills/okf-write/SKILL.md:28,36,46 documents six effects the v0.1.0 runtime refuses with UNSUPPORTED_INPUT plus a finding code presented as a data.code, skills/okf-lifecycle/SKILL.md:51 gets the INVALID_SCOPE status wrong, and two operation-map rows in docs/spec/okf-agent-skills-v0.1.0.md:3376 still name a mechanic the same pass removed from their siblings. These should be regenerated from the runtime's actual refusal codes, not edited line by line.
- The specification documents assert, in the present tense, a repository state this branch has already changed — including four open-item rows that a recorded decision in the same chunk closes. docs/spec/okf-agent-skills-v0.1.0.md:3651 keeps four rows at `Open` that the completion spec's D5-D8 answer (the AGENTS.md rule says an Open row must be closed by a recorded decision before its behavior ships, and the behavior shipped), the new MUST at line 2763 fixes the shared module set at five when fifteen ship, the undefined-terms gap list at 3669 lists terms the branch defines, and the completion spec's Problem Statement (line 20) describes the pre-branch tree. The spec's status columns and gap lists are the root: they are hand-maintained snapshots with no check tying them to the tree.
- Release policy is enforced by grepping source text rather than by exercising behaviour, so the gates are coupled to formatting and several are inert. test/issue-68-agents.test.js:144 regex-scans a source file guarded only by size > 0, test/issue-48.test.js:147 hard-codes the exhaustive scripts/lib listing to test wrapper count, test/issue-72.test.js:74 pins a source line without checking the replacement landed, test/issue-51-policy.test.js:123 bans the bare English word "follow" across every shipped script including comments, and test/issue-50.test.js:20 splits and re-joins retired vocabulary to dodge a self-match. Every one of these fails on a rename that changes nothing and passes on a behavioural regression.
- Tests are written shape-tolerantly, which means the wrapper contract they exist to pin can change without a failure. test/issue-50.test.js:154 accepts the navigation envelope under either name, test/issue-49.test.js:103 accepts a report as string or array, test/issue-105.test.js:119 passes vacuously when install does nothing, test/issue-97-delegation-narrowing.test.js:33 only checks a leading 'agents/' rather than any agents path segment, test/issue-51-links.test.js:93 never proves the search it claims independence from ran, and the four test/fixtures/obligations/* files plus test/fixtures/roots/* are never read by any test at all. Pin the response envelope in one shared assertion helper and delete the dead fixtures.
- The refutations cluster on one misreading, and it is the documents' fault, not the reviewers': six of the fifteen refuted findings treat docs/research/* as policy and report a research recommendation the project later declined as a contradiction with the shipped code (adapter-install-targets.md 113/132/197, agent-skill-evaluation-automation.md:15, flue-skill-evaluation.md:28,204, langfuse-coding-agent-skill-evaluation.md:426,528,571). Each record does carry an "evidence, not policy" disclaimer and a date, but far from the recommendation lines that get read; a status line at the head of each record naming it superseded-by-decision would have prevented most of this review's wasted effort. A second, smaller cluster refuses proposals that would add an internal module seam (validation.js split, adapters.js split, protocol.js field removal) — the one-contract-seam rule is in AGENTS.md but not visible in the files it governs.
- Read as a release-candidate judgement: two classes of finding block and the rest follow. Blocking are the adapters.js crash-replay defect (a crashed run is un-claimed and dispatched again, breaking the at-most-once claim the file's own header states, alongside the non-atomic claim ledger at line 118 and install reporting ok:true without observing the copy), the agent-facing SKILL.md files that instruct operations the runtime refuses, okf/index.md declaring knowledge-only for a code-backed repository and thereby disabling the CODE_RECOVERABLE_MATERIAL gate on this very tree, and the spec's four Open rows that the branch's own behaviour has already answered. The duplication, the test-harness copies, the source-grep gates and the stale research records are real but are post-RC cleanups: none of them changes what a user of the tag observes.

## Must-fix checklist

- [ ] **F001** (major) `scripts/lib/validation.js` — Write path warns UNRESOLVED_INTERNAL_LINK for external source resources; the read path correctly skips them
- [ ] **F013** (major) `scripts/lib/navigation.js` — visibleMarkdown advances twice after a closed inline-code span
- [ ] **F025** (major) `scripts/lib/runtime.js` — writeResponse takes 11 positional parameters, so callers pass `undefined` placeholders
- [ ] **F026** (major) `scripts/lib/runtime.js` — executeBounded repeats the same ten-argument blocked return nine times
- [ ] **F027** (major) `scripts/lib/runtime.js` — Outside-worktree refusal reports concept-revise for a sync that plans a create
- [ ] **F040** (blocker) `scripts/lib/adapters.js` — A crashed okf-read run is silently un-claimed and replayed, discarding the failure response it printed
- [ ] **F041** (major) `scripts/lib/adapters.js` — install reports ok:true without observing that the copy completed, and leaves unremovable residue when it fails
- [ ] **F042** (major) `scripts/lib/adapters.js` — The at-most-once claim ledger is a non-atomic read-modify-write, so concurrent hooks lose claims
- [ ] **F052** (major) `okf/index.md` — The shipped bundle root declares knowledge-only for a code-backed repository, disabling the code-recoverable gate
- [ ] **F053** (major) `skills/okf-read/SKILL.md` — The documented `resolve` request shape crashes the wrapper instead of returning a result
- [ ] **F054** (major) `skills/okf-write/SKILL.md` — The performed-effects list names six effects the v0.1.0 runtime refuses with UNSUPPORTED_INPUT
- [ ] **F070** (blocker) `docs/spec/okf-agent-skills-v0.1.0.md` — Four rows the completion spec records decisions for are still `Open` in the open-item table
- [ ] **F071** (major) `docs/spec/okf-agent-skills-v0.1.0-completion.md` — The completion spec's Problem Statement asserts, in the present tense, a repository state this branch has already changed
- [ ] **F072** (major) `docs/spec/okf-agent-skills-v0.1.0.md` — A new MUST fixes the shared module set at five modules; fifteen ship
- [ ] **F073** (major) `docs/spec/okf-agent-skills-v0.1.0.md` — The undefined-terms gap list is stale: this branch defines every term it lists
- [ ] **F079** (major) `test/issue-46.test.js` — issue-46 opens a second seam into scripts/lib/reach.js for behaviour the wrapper already proves
- [ ] **F092** (major) `test/issue-48.test.js` — A test about wrapper count hard-codes the exhaustive scripts/lib file listing
- [ ] **F093** (major) `test/issue-49.test.js` — The fixture harness is copy-pasted across all four new test files
- [ ] **F094** (major) `test/issue-50.test.js` — Retired vocabulary words are split and re-joined to dodge a self-match that cannot happen
- [ ] **F095** (major) `test/issue-50.test.js` — Shape-tolerant accessors let the navigation envelope be renamed without failing a test
- [ ] **F104** (major) `test/issue-53.test.js` — issue-53's run() returns exit status and stderr that no test ever asserts, and nulls the response on a crash
- [ ] **F105** (major) `test/issue-53.test.js` — digest() hashes only top-level files, so the "nothing was written" proof in four tests has a hole
- [ ] **F112** (major) `test/issue-68-agents.test.js` — Status-vocabulary test degrades silently: a regex source scan guarded only by size > 0
- [ ] **F121** (major) `test/fixtures/obligations/runtime.md` — The four obligations fixtures are dead files: no test reads test/fixtures at all
- [ ] **F122** (major) `test/issue-105.test.js` — The OpenCode uninstall test passes vacuously when install does nothing
- [ ] **F127** (major) `docs/transcript.md` — Orphaned, unattributed third-party talk transcript committed under docs/
- [ ] **F137** (major) `docs/research/adapter-install-targets.md` — OpenCode section documents an installs list the shipped manifest does not have
- [ ] **F138** (major) `docs/research/agent-skill-evaluation-automation.md` — Recorded v0.1.0 sequence forbids exactly what this branch ships in eval/

## Findings by chunk

### runtime-validation

10 real, 2 refuted or already-correct.

#### F001 — Write path warns UNRESOLVED_INTERNAL_LINK for external source resources; the read path correctly skips them

File: scripts/lib/validation.js
Severity: major
anchor:     const file = sourceResourcePath(bundleRoot, resource);

- Line: 453 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

CONFIRMED at scripts/lib/validation.js:453 (checkLinks) and :464 (checkUpstreams). Both resolve sources[].resource through sourceResourcePath (line 701), which has no URI-scheme guard and no root-absolute handling: path.resolve(bundleRoot, 'https://example.com/x') lands inside the bundle, resolvesToFile is false, and a non-blocking UNRESOLVED_INTERNAL_LINK is emitted for a link that is not internal. validateRead line 787 uses internalResourcePath (line 693), which returns null for a scheme URI and skips it, and maps '/foo' to bundle-root-relative while sourceResourcePath maps it to filesystem-absolute (then reports it broken). Spec confirms the scoping: 'An unresolved internal link MUST raise a default soft warning (#7)' and 'A review dependency MAY reference a standard sources[].id or directly name a local file, recursive directory, OKF concept, or external resource.' The divergence also has teeth beyond the spurious warning: a root-absolute '/foo' source that exists bundle-relative is skipped by checkUpstreams, so DEPENDS_ON_BLOCKED_CONCEPT is never raised for it.

**Fix.** Use internalResourcePath in checkLinks and checkUpstreams (skip the resource when it returns null, exactly as validateRead does at line 788) and delete sourceResourcePath, which then has no callers.

<details><summary>Proof of in-diff scope</summary>

```
diff --git a/scripts/lib/validation.js b/scripts/lib/validation.js
new file mode 100644
index 0000000..bf432aa
--- /dev/null
+++ b/scripts/lib/validation.js
@@ -0,0 +1,961 @@
```

</details>

#### F002 — Third copy of the gate-finding constructor with the named_by_user path rule

File: scripts/lib/admission.js
Severity: minor
anchor: function gateFinding(code, gate, candidate) {

- Line: 34 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`
- Spans: scripts/lib/admission.js, scripts/lib/presence.js, scripts/lib/reach.js

CONFIRMED, location off by one: the function is at scripts/lib/admission.js:34, not 35. The same four lines appear at presence.js:6-10 (finding) and reach.js:15-19 (detailFor, used by refusal/anomaly). All three attach detail.path only when candidate.named_by_user === true — the redaction rule that keeps an unnamed path out of the response, written three times.

**Fix.** Export one `gateFinding(code, gate, candidate, extra)` (reach.js already owns the closest version in detailFor) and have reach.js, presence.js and admission.js all build their gate findings through it; keep the severity/blocks difference as an argument rather than a duplicated literal.

#### F003 — Defensive typeof guard on an injected service that always provides isFile

File: scripts/lib/presence.js
Severity: minor
anchor:     ? typeof services.isFile === 'function' && services.isFile(bundlePath)

- Line: 50 · Lens: deslop · Kind: slop · Verdict: `confirmed` · Disposition: `fix-as-proposed`

CONFIRMED at scripts/lib/presence.js:50. services.js:83 always exports isFile, and every other runtime call site is unguarded (trust.js:13, navigation.js:779, validation.js:448 via resolvesToFile). The guard is not inert decoration: with a services object lacking isFile the expression is false, bundleMissing is false, and the allowMissingIndex read path admits a bundle root it never inspected. Not load-bearing as documentation either — the PROVISIONAL comment above it explains the index.md predicate, not the typeof check.

**Fix.** Call `services.isFile(bundlePath)` directly, as trust.js and validation.js do.

#### F004 — Containment helper re-implemented instead of reusing reach.js's canonical `inside`, and the copy loses the filesystem-root guard

File: scripts/lib/validation.js
Severity: minor
anchor: function inside(target, root) {

- Line: 373 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed` · Re-graded major → minor

CONFIRMED at scripts/lib/validation.js:373-375. reach.js:11-13 exports the same predicate with the guard `root === path.parse(root).root ? root : root + path.sep`; presence.js:4 already imports it. The copy in validation.js omits the guard, so with bundleRoot '/' the prefix becomes '//' and inside('/x/index.md','/') is false — every concept is rejected CONCEPT_OUTSIDE_BUNDLE by evaluate/evaluateCreate and reported unexpectedly-broken by linkVerdict. Reuse ground is solid (rung 2: the helper already lives two files over, and reach.js does not require validation.js, so no cycle). Severity lowered to minor: the divergent behavior needs a bundle root at a filesystem/drive root, which no real bundle has — the defect that actually bites is the duplication of an authority predicate. Note for the fixer: navigation.js:779 carries a third copy named `contained`, outside this finding's scope.

**Fix.** Delete this function and add `const { inside } = require('./reach');` at the top of validation.js, matching presence.js:4.

<details><summary>Proof of in-diff scope</summary>

```
diff --git a/scripts/lib/validation.js b/scripts/lib/validation.js
new file mode 100644
index 0000000..bf432aa
--- /dev/null
+++ b/scripts/lib/validation.js
@@ -0,0 +1,961 @@
```

</details>

#### F005 — Unused injection point: serializeFrontmatter is spread into the services object but never supplied by any caller

File: scripts/lib/validation.js
Severity: minor
anchor:   const svc = { serializeFrontmatter, ...services };

- Line: 495 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

CONFIRMED at scripts/lib/validation.js:495 (evaluate) and :556 (evaluateCreate). `grep -rn serializeFrontmatter scripts test` outside validation.js returns no matches, so nothing ever injects it; the spread order also lets an injected services object override the module's own writer, which is the writer the round-trip check is supposed to validate.

**Fix.** Drop `svc` from both functions and call serializeFrontmatter directly, passing `services` through unchanged to the gate helpers.

#### F006 — Comma-operator side effect hidden inside an argument

File: scripts/lib/validation.js
Severity: minor
anchor:   if (root) return done('blocked', (findings.push(root), { path: rel }));

- Line: 562 · Lens: deslop · Kind: slop · Verdict: `confirmed` · Disposition: `fix-as-proposed`

CONFIRMED at scripts/lib/validation.js:562, verbatim. The sibling evaluate() writes the same logic plainly at lines 502-505 (`findings.push(root); return done('blocked', {});`). Nothing depends on the expression form; not load-bearing.

**Fix.** Write it as the sibling does: `if (root) { findings.push(root); return done('blocked', { path: rel }); }`.

#### F007 — postWrite discards the root-declaration and project-mode findings it just collected

File: scripts/lib/validation.js
Severity: minor
anchor:     if (current.finding) return { valid: false, findings: [current.finding] };

- Line: 609 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

CONFIRMED at scripts/lib/validation.js:609. Lines 604 and 606 push ROOT_DECLARATION_NOT_EXACT and PROJECT_MODE_INVALID into `findings`, and this early return replaces the array with `[current.finding]`. Every other exit (line 618, 620) returns the accumulated set. A post-write state with an unparseable saved concept plus a bad okf_version reports only the read failure.

**Fix.** Push the read finding and return the accumulated set: `if (current.finding) { findings.push(current.finding); return { valid: false, findings: sortFindings(findings) }; }`.

#### F008 — Verbatim duplicated verdict-recording block in validateRead's two link loops

File: scripts/lib/validation.js
Severity: minor
anchor:       linkVerdicts.push({ path: entry.path, resource, verdict });

- Line: 790 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

CONFIRMED: lines 789-796 (source-link loop) and 805-812 (markdown-link loop) are identical once the target path is computed — same linkVerdict call, same linkVerdicts.push, same conditional UNRESOLVED_INTERNAL_LINK warning with the same two detail fields.

**Fix.** Extract `recordLink(entry, resource, target)` in validateRead's scope (or a module-level helper taking root/services/linkVerdicts/conceptFindings), then have each loop resolve its target and call it once.

#### F009 — ~300-character single-line object literals in a file that otherwise formats objects one field per line

File: scripts/lib/admission.js
Severity: nit
anchor:     return { path: ownerRoot, bundle: owner ? path.relative(ownerRoot, path.resolve(ownerRoot, bundle.root)) : bundle.root, bundle_alias: bundle.alias, owner: owner ? ownerRoot : null, mode: bundle.mode, required: bundle.required, declared: true, requires_repository: Boolean(owner) };

- Line: 106 · Lens: deslop · Kind: slop · Verdict: `fixed-with-deviation` · Disposition: `fix-with-deviation`

CONFIRMED at scripts/lib/admission.js:106 and the invalid() literal at line 15; makeCandidate (lines 40-54) formats the same kind of record one field per line. The formatting half stands. The asymmetry half is wrong: when owner is null, ownerRoot is root and bundle.root is typically '.', and `path.relative('/a', path.resolve('/a','.'))` returns the empty string, not '.' — so the reviewer's 'both branches could use the same expression' is a silent behavior change (bundle becomes ''), not a normalization. bundle_alias is safe (manifest.js:54 requires a non-empty alias) but there is no reason to take the risk for a formatting nit.

**Fix.** Format both literals one field per line like makeCandidate. Leave the `owner ? path.relative(...) : bundle.root` conditional exactly as it is — the two branches are not equivalent, path.relative collapses the no-owner '.' to ''.

#### F010 — Bare 'path' import where the rest of scripts/lib uses the node: prefix

File: scripts/lib/validation.js
Severity: nit
anchor: const path = require('path');

- Line: 17 · Lens: deslop · Kind: slop · Verdict: `confirmed` · Disposition: `fix-as-proposed`

CONFIRMED at scripts/lib/validation.js:17. Eleven of thirteen modules in scripts/lib use `require('node:path')`; only validation.js and services.js use the bare specifier. The three other new files in this chunk (admission.js:8, presence.js:3, trust.js:7) use the prefix. Purely cosmetic, no consumer reads this text.

**Fix.** Change to `const path = require('node:path');`.

### runtime-navigation

12 real, 0 refuted or already-correct.

#### F013 — visibleMarkdown advances twice after a closed inline-code span

File: scripts/lib/navigation.js
Severity: major
anchor: start += length - 1;

- Line: 710 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed by execution, not just by reading. The inner while sets `start = end + length - 1` and breaks; control then falls through to line 710 which adds `length - 1` again, so for a run of length >= 2 the scanner skips `length - 1` characters of unmasked text past the closing fence. Extracted visibleMarkdown and ran it: input "x ```lit``` `[a](b)` end\n" returns "x           `[a](b)` end\n" (second span left visible), while the same input with single backticks returns "x                end\n" (masked). markdownLinks therefore enumerates `[a](b)` as an inbound link and emits UNRESOLVED_INTERNAL_LINK for text that is code.

**Fix.** Advance once. Keep the reviewer's shape: set a `closed` flag when the closing run matches and `continue` the outer loop (`start = end + length - 1; continue;` inside the match branch, leaving `start += length - 1` for the unterminated case only). With `continue` the for-increment still runs, so the next scanned index is `end + length` — the first character after the closing run, which is correct.

**Validator note.** Status A, whole file added, so the defect is in-diff. Reproduced with node by slicing the function out of the file (read-only).

<details><summary>Proof of in-diff scope</summary>

```
$ git diff --unified=0 origin/main...feat/issue-41-spec-v0.1.0 -- scripts/lib/navigation.js | head -6
diff --git a/scripts/lib/navigation.js b/scripts/lib/navigation.js
new file mode 100644
index 0000000..f205e82
--- /dev/null
+++ b/scripts/lib/navigation.js
@@ -0,0 +1,845 @@
$ git diff --unified=0 origin/main...feat/issue-41-spec-v0.1.0 --numstat -- ...
87	0	scripts/lib/manifest.js
845	0	scripts/lib/navigation.js
131	0	scripts/lib/reach.js
168	0	scripts/lib/routing.js
```

</details>

#### F014 — Repository path validity and its reason are computed in one nested ternary, and a missing path reports parent_segment

File: scripts/lib/manifest.js
Severity: minor
anchor: if (!relative(repo.path) || paths.has(repo.path)) return invalid(paths.has(repo.path) ? 'duplicate_repository_path' : (repo.path && path.isAbsolute(repo.path) ? 'absolute_path' : 'parent_segment'));

- Line: 41 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed, but at line 41 (not 47); the bundle copy is line 55 (not 61). Traced: `repo.path` absent, non-string, or '' makes `relative()` false, `paths.has` false, and the `repo.path && isAbsolute` arm falsy, so `invalid('parent_segment')` is returned for input containing no segment at all. Line 55 does the same for `bundle.root`. test/issue-47.test.js:49-50 only asserts the '/tmp' and '..' cases, so a new reason string for the absent/empty case does not break existing assertions.

**Fix.** Replace `relative()` with a single `relativePathReason(value)` that returns `null` or one of `'missing_path' | 'absolute_path' | 'parent_segment'`, then write `const reason = paths.has(repo.path) ? 'duplicate_repository_path' : relativePathReason(repo.path); if (reason) return invalid(reason);` and reuse the same helper for `bundle.root`.

**Validator note.** Location drifted: the anchor is at manifest.js:41 and the bundle branch at :55. Defect itself confirmed.

#### F015 — contained() is a byte-identical copy of reach.js's exported inside()

File: scripts/lib/navigation.js
Severity: minor
anchor: return target === root || target.startsWith(root === path.parse(root).root ? root : root + path.sep);

- Line: 28 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`
- Spans: scripts/lib/navigation.js, scripts/lib/reach.js

Verified byte-identical: navigation.js:27-29 vs reach.js:11-13. presence.js:4 already does `const { inside } = require('./reach')`. reach.js requires only node:path, so importing it into navigation.js introduces no cycle. Call sites in navigation.js are guardPath (line 69) and the enumerate link check (line 779), both as stated.

**Fix.** Delete `contained` from navigation.js and use `const { inside } = require('./reach');` at both call sites (guardPath line 69 and the enumerate link check line 779).

**Validator note.** There is in fact a third variant, runtime.js:87 `function inside(root, file)` with the arguments reversed; out of this chunk's file set, noted only so the fix is not applied half-way.

#### F016 — safeExists distrusts the injected service that routing.js calls bare

File: scripts/lib/navigation.js
Severity: minor
anchor: try { return typeof services.exists === 'function' && services.exists(file); } catch { return false; }

- Line: 32 · Lens: deslop · Kind: slop · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed: services.js:78 binds `exists: fs.existsSync` (never throws, returns a boolean), and routing.js:89 calls `services.exists(file)` with no probe and no catch. The only service stubs in the test suite (test/issue-46.test.js:170) are passed to reach.evaluate, not to navigation, so nothing depends on navigation tolerating a missing `exists`. The wrapper is not load-bearing.

**Fix.** Call `services.exists(file)` directly, as routing.js does, and let a broken binding fail loudly. If absence of the method must stay tolerated, tolerate it once at the service boundary rather than per call site.

#### F017 — Empty catch hides a realpath failure that guardPath reports as unobservable

File: scripts/lib/navigation.js
Severity: minor
anchor: if (typeof services.realpath === 'function') root = services.realpath(root);

- Line: 186 · Lens: deslop · Kind: slop · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed: `catch {}` at line 187 leaves `root` lexical and enumeration proceeds, so `path.relative(root, file)` and the `envelopeRoot` handed to guardPath at line 806 are computed against a root that guardPath's own branch (line 81) would have called unobservable. The practical effect is muted — a realpath that throws for the root will usually throw for the files too, so guardPath returns 'unobservable' per file — but the two paths still disagree about the same failure, and the empty catch documents nothing.

**Fix.** Handle the failure the way guardPath does: on a realpath error, add the `enumeration_unobservable` finding and return null, instead of continuing with the lexical root.

#### F018 — Several helpers return fields no caller reads

File: scripts/lib/navigation.js
Severity: minor
anchor: complete: observable && files <= MAX_SOURCE_FILES && bytes <= MAX_SOURCE_BYTES && depth <= MAX_DIRECTORY_DEPTH,

- Line: 272 · Lens: deslop · Kind: slop · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Grepped each claim. supportEvidence's `observable`/`files`/`bytes`/`depth`: no reader anywhere (the only `.observable` reads are on nativeSearchResults, a different object); every consumer reads `support.complete` (line 293). guardPath's `root`/`real`: no reader (search passes envelopeRoot separately at line 483). inspectIndex's `invalid` (line 146): no reader. readObservation's `error` (lines 45, 55, 59): no reader; the two `new Error('read service returned no text')` objects are constructed and discarded. None of these are consumed by a tool or fixture.

**Fix.** Return only what is consumed: `supportEvidence` -> a boolean (or `{ complete }`), `guardPath` -> `{ state, relative, detail }`, `inspectIndex` -> `{ complete }`, `readObservation` -> `{ ok, content, complete }`. Delete the two synthesized Error objects with it.

#### F019 — routeScope guards against a route shape locate never returns

File: scripts/lib/navigation.js
Severity: minor
anchor: if (route && Array.isArray(route.eligible)) return route.eligible;

- Line: 287 · Lens: deslop · Kind: slop · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed against routing.js:65-99: all four returns (lines 77, 84, 85, 99) are object literals carrying `eligible`, and `eligible` is only ever `candidates`, `[match]`, a `filter` result or `ordered(...)` — always an array. So neither branch of the guard can fire, and the not-load-bearing wrapper hides that read has exactly one scope source.

**Fix.** Delete `routeScope` and use `route.eligible` directly at the one call site in `read` (line 301). Note `getActiveCandidates` is still needed in `read` for the two `admissionSupport` calls, so only routeScope's use of it goes away.

#### F020 — getActiveCandidates and locate are injected only to dodge a require cycle

File: scripts/lib/navigation.js
Severity: minor
anchor: function read(data, payload, services, locate, getActiveCandidates) {

- Line: 299 · Lens: thermo · Kind: quality · Verdict: `fixed-with-deviation` · Disposition: `fix-with-deviation` · Re-graded major → minor
- Spans: scripts/lib/navigation.js, scripts/lib/routing.js

Verified: routing.js line 8 requires navigation.js, navigation.js requires only node:path and ./validation, and routing.js lines 156-166 are three pass-through wrappers that bind `locate` and `activeCandidates`. Grep confirms navigation.read/search/enumerate have exactly one caller each and exactly one implementation is ever passed, so the parameters buy no substitutability — five extra parameters threaded into admissionSupport and routeScope. But the injection does buy something the reviewer dismisses: `locate` genuinely cannot move into navigation.js without inverting the direction for routing.resolve, which also calls it.

**Fix.** Two independent halves. (a) `activeCandidates` (routing.js:42-44) is a one-line filter with no routing dependency: move it into navigation.js, export it, and have routing.js use `navigation.activeCandidates` in `ordered`/`locate`; then drop the `getActiveCandidates` parameter from read, search, enumerate, admissionSupport and routeScope. (b) Leave `locate` in routing.js but stop passing the function: have routing.read call `locate(data, payload, services)` itself and pass the resulting `route` object into `navigation.read(data, payload, services, route)`. That removes the injected callable without moving locate away from routing.resolve.

**Validator note.** Defect real but downgraded to minor: the injection is inelegant, not incorrect, and the cycle it dodges is a real constraint. The reviewer's fix is unsound as written for `locate` (it is used by routing.resolve), so a split fix is substituted.

<details><summary>Proof of in-diff scope</summary>

```
$ git diff --unified=0 origin/main...feat/issue-41-spec-v0.1.0 --numstat -- scripts/lib/navigation.js scripts/lib/routing.js
845	0	scripts/lib/navigation.js
168	0	scripts/lib/routing.js
(both new file mode 100644, @@ -0,0 +1,845 @@ and @@ -0,0 +1,168 @@)
$ grep -rn navigation scripts/ test/ --include=*.js
scripts/lib/routing.js:8:const navigation = require('./navigation');
scripts/lib/routing.js:157:  return navigation.read(data, payload, services, locate, activeCandidates);
scripts/lib/routing.js:161:  return navigation.search(data, payload, services, activeCandidates);
scripts/lib/routing.js:165:  return navigation.enumerate(data, payload, services, activeCandidates);
```

</details>

#### F021 — The guard-then-finding step is written three times in one file

File: scripts/lib/navigation.js
Severity: minor
anchor: const safeMatches = [{ ...selectedItem, guard: selectedGuard }];

- Line: 345 · Lens: thermo · Kind: quality · Verdict: `fixed-with-deviation` · Disposition: `fix-with-deviation` · Re-graded major → minor

The duplication is real but narrower than claimed. Lines 326-344 (unrolled first match), 347-362 (loop over route.unique.slice(1)) and 483-493 (search) each repeat guardPath -> 'invalid' finding -> 'unobservable' finding. The read/parse tail is NOT a third copy: search adds the .md/index/log filter, archiveUnevaluated and include_deprecated handling, so only the guard step is genuinely triplicated. One drift claim is wrong: the loop at 355-357 also omits `bundle_alias`; the divergence is read (both sites omit it) versus search (includes it). The safeExists claim is right — line 348 checks existence for later matches, the unrolled first match does not.

**Fix.** Do NOT fold the first match into the loop as proposed: the unrolled copy returns 'unavailable' immediately when the first match's guard fails, whereas the loop skips and continues, so pushing route.unique through one loop and taking the first survivor silently changes the read contract (a scope-violating top-precedence match would be replaced by a lower-precedence one). Extract only the shared step: `function guardMatch(candidate, file, services, findings, envelopeRoot)` returning the guard or null, appending the 'invalid'/'unobservable' finding with `bundle_alias` in the detail at all three sites. Call it at 326, 349 and 483, keep read's early return for the first match, and add the missing safeExists check there so the two read paths agree.

**Validator note.** Defect real (three copies of the guard step, plus the safeExists asymmetry), but downgraded to minor: no correctness bug, and the evidence of 'already drifting' was partly misread. The reviewer's fix would change read's precedence behavior, so it is replaced.

<details><summary>Proof of in-diff scope</summary>

```
$ git diff --unified=0 origin/main...feat/issue-41-spec-v0.1.0 -- scripts/lib/navigation.js | head -6
diff --git a/scripts/lib/navigation.js b/scripts/lib/navigation.js
new file mode 100644
index 0000000..f205e82
--- /dev/null
+++ b/scripts/lib/navigation.js
@@ -0,0 +1,845 @@
```

</details>

#### F022 — Result state is decided by three different rules in three places

File: scripts/lib/navigation.js
Severity: minor
anchor: const state = searchUnavailable && readRecords.length === 0

- Line: 533 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed as described: `resultState` (line 291) takes five positional arguments including two booleans; `search` overrides it with the nested ternary at 533-537; `read` computes 'unavailable' from `scopeCandidates.length === 0` at line 314; `enumerate` never calls it and derives the state from `reasons.length` at line 834.

**Fix.** Give `resultState` one argument object that names the observations (`{ findings, support, indexes, hasRead, searched, archiveUnevaluated }`) and fold the search-specific 'unavailable' and 'degraded' rules into it, so each operation states its observations and one function maps them to a state.

#### F023 — enumerate passes the current carrier to add() through a mutable outer variable

File: scripts/lib/navigation.js
Severity: minor
anchor: let current;

- Line: 756 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed: `let current;` at line 756, `add` reads current.candidate/root/file/path/text at 763-783, and `current` is assigned at line 818, inside the nested loop 60 lines below the closure. Also confirmed: `const reference = ...` at line 760 shadows the module-level `reference()` helper (line 98) for the whole body of `add`, exactly where reference records are built.

**Fix.** Make the carrier explicit: `function addLink(context, carrier, value, offset, base)` and pass `current` at the two call sites in the try block, or bind it per file with `const add = linkCollector(context, findings, links, addReason)`. Rename the local `reference` to `resolved` so the module helper stays reachable.

#### F024 — The empty-candidate short circuit in search sits after the loop it should skip

File: scripts/lib/navigation.js
Severity: nit
anchor: if (candidates.length === 0) {

- Line: 441 · Lens: deslop · Kind: slop · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed: the envelope loop runs at 427-439 and the `candidates.length === 0` return is at 441-447. Moving it above the loop is behavior-preserving, since an empty `candidates` makes the loop a no-op and leaves `indexes`/`searchCandidates` empty.

**Fix.** Move the `candidates.length === 0` early return above the envelope loop, next to `const scope = navigationScope(...)`.

### runtime-core

14 real, 1 refuted or already-correct.

#### F025 — writeResponse takes 11 positional parameters, so callers pass `undefined` placeholders

File: scripts/lib/runtime.js
Severity: major
anchor: function writeResponse(request, result, authorization, effects, evidence, validationState, findings = [], code = undefined, scope = request.scope || null, completedEffects = [], residue = []) {

- Line: 43 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Verified at scripts/lib/runtime.js:43: the signature has 11 parameters, six of them optional positionals. Call sites match the complaint — line 210 passes `undefined` in the `code` slot purely to reach `scope`, line 225 passes `..., undefined, scoped.scope, completedEffects)`, and line 237 adds a residue array in the eleventh slot. I checked the correlated trio claim against every call: blocked always pairs with ('blocked','not-run'), applied with ('notice','valid'), failed/incomplete with ('notice','failed'), no-op with ('notice','not-needed'), abstained with ('allowed','not-run') at line 414. So authorization and validationState are functions of `result` at every one of the ~20 call sites, and a caller can currently desynchronise them silently. writeResponse is module-private (module.exports at line 468 exports only run/respond/routerOwners/primaryEffects/derivedEffects), so the refactor is confined to this file.

**Fix.** Change the signature to `writeResponse(request, options)` with named fields (`result`, `effects`, `evidence`, `findings`, `code`, `scope`, `completed`, `residue`) and derive `authorization`/`validation` from `result` inside the function using the observed pairing (blocked→blocked/not-run, abstained→allowed/not-run, applied→notice/valid, no-op→notice/not-needed, failed/incomplete→notice/failed). Update all call sites in scripts/lib/runtime.js; no other module imports it.

<details><summary>Proof of in-diff scope</summary>

```
$ git -C /Users/artem.veduta/proj/okf-agent-skills diff --unified=0 origin/main...feat/issue-41-spec-v0.1.0 -- scripts/lib/runtime.js | head -4
diff --git a/scripts/lib/runtime.js b/scripts/lib/runtime.js
new file mode 100644
index 0000000..e93633c
--- /dev/null
(anchor present in that diff as line 49 of its output: "+function writeResponse(request, result, authorization, effects, evidence, validationState, findings = [], code = undefined, scope = request.scope || null, completedEffects = [], residue = []) {"; status A so no blame was run)
```

</details>

#### F026 — executeBounded repeats the same ten-argument blocked return nine times

File: scripts/lib/runtime.js
Severity: major
anchor: return writeResponse(request, 'blocked', 'blocked', effectRecords(provisionalEffects, 'blocked'), [], 'not-run', [finding], 'UNSUPPORTED_INPUT');

- Line: 150 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Verified: executeBounded spans scripts/lib/runtime.js:145-241. Counted the refusal returns — lines 150 (UNSUPPORTED_INPUT), 157 (TASK_KIND_NOT_WRITE_ELIGIBLE), 162 (INVALID_SCOPE), 170 (WRITE_OWNERSHIP_UNKNOWN), 186 (BUNDLE_NOT_ADMITTED), 191 (PROJECT_MODE_INVALID), 195 (CODE_RECOVERABLE_MATERIAL), 201 (EVIDENCE_UNAVAILABLE/EVIDENCE_REQUIRED), 212 (validator blocked) and 222 (TARGET_CHANGED) — all hand-repeat `writeResponse(request, 'blocked', 'blocked', effectRecords(provisionalEffects, 'blocked'), <evidence>, 'not-run', ...)`, plus five failed/incomplete twins at 210, 213, 225, 228, 237. The count is nine blocked forms as claimed (ten including the validator passthrough at 212). This is the same copy that produced the line 172 effect mismatch reported separately, which is direct evidence the duplication is load-bearing risk and not cosmetic.

**Fix.** After `provisionalEffects` and `scoped` are known, define local closures over the options-object writeResponse from finding 1 — `const refuse = (code, detail, evidence = []) => writeResponse(request, { result: 'blocked', effects: effectRecords(provisionalEffects, 'blocked'), evidence, findings: [suiteFinding(code, detail)], code, scope: scoped.scope })` and an `incomplete(findings, completed, residue)` twin — and reduce each gate to one line. Note the first two gates (lines 148-158) run before `scoped` exists, so either compute `scoped` first or let `refuse` take an explicit scope default of `request.scope || null` for those two.

<details><summary>Proof of in-diff scope</summary>

```
$ git -C /Users/artem.veduta/proj/okf-agent-skills diff --unified=0 origin/main...feat/issue-41-spec-v0.1.0 -- scripts/lib/runtime.js | head -4
diff --git a/scripts/lib/runtime.js b/scripts/lib/runtime.js
new file mode 100644
index 0000000..e93633c
--- /dev/null
(status A: whole file added by the branch, so every line cited is in-diff; no blame run)
```

</details>

#### F027 — Outside-worktree refusal reports concept-revise for a sync that plans a create

File: scripts/lib/runtime.js
Severity: major
anchor: const effect = primaryEffects.get(request.operation) || 'concept-revise';

- Line: 350 · Lens: thermo · Kind: quality · Verdict: `fixed-with-deviation` · Disposition: `fix-with-deviation`

Traced end to end and the bug is real. runActive line 416 calls `executeBounded(planned.request, services, planned.operation, true)`; lifecycle.plan (scripts/lib/lifecycle.js:12-16) does not rewrite `operation`, so `request.operation` is still 'sync' inside executeBounded while `provisionalEffects` (line 147) is ['concept-create'] or ['concept-revise'] from `planned.operation`. Line 172 delegates to `targetOutsideWorktreeBlocked({ ...request, scope: scoped.scope })`, which at line 350 recomputes `primaryEffects.get('sync')` — undefined, since primaryEffects (lines 17-20) has no 'sync' — and the `|| 'concept-revise'` fallback reports `effects: [concept-revise]` for a sync that would have created a concept. Every sibling refusal in the same function reports provisionalEffects. No test pins this: test/issue-48.test.js:218 covers only direct/routed revise, and nothing asserts the effects array for the lifecycle outside-worktree path.
The proposed fix is unsound in its second half. automaticMutationBlocked (line 298) is reached from run() line 464 before any planning has happened, so for an automatic sync there is no computed effect to pass; deleting its fallback yields `effectRecords([undefined])`, which JSON.stringify drops, emitting an effect record with no `effect` key. That is worse than the current guess.

**Fix.** Pass the effects the caller already has: `function targetOutsideWorktreeBlocked(request, effects)` used as `targetOutsideWorktreeBlocked({ ...request, scope: scoped.scope }, provisionalEffects)` at line 172, and delete the `|| 'concept-revise'` fallback there. Leave automaticMutationBlocked reporting an effect, but make it explicit rather than a silent default — e.g. add a local `const blockedEffect = primaryEffects.get(request.operation) ?? (request.operation === 'sync' ? 'concept-revise' : null)` and refuse with an empty effects array when it is null, so an operation with no primary effect can no longer be mislabelled as a revise.

**Validator note.** Defect confirmed at the sync/outside-worktree path. Location moved from the function header (line 349) to the offending fallback line 350; the reviewer's line was one off but pointed at the right function. Fix replaced because deleting the fallback in automaticMutationBlocked would emit an effect record with a missing `effect` key.

<details><summary>Proof of in-diff scope</summary>

```
$ git -C /Users/artem.veduta/proj/okf-agent-skills diff --unified=0 origin/main...feat/issue-41-spec-v0.1.0 -- scripts/lib/runtime.js | grep -n "^+  const effect = primaryEffects.get(request.operation)"
305:+  const effect = primaryEffects.get(request.operation) || 'concept-revise';
356:+  const effect = primaryEffects.get(request.operation) || 'concept-revise';
(status A: file is new in the branch, so both fallbacks are branch-introduced; no blame run)
```

</details>

#### F028 — Every brief refusal re-types the status/code/gate triple

File: scripts/lib/delegation.js
Severity: minor
anchor: if (!Object.hasOwn(brief, field)) return blocked('blocked: incomplete-brief', 'INCOMPLETE_BRIEF', { gate: 'brief', field });

- Line: 58 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Counted at scripts/lib/delegation.js: the incomplete-brief triple is repeated at lines 56, 58, 59, 62, 65, 71, 75, 78 (eight) and the conflicting-rules triple at 82, 88, 92 (three). Status and code are one-to-one and `gate: 'brief'` is constant across all eleven. Not refuted by hard constraint 6: this is the readability of validateBrief's own text, not a claim that the delegated path is reachable or that a setting should act.

**Fix.** Replace the generic `blocked` helper with `incompleteBrief(field)` and `conflictingRules(reason)`, each owning the status string, code, and `gate: 'brief'`. Each check then reads `if (...) return incompleteBrief('paths');`.

#### F029 — lifecycle.plan returns an identity-rebuilt copy of its own input request

File: scripts/lib/lifecycle.js
Severity: minor
anchor: request: { ...request, payload: { ...request.payload, concept } },

- Line: 15 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed. `concept` at scripts/lib/lifecycle.js:10 is `request.payload.concept`, so the object at line 15 is deep-equal to `request`. The only consumer is scripts/lib/runtime.js:416, `executeBounded(planned.request, services, planned.operation, true)`; nothing else reads `planned.request`.

**Fix.** Drop the `request` field from plan's return value (leaving `{ result, operation }`) and call `executeBounded(request, services, planned.operation, true)` at scripts/lib/runtime.js:416.

#### F030 — parseRequest is a per-operation if-chain that duplicates the same field checks

File: scripts/lib/protocol.js
Severity: minor
anchor: const boundedOperation = ['create', 'revise', 'format', 'relationship', 'machine-verify', 'sync'].includes(obj.operation);

- Line: 36 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed` · Re-graded major → minor

Confirmed as described: scripts/lib/protocol.js:36-91 is a chain of per-operation blocks, and the okf-review block at lines 46-58 re-implements the bounded cwd/bundle/concept checks with the two error strings ('Missing payload.cwd', 'Missing payload.bundle or payload.concept') copied verbatim from lines 38 and 44. `read` (77-80), `search` (81-84) and `orient` (86-91) each restate the `typeof x !== 'string' || x === ''` idiom. Downgraded from major: the messages are diagnostic only — scripts/wrapper.js:22 writes them to stderr with exit 64 and no test asserts any of them (grep for 'Missing payload'/'Unknown field' across test/ returns nothing), so the duplication carries no behavioural divergence risk, only reading cost. That makes it a readability defect, not a correctness hazard.

**Fix.** Replace the chain with one table: `const requiredPayload = new Map([['create', ['cwd','bundle','concept']], ['revise', [...]], ..., ['review', ['cwd','bundle','concept']], ['read', ['cwd','target']], ['search', ['cwd','query']], ['orient', ['cwd','harness','context_id','logical_cause']]])`, loop it with a single non-empty-string check, and keep the genuinely irregular rules after the loop: validate's bundle-or-candidates alternative (lines 60-75), review's optional ISO `today` (line 56), and resolve's `typeof obj.payload.target !== 'string'` which deliberately admits the empty string (line 76 — do not fold it into the non-empty check).

**Validator note.** Defect real but re-graded to minor: the duplicated strings are stderr diagnostics with no test or caller coupling. Fix kept with one correction — resolve's target check must stay empty-string-tolerant, which a blanket non-empty loop would silently tighten.

<details><summary>Proof of in-diff scope</summary>

```
$ git -C /Users/artem.veduta/proj/okf-agent-skills diff --unified=0 origin/main...feat/issue-41-spec-v0.1.0 -- scripts/lib/protocol.js | head -4
diff --git a/scripts/lib/protocol.js b/scripts/lib/protocol.js
new file mode 100644
index 0000000..1921a2f
--- /dev/null
(status A: whole file added by the branch; no blame run)
```

</details>

#### F031 — Validation for a 'route' operation that does not exist anywhere in the suite

File: scripts/lib/protocol.js
Severity: minor
anchor: if (obj.operation === 'route' && typeof obj.payload.concept !== 'string') throw new Error('Missing payload.concept');

- Line: 85 · Lens: deslop · Kind: slop · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed dead. Grepped the whole repo for an operation named 'route': the only occurrences of the bare token are this line and navigation.js's local `route`/`routeScope` variables (scripts/lib/navigation.js:286-347), which are unrelated. It is absent from routerOwners (scripts/lib/runtime.js:10-15), from every branch of runActive, from every skills/*/SKILL.md dispatch table, and from test/. A request carrying `operation: 'route'` reaches unknownOperation and is refused. Not load-bearing: removing the line changes no accepted or rejected request.

**Fix.** Delete the line. The router's dispatch operation is `resolve`, validated on line 76.

#### F032 — Operation smuggled into readEvidence as an underscore-prefixed fake payload field

File: scripts/lib/runtime.js
Severity: minor
anchor: const required = ['create', 'revise', 'relationship', 'machine-verify'].includes(payload._operation);

- Line: 93 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed. readEvidence reads `payload._operation` at scripts/lib/runtime.js:93 and the sole caller at line 197 fabricates it: `readEvidence({ ...payload, _operation: operation }, bundleRoot, services)`, even though `operation` is already a parameter of executeBounded. readEvidence has no other caller in the repo.

**Fix.** Change the signature to `readEvidence(payload, operation, bundleRoot, services)` and call it as `readEvidence(payload, operation, bundleRoot, services)` at line 197. Drop the underscore key and the spread.

#### F033 — listFiles hides its completeness flag as a non-enumerable array property

File: scripts/lib/services.js
Severity: minor
anchor: Object.defineProperty(files, 'complete', { value: complete, enumerable: false, writable: true });

- Line: 71 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed at scripts/lib/services.js:71. Consumers hedge exactly as described: scripts/lib/navigation.js:201 and :222 test `entries.complete === false` (missing flag reads as complete) while :162 and :376 test `!observation.complete` (missing flag reads as incomplete). Any copy of the listing loses the property.

**Fix.** Return `{ files, complete }` from listFiles and destructure at the call sites (scripts/lib/navigation.js:191 and scripts/lib/adapters.js:39), collapsing the truthiness hedges to one explicit boolean. Note the test double at test/issue-50.test.js:550-554 mutates `entries.complete = false` on the returned array and must be updated in the same change or that test fails.

**Validator note.** Defect real; fix stands but is not confined to scripts/ — test/issue-50.test.js:550 and scripts/lib/adapters.js:39 also consume the return value.

#### F034 — Boolean-inequality XOR states the changes rule in the least readable way

File: scripts/lib/delegation.js
Severity: nit
anchor: if (isWriter !== !isEmpty(brief.changes)) {

- Line: 77 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed at scripts/lib/delegation.js:77. isEmpty (lines 18-23) does return a strict boolean on every path, so the comparison is correct today but depends on that; the rewrite removes the dependency.

**Fix.** State the two cases directly: `if (isWriter ? isEmpty(brief.changes) : !isEmpty(brief.changes)) return incompleteBrief('changes');`.

#### F036 — modeOf is a pass-through wrapper around validation.projectMode

File: scripts/lib/runtime.js
Severity: nit
anchor: function modeOf(bundleRoot, services) {

- Line: 83 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed: scripts/lib/runtime.js:83-85 forwards both arguments unchanged, and the single call site is line 188.

**Fix.** Delete modeOf and call `validation.projectMode(bundleRoot, services)` directly at line 188.

#### F037 — appendDerivative parses the frontmatter three times, but only the middle call is redundant

File: scripts/lib/runtime.js
Severity: nit
anchor: if (parsed) validation.parseYAML(validation.parseFrontmatter(rendered).frontmatter);

- Line: 139 · Lens: deslop · Kind: slop · Verdict: `fixed-with-deviation` · Disposition: `fix-with-deviation` · Re-graded minor → nit

Two of the three calls are as described (scripts/lib/runtime.js:135, 139, 141) and appending after the closing `---` cannot change the frontmatter, so line 139's check on `rendered` cannot fail if line 135 passed — that one is genuine noise. Line 141 is load-bearing and must stay: it is the only post-publication validation of the derivative, and the spec requires one — docs/spec/okf-agent-skills-v0.1.0.md:927 'After an accepted mutation, the suite MUST validate the affected concepts and directly affected navigation and history derivatives. (#6)', while the same spec at line 3439 states validation.postWrite 'validates the primary concept only, not the later index and log derivative writes'. Nothing else in the runtime validates index.md/log.md after they are written. The DERIVATIVE_WRITE_FAILED test at test/issue-54.test.js:196 throws from publishFile, so it does not pin any of the three parses.

**Fix.** Delete only line 139. Keep line 135 (detects an already-corrupt derivative before touching it) and line 141 (the spec-required post-publication validation of the derivative, whose throw is what raises DERIVATIVE_WRITE_FAILED at line 236).

**Validator note.** Partially refuted: line 139 is redundant, but the reviewer's proposal to delete line 141 and the post-publish re-read would remove the suite's only post-mutation validation of a navigation/history derivative, which docs/spec line 927 makes a MUST. Location moved from 141 to 139, the line that is actually the defect.

#### F038 — filter(Boolean) guards a case executeBounded cannot reach

File: scripts/lib/runtime.js
Severity: nit
anchor: const provisionalEffects = effectsResult.effects.length ? effectsResult.effects : [primaryEffects.get(operation)].filter(Boolean);

- Line: 147 · Lens: deslop · Kind: slop · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed: the only two calls to executeBounded are scripts/lib/runtime.js:419 (guarded by `if (!primaryEffects.has(request.operation)) return unknownOperation(request)` at line 418) and line 416 with `planned.operation`, which lifecycle.plan (scripts/lib/lifecycle.js:14) sets to 'revise' or 'create'. `primaryEffects.get(operation)` is therefore never undefined here.

**Fix.** Drop `.filter(Boolean)`, leaving `: [primaryEffects.get(operation)]`. Leave boundedEffects' `if (!primary)` branch (line 65) alone unless you also want to prove no future caller reaches it — removing it converts an unreachable guard into an undefined-primary crash path for no gain.

**Validator note.** Defect real; the reviewer's optional second suggestion (deleting boundedEffects' `if (!primary)` guard) is trimmed from the fix because that guard is the invariant's only in-function statement and costs one line.

#### F039 — Bare 'fs'/'path' requires in the one module that departs from the node: prefix

File: scripts/lib/services.js
Severity: nit
anchor: const fs = require('fs');

- Line: 8 · Lens: deslop · Kind: slop · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed by grep: every other module uses the prefixed form (admission.js:8, adapters.js:17, navigation.js:1, lifecycle.js:1, reach.js:9, orientation.js:7, manifest.js:10, presence.js:3, routing.js:7, trust.js:7, runtime.js:1, adapter-hook.js:9, adapter-bridge.js:2, okf-delegate.js:2-3 all `node:`); scripts/lib/services.js:8-9 alone uses `require('fs')` and `require('path')`. Not load-bearing — both specifiers resolve identically on every supported Node version.

**Fix.** Use `require('node:fs')` and `require('node:path')` here, matching every other module.

### wrappers-adapters-ci

11 real, 1 refuted or already-correct.

#### F040 — A crashed okf-read run is silently un-claimed and replayed, discarding the failure response it printed

File: scripts/lib/adapters.js
Severity: blocker
anchor:     response = dispatched.status === 0 ? JSON.parse(dispatched.stdout) : null;

- Line: 165 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed against the code. scripts/wrapper.js:31-35 emits a well-formed `failed/incomplete` response with code RUNTIME_FAILURE on stdout AND sets exitCode 70. adapters.js:165 parses stdout only when `status === 0`, so `response` is null, line 170 deletes the pre-claim, and `present(null, ...)` at line 174 returns null. Net effect of a runtime crash inside orientation: nothing reported, occurrence released, next duplicate native signal dispatches again — contradicting this file's own header invariant (lines 7-8: 'pre-claim it unavailable before dispatch (so a crash mid-dispatch is reported, never replayed)') and the two spec MUSTs at docs/spec/okf-agent-skills-v0.1.0.md:3106-3107. The lines 160-161 comment claims these cases are indistinguishable from no response, but they are distinguishable: exit 0 with empty stdout is the deliberate silent-seam/no-response, a non-zero exit is a real failed attempt. Verified present as an added line in the new file.

**Fix.** Parse stdout unconditionally: `let parsed = null; try { parsed = JSON.parse(dispatched.stdout); } catch {}`. If `parsed` is an `okf-wrapper/1` response, use it regardless of exit status (claimOutcome already maps `failed/incomplete` to 'failed', so the claim is recorded and present() emits a diagnostic). Release the claim (`delete claims[key]`) only when the child exited 0 with empty or unparseable stdout; when the exit is non-zero and nothing parseable came back (e.g. exit 64 from protocol.parseRequest), leave the pre-claim 'unavailable' in place and return a diagnostic outcome. Update the comment on lines 160-161 to match. Existing behaviour for the exit-0 unavailable path (test/issue-66-adapters.test.js:238-246) is unaffected.

<details><summary>Proof of in-diff scope</summary>

```
diff --git a/scripts/lib/adapters.js b/scripts/lib/adapters.js
new file mode 100644
index 0000000..76ef27a
--- /dev/null
+++ b/scripts/lib/adapters.js
@@ -0,0 +1,177 @@
+    response = dispatched.status === 0 ? JSON.parse(dispatched.stdout) : null;
```

</details>

#### F041 — install reports ok:true without observing that the copy completed, and leaves unremovable residue when it fails

File: scripts/lib/adapters.js
Severity: major
anchor:   copyScriptsTree(manifest.scripts_tree, resolvedTarget, services, installedFiles);

- Line: 67 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Both halves confirmed. (1) services.listFiles attaches a `complete` flag (scripts/lib/services.js:71) that is false when a symlink or unreadable directory truncated the walk; copyScriptsTree (lines 38-46) only iterates the array and drops the flag, so a truncated scripts tree is copied and install still returns `ok: true` at line 71. The installed adapter then fails closed silently at every session start (adapter-hook.js catch, plugin.js:47) after the CLI claimed success. (2) The receipt is written last (line 70), so a throw from any mkdir/writeFile mid-copy is caught by scripts/okf-adapter.js:19-25, which emits ADAPTER_CLI_FAILURE and exits 70, leaving a partially populated target with no receipt; `uninstall` keys entirely off the receipt (lines 100-101) and answers NOT_INSTALLED, so the residue must be deleted by hand. Verified in-diff (whole file added).

**Fix.** Have copyScriptsTree read the listing's `complete` flag and, when it is false, remove what it already copied and return a failure so install answers `{ ok: false, code: 'INCOMPLETE_SOURCE_TREE' }` instead of a success receipt (okf-adapter.js already emits any `ok: false` outcome and exits 1). Make install atomic against the receipt: on the throw path, write the receipt with the `installedFiles` accumulated so far before rethrowing (or write an in-progress receipt before the first copy and finalise it at the end), so `uninstall` can always remove exactly what install created.

<details><summary>Proof of in-diff scope</summary>

```
diff --git a/scripts/lib/adapters.js b/scripts/lib/adapters.js
new file mode 100644
index 0000000..76ef27a
--- /dev/null
+++ b/scripts/lib/adapters.js
@@ -0,0 +1,177 @@
+  copyScriptsTree(manifest.scripts_tree, resolvedTarget, services, installedFiles);
```

</details>

#### F042 — The at-most-once claim ledger is a non-atomic read-modify-write, so concurrent hooks lose claims

File: scripts/lib/adapters.js
Severity: major
anchor: function writeClaims(file, claims, services) {

- Line: 118 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed. claimAndDispatch (lines 145-175) does readClaims -> mutate in memory -> writeClaims via `services.writeFile` (plain fs.writeFileSync, services.js:80) twice, with no compare-and-swap and no lock. Two hook processes on one target dir interleave read/read/write/write and the loser's record vanishes. orientation.deriveKey (scripts/lib/orientation.js:51-57) keys on [harness, gitRoot, context_id, logical_cause, native_event_id]; adapter-hook.js omits native_event_id when `transcript_path` is absent, so parallel Codex subagents under one session_id with logical_cause 'subagent-start' derive an identical key, both see `firstAttempt === true`, and both inject orientation — breaking the at-most-once MUST at docs/spec/okf-agent-skills-v0.1.0.md:3102. services.publishFile (services.js:17-30) is the existing CAS primitive the writer path uses (runtime.js:140, 217) and the ledger ignores it. Verified in-diff (whole file added).

**Fix.** Route each ledger mutation through the existing `services.publishFile`: read the raw ledger text (not just the parsed object) so it can be passed as `expected` (null when the file is absent), and on the thrown TARGET_CHANGED re-read, re-check whether the key is now claimed, then either bail out as a duplicate replay or retry a bounded number of times. Keep both the pre-claim and the finalize step on the same CAS path so a lost update can never present twice. No new dependency and no new seam — publishFile is already injected in `services`.

<details><summary>Proof of in-diff scope</summary>

```
diff --git a/scripts/lib/adapters.js b/scripts/lib/adapters.js
new file mode 100644
index 0000000..76ef27a
--- /dev/null
+++ b/scripts/lib/adapters.js
@@ -0,0 +1,177 @@
+function writeClaims(file, claims, services) {
```

</details>

#### F043 — Hand-rolled three-way stream copying where inherited stdio does the same thing

File: scripts/adapter-bridge.js
Severity: minor
anchor:   const child = childProcess.spawn(process.execPath, [path.join(__dirname, wrapper)], { stdio: ['pipe', 'pipe', 'pipe'] });

- Line: 19 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed: lines 19-27 spawn async, pipe stdin, copy stdout/stderr chunk by chunk, and reconcile the exit code across an 'error' handler (70) and a 'close' handler that overwrites it. The bridge is a pure pass-through and test/issue-66-adapters.test.js:196-227 asserts byte-identical stdout/stderr/status versus a direct wrapper spawn, which inherited stdio satisfies (the test spawns the bridge with piped stdio, and the grandchild inherits those same descriptors). test/issue-66-adapters.test.js:255-259 only forbids runtime/policy vocabulary in the source, so the rewrite stays compliant.

**Fix.** Replace the block with a synchronous inherited-stdio spawn: `const result = childProcess.spawnSync(process.execPath, [path.join(__dirname, wrapper)], { stdio: 'inherit' }); process.exitCode = result.error || result.status === null ? 70 : result.status;` and write the 'Adapter bridge failure' line only on `result.error`.

#### F045 — The harness enum is hard-coded three times

File: scripts/lib/adapters.js
Severity: minor
anchor: const harnesses = new Set(['claude-code', 'codex', 'opencode']);

- Line: 22 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`
- Spans: scripts/lib/adapters.js, scripts/adapter-bridge.js, scripts/lib/orientation.js

Confirmed at all three sites: scripts/lib/orientation.js:11 (`validHarnesses`, owner of the seam table), scripts/lib/adapters.js:22, scripts/adapter-bridge.js:4 — identical literal sets. adapters.js already requires orientation.js (line 19). One correction to the fix: orientation.js currently exports only `{ suiteVersion, seamTable, orient, deriveKey }` (line 153), so `validHarnesses` has to be added to that export.

**Fix.** Export `validHarnesses` from scripts/lib/orientation.js as the single source of truth. Use it in adapters.js and re-export it as `adapters.harnesses` (okf-adapter.js:10 depends on that name), and have adapter-bridge.js check against the same exported set instead of its own literal.

#### F046 — wrapper.js's diagnostic() is copy-pasted into the adapter CLI

File: scripts/okf-adapter.js
Severity: minor
anchor:     const text = error && typeof error.message === 'string' ? error.message : String(error || '');

- Line: 21 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`
- Spans: scripts/okf-adapter.js, scripts/wrapper.js

Confirmed byte-for-byte: okf-adapter.js:21-22 reproduces scripts/wrapper.js:4-7 `diagnostic()` — same message extraction, same `replace(/\s+/g, ' ').trim().slice(0, 200)`, same fallback. wrapper.js currently exports only `{ main }` (line 39), so the fix must add `diagnostic` to that export.

**Fix.** Export `diagnostic` from scripts/wrapper.js alongside `main` and call it from okf-adapter.js: `process.stderr.write(`${wrapper.diagnostic(error, 'internal failure')}\n`)`.

#### F047 — Header comment restates another module's design essay, with an unreflowed patched-in sentence

File: adapters/opencode/plugin.js
Severity: nit
anchor:  * okf-agent-skills/scripts/lib copy, shared with every other harness. A disabled install (`.okf-adapter-disabled`)

- Line: 6 · Lens: deslop · Kind: slop · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Target text is still at plugin.js:6 and the line does run well past the wrap width of every other line in the block, with the `.okf-adapter-disabled` sentence appended mid-paragraph. The claim-cycle/presentation narration duplicates scripts/lib/adapters.js:6-14 and scripts/adapter-hook.js:4-6. Not load-bearing: no tool consumes it, and `if (disabled()) return;` (line 33) states the no-op by itself. The sentences that ARE load-bearing — the transform-only injection seam, the generation counter, and lines 9-10 on adapter-owned paths resolving from the parent directory — the fix explicitly keeps.

**Fix.** Rewrap the paragraph and cut it to what is local to this file: the transform-only injection seam, the generation counter, and the parent-directory path note. Drop the duplicated claim-cycle/presentation narration and the `.okf-adapter-disabled` sentence, leaving lib/adapters.js as the one description of the shared cycle.

#### F048 — Unreflowed sentence appended to the module header comment

File: scripts/lib/adapters.js
Severity: nit
anchor:  * the one contract seam), then finalize the claim from the real result. Presentation is decided

- Line: 10 · Lens: deslop · Kind: slop · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed at adapters.js:10 — the line runs clearly past every other line in the same comment block because 'Presentation is decided' was tacked onto the end. Prose only, nothing consumes it. Note the invariant on lines 7-8 is currently FALSE in the code (see the blocker); the rewrap must not be done in a way that hides that until the blocker fix lands.

**Fix.** Rewrap the header block to the file's own comment width and trim it to the invariants a reader cannot get from the code (pre-claim before dispatch, inject only on 'clean', replay reports the original reason), keeping it truthful about the crash path after the blocker fix.

#### F049 — Second, inconsistent derivation of the scripts directory

File: scripts/lib/adapters.js
Severity: nit
anchor: const readWrapper = path.join(__dirname, '..', 'okf-read.js');

- Line: 29 · Lens: deslop · Kind: slop · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed: line 28 defines `scriptsRoot = path.join(suiteRoot, 'scripts')` and line 29 re-derives the same directory as `__dirname/..` (both resolve to suiteRoot/scripts since __dirname is suiteRoot/scripts/lib). Two spellings of one path in adjacent lines, breaking the deliberate suiteRoot -> adaptersRoot -> scriptsRoot chain above it.

**Fix.** Write `const readWrapper = path.join(scriptsRoot, 'okf-read.js');` so every adapter-owned path descends from the single `suiteRoot` derivation.

#### F050 — The occurrence ledger grows without bound and is fully parsed on every session start

File: scripts/lib/adapters.js
Severity: nit
anchor:   const claims = file ? readClaims(file, services) : {};

- Line: 148 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed: nothing in the file prunes `.okf-occurrences.json`; readClaims (line 115) JSON.parses the whole object on every session start, subagent start, and system transform, and every claimed occurrence adds a permanent entry. Because deriveKey embeds context_id (orientation.js:56), an old session's entry can never be matched again, so the growth is pure cost.

**Fix.** Bound the ledger in `writeClaims`: keep insertion order and drop the oldest entries beyond a fixed cap (a few hundred), so lookup stays correct for live occurrences and the file stops growing forever.

#### F051 — The delegate wrapper re-derives the skill by reaching into delegation.ROLES

File: scripts/okf-delegate.js
Severity: nit
anchor:       const response = validated.ok ? dispatch(delegation.ROLES[brief.role].skill, validated.request) : validated;

- Line: 50 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed: scripts/lib/delegation.js:47 (`buildRequest`) already sets `skill: ROLES[brief.role].skill` in the request returned by validateBrief, so line 50 re-reads the role map for a value it was just handed and `dispatch(skill, request)` carries a parameter that must always equal `request.skill`. This is a duplication cleanup only; it does not touch the delegation reachability question settled by decision 91.

**Fix.** Use the validated request as the single source: `const response = validated.ok ? dispatch(validated.request) : validated;` and inside `dispatch` take `request.skill` for the wrapper path and the finding detail. Keep the `ROLES` export (test/issue-68-agents.test.js uses it) but stop reading it here.

### skills-and-bundle

9 real, 0 refuted or already-correct.

#### F052 — The shipped bundle root declares knowledge-only for a code-backed repository, disabling the code-recoverable gate

File: okf/index.md
Severity: major
anchor: project_mode: "knowledge-only"

- Line: 3 · Lens: docs · Kind: docs · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed. scripts/lib/runtime.js gates exactly as claimed: `if (mode === 'code-backed' && payload.code_recoverable === true)` → blocked with `CODE_RECOVERABLE_MATERIAL` (the branch sits directly after the `PROJECT_MODE_INVALID` check in `executeBounded`), and test/issue-53.test.js:205-210 exercises it with a `code-backed` fixture. docs/spec/okf-agent-skills-v0.1.0.md:68-69 defines the two models: code stays authoritative in a code-backed project, whereas knowledge-only means "the bundle is the complete source of truth". This repository's bundle holds only release notes while scripts/ and test/ are authoritative, so knowledge-only is the wrong declaration and the one guard against absorbing code-recoverable material into this bundle is off. No test references okf/index.md, so the change is content-only.

**Fix.** Set `project_mode: "code-backed"` in okf/index.md, then re-run `node --test "test/*.test.js"` (pass count equals test count, zero fail).

<details><summary>Proof of in-diff scope</summary>

```
$ git diff --unified=0 origin/main...feat/issue-41-spec-v0.1.0 -- okf/index.md
diff --git a/okf/index.md b/okf/index.md
new file mode 100644
index 0000000..aac03c7
--- /dev/null
+++ b/okf/index.md
@@ -0,0 +1,8 @@
+---
+okf_version: "0.2"
+project_mode: "knowledge-only"
+---
+
+# OKF Agent Skills
+
+- [Releases](releases/index.md)
```

</details>

#### F053 — The documented `resolve` request shape crashes the wrapper instead of returning a result

File: skills/okf-read/SKILL.md
Severity: major
anchor: - `resolve` — `payload.target`.

- Line: 31 · Lens: docs · Kind: docs · Verdict: `confirmed` · Disposition: `fix-as-proposed`
- Spans: skills/okf-read/SKILL.md, scripts/lib/protocol.js

Confirmed empirically. scripts/lib/protocol.js:76 is the only validation for `resolve` and checks `typeof obj.payload.target !== 'string'` — no `cwd` check, unlike `read` (77-79) and `search` (81-83). Running the shipped wrapper with exactly the documented payload:
  $ echo '{"protocol":"okf-wrapper/1","skill":"okf-read","operation":"resolve","payload":{"target":"note.md"}}' | node skills/okf-read/scripts/okf-read.js
  {... "result":"failed/incomplete", "data":{"code":"RUNTIME_FAILURE"} ...}
  Runtime failure: The "paths[0]" argument must be of type string. Received undefined
  exit=70
So the documented shape can only land in exit-condition 3. `resolve` routes through admitAndRoute → admission, which resolves paths from `payload.cwd`.

**Fix.** Change the bullet to "`resolve` — `payload.cwd` and `payload.target`." and add to scripts/lib/protocol.js:76 the same `cwd` guard `read`/`search` already use (`if (typeof obj.payload.cwd !== 'string' || obj.payload.cwd === '') throw new Error('Missing payload.cwd');`), so an omitted `cwd` is invalid wrapper input (exit 64) rather than a RUNTIME_FAILURE.

<details><summary>Proof of in-diff scope</summary>

```
$ git diff --unified=0 origin/main...feat/issue-41-spec-v0.1.0 -- skills/okf-read/SKILL.md
diff --git a/skills/okf-read/SKILL.md b/skills/okf-read/SKILL.md
new file mode 100644
index 0000000..b9bc4ef
--- /dev/null
+++ b/skills/okf-read/SKILL.md
[whole file added; no blame run per method]
$ git diff --name-status origin/main...feat/issue-41-spec-v0.1.0 -- scripts/lib/protocol.js
A	scripts/lib/protocol.js
```

</details>

#### F054 — The performed-effects list names six effects the v0.1.0 runtime refuses with UNSUPPORTED_INPUT

File: skills/okf-write/SKILL.md
Severity: major
anchor: `okf-write` performs each atomic effect below:

- Line: 46 · Lens: docs · Kind: docs · Verdict: `confirmed` · Disposition: `fix-as-proposed`
- Spans: skills/okf-write/SKILL.md, skills/okf-lifecycle/SKILL.md

Confirmed against the code. scripts/lib/runtime.js:22 `forbiddenEffectKeys = ['deprecate', 'move', 'rename', 'rewrite']` and `unsupportedPayload` (106-119) reject those keys, `effects` containing `link-rewrite`, a top-level `delete`/`status`/`redirect`/`alias`/`purge` key, and any `set.status`/`set.stale_after`. runtime.js:17-21 defines the only performable primary effects (concept-create, concept-revise, format, relationship, machine-verify) plus derived index-maintenance/log-append, so `mechanical-link-maintenance` is refused too (test/issue-54.test.js:146-157 asserts `UNSUPPORTED_INPUT` for it), and "Create merge or split outputs" maps to no operation at all. test/issue-53.test.js:115-124 asserts `delete` → UNSUPPORTED_INPUT and issue-53.test.js:211-212 asserts `set: { status: 'stable' }` → UNSUPPORTED_INPUT. Lines 52, 54, 55, 56, 57, 60 of this file therefore describe refusals as performed effects, with no v0.1.0 note — unlike okf-lifecycle, which says outright that `init`/`migrate`/`compact` are not v0.1.0 operations. The paired mechanical-link claim is at skills/okf-lifecycle/SKILL.md:59 ("Those three belong to `okf-write`"), not line 31 as the reviewer wrote.

**Fix.** Keep the ownership list inline (recorded decision 4) but split it as okf-lifecycle splits its operations: an "owned and performed in v0.1.0" group (create a concept, claim update, metadata/format update, qualifying machine verification, add or remove one semantic relationship, regenerate a directly affected index, append a directly affected log entry) and an "owned, not a v0.1.0 operation — the runtime returns `UNSUPPORTED_INPUT`" group (the three status transitions, move or rename, broad inbound-link or graph rewrite, merge or split outputs, delete a redundant concept, repair a directly affected mechanical link). Add the same `UNSUPPORTED_INPUT` qualifier to the mechanical-link clause at skills/okf-lifecycle/SKILL.md:59.

**Validator note.** Location of the primary anchor is correct; the cross-file reference was wrong (okf-lifecycle line 59, not 31) and is corrected in detail and fix. Fix respects hard constraint 4 — the list stays inline.

<details><summary>Proof of in-diff scope</summary>

```
$ git diff --unified=0 origin/main...feat/issue-41-spec-v0.1.0 -- skills/okf-write/SKILL.md
diff --git a/skills/okf-write/SKILL.md b/skills/okf-write/SKILL.md
new file mode 100644
index 0000000..e13eb8d
--- /dev/null
+++ b/skills/okf-write/SKILL.md
[whole file added; no blame run per method]
```

</details>

#### F055 — The shipped bundle's directory index has no frontmatter, so the repository's own enumerate degrades

File: okf/releases/index.md
Severity: minor
anchor: # Releases

- Line: 1 · Lens: docs · Kind: docs · Verdict: `confirmed` · Disposition: `fix-as-proposed` · Re-graded major → minor

Reproduced against the shipped wrapper from the repo root:
  $ echo '{"protocol":"okf-wrapper/1","skill":"okf-read","operation":"enumerate","payload":{"cwd":"<repo root>"}}' | node skills/okf-read/scripts/okf-read.js
  {... "result":"degraded" ... "coverage":"non-exhaustive","inbound_links":{"complete":false,"incomplete_reasons":["frontmatter_unparseable"] ...}}
Cause is in scripts/lib/navigation.js:820-828: `validation.parseFrontmatter` throws on a file with no frontmatter and the catch records `frontmatter_unparseable`; okf/index.md and okf/releases/v0.1.0.md both have frontmatter, this file does not. Downgraded to minor: the result is `degraded`, not blocked, so nothing breaks — it is a self-dogfooding quality defect in the only bundle the release ships.

**Fix.** Give okf/releases/index.md a frontmatter block consistent with its siblings (`---\ntitle: Releases\ntype: Index\n---`), then re-run the enumerate request above and confirm `result: "ok"` with `coverage: "complete"` and `inbound_links.complete: true`.

**Validator note.** Defect real and reproduced. Note for the fix phase: the runtime also conflates "absent frontmatter" with "unparseable frontmatter" (navigation.js:820-828) — that is a separate runtime question, out of this chunk; the content fix is the lazy in-scope one.

<details><summary>Proof of in-diff scope</summary>

```
$ git diff --unified=0 origin/main...feat/issue-41-spec-v0.1.0 -- okf/releases/index.md
diff --git a/okf/releases/index.md b/okf/releases/index.md
new file mode 100644
index 0000000..8a73f91
--- /dev/null
+++ b/okf/releases/index.md
@@ -0,0 +1,3 @@
+# Releases
+
+- [v0.1.0 release candidate](v0.1.0.md)
```

</details>

#### F056 — INVALID_SCOPE arrives as `blocked`, not as `abstained`

File: skills/okf-lifecycle/SKILL.md
Severity: minor
anchor: an `abstained` result carrying an `INVALID_SCOPE` finding when the scope cannot be resolved is a completed answer

- Line: 51 · Lens: docs · Kind: docs · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Text is at line 51 as claimed. Verified in scripts/lib/runtime.js `runActive`, `okf-lifecycle` branch: when `lifecycle.plan` abstains and `scopeFor(...).invalid`, it returns `writeResponse(request, 'blocked', 'blocked', ..., [INVALID_SCOPE finding], 'INVALID_SCOPE', scoped.scope)`; the next line returns the `abstained` response with an empty findings array. So `abstained` never carries `INVALID_SCOPE`.

**Fix.** Rewrite as: "A refusal is a valid response, not a failure — an `abstained` result with no findings, or a `blocked` result carrying `data.code: \"INVALID_SCOPE\"` when the scope cannot be resolved, is a completed answer."

#### F057 — Procedure step 3 restates the Navigation path section in full

File: skills/okf-read/SKILL.md
Severity: minor
anchor: walk the three levels above — bundle-root index, then the directory index for the concept's location, then the concept body

- Line: 59 · Lens: deslop · Kind: slop · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Text present, but at line 59 (last line of the file), not 60. It duplicates line 12 verbatim in substance: the same three-level enumeration, the same "choosing each entry by reading the index content in front of you", the same "reported as a `resolve` or `read` result rather than a thrown error". Not load-bearing beyond the pointer plus the completion criterion, and the duplicate copy can drift. Hard constraint 4 is not engaged: the three-level walk is not one of the enumerated inline-safety items, and the fix keeps the full statement inside this same SKILL.md.

**Fix.** Reduce step 3 to the pointer plus its completion criterion, e.g. "Walk the fixed three-level path from Navigation path. Done when the concept's content is in hand; not done if no bundle root resolves, no index entry matches, or the path does not exist under the bundle root — each reported as a `resolve` or `read` result, never a thrown error." Keep the full description only at line 12.

**Validator note.** Original line 60 was off by one; corrected to 59.

#### F058 — `set` is documented as the way to change a status, which the runtime blocks

File: skills/okf-write/SKILL.md
Severity: minor
anchor: `set` for a status or field change

- Line: 28 · Lens: docs · Kind: docs · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Text is at line 28 as claimed. Verified: scripts/lib/runtime.js:112 `if (set && (Object.hasOwn(set, 'status') || Object.hasOwn(set, 'stale_after'))) return true;` inside `unsupportedPayload`, reached from runtime.js:148, and test/issue-53.test.js:211-212 asserts `set: { status: 'stable' }` → `UNSUPPORTED_INPUT`.

**Fix.** Narrow the clause to what the runtime accepts: "`set` for a field change (a status value is not writable in v0.1.0 — `set.status` returns `UNSUPPORTED_INPUT`)".

#### F059 — ROOT_DECLARATION_NOT_EXACT is a finding code, never a `data.code`

File: skills/okf-write/SKILL.md
Severity: minor
anchor: a `blocked` result carrying a `data.code` such as `ROOT_DECLARATION_NOT_EXACT`

- Line: 36 · Lens: docs · Kind: docs · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Text is at line 36 as claimed. Verified: the only production occurrence of the string is scripts/lib/validation.js, where it is emitted as `blocker('ROOT_DECLARATION_NOT_EXACT', 'suite', { observed, observed_type })` — a finding. It is never passed as the `dataCode` argument of `writeResponse` anywhere in scripts/. test/write-gate.test.js:43-51 asserts precisely that shape (result `blocked`, the code found in `response.findings`).

**Fix.** Rewrite as: "a `blocked` result carrying a `data.code` such as `WRITE_TARGET_OUTSIDE_WORKTREE`, `TARGET_CHANGED`, or `EVIDENCE_UNAVAILABLE`, or a blocking finding such as `ROOT_DECLARATION_NOT_EXACT`, is a completed answer."

#### F060 — The "never an adapter or a hook" rule is restated three times in one 66-line file

File: skills/okf-lifecycle/SKILL.md
Severity: nit
anchor: It is still the agent that sends this request through the wrapper, never an adapter or a hook — see "The wrapper request" below.

- Line: 12 · Lens: deslop · Kind: slop · Verdict: `confirmed` · Disposition: `fix-as-proposed`

All three sites confirmed at the stated locations: line 12 (with the see-below pointer), line 41 (the authoritative statement, which also names `AUTOMATIC_MUTATION_BLOCKED`), and Procedure step 2 at line 64. Line 41 is normative and stays; the other two are wording to keep in sync. Hard constraint 4 is satisfied because the refusal rule remains inline in this SKILL.md at line 41 — nothing is extracted into a reference.

**Fix.** Keep the normative statement once, at line 41 where `invocation` is defined. Drop the trailing "— see \"The wrapper request\" below" pointer from line 12, and shorten Procedure step 2 (line 64) to "Both trigger classes send `invocation: \"explicit\"`" plus its existing completion criterion.

**Validator note.** Confirmed. The fix must not touch line 41 — that sentence is the inline safety statement hard constraint 4 protects.

### eval-harness

9 real, 0 refuted or already-correct.

#### F061 — The load-bearing `.git` sentinel is undocumented while the comment above enumerates everything else

File: eval/lib/fixture.js
Severity: minor
anchor:   fs.mkdirSync(path.join(root, '.git'));

- Line: 27 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed at eval/lib/fixture.js:27. The comment at lines 25-26 lists 'declared okf_version, the activation marker, and one concept' and omits the empty `.git`. It is load-bearing: scripts/lib/services.js:33-42 (`gitRootOf`) walks parents for a `.git` entry to resolve the repository root, so removing it would make cases fail with a root-resolution error far from this line.

**Fix.** Extend the comment to name it and its purpose, e.g. 'and an empty `.git` so the wrapper's repository-root walk (scripts/lib/services.js) stops here'.

<details><summary>Proof of in-diff scope</summary>

```
eval/lib/fixture.js is status A; the branch diff adds the file including `+  fs.mkdirSync(path.join(root, '.git'));`.
```

</details>

#### F062 — Header comment hardcodes five test filenames from another directory

File: eval/lib/wrapper-contract.check.js
Severity: minor
anchor: // (test/write-gate.test.js, test/issue-53.test.js, test/issue-64.test.js)

- Line: 3 · Lens: deslop · Kind: slop · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed at eval/lib/wrapper-contract.check.js:3-4; five files are named (write-gate, issue-53, issue-64, issue-48, issue-52) and all five currently exist under test/, so the list is accurate but purely provenance and outside this directory's control. The load-bearing clause is lines 5-7 — the one-gated-contract-seam rule and 'this file checks only classifyWrapperExit itself, against literal fixtures' — which the fix retains, so nothing a reader needs is lost.

**Fix.** Cut the file list and keep the rule: "The gated suite under `test/` owns the real wrapper spawn (AGENTS.md, one contract seam). This file checks only `classifyWrapperExit` against literal fixtures."

<details><summary>Proof of in-diff scope</summary>

```
eval/lib/wrapper-contract.check.js is status A; the branch diff adds the whole file.
```

</details>

#### F063 — The subset-run instruction is documented twice, the second time inside an unrelated section

File: eval/README.md
Severity: minor
anchor: Add one optional argument to run a subset of the cases by id:

- Line: 144 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed. '## Running it' (line 83) already documents id filtering twice: `./eval.sh okf-write` (line 91) and, implicitly, the `npm run eval` steps block. Line 144 plus its code fence (145-148) is a third copy, and it sits inside '## Credentials' (line 102, next heading '## What a case result looks like' at line 150), a section about provider keys.

**Fix.** Delete lines 144-148 and keep the filtering documentation only under '## Running it'.

<details><summary>Proof of in-diff scope</summary>

```
eval/README.md is status A; the branch diff adds the whole file, so both copies are in-diff.
```

</details>

#### F064 — Two different shapes for one concept: a case object and an ES module namespace used as a case

File: eval/run.js
Severity: minor
anchor: const CASES = [...ACTIVATION_CASES.map(activationCaseModule), wrapperContract];

- Line: 17 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`
- Spans: eval/run.js, eval/cases/wrapper-contract.eval.js

Confirmed. run.js:15 does `import * as wrapperContract` and line 17 pushes the namespace object into `CASES` alongside objects built by `activationCaseModule()`. eval/cases/wrapper-contract.eval.js exports `id`, `kind`, `description`, `run` as four separate top-level bindings (lines 11-16), so the case contract is implicit at the import site and the file is pinned to exactly one case.

**Fix.** In eval/cases/wrapper-contract.eval.js export a single record — `export const CASE = { id, kind, description, run }` — and import it as `import { CASE as wrapperContract } from './cases/wrapper-contract.eval.js';` so every element of `CASES` has the same shape.

<details><summary>Proof of in-diff scope</summary>

```
Both files are status A; the branch diff adds them whole, including `+const CASES = [...ACTIVATION_CASES.map(activationCaseModule), wrapperContract];`.
```

</details>

#### F065 — The transport-failure regex matches any bare 5xx-shaped number in prose, turning real failures into `blocked`

File: eval/run.js
Severity: minor
anchor: const PROVIDER_TRANSPORT_FAILURE = /stream ended without finish_reason|\b(429|5\d\d)\b|overloaded|rate limit/i;

- Line: 33 · Lens: thermo · Kind: quality · Verdict: `fixed-with-deviation` · Disposition: `fix-with-deviation` · Re-graded major → minor

Confirmed at eval/run.js:33 exactly as quoted. `failureReason()` (lines 37-41) joins `error.message`, `error.meta.reason` and `error.cause.message` with ' | ', and line 60 tests this regex against that prose. `\b(429|5\d\d)\b` matches any standalone three-digit 429/500-599 token anywhere in it — a token count, byte offset, port, line number — so a genuine skill/sandbox/fixture failure carrying such a number is reported `blocked`, which eval/lib/report.js:6-10 documents as explicitly not a claim about the OKF skills. Re-graded major -> minor: the trigger needs a bare 5xx-shaped standalone number in the runtime's error prose (word boundaries exclude e.g. 1512, 5.00), and this harness is non-gating (run.js:3-4), so a miss costs a re-run, not a release gate. Real defect, low incidence.

**Fix.** Drop the bare-number alternative and match provider status phrasing instead, which is what the runtime actually prints: `const PROVIDER_TRANSPORT_FAILURE = /stream ended without finish_reason|overloaded|rate limit|too many requests|internal server error|bad gateway|service unavailable|gateway timeout/i;`. This keeps every observed transport condition and removes the numeric false-positive class entirely. (The reviewer's `\b(?:http|status(?: code)?)[: ]*(?:429|5\d\d)\b` variant is unsound: providers commonly emit a bare `429 Too Many Requests` / `500 Internal Server Error` with no `http`/`status` prefix, so it would silently stop detecting the real cases.)

<details><summary>Proof of in-diff scope</summary>

```
$ git -C /Users/artem.veduta/proj/okf-agent-skills diff --unified=0 origin/main...feat/issue-41-spec-v0.1.0 -- eval/run.js
diff --git a/eval/run.js b/eval/run.js
new file mode 100644
index 0000000..2a1ad30
--- /dev/null
+++ b/eval/run.js
@@ -0,0 +1,109 @@
+// the case's own logic, so it reports `blocked`, not `fail`. A `fail` must
+// mean the OKF skills did something wrong. Observed: a `deepseek-v4-pro` run
+// that ended with "Stream ended without finish_reason", and passed on retry.
+const PROVIDER_TRANSPORT_FAILURE = /stream ended without finish_reason|\b(429|5\d\d)\b|overloaded|rate limit/i;
(status A: whole file added by this branch, no blame run)
```

</details>

#### F066 — A failure with no message produces a `fail` record whose detail is the empty string

File: eval/run.js
Severity: minor
anchor:     throw new Error(reason, { cause: error });

- Line: 65 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed. `failureReason()` returns '' when the thrown value is falsy or carries no string `message`/`meta.reason`/`cause.message` (run.js:38-40). Line 65 then throws `new Error('')`, and eval/lib/report.js:31 records `detail: error.message` — the empty string — so the printed JSON line says nothing about why the case failed, the outcome the comment at run.js:63-64 warns against.

**Fix.** Fall back to the raw value: `throw new Error(reason || String(error), { cause: error });`.

<details><summary>Proof of in-diff scope</summary>

```
eval/run.js is status A; the branch diff adds the whole file (`new file mode 100644`, `@@ -0,0 +1,109 @@`), including `+    throw new Error(reason, { cause: error });`.
```

</details>

#### F067 — "Red-before-green seam" narrates how the file was authored, not what it does

File: eval/lib/report.check.js
Severity: nit
anchor: // Red-before-green seam: the eval runner's case-result shape.

- Line: 1 · Lens: deslop · Kind: slop · Verdict: `confirmed` · Disposition: `fix-as-proposed`
- Spans: eval/lib/report.check.js, eval/lib/wrapper-contract.check.js

Confirmed: the prefix is at line 1 of eval/lib/report.check.js and line 1 of eval/lib/wrapper-contract.check.js. It describes a TDD authoring step, is not consumed by any tool, and is duplicated verbatim, so it carries no distinguishing information. The descriptive remainder after the colon is the useful half. Note eval/lib/report.js:1 already reads '// The eval runner's case-result shape.', so the trimmed comment should keep its own second sentence to stay distinct from the module it checks.

**Fix.** In both files, drop the 'Red-before-green seam:' prefix and keep the descriptive remainder, e.g. "// The eval runner's case-result shape. Pure function, no Flue runtime and no model."

<details><summary>Proof of in-diff scope</summary>

```
Both .check.js files are status A; the branch diff adds them whole.
```

</details>

#### F068 — Stale cross-reference: "Setup choice" is above, and it does not discuss isolation

File: eval/README.md
Severity: nit
anchor: has no isolation of its own (see "Setup choice" below), so the fixture must

- Line: 48 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed. '## Setup choice' is line 12, above '## The fixture workspace' (line 40), and its body (lines 14-20) is about the chosen `start()`/`init()` API surface and the ruled-out alternatives — it says nothing about `local()` lacking filesystem isolation. The pointer both mis-states direction and sends the reader to a section that cannot support the claim.

**Fix.** Drop the parenthetical, or point it at the source that actually establishes the claim: `(see docs/research/flue-skill-evaluation.md)`.

<details><summary>Proof of in-diff scope</summary>

```
eval/README.md is status A; the branch diff adds the whole file.
```

</details>

#### F069 — Session anecdote embedded in a source comment

File: eval/run.js
Severity: nit
anchor: // mean the OKF skills did something wrong. Observed: a `deepseek-v4-pro` run

- Line: 31 · Lens: deslop · Kind: slop · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed at eval/run.js:31-32. The clause is a one-model, one-session note, and docs/flue-eval-results.md is already cited six lines above (line 24) as the home for recorded runs. Partly load-bearing: the quoted literal "Stream ended without finish_reason" is the justification for the first regex alternative, so the trimmed comment must still say that the runtime emits that exact string — only the `deepseek-v4-pro`/'passed on retry' provenance is expendable.

**Fix.** Keep the rule and the literal it justifies (e.g. 'the runtime reports a dropped stream as "Stream ended without finish_reason"; a provider transport failure reports blocked, not fail') and move the per-run, per-model observation to docs/flue-eval-results.md, which the surrounding comment already cites.

<details><summary>Proof of in-diff scope</summary>

```
eval/run.js is status A; the branch diff adds `+// mean the OKF skills did something wrong. Observed: a `deepseek-v4-pro` run` as part of the new file.
```

</details>

### docs-spec-and-context

9 real, 0 refuted or already-correct.

#### F070 — Four rows the completion spec records decisions for are still `Open` in the open-item table

File: docs/spec/okf-agent-skills-v0.1.0.md
Severity: blocker
anchor: | New-bundle write-gate bootstrap exception | Open |

- Line: 3651 · Lens: docs · Kind: docs · Verdict: `confirmed` · Disposition: `fix-as-proposed`
- Spans: docs/spec/okf-agent-skills-v0.1.0.md, docs/spec/okf-agent-skills-v0.1.0-completion.md

Confirmed as described. The new `Status` column was added by this branch, and it reads `Open` at line 3651 (New-bundle write-gate bootstrap exception), 3662 (Adapter package location and per-adapter install command), 3666 (Coexistence with the existing third-party OKF skill) and 3667 (Support-ceiling fixture corpora and strata), while the completion spec added in the same chunk carries the closing decisions: `### D5 — New-bundle write-gate bootstrap exception is reclassified `Deferred`, not answered` (line 68), `### D6 — Adapter location is `adapters/<harness>/` in the same tag; the install command is the adapter script with a user-supplied target directory` (line 86), `### D9 — Coexistence with the third-party OKF skill is closed by explicit deferral` (line 137), `### D10 — The support ceiling ships labeled provisional; its fixture corpora are deferred` (line 146). Only D7 (line 3663) and D8 (line 3648 region) were transcribed into the table. The behavior is already shipped: `adapters/claude-code`, `adapters/codex`, `adapters/opencode` exist. Hard constraint 3 does not refute this: the four values are recorded decisions in the completion spec, not guesses, so setting the status transcribes rather than invents. Note the completion spec's own count line is line 33 (`- Six rows of the pinned spec's open-item table are still `Open`.`), which is what needs reconciling.

**Fix.** Set the status of all four rows to match their recorded decisions, in the same style already used for D7 and D8: line 3651 `Deferred (#9)` per D5, line 3662 `Closed (D6)` with the adopted value transcribed verbatim from D6 (`adapters/<harness>/` in the same tag; the adapter entry script with a user-supplied target directory), line 3666 `Closed (D9)` with the explicit deferral, line 3667 `Deferred (D10)`. Then correct line 33 of docs/spec/okf-agent-skills-v0.1.0-completion.md so the two documents agree on how many rows remain `Open`.

<details><summary>Proof of in-diff scope</summary>

```
git diff --unified=0 origin/main...feat/issue-41-spec-v0.1.0 -- docs/spec/okf-agent-skills-v0.1.0.md:
@@ -3537,31 +3637,31 @@
-| New-bundle write-gate bootstrap exception | `okf init`; migration into a repository with no bundle; ... |
+| New-bundle write-gate bootstrap exception | Open | `okf init`; migration into a repository with no bundle; ... |
-| Adapter package location and per-adapter install command | Installation and upgrade of the three native adapters | ... |
+| Adapter package location and per-adapter install command | Open | Installation and upgrade of the three native adapters | ... |
-| Coexistence with the existing third-party OKF skill | Release positioning and installation conflicts | ... |
+| Coexistence with the existing third-party OKF skill | Open | Release positioning and installation conflicts | ... |
-| Support-ceiling fixture corpora and strata | Any calibrated claim about the declared support ceiling | ... |
+| Support-ceiling fixture corpora and strata | Open | Any calibrated claim about the declared support ceiling | ... |

git blame feat/issue-41-spec-v0.1.0 -- docs/spec/okf-agent-skills-v0.1.0.md (relevant lines):
0919661e (artem 2026-08-03 17:38:52 +0000 3651) | New-bundle write-gate bootstrap exception | Open |
0919661e (artem 2026-08-03 17:38:52 +0000 3662) | Adapter package location and per-adapter install c
0919661e (artem 2026-08-03 17:38:52 +0000 3666) | Coexistence with the existing third-party OKF skil
0919661e (artem 2026-08-03 17:38:52 +0000 3667) | Support-ceiling fixture corpora and strata | Open

$ git merge-base --is-ancestor 0919661e origin/main -> NO (branch-only commit)
```

</details>

#### F071 — The completion spec's Problem Statement asserts, in the present tense, a repository state this branch has already changed

File: docs/spec/okf-agent-skills-v0.1.0-completion.md
Severity: major
anchor: - `skills/` does not exist. There are zero `SKILL.md` files. `npx skills add

- Line: 20 · Lens: docs · Kind: docs · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed on every checked point. Line 20 is the `skills/` bullet; `ls skills` returns okf, okf-lifecycle, okf-read, okf-review, okf-write. `grep -c "existsSync(skills)" test/issue-55.test.js` returns 0, so the "the one test that checks the shipped inventory passes because skills/ is missing" claim is false. `.github/`, `AGENTS.md` (794B), `CLAUDE.md` (26B), `CONTRIBUTING.md` (776B) and `SECURITY.md` (381B) all exist, and `wc -c README.md` is 10565, not "556 bytes of research-phase placeholder". All seven terms listed as absent (`Concept ID`, `operation store`, `atomic effect`, `WRITE_AUTHORITY`, `verified EOF`, `adapter generation`, `skill binding`) are now CONTEXT.md entries. The cited gap-list location "lines 3590-3603" is now line 3669 onward. The whole file is status `A`, so all of this text is introduced by this branch. Hard constraint 7 is not engaged: the finding does not touch the 220-test figure on line 25.

**Fix.** Give the Problem Statement the same dated framing the Further Notes section already uses: open it with one line such as "State at authoring time (2026-08-05), before #9 landed:" and put the bullets in the past tense. Replace the stale `lines 3590-3603` reference with the section heading ("Terms used normatively that `CONTEXT.md` does not define") so it cannot drift again, and drop or past-tense the `existsSync(skills)` sentence since that line no longer exists in test/issue-55.test.js.

<details><summary>Proof of in-diff scope</summary>

```
git diff --unified=0 origin/main...feat/issue-41-spec-v0.1.0 -- docs/spec/okf-agent-skills-v0.1.0-completion.md:
@@ -0,0 +1,NNN @@
+## Problem Statement
+
+A developer cannot use this project. The suite that gives the repository its name is not
+installable and not invocable.
+
+- `skills/` does not exist. There are zero `SKILL.md` files. `npx skills add
+  'artemVeduta/okf-agent-skills#v0.1.0'` — the single documented install command — has nothing to
+  install.
... (whole file added; status A, no blame run per method)
```

</details>

#### F072 — A new MUST fixes the shared module set at five modules; fifteen ship

File: docs/spec/okf-agent-skills-v0.1.0.md
Severity: major
anchor: - The `v0.1.0` shared module set MUST be exactly `protocol.js`, `runtime.js`, `admission.js`, `validation.js`, and `lifecycle.js`. `scripts/lib/guard.js` MUST NOT be part of this set. (#42, #43, #41)

- Line: 2763 · Lens: docs · Kind: docs · Verdict: `confirmed` · Disposition: `fix-as-proposed`
- Spans: docs/spec/okf-agent-skills-v0.1.0.md, docs/spec/okf-agent-skills-v0.1.0-completion.md

Confirmed. `ls scripts/lib/` returns fifteen modules: adapters.js, admission.js, delegation.js, lifecycle.js, manifest.js, navigation.js, orientation.js, presence.js, protocol.js, reach.js, routing.js, runtime.js, services.js, trust.js, validation.js — and no guard.js. `scripts/lib/runtime.js` lines 2-6 require validation, admission, routing, lifecycle and orientation, and routing.js line 8 requires navigation, so the extra modules are load-bearing internals reached from runtime. The word `exactly` therefore makes the shipped tree non-conformant with its own MUST, while the decision actually recorded at line 3661 (`**Adopted (#42):** protocol.js, runtime.js, admission.js, validation.js, lifecycle.js ... guard.js is **not** part of the v0.1.0 module set`) is about the fixed public interfaces and the exclusion of guard.js. Side note for the fixer: the completion spec's `14 modules under scripts/lib/` (line 24) is also off by one against the shipped fifteen.

**Fix.** Reword line 2763 to the decision actually taken, e.g. "The `v0.1.0` shared modules with a fixed public interface MUST be `protocol.js`, `runtime.js`, `admission.js`, `validation.js`, and `lifecycle.js`; other modules under `scripts/lib/` are internal to them. `scripts/lib/guard.js` MUST NOT ship. (#42, #43, #41)", and use matching wording in the line 3661 open-item row. While there, correct the completion spec's `14 modules under `scripts/lib/`` to 15.

<details><summary>Proof of in-diff scope</summary>

```
git diff --unified=0 origin/main...feat/issue-41-spec-v0.1.0 -- docs/spec/okf-agent-skills-v0.1.0.md:
@@ -2754 +2762,2 @@
+- The `v0.1.0` shared module set MUST be exactly `protocol.js`, `runtime.js`, `admission.js`, `validation.js`, and `lifecycle.js`. `scripts/lib/guard.js` MUST NOT be part of this set. (#42, #43, #41)

git blame -L 2763,2763 feat/issue-41-spec-v0.1.0 -- docs/spec/okf-agent-skills-v0.1.0.md:
0919661e (artem 2026-08-03 17:38:52 +0000 2763) - The `v0.1.0` shared module set MUST be exactly `protocol.js`, ...

$ git merge-base --is-ancestor 0919661e origin/main -> NO (branch-only commit)
```

</details>

#### F073 — The undefined-terms gap list is stale: this branch defines every term it lists

File: docs/spec/okf-agent-skills-v0.1.0.md
Severity: major
anchor: #### Terms used normatively that `CONTEXT.md` does not define

- Line: 3669 · Lens: docs · Kind: docs · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed. The heading is at line 3669 and its two MUST bullets follow at 3671-3672. Spot-checking eighteen of the listed terms against CONTEXT.md entry headings (`Operation store`, `Atomic effect`, `WRITE_AUTHORITY`, `Verified EOF`, `Adapter generation`, `Skill binding`, `Concept ID`, `Preview token`, `Policy hash`, `Residue classification`, `Canonical lock order`, `Logical cause`, `Native event ID`, `Parse finding`, `Coverage labels`, `Bootstrap exception`, `Transformed output`, `Invocation class`) finds all eighteen present as `**Term**:` entries; CONTEXT.md now carries 127 entries. `target-owner consent` is the deliberate exception, handled in the `Foreign-write authority` entry at CONTEXT.md lines 275-278 (`The specification's target-owner consent is the ...` / `_Avoid_: ... target-owner consent`). The branch edited this very list (it struck `invocation class` from the Operation and approval line), so the staleness is in-diff, not pre-existing drift. Hard constraint 3 does not refute the fix: the entries that still say "its exact schema is an open item" (CONTEXT.md lines 482, 531, 567, 629, 646, 652, 659, 667, 676, 711, 719, 837) are held open by the open-item table, not by this gap list, and the fix touches no CONTEXT.md content.

**Fix.** Replace the section with a discharge record: rename it to something like "Terms formerly undefined, now in `CONTEXT.md` (#9)", restate the two MUST bullets as the standing rule for *future* normative terms rather than a blocking gap, and note that `target-owner consent` is intentionally not a separate entry because `Foreign-write authority` names the same grant and lists it under `_Avoid_`. Leave the per-term "exact schema is an open item" wording in CONTEXT.md untouched.

<details><summary>Proof of in-diff scope</summary>

```
git diff --unified=0 origin/main...feat/issue-41-spec-v0.1.0 -- docs/spec/okf-agent-skills-v0.1.0.md:
@@ -3576 +3676 @@ Authority and consent: `WRITE_AUTHORITY`, `WRITE_NOT_AUTHORIZED`, authority gene
-Operation and approval: operation identity, operation class, atomic effect, invocation class, approval record, approval fingerprint, policy hash, required checks, manual-operation occurrence, bundle epoch, ledger generation, preview token, spent record, invocation attestation (#11, #29, #31, #37, #38).
+Operation and approval: operation identity, operation class, atomic effect, approval record, approval fingerprint, policy hash, required checks, manual-operation occurrence, bundle epoch, ledger generation, preview token, spent record, invocation attestation (#11, #29, #31, #37, #38).

git blame -L 3669,3669 feat/issue-41-spec-v0.1.0 -- docs/spec/okf-agent-skills-v0.1.0.md:
977f1061 (artem 2026-08-02 21:44:56 +0000 3669) #### Terms used normatively that `CONTEXT.md` does n

Heading line itself is pre-existing (977f1061 is an ancestor of origin/main), but the section body was edited in-diff at line 3676 and the CONTEXT.md definitions that invalidate it are all added by this branch, so the defect is introduced here.
```

</details>

#### F074 — The #91 narrowing paragraph is written three times in CONTEXT.md

File: CONTEXT.md
Severity: minor
anchor: shared rules. The placement choice is retained design for a later release, in

- Line: 207 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Locations confirmed: the narrowing appears in `Execution preference` (line 207), `Session override` (line 752) and `Effective settings` (line 760-762), each with a different tail clause, and the full sentence also lives in the completion spec's `## Narrowed claims (issue #91, issue #97)` section at line 407. The badly reflowed short line in `Execution preference` (`no `v0.1.0` adapter installs a`) is there as described. Hard constraint 6 does not refute this: the finding does not call the unreachable delegation path or the inert setting a bug, only the triplicated prose that describes them.

**Fix.** Keep the full narrowing sentence in exactly one place — `## Narrowed claims (issue #91, issue #97)` in docs/spec/okf-agent-skills-v0.1.0-completion.md already carries it — and reduce all three CONTEXT.md entries to a one-clause pointer of the form "Retained design for a later release; see the #91 narrowing in `docs/spec/okf-agent-skills-v0.1.0-completion.md`. (#91)". Rewrap the `Execution preference` paragraph while editing it.

#### F075 — CONTEXT.md restates the D8 table's implementation detail, creating a third copy of the count twelve

File: CONTEXT.md
Severity: minor
anchor: The twelve-check boundary `validation.postWrite` runs against the primary

- Line: 681 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed at CONTEXT.md line 681 onward: the entry states `twelve-check`, the `six defense-in-depth copies`, and `not a thirteenth check`. The same three facts are in docs/spec/okf-agent-skills-v0.1.0.md at line 3435 ("These twelve checks are the primary-concept boundary..."), 3442 ("Six of the twelve — Reserved bundle files, ...") and 3474-3476 ("The exception handler around this boundary is error containment, not a thirteenth check ... not counted in the twelve"), and a fourth statement of the thirteenth-check ruling sits in the completion spec's D8 section.

**Fix.** Trim the entry to what makes it a definition — the boundary `validation.postWrite` runs against the primary saved concept immediately after a bounded write publishes it, narrower than post-operation checks because it validates the primary concept only — and drop `twelve`, `six`, and the thirteenth-check ruling, leaving those in the D8 section that owns the table.

#### F076 — Two v0.1.0-reachable operation-map rows still name "guarded write checks" after the same pass removed it from their siblings

File: docs/spec/okf-agent-skills-v0.1.0.md
Severity: minor
anchor: | Add machine verification without complete qualifying evidence | `okf-write` | Verification-evidence validation and fail-closed guarded write checks | Not assigned | B | B | B | B | B | B |

- Line: 3376 · Lens: docs · Kind: docs · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed. Lines 3376 and 3386 still read `fail-closed guarded write checks` and both are all-`B` rows. The diff's de-guarding pass touched only the `N` rows (hunks at -3363,4/+3372,4 changing "Evidence, mode, ownership, semantic-preservation, and guarded write checks" to "... and semantic-preservation validation", plus -3372/+3381 and -3378/+3387 changing "Evidence validation and guarded write checks" and "Relationship validation and guarded write checks" to "... and write checks"), so the inconsistency is introduced by this branch even though lines 3376 and 3386 are themselves unmodified pre-existing text.

**Fix.** Drop `guarded` from both rows: "Verification-evidence validation and fail-closed write checks" (3376) and "OKF validation and fail-closed write checks" (3386). Then re-scan the column for the remaining `A`/`N`/`B` rows — `Standalone removal of verification` at line 3379 has `A` cells and is the next candidate — and leave `guarded` only where the row's outcome is `P` or `P+R`.

#### F077 — The D7 ownership table's Runtime module column is the constant `scripts/lib/runtime.js`

File: docs/spec/okf-agent-skills-v0.1.0.md
Severity: minor
anchor: | Read, validate, or read-only analysis | `okf-read` | `scripts/lib/runtime.js` | Not assigned |

- Line: 3404 · Lens: docs · Kind: docs · Verdict: `fixed-with-deviation` · Disposition: `fix-with-deviation` · Re-graded major → minor

The constant column is confirmed: the D7 table starts at line 3400 and all 28 data rows (3404-3431) read `scripts/lib/runtime.js`, so the column the row was opened for carries no information — D7 (completion spec line 103) promised a transcription "derived from ... the runtime's declared primary and derived effects". Two parts of the reviewer's reasoning do not hold, which is why I downgrade to minor. First, `runtime.js` is not factually wrong: every wrapper enters `scripts/lib/runtime.js`, which requires and delegates to validation, admission, routing (which requires navigation), lifecycle and orientation, so runtime.js genuinely is the single dispatch point for all 28 effects. Second, the claimed contradiction with the #42 closure at line 3664 is a category error: that row closes *incremental synchronization* (automatic narrow maintenance) with lifecycle.js, whereas the D7 row the reviewer compares it to is `Standalone broad rebuild`, a different effect. So the defect is an uninformative column, not a false one.

**Fix.** Take the reviewer's second option, which is the smaller and truthful edit: drop the `Runtime module` column and state once above the table that every atomic effect enters `scripts/lib/runtime.js`, which delegates internally (routing/navigation for reads, validation for write gates, lifecycle for sync and rebuild planning). If the column is kept because the closing decision's wording requires three columns, name the delegated implementation module per row instead of the entry point, and reword the D7 closure at line 3663 to say the module named is the delegated implementation.

<details><summary>Proof of in-diff scope</summary>

```
git diff --unified=0 origin/main...feat/issue-41-spec-v0.1.0 -- docs/spec/okf-agent-skills-v0.1.0.md:
@@ -3390,0 +3400,79 @@ The table uses `A` for allowed, `N` for notice, `P` for occurrence-bound preview
+#### Atomic-effect ownership (D7)

git blame -L 3400,3406 feat/issue-41-spec-v0.1.0 -- docs/spec/okf-agent-skills-v0.1.0.md:
f6fbf1cb (artem 2026-08-05 17:57:50 +0000 3400) #### Atomic-effect ownership (D7)
f6fbf1cb (artem 2026-08-05 17:57:50 +0000 3402) | Atomic effect | Owning skill | Runtime module | Invocation class |
f6fbf1cb (artem 2026-08-05 17:57:50 +0000 3404) | Read, validate, or read-only analysis | `okf-read` | `scripts/lib/runtime.js` | Not assigned |

$ git merge-base --is-ancestor f6fbf1cb origin/main -> NO (branch-only commit)
```

</details>

#### F078 — A paragraph that argues with an imagined objection instead of stating a decision

File: docs/spec/okf-agent-skills-v0.1.0-completion.md
Severity: nit
anchor: **Why the open items could be closed without a grilling session.** Four of the six were closed by

- Line: 475 · Lens: deslop · Kind: slop · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Target text is still at line 475 and is not load-bearing: it is prose defending the authoring process, states no rule or behavior, and nothing parses or cites it. The four reclassifications and the two authored tables are each recorded with their own justification in D5-D10 above (lines 68-155).

**Fix.** Delete the paragraph. If the process constraint matters, it is already covered by the open-item rules the same file cites ("a row that is closed later MUST be closed by naming the decision").

### tests-read-navigation

13 real, 0 refuted or already-correct.

#### F079 — issue-46 opens a second seam into scripts/lib/reach.js for behaviour the wrapper already proves

File: test/issue-46.test.js
Severity: major
anchor: test('reach recomputes realpath containment on every call within one process', () => {

- Line: 365 · Lens: thermo · Kind: quality · Verdict: `fixed-with-deviation` · Disposition: `fix-with-deviation`

Confirmed. AGENTS.md:8 states "One contract seam. Only a skill's wrapper script, run as a process, is" a tested boundary. issue-46.test.js:366 and :168 both `require(path.join(repo, 'scripts', 'lib', 'reach.js'))` and call reach.evaluate(entry, context, services) with hand-built service objects, pinning the internal signature and the exact shapes of {exists, realpath, isLink, gitRootOf} and {cwd, gitRoot, workspaceRoot}. The recompute claim is already proven end to end at lines 188-201, which retargets the same symlink and gets SYMLINK_ESCAPE through the wrapper. One caveat the finding misses: the lexical loop at 174-179 is the ONLY coverage anywhere of CWD_NOT_A_WORKSPACE (grep shows it only in issue-46:176 and scripts/lib/reach.js:67), so a blind deletion silently drops that refusal.

**Fix.** Delete the in-process halves (lines 168-186 and the whole test at 365 onward) and keep the subprocess coverage — the 'no stat' claim is already observable at line 166's assertion that no PRESENCE-gate finding appears for a lexically refused candidate. Before deleting, add one wrapper-level case for CWD_NOT_A_WORKSPACE: run okf-read admit with cwd set to a temp directory that has no .git and no workspace_root, and assert the REACH finding code, since nothing else covers that branch.

**Validator note.** Real and in-diff (status A). Original fix was unsound because it would delete the only CWD_NOT_A_WORKSPACE coverage; replaced with a fix that preserves it at the wrapper seam. Note for the implementer: test/issue-51-navigation.test.js:8-9 requires scripts/lib/runtime and scripts/lib/services in-process, the same class of second seam, but that is outside this finding.

<details><summary>Proof of in-diff scope</summary>

```
$ git diff --unified=0 origin/main...feat/issue-41-spec-v0.1.0 -- test/issue-46.test.js
(status A: whole file added; the diff contains +test('reach recomputes realpath containment on every call within one process', () => { and +  const reach = require(path.join(repo, 'scripts', 'lib', 'reach.js')); twice, at added-file lines 168 and 366)
```

</details>

#### F080 — The whole test/fixtures tree is added but never referenced by any file

File: test/fixtures/roots/undeclared-index.md
Severity: minor
anchor: # Undeclared

- Line: 3 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed` · Re-graded major → minor
- Spans: test/fixtures/base/concept.md, test/fixtures/base/derivative.md, test/fixtures/base/downstream.md, test/fixtures/base/independent.md, test/fixtures/base/index.md, test/fixtures/base/should.md, test/fixtures/base/upstream.md, test/fixtures/roots/future-index.md, test/fixtures/roots/number-index.md, test/fixtures/roots/old-index.md, test/fixtures/roots/undeclared-index.md, test/fixtures/obligations/generated-by.md, test/fixtures/obligations/human-prefix.md, test/fixtures/obligations/runtime.md, test/fixtures/obligations/source-resource.md

Confirmed. `grep -rn "test/fixtures\|fixtures/base\|fixtures/roots" . --exclude-dir=.git` returns only the fixture files themselves; the only "fixtures" hits in test/*.js, README.md and eval/lib/wrapper-contract.check.js are the word used in prose or test names, never a path. Every test in this chunk builds its tree in os.tmpdir() (issue-46 temporaryRoot, issue-47 temporaryRoot, issue-51-links rootFor, issue-51-navigation repository, issue-52 rootFor). concept.md does carry a hand-maintained third_party type matrix and the claim "The body must remain byte-identical" that nothing exercises. Scope correction: the same commit also adds test/fixtures/obligations/{generated-by,human-prefix,runtime,source-resource}.md — 15 dead files, not 11.

**Fix.** Delete the whole test/fixtures tree (base/, roots/, obligations/). No test, script, adapter, doc or eval file loads any of it. If the version-declaration cases and the source-link graph are coverage worth keeping, express them inline in the okf-read wrapper tests that already synthesise the same shapes.

**Validator note.** Real and wholly in-diff (status A). Re-graded major->minor: dead test fixtures carry no runtime or contract risk, only reader cost; the fix is a pure deletion.

<details><summary>Proof of in-diff scope</summary>

```
$ git diff --unified=0 origin/main...feat/issue-41-spec-v0.1.0 -- test/fixtures/roots/undeclared-index.md
test/fixtures/roots/undeclared-index.md | 3 +++
 1 file changed, 3 insertions(+)

Changes:

test/fixtures/roots/undeclared-index.md
  @@ -0,0 +1,3 @@
  +---
  +---
  +# Undeclared
  +3 -0
```

</details>

#### F081 — Top-level assertion that a stdlib function exists

File: test/issue-46.test.js
Severity: minor
anchor: assert.equal(typeof fs.symlinkSync, 'function', 'symlinkSync is required by issue 46 fixtures');

- Line: 10 · Lens: deslop · Kind: slop · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Text confirmed verbatim at line 10 and not load-bearing: it is a module-load assertion about node:fs that the whole suite already depends on, no other test file does anything comparable, and a platform without symlink support would fail at the first fs.symlinkSync call with a better error.

**Fix.** Delete line 10.

**Validator note.** Confirmed present and non-load-bearing; file is status A.

#### F082 — The temporary-repository harness is copy-pasted per test file and has already drifted

File: test/issue-46.test.js
Severity: minor
anchor: function repository(prefix) {

- Line: 21 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed` · Re-graded major → minor
- Spans: test/issue-46.test.js, test/issue-47.test.js, test/issue-51-links.test.js, test/issue-51-navigation.test.js, test/issue-52.test.js

Partly confirmed. The duplication is real: issue-46:12-34 and issue-47:12-25 declare temporaryRoot/activate/repository/bundle; issue-48:13-30, issue-50:28-36, issue-51-links:10-17 (rootFor), issue-51-navigation:11-18 (repository), issue-52:12-19 (rootFor), issue-53:13, issue-54:12 each re-create the same mkdtemp + .git + .okf-active + index.md shape. Two details in the finding are imprecise: 51-links, 51-navigation and 52 do not re-declare functions of these names, they inline the same shape under rootFor/repository; and the index.md drift is benign, not a bug — issue-46's bundle() writes '# Bundle\n' with no front matter and issue-46:221 still asserts state 'active', so both forms are legitimately active bundles. The maintenance cost stands: a change to what an active bundle looks like has to be found in nine files.

**Fix.** Extract one test/support/fixtures.js (outside the test/*.test.js glob, so no new test file and no new contract seam) exporting temporaryRoot(t), repository(t), bundle(root, relative) and runWrapper(wrapper, value, cwd), with one definition of the index.md front matter, and require it from these five files. Where a file deliberately needs a front-matter-free index.md, make that an explicit argument rather than a second copy.

**Validator note.** Real duplication, in-diff (all five files status A). Re-graded major->minor: the claimed drift is two valid bundle shapes rather than an inconsistency that can produce a wrong verdict, so the cost is reader/maintenance only.

<details><summary>Proof of in-diff scope</summary>

```
$ git diff --unified=0 origin/main...feat/issue-41-spec-v0.1.0 -- test/issue-46.test.js
(status A: whole file added; diff shows +function temporaryRoot(prefix = 'okf-46-') {, +function activate(root) {, +function repository(prefix) {, +function bundle(root, relative = '.') {, +function runWrapper(value, cwd) { at lines 12/17/21/28/36 of the added file)
```

</details>

#### F083 — runWrapper swallows a JSON parse failure

File: test/issue-46.test.js
Severity: minor
anchor:   try {

- Line: 43 · Lens: deslop · Kind: slop · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed: line 42 is `let response;`, 43 `try {`, 44 the parse, 45-46 `} catch { response = undefined; }`. Unparseable stdout is exactly the defect a wrapper test should shout about; here it is discarded and surfaces later as an opaque assert.ok(result.response) or a TypeError with the malformed stdout thrown away. issue-47:30, issue-51-links:39 and issue-52:46 all parse without a catch. The catch is decoration, not a constraint.

**Fix.** Parse unconditionally as the sibling files do — response: run.stdout ? JSON.parse(run.stdout) : undefined — letting a malformed envelope fail loudly with its content in the stack.

**Validator note.** Confirmed at the claimed location; file is status A.

#### F084 — notEqual on a value the previous line already pinned exactly

File: test/issue-46.test.js
Severity: minor
anchor:   assert.notEqual(finding.code, 'SYMLINK_UNRESOLVABLE');

- Line: 164 · Lens: deslop · Kind: slop · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed at line 164. Line 163 is `const finding = assertReach(result, 'OUTSIDE_WORKSPACE');`, and assertReach asserts findings[0].code === the given code, so line 164 can never fail. Not load-bearing.

**Fix.** Delete line 164.

**Validator note.** Confirmed at the claimed location; file is status A.

#### F085 — Assertions that restate assertions three lines above

File: test/issue-46.test.js
Severity: minor
anchor:     assert.notEqual(result.status, 70);

- Line: 214 · Lens: deslop · Kind: slop · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed but relocated: the loop begins at line 209, and the assertions are 210 assert.equal(result.status, 0), 211 assert.equal(result.stderr, ''), 212 assert.ok(result.stdout), 213 assert.ok(result.response), 214 assert.notEqual(result.status, 70). Line 214 cannot fail after 210, and 212 cannot fail once 213 holds, since response is only set when stdout parsed.

**Fix.** Keep lines 210, 211 and 213; delete lines 212 and 214.

**Validator note.** Real; the reviewer's line numbers were off by one (claimed 213/215, actual 212/214). File is status A.

#### F086 — issue-46 and issue-47 never remove their temporary trees

File: test/issue-47.test.js
Severity: minor
anchor: function temporaryRoot(prefix = 'okf-47-') { return fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), prefix))); }

- Line: 12 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`
- Spans: test/issue-46.test.js, test/issue-47.test.js

Confirmed. grep for rmSync shows t.after cleanup in issue-48:15, issue-50:32, issue-51-links:12, issue-51-navigation:16, issue-52:14, issue-53:15, issue-54:14 — and none in issue-46 or issue-47, whose temporaryRoot/repository do not even take t. issue-47's first test creates a fresh repository per loop iteration (eight cases at lines 43-50), issue-46 creates two or three per test, and issue-47:239 chmods a directory to 0o000 mid-test, so a run leaves dozens of okf-46-*/okf-47-* trees, some with symlink cycles, behind in tmpdir.

**Fix.** Give both files the issue-48 signature temporaryRoot(t, prefix) / repository(t, prefix) that registers t.after(() => fs.rmSync(root, { recursive: true, force: true })), and pass t from each test — best done once in the shared support helper.

**Validator note.** Confirmed at the claimed location; both files are status A so wholly in-diff.

#### F087 — issue-47 asserts nothing about exit status or stderr, so a wrapper crash reports as a null dereference

File: test/issue-47.test.js
Severity: minor
anchor:   return { ...run, response: run.stdout ? JSON.parse(run.stdout) : null };

- Line: 30 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed. `grep -n 'status\|stderr' test/issue-47.test.js` returns nothing outside the process.getuid guard — no test in the file checks either. Every assertion goes straight to result.response.data..., and runWrapper yields response: null on empty stdout, so a wrapper exiting 64 or 70 with a diagnostic on stderr fails as "Cannot read properties of null (reading 'data')" and discards the diagnostic, on the file covering workspace federation.

**Fix.** Assert the envelope inside runWrapper — assert.equal(run.status, 0) and assert.equal(run.stderr, '') before parsing, with negative cases opting out explicitly — or reuse the shared runWrapper that already does this.

**Validator note.** Confirmed at the claimed location; file is status A.

#### F088 — localRepo ignores its first parameter while every call site passes a meaningful root

File: test/issue-47.test.js
Severity: minor
anchor: function localRepo(root, name = 'app') { return { name, path: '.', local: true }; }

- Line: 36 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed. Line 36 never reads root and hard-codes path: '.'. Call sites pass localRepo('.') (line 45, twice), localRepo(root) (55, 75, 93, 142, 171, 181, 199, 207, 217), localRepo(child) (76, 95) and localRepo(parent) (85). At lines 75-76 the root and child manifests are given different arguments and emit an identical record, so a reader believes two manifests describe different repositories.

**Fix.** Drop the parameter: function localRepo(name = 'app') { return { name, path: '.', local: true }; } and update the call sites, or honour it as path: root if a non-'.' record is ever wanted.

**Validator note.** Confirmed at the claimed location; file is status A.

#### F089 — The 'independent of an earlier search' test never proves the search did anything

File: test/issue-51-links.test.js
Severity: minor
anchor:   assert.equal(search.operation, 'search');

- Line: 93 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed. Line 93 is the only assertion about the search run at lines 85-89; run() checks status and stderr but the echoed operation name is true even if the query matched nothing. If search stopped observing deprecated.md, beforeSearch and afterSearch would still be deep-equal and the independence claim would be vacuous.

**Fix.** Assert the search actually observed the deprecated concept — that its result is ok and its data reports deprecated.md as read or as excluded for deprecated_concept — before re-validating.

**Validator note.** Confirmed at the claimed location; file is status A.

#### F090 — issue-52's no-mutation hash is a weakened copy of issue-48's and cannot see symlink changes

File: test/issue-52.test.js
Severity: minor
anchor: function treeHash(root) {

- Line: 51 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed. issue-52:51-68 hashes name, then 'directory' or 'file' only; issue-48:103-131 has the same shape plus an entry.isSymbolicLink() branch that hashes fs.readlinkSync and an else 'other\0' fallback. For a symlink both isDirectory() and isFile() are false in issue-52's version, so only the entry name is folded in and a retargeted link inside the bundle leaves assert.equal(treeHash(root), before) passing.

**Fix.** Move issue-48's symlink-aware treeHash into the shared test support helper and require it from both files; delete the local copy here.

**Validator note.** Confirmed at the claimed location; file is status A.

#### F091 — Unused writeWrapper binding, and a manifest written twice in one test

File: test/issue-47.test.js
Severity: nit
anchor: const writeWrapper = path.join(repo, 'scripts', 'okf-write.js');

- Line: 10 · Lens: deslop · Kind: slop · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Both confirmed. `grep -n writeWrapper test/issue-47.test.js` returns only line 10 — the binding is never used. And line 226 is `const base = manifest([...]); writeManifest(root, base);` inside 'read scope widening retains the selected peer', immediately overwritten by line 227's writeManifest with the wider manifest, so neither the variable nor the first write affects any assertion.

**Fix.** Delete line 10, and on line 226 delete the base variable and its writeManifest call, keeping only the wider manifest write.

**Validator note.** Confirmed at both claimed locations; file is status A.

### tests-write-gate

12 real, 0 refuted or already-correct.

#### F092 — A test about wrapper count hard-codes the exhaustive scripts/lib file listing

File: test/issue-48.test.js
Severity: major
anchor:   assert.deepEqual(fs.readdirSync(path.join(scripts, 'lib')).sort(), [

- Line: 147 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed. Lines 147-150 pin the complete 15-entry contents of scripts/lib inside the test named 'ships exactly five process wrappers without skill or guard modules' (line 140). CONTRIBUTING.md:13 rules this out explicitly ('not the shape of a runtime module'). The stated intent is already covered later in the same file: the recursive walk at lines 373-382 collects any file named guard.js and asserts the list is empty (line 382), under the test at line 362.

**Fix.** Replace the exhaustive deepEqual at 147-150 with the negative property actually claimed: walk scripts/lib and assert no entry is named guard.js and no entry matches /^okf-/ (a per-skill module), leaving the rest of the directory unconstrained. Reuse the walker from lines 373-381 (hoisting it to a module-level helper) rather than keeping two.

<details><summary>Proof of in-diff scope</summary>

```
diff --git a/test/issue-48.test.js b/test/issue-48.test.js
new file mode 100644
index 0000000..cdbea61
--- /dev/null
+++ b/test/issue-48.test.js
```

</details>

#### F093 — The fixture harness is copy-pasted across all four new test files

File: test/issue-49.test.js
Severity: major
anchor: function treeHash(root) {

- Line: 52 · Lens: thermo · Kind: quality · Verdict: `fixed-with-deviation` · Disposition: `fix-with-deviation`
- Spans: test/issue-48.test.js, test/issue-49.test.js, test/issue-50.test.js, test/issue-52.test.js, test/issue-60.test.js, test/issue-64.test.js, test/issue-66.test.js, test/issue-51-policy.test.js

Duplication confirmed, but the claimed extent is wrong in both directions. Confirmed: responseKeys is byte-identical at issue-48.test.js:11 and issue-49.test.js:10; assertEnvelope at issue-48.test.js:83 and issue-49.test.js:77; treeHash at issue-48.test.js:103 and issue-49.test.js:52, and the two copies do differ in branch order exactly as claimed (48 tests isSymbolicLink first, 49 tests isDirectory first, so 49's link branch is unreachable on a symlink and the two 'same-named' immutability proofs are different assertions). REFUTED for write-gate.test.js: it defines only a file-specific bundle(t, index) and run(value) (100 lines total), no responseKeys, no assertEnvelope, no treeHash, no runWrapper — it shares nothing to extract. UNDERSTATED elsewhere: responseKeys is duplicated in SEVEN test files (48, 49, 52, 60, 64, 66, 72) and treeHash in FOUR (48, 49, 51-policy, 52), all of which are status A on this branch, so a four-file fix leaves three-plus live copies of the envelope key list behind. Also note the local signatures have already diverged (writeConcept(root) in 48 vs writeConcept(bundleRoot, relative, frontmatter, body) in 50; runWrapper(skill, value) vs runWrapper(value); bundle(root, relative) vs bundle(t, index)), so the extraction is a reconciliation, not a lift-and-shift.

**Fix.** Do not add test/helpers.js — the repo already has the shared-test-module location: test-support/, required as require('../test-support/snapshot') by test/issue-105.test.js:16 and test/issue-66-adapters.test.js:7. Add test-support/harness.js there exporting responseKeys, assertEnvelope and ONE treeHash (keep issue-48's branch order: isSymbolicLink before isDirectory, since lstat-typed symlinks must be classified as links first), plus temporaryRoot/repository/bundle. Convert every file that duplicates them — issue-48, issue-49, issue-50, issue-52, issue-60, issue-64, issue-66, issue-51-policy — not just this chunk's four, or the drift the finding warns about survives the fix. Leave write-gate.test.js alone; its fixtures are genuinely local. Keep per-file request builders local.

**Validator note.** Defect real. Two corrections: write-gate.test.js is not a participant (nothing shared), and the duplication reaches four more files outside the chunk, so the proposed 4-file scope is incomplete. Proposed new module location refuted: test-support/ already exists for exactly this.

<details><summary>Proof of in-diff scope</summary>

```
diff --git a/test/issue-49.test.js b/test/issue-49.test.js
new file mode 100644
index 0000000..6ee82e6
--- /dev/null
+++ b/test/issue-49.test.js
```

</details>

#### F094 — Retired vocabulary words are split and re-joined to dodge a self-match that cannot happen

File: test/issue-50.test.js
Severity: major
anchor:   ['in', 'sufficient'].join(''),

- Line: 20 · Lens: deslop · Kind: slop · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed and not load-bearing. retiredLabels at lines 19-26 splits each word into two fragments joined back together. sourceFiles() (lines 160-172) calls visit(scripts) only, so the scan at lines 178-183 never reads test/, and no other test file walks the test directory for these words. The same words appear as plain literals in docs/spec/okf-agent-skills-v0.1.0.md:1721 and :1799, so nothing is being hidden from any scanner either.

**Fix.** Write the six words as plain string literals: const retiredLabels = ['insufficient', 'CLIPPED', 'MISS', 'UNDISCOVERED', 'UNSEARCHED', 'FILTERED'];

<details><summary>Proof of in-diff scope</summary>

```
diff --git a/test/issue-50.test.js b/test/issue-50.test.js
new file mode 100644
index 0000000..2a12b86
--- /dev/null
+++ b/test/issue-50.test.js
```

</details>

#### F095 — Shape-tolerant accessors let the navigation envelope be renamed without failing a test

File: test/issue-50.test.js
Severity: major
anchor: function provenanceOf(record) {

- Line: 154 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed against the shipped runtime. scripts/lib/navigation.js:117 sets record.provenance and nothing in navigation.js ever assigns record.sources, so the `Object.hasOwn(record, 'sources')` branch at line 156 is unreachable. navigationData (scripts/lib/navigation.js:18) always emits found and read as separate arrays, and every read record is pushed into readRecords, so readRecord's fallback loop over ['read', 'found'] at line 135 can only ever hit 'read' for a genuine read record. Both are dead tolerance that would keep this file green through a provenance→sources rename.

**Fix.** Drop the alternatives: read record.provenance directly and search records(response, 'read') only. One caveat when applying: line 509's assert.equal(readRecord(response, 'missing-near.md'), undefined) currently also asserts absence from `found` via the fallback; that intent is already covered by the following JSON.stringify(...).includes(...) === false assertion, so narrowing readRecord loses nothing there. Where a test genuinely means 'either listed or read', assert both fields explicitly at the call site.

<details><summary>Proof of in-diff scope</summary>

```
diff --git a/test/issue-50.test.js b/test/issue-50.test.js
new file mode 100644
index 0000000..2a12b86
--- /dev/null
+++ b/test/issue-50.test.js
```

</details>

#### F096 — writeManifest is defined and never called

File: test/issue-48.test.js
Severity: minor
anchor: function writeManifest(root) {

- Line: 131 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed dead. A repo-wide grep for writeManifest returns test/issue-47.test.js (its own definition at :37 plus ~20 call sites) and test/issue-48.test.js:131 — the definition only, with no call anywhere in issue-48.test.js.

**Fix.** Delete writeManifest (lines 131-138). The manifest fixture already exists in test/issue-47.test.js:37 if a manifest-routed case is ever wanted here.

<details><summary>Proof of in-diff scope</summary>

```
diff --git a/test/issue-48.test.js b/test/issue-48.test.js
new file mode 100644
index 0000000..cdbea61
--- /dev/null
+++ b/test/issue-48.test.js
```

</details>

#### F097 — The ACTIVATION_MARKER_INVALID expectation literal is duplicated inside one file

File: test/issue-48.test.js
Severity: minor
anchor:   const expectedData = {

- Line: 289 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed. The eight-field expectedData literal appears at line 289 and again at line 453; the matching five-field finding literal appears at line 299 (as expectedFinding, wrapped in an array) and at line 444 (as `expected`, unwrapped, then used as [expected] at line 478). Same content, two names, two copies.

**Fix.** Hoist both literals to module-level constants (ACTIVATION_BLOCKED_DATA, ACTIVATION_INVALID_FINDING) declared once near responseKeys, and reference them from both tests; normalise the array wrapping at the assertion site.

<details><summary>Proof of in-diff scope</summary>

```
diff --git a/test/issue-48.test.js b/test/issue-48.test.js
new file mode 100644
index 0000000..cdbea61
--- /dev/null
+++ b/test/issue-48.test.js
```

</details>

#### F098 — assertReport accepts a report of either string or array shape

File: test/issue-49.test.js
Severity: minor
anchor:   assert.ok(typeof rawReport === 'string' || Array.isArray(rawReport));

- Line: 103 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed. scripts/lib/validation.js:832 builds report as an array literal ([`OKF v0.2 bundle-conformant: ...`], pushed to at 834), so the string branch at line 103 and the ternary at line 104 cover a shape the runtime never emits.

**Fix.** Assert the shipped shape once — assert.ok(Array.isArray(rawReport)) — then join unconditionally and delete the ternary.

<details><summary>Proof of in-diff scope</summary>

```
diff --git a/test/issue-49.test.js b/test/issue-49.test.js
new file mode 100644
index 0000000..6ee82e6
--- /dev/null
+++ b/test/issue-49.test.js
```

</details>

#### F099 — Banned-word check uses an asymmetric lookbehind/lookahead over a string-replaced copy of stdout

File: test/issue-49.test.js
Severity: minor
anchor:   assert.doesNotMatch(withoutConformanceLine, /(?<![-\w])(?:conformant|compliant|succeeded)(?!\w)/i);

- Line: 109 · Lens: deslop · Kind: slop · Verdict: `fixed-with-deviation` · Disposition: `fix-with-deviation`

Confirmed present at line 109 over the replaced copy built at line 108. The assertion itself is load-bearing (it enforces 'no conformance claim outside the sanctioned line'), but the guard shape is not: 'conformant' occurs exactly once in scripts/ (scripts/lib/validation.js:832, the report line, which line 108 strips whole), and 'compliant'/'succeeded' occur nowhere, so neither the lookbehind nor the lookahead does any work today — they only make a reader hunt for the case they protect.

**Fix.** Keep result.stdout as the scan target — narrowing to data.report as originally proposed would stop covering the rest of the envelope, which is the point of the check. Replace the asymmetric guards with a plain \b regex over withoutConformanceLine and add a one-line comment naming the forbidden claim, e.g. // no conformance/compliance claim may appear outside the single 'OKF v0.2 bundle-conformant' line ; assert.doesNotMatch(withoutConformanceLine, /\b(?:conformant|compliant|succeeded)\b/i);

**Validator note.** Slop shape confirmed; the assertion is load-bearing so it must stay, and the original fix's move from stdout to report would weaken it. Fix replaced with a \b regex over the same stdout target.

<details><summary>Proof of in-diff scope</summary>

```
diff --git a/test/issue-49.test.js b/test/issue-49.test.js
new file mode 100644
index 0000000..6ee82e6
--- /dev/null
+++ b/test/issue-49.test.js
```

</details>

#### F100 — The scope-guard test injects a failure keyed to the internal call order of isFile and realpath

File: test/issue-50.test.js
Severity: minor
anchor:       if (path.resolve(file) === path.resolve(rootBundle)) navigationPhase = true;

- Line: 527 · Lens: thermo · Kind: quality · Verdict: `fixed-with-deviation` · Disposition: `fix-with-deviation` · Re-graded major → minor

The call-order coupling is real and is exactly what CONTRIBUTING.md:13 excludes ('not ... an internal call order'): the isFile stub at line 526-527 flips navigationPhase when it happens to be called with the bundle root, and the realpath stub at 522 only throws afterwards, so the injection fires only if the runtime probes isFile(bundleRoot) before navigation re-resolves realpath(bundleRoot). But the claimed failure mode is REFUTED: if a reordering stops the injection from firing, the scope guard passes, navigation proceeds, and the stubbed search() at line 529 runs, setting searched = true — so assert.equal(searched, false) at line 535 fails loudly. There is no silent vacuous pass. The defect is brittleness with a confusing failure message, not lost coverage, hence the downgrade to minor.

**Fix.** The proposed fix is unsound: realpath cannot be made to throw unconditionally for the bundle root, because admission resolves the same path first — scripts/lib/presence.js:29 calls services.realpath(bundlePath) and scripts/lib/reach.js:87 realpaths the boundary — so an unconditional throw aborts admission and the response is no longer the navigation 'unavailable'/'unobservable' pair this test is about. The phase flag exists because the two call sites take the identical argument (presence.js:29 and navigation.js:89 both resolve the bundle root), so argument-keying cannot separate them. Instead make the coupling self-checking: add `let injected = false;` set inside the realpath throw branch, and assert `assert.equal(injected, true)` before the other assertions, so a reordering fails with the injection never firing rather than with an unexplained searched/result mismatch. Add a one-line comment above the isFile stub naming the assumption (navigation re-resolves the bundle root after the admission isFile probe).

**Validator note.** Coupling to internal call order is real (CONTRIBUTING.md:13), but the 'vacuous pass' scenario is refuted — the stubbed search() would run and fail the searched===false assertion. Proposed unconditional-throw fix breaks admission (presence.js:29 realpaths the same path); replaced with an injection-fired assertion.

<details><summary>Proof of in-diff scope</summary>

```
diff --git a/test/issue-50.test.js b/test/issue-50.test.js
new file mode 100644
index 0000000..2a12b86
--- /dev/null
+++ b/test/issue-50.test.js
```

</details>

#### F101 — refusingRoot asserts the code it just searched by, and crashes instead of failing when the finding is absent

File: test/write-gate.test.js
Severity: minor
anchor:   assert.equal(finding.code, 'ROOT_DECLARATION_NOT_EXACT');

- Line: 48 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed at all three sites. Line 46 selects with find(item => item.code === 'ROOT_DECLARATION_NOT_EXACT'), so line 48's assert.equal(finding.code, ...) cannot fail; line 49 then dereferences finding.origin with no assert.ok(finding) in between. Same missing guard at line 77 → 79 (finding.detail.construct) and line 96 → 98 (finding.detail). When the gate does not fire the failure is a TypeError, not a named missing refusal.

**Fix.** Replace line 48 with assert.ok(finding, 'ROOT_DECLARATION_NOT_EXACT') immediately after the find, and add the same guard before dereferencing finding at lines 79 and 98.

<details><summary>Proof of in-diff scope</summary>

```
diff --git a/test/write-gate.test.js b/test/write-gate.test.js
new file mode 100644
index 0000000..d33eddb
--- /dev/null
+++ b/test/write-gate.test.js
```

</details>

#### F102 — assertFinding re-asserts the code it searched by and the label set membership twice

File: test/issue-50.test.js
Severity: nit
anchor:   assert.equal(item.code, code);

- Line: 149 · Lens: deslop · Kind: slop · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed. finding() at line 146-148 selects by item.code === code, so line 149 cannot fail. Line 150's findingLabels.has check duplicates assertNavigation line 110, which runs it over every finding. One caveat on the label check: assertFinding is called 10 times in the file and the duplicate is only redundant where assertNavigation has already run on the same response — spot-check each call site while applying; every one I read (e.g. lines 506-507) does call assertNavigation first.

**Fix.** Reduce assertFinding to the find plus assert.ok(item, code) and return item; drop lines 149-150.

<details><summary>Proof of in-diff scope</summary>

```
diff --git a/test/issue-50.test.js b/test/issue-50.test.js
new file mode 100644
index 0000000..2a12b86
--- /dev/null
+++ b/test/issue-50.test.js
```

</details>

#### F103 — notDeepEqual against [] is subsumed by the assert.equal(..., undefined) on the next line

File: test/issue-50.test.js
Severity: nit
anchor:   assert.notDeepEqual(provenanceOf(emptyRead), []);

- Line: 212 · Lens: deslop · Kind: slop · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed. Line 212 is assert.notDeepEqual(provenanceOf(emptyRead), []) and line 213 is assert.equal(provenanceOf(emptyRead), undefined); the second implies the first. Not load-bearing.

**Fix.** Delete line 212 and keep only assert.equal(provenanceOf(emptyRead), undefined).

<details><summary>Proof of in-diff scope</summary>

```
diff --git a/test/issue-50.test.js b/test/issue-50.test.js
new file mode 100644
index 0000000..2a12b86
--- /dev/null
+++ b/test/issue-50.test.js
```

</details>

### tests-lifecycle-review-router

7 real, 1 refuted or already-correct.

#### F104 — issue-53's run() returns exit status and stderr that no test ever asserts, and nulls the response on a crash

File: test/issue-53.test.js
Severity: major
anchor:   return { status: result.status, stderr: result.stderr, response: result.stdout ? JSON.parse(result.stdout) : null };

- Line: 50 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Verified: line 50 is the only occurrence of `status` or `stderr` in test/issue-53.test.js (grep shows the string on line 50 and nowhere else); all other 20+ call sites read `.response` only. Siblings do assert the process contract inside the helper: issue-51-policy.test.js:38-39 (`assert.equal(processResult.status, 0); assert.equal(processResult.stderr, '')`), issue-60.test.js:57-58, issue-62.test.js:45-46. issue-54.test.js:47 returns the same two fields and asserts them in the first test only (lines 58-60). Confirmed too that a blocked response still exits 0 (issue-62 asserts status 0 on its blocked payload loop), so asserting status 0 in the helper is safe. The `result.stdout ? ... : null` branch does discard stderr at the moment a wrapper crash makes it the only diagnostic.

**Fix.** Assert the contract inside the helper as the sibling files do: `assert.equal(result.status, 0, result.stderr); assert.equal(result.stderr, '');` then `return JSON.parse(result.stdout);` and drop the `status`/`stderr` fields and the `.response` unwrapping at the call sites. Apply the same collapse to issue-54.test.js's run(), which asserts them in one test only.

<details><summary>Proof of in-diff scope</summary>

```
$ git -C /Users/artem.veduta/proj/okf-agent-skills diff --unified=0 origin/main...feat/issue-41-spec-v0.1.0 -- test/issue-53.test.js
diff --git a/test/issue-53.test.js b/test/issue-53.test.js
new file mode 100644
index 0000000..a1eb05c
--- /dev/null
+++ b/test/issue-53.test.js
@@ -0,0 +1,214 @@
...
56:+  return { status: result.status, stderr: result.stderr, response: result.stdout ? JSON.parse(result.stdout) : null };
(status A: whole file added by this branch; diff line 56 = file line 50)
```

</details>

#### F105 — digest() hashes only top-level files, so the "nothing was written" proof in four tests has a hole

File: test/issue-53.test.js
Severity: major
anchor: function digest(root) {

- Line: 53 · Lens: thermo · Kind: quality · Verdict: `fixed-with-deviation` · Disposition: `fix-with-deviation`

Verified at test/issue-53.test.js:53-59: a single `fs.readdirSync(root).sort()` pass hashing only `statSync(file).isFile()` entries — anything written into a subdirectory is invisible. It is the safety assertion at lines 122, 132/135 (refused composite, AUTOMATIC_MUTATION_BLOCKED). Line 212's guard/ledger/manifest scan is likewise a single top-level `readdirSync`. The recursive equivalent exists three times in the chunk: `treeHash` (issue-51-policy.test.js:44) and `snapshot` (issue-60.test.js:34, issue-62.test.js:22, the latter two identical).

**Fix.** Make `digest` recursive in place — mirror the existing `snapshot` body from issue-60.test.js:34-46 (an inner `visit(directory, relative)` over `readdirSync(dir, { withFileTypes: true })`, recursing on directories) — and make the line 212 guard/ledger/manifest scan recursive over the same walk. Do NOT extract a shared test/lib/tree-hash.js: that touches four otherwise-passing files and adds a shared module for a nine-line stdlib walk, for zero behavioural gain.

<details><summary>Proof of in-diff scope</summary>

```
$ git -C /Users/artem.veduta/proj/okf-agent-skills diff --unified=0 origin/main...feat/issue-41-spec-v0.1.0 -- test/issue-53.test.js
diff --git a/test/issue-53.test.js b/test/issue-53.test.js
new file mode 100644
index 0000000..a1eb05c
--- /dev/null
+++ b/test/issue-53.test.js
@@ -0,0 +1,214 @@
...
59:+function digest(root) {
(status A: whole file added by this branch; diff line 59 = file line 53)
```

</details>

#### F106 — The forbidden-mechanic scan bans the bare English word "follow" across every shipped script, comments included

File: test/issue-51-policy.test.js
Severity: minor
anchor:   const forbidden = ['superseded_by', 'deprecation_reason', 'retain_until', 'concept_alias', 'conceptAlias', 'follow'];

- Line: 123 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed. `hasForbiddenMechanic` (lines 76-79) builds `(^|[^A-Za-z0-9_])follow(?=$|[A-Z_]|[^A-Za-z0-9_])` and is applied to the raw source of every .js under scripts/ (sourceFiles, lines 68-74), so prose in comments is in scope: `// follow the reference-definition` or an identifier `followLinks` would fail the release gate. `grep -rn follow scripts` currently returns nothing, so the entry is inert today and purely a future tripwire on vocabulary rather than mechanism, unlike its five schema-key siblings.

**Fix.** Ban the mechanism, not the word: replace `'follow'` with the concrete identifiers the policy actually forbids (e.g. `'followSuccessor'`, `'follow_successor'`) so the assertion states what it means and cannot fire on prose.

#### F107 — Table-driven loops declare a case label and then destructure it away, so failures cannot be attributed

File: test/issue-53.test.js
Severity: minor
anchor:   for (const [, change, result, code] of cases) {

- Line: 82 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`
- Spans: test/issue-53.test.js, test/issue-54.test.js, test/issue-55.test.js, test/issue-62.test.js

Confirmed at issue-53.test.js:82 and :184 (three rows of the :184 table all expect PROJECT_MODE_INVALID and differ only in the dropped label), issue-54.test.js:139, issue-55.test.js:210 and :225. issue-62's payload loop starts at line 302 (reviewer cited 311, the bare `assert.equal(response.result, 'blocked')` inside it) and iterates twelve payloads with no discriminating message. The convention exists in the chunk: issue-54 passes `String(taskKind)`, issue-62 passes `operation` (line 300).

**Fix.** Bind the label (`for (const [label, change, result, code] of cases)`) and pass it as the third argument of each assertion in the loop body; in issue-62's payload loop (starts line 302) pass `JSON.stringify(payload)`.

#### F108 — Inbound-link tests assert hand-counted absolute byte offsets into the fixture

File: test/issue-62.test.js
Severity: minor
anchor:     { carrier: 'frontmatter.resource', reference: 'target.md', byte_offset: 25 },

- Line: 179 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed: offsets 25, 90, 134, 188 asserted at issue-62.test.js:179-182 against a multi-line template-literal fixture written a few lines above, and offset 32 at line 68. The fixture is authored by the test itself, so any edit to earlier bytes shifts every later expectation with a failure message (`expected 90 to equal 91`) that names nothing.

**Fix.** Keep the fixture text in a `const source = ...` and derive the expectation from it, e.g. collect the occurrence positions with `[...source.matchAll(/target\.md/g)].map((m) => m.index)` and index into that list, so the fixture stays the single source of truth for the offsets.

#### F109 — Second test wraps its index fixture in a Buffer and compares Buffers, unlike the first test's string

File: test/issue-51-policy.test.js
Severity: nit
anchor:   const index = Buffer.from('---\nokf_version: "0.2"\n---\n# Bundle\n\n- [Old note](old.md)\n');

- Line: 102 · Lens: deslop · Kind: slop · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed and not load-bearing: line 83 builds the same shape of fixture as a plain string and compares with `fs.readFileSync(..., 'utf8')` at line 96; line 102 wraps it in `Buffer.from`, forcing `assert.deepEqual(fs.readFileSync(path.join(root, 'index.md')), index)` at line 118 to drop the encoding. Both files are ASCII Markdown written by the test itself, so byte-level comparison buys nothing and costs a hex-dump failure message plus two conventions in one file.

**Fix.** Drop `Buffer.from` and keep the string, then assert `fs.readFileSync(path.join(root, 'index.md'), 'utf8')` equals it, matching the first test.

#### F111 — Two statements crammed onto one line, inconsistent with the rest of the file

File: test/issue-53.test.js
Severity: nit
anchor:   const noScope = request(root, 'okf-lifecycle', 'sync'); delete noScope.scope;

- Line: 168 · Lens: deslop · Kind: slop · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed at lines 168 and 193 (`const invalidScope = request(root, 'okf-write', 'revise'); invalidScope.scope = { concepts: ['other.md'] };`). The same declare-then-mutate construction is written on two lines at 105-107, 122-123 and 130-131 of the same file. Not load-bearing — pure formatting.

**Fix.** Split both lines into two statements, matching the surrounding style.

### tests-adapters-delegation

9 real, 0 refuted or already-correct.

#### F112 — Status-vocabulary test degrades silently: a regex source scan guarded only by size > 0

File: test/issue-68-agents.test.js
Severity: major
anchor:   assert.ok(statuses.size > 0);

- Line: 144 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed at the claimed line. statusVocabulary() (lines 60-69) joins the source text of scripts/lib/delegation.js and scripts/okf-delegate.js and collects matches of /'(clean|failed|partially-applied|indeterminate|blocked: [a-z-]+)'/g into a Set; the test's only assertion about the set itself is `assert.ok(statuses.size > 0)`. I reproduced the scan: it currently yields exactly the 11 statuses the reviewer names (clean, failed, partially-applied, indeterminate, and 7 `blocked: <reason>` values). Any status literal that stops being a bare single-quoted literal in those two files -- hoisted to a constant, built by template, renamed -- drops out of the set, the loop asserts less, and the test stays green. The assertion strength is a function of source formatting, not of behavior.

**Fix.** Pin the closed vocabulary as an explicit literal in the test and assert both directions: `const expected = ['clean','failed','partially-applied','indeterminate','blocked: incomplete-brief','blocked: conflicting-rules','blocked: repository-instance-mismatch','blocked: target-conflict','blocked: stale-handoff','blocked: missing-skill','blocked: incompatible-skill'];` then `assert.deepEqual([...statusVocabulary()].sort(), expected.slice().sort())` before looping the agent-text checks. Adding or renaming a status in either script then fails loudly instead of quietly shrinking coverage.

**Validator note.** Confirmed verbatim. Fix does not add a new seam or a unit test against an internal module (constraint 2): it pins an expected literal inside an existing source-text scan.

<details><summary>Proof of in-diff scope</summary>

```
git diff --unified=0 origin/main...feat/issue-41-spec-v0.1.0 -- test/issue-68-agents.test.js => 'test/issue-68-agents.test.js | 157 +++++++++++++++' (status A, whole file added; no blame run per instructions). The 11-status scan was reproduced with node against the current worktree.
```

</details>

#### F113 — The wrapper response contract is re-implemented in three helpers in this chunk

File: test/issue-64.test.js
Severity: minor
anchor: function run(wrapper, request) {

- Line: 31 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`
- Spans: test/issue-64.test.js, test/issue-66.test.js, test/issue-72.test.js

Confirmed with one correction. issue-64.test.js run() (line 31) and issue-66.test.js runOk() (line 57) are near-identical: status 0, empty stderr, key order equals responseKeys, stdout byte-identical to JSON.stringify(response)+'\n'. issue-72.test.js response() (line 51) restates the trailing-newline, single-line and key-order parts but NOT the exit-status/stderr checks, because its run() returns only .stdout -- so the duplication there is partial, not identical. responseKeys is a verbatim nine-name literal in seven test files on the branch (issue-48, 49, 52, 60, 64, 66, 72). A change to the response envelope means editing the same array and the same assertions in several places, and a file that omits one assertion is indistinguishable from one that keeps it.

**Fix.** Add test-support/wrapper.js next to test-support/snapshot.js exporting responseKeys plus `runWrapper(wrapper, request)` (spawn, assert status/stderr/single-line/key-order/canonical bytes, return the parsed response) and a `runSilent` variant, and have these three files call it instead of defining local copies. Note issue-72's response() currently checks less than the other two, so routing it through runWrapper strengthens it -- keep the stdout-only shape only if a test genuinely needs the raw bytes.

**Validator note.** Detail corrected: issue-72's response() does not assert exit status or stderr. A shared test helper is test infrastructure, not a new product seam, so constraint 2 does not refute this.

<details><summary>Proof of in-diff scope</summary>

```
git diff --unified=0 origin/main...feat/issue-41-spec-v0.1.0 -- test/issue-64.test.js test/issue-66.test.js test/issue-72.test.js => all three are whole-file additions in the diff stat (234 / 277 / 85 insertions), status A.
```

</details>

#### F114 — The same three-key projection is written out verbatim five times

File: test/issue-66.test.js
Severity: minor
anchor:   assert.deepEqual({ bundle: response.data.bundle, root_index_path: response.data.root_index_path, workspace_health: response.data.workspace_health }, emptyBundleFields);

- Line: 84 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed: emptyBundleFields is defined at line 13 and the identical three-key projection appears at lines 84, 102, 158, 172 and 198, the last against `failedClaim` rather than `response`. assertDataKeys() at line 15 is the existing pattern this should follow.

**Fix.** Replace emptyBundleFields with one helper beside assertDataKeys: `function assertNoBundle(response) { assert.deepEqual({ bundle: response.data.bundle, root_index_path: response.data.root_index_path, workspace_health: response.data.workspace_health }, { bundle: null, root_index_path: null, workspace_health: null }); }` and call `assertNoBundle(response)` / `assertNoBundle(failedClaim)` at the five sites.

<details><summary>Proof of in-diff scope</summary>

```
git diff --unified=0 origin/main...feat/issue-41-spec-v0.1.0 -- test/issue-66.test.js => 'test/issue-66.test.js | 277 ++++++++++++++++++++++++++' (status A).
```

</details>

#### F115 — Required-brief-field list is regex-scraped here while the same list sits as a literal in issue-68.test.js

File: test/issue-68-agents.test.js
Severity: minor
anchor:   const match = source.match(/requiredFields\s*=\s*\[([\s\S]*?)\]/);

- Line: 52 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`
- Spans: test/issue-68-agents.test.js, test/issue-68.test.js

Confirmed on both sides. Here the list is recovered by matching delegation.js source text; test/issue-68.test.js:72-76 enumerates the same twelve fields as an explicit literal array in the per-field deletion loop, and delegation.js's requiredFields is that same flat array of single-quoted literals. Two mechanisms for one fact, neither authoritative: a thirteenth field in delegation.js leaves the deletion loop testing twelve while this file quietly starts demanding the thirteenth in the agent docs.

**Fix.** Drop requiredBriefFields() and its comment, and share one literal list between the two tests: put `const REQUIRED_BRIEF_FIELDS = ['role', 'task_kind', ...]` in test-support (alongside snapshot.js) and require it from both files, so a field added to delegation.js fails one place -- the deletion loop in issue-68.test.js -- and the agent-doc check follows from the same array.

**Validator note.** test-support/ already exists (test-support/snapshot.js), so the shared literal is not a new module convention and does not create a product seam (constraint 2 unaffected).

<details><summary>Proof of in-diff scope</summary>

```
git diff --unified=0 origin/main...feat/issue-41-spec-v0.1.0 -- test/issue-68-agents.test.js test/issue-68.test.js => both listed as whole-file additions ('test/issue-68-agents.test.js | 157 +++', 'test/issue-68.test.js | 344 +++'), status A.
```

</details>

#### F116 — Empty catch swallows a parse failure that every caller then dereferences

File: test/issue-68.test.js
Severity: minor
anchor:   try { response = JSON.parse(result.stdout); } catch {}

- Line: 63 · Lens: deslop · Kind: slop · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed at line 63, inside run() (line 58). I checked every use of the returned `response`: lines 241-244, 251, 264-267, 284-288, 295, 312-313 and 323-325 all dereference it immediately (.receipt/.status/.role/.findings/.data), and line 340 discards the result entirely. No call site asserts response === null or otherwise depends on the guard, so the catch is not load-bearing -- it converts a clear "wrapper printed unparseable stdout" diagnostic into a downstream `Cannot read properties of null` at an unrelated line and hides the emitted bytes. Sibling helpers in this chunk (issue-64.test.js run(), issue-66.test.js runOk()) parse without a guard.

**Fix.** Parse directly -- `const response = JSON.parse(result.stdout);` -- and drop the `let`/try/catch. A wrapper that prints non-JSON should fail the test at the parse with the stdout in the message.

<details><summary>Proof of in-diff scope</summary>

```
git diff --unified=0 origin/main...feat/issue-41-spec-v0.1.0 -- test/issue-68.test.js => 'test/issue-68.test.js | 344 ++++++++++++++++++++++++++++++++' (status A).
```

</details>

#### F117 — Comment states a premise the branch contradicts: SKILL.md files do ship

File: test/issue-72.test.js
Severity: minor
anchor:   // The repository has no shipped SKILL.md yet, so this fixture copies only

- Line: 28 · Lens: deslop · Kind: slop · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed at lines 28-29, and the premise is false on this branch: `ls skills/*/SKILL.md` returns skills/okf/SKILL.md, skills/okf-lifecycle/SKILL.md, skills/okf-read/SKILL.md, skills/okf-review/SKILL.md and skills/okf-write/SKILL.md. The comment is not load-bearing as written -- it does not explain a real constraint, it asserts a non-fact, so a reader concludes the fixture is complete when it in fact omits a file that exists (the fixture builds skills/okf-read/ with only a scripts symlink, line 31-32).

**Fix.** Replace the two comment lines with what is actually true and load-bearing: the fixture exercises wrapper resolution through the skill-store symlinks, so only scripts/ needs to be present; SKILL.md is irrelevant to that path. Or copy skills/okf-read/SKILL.md into the fixture and delete the comment.

<details><summary>Proof of in-diff scope</summary>

```
git diff --unified=0 origin/main...feat/issue-41-spec-v0.1.0 -- test/issue-72.test.js => 'test/issue-72.test.js | 85 ++++++++' (status A). skills/*/SKILL.md listing confirms five shipped SKILL.md files.
```

</details>

#### F118 — Installed-drift test hardcodes a source line and never checks the replacement landed

File: test/issue-72.test.js
Severity: minor
anchor:     "const suiteVersion = '0.1.0';", "const suiteVersion = '0.0.0';",

- Line: 74 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed at the claimed line. scripts/lib/orientation.js:10 is `const suiteVersion = '0.1.0';`, matched here as a raw string. A no-match makes String.replace a no-op, writeFileSync rewrites the file unchanged, and the test still observes result 'invalid' -- but for the unrelated reason that the payload's hardcoded `suite_version: '0.1.0'` (line 77) no longer equals the bumped suiteVersion, a case already covered elsewhere. So the next version bump leaves this test green while testing something else. test/issue-66-adapters.test.js:16 already requires scripts/lib/orientation and reads orientation.suiteVersion at line 85, so the canonical value is available to the suite.

**Fix.** Require scripts/lib/orientation at the top of the file and build both the search string and the payload from orientation.suiteVersion, then assert the mutation happened: `const patched = source.replace(`const suiteVersion = '${orientation.suiteVersion}';`, "const suiteVersion = '0.0.0';"); assert.notEqual(patched, source);` before writing. Use orientation.suiteVersion for the `suite_version` payload field in all three tests in this file.

<details><summary>Proof of in-diff scope</summary>

```
git diff --unified=0 origin/main...feat/issue-41-spec-v0.1.0 -- test/issue-72.test.js => 'test/issue-72.test.js | 85 ++++++++' (status A, whole file added).
```

</details>

#### F119 — The three-harness list is inlined six times in one file

File: test/issue-66-adapters.test.js
Severity: nit
anchor:   for (const harness of ['claude-code', 'codex', 'opencode']) {

- Line: 82 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed exactly: the literal appears at lines 82, 95, 103, 166, 208 and 230 of this file, plus lines 96 and 105 of test/issue-68-agents.test.js -- eight identical occurrences. A fourth adapter means finding all eight; a miss leaves a test silently covering three of four harnesses.

**Fix.** Declare `const harnesses = ['claude-code', 'codex', 'opencode'];` once at module level in each file (or export it from the shared test-support module) and loop over it.

<details><summary>Proof of in-diff scope</summary>

```
git diff --unified=0 origin/main...feat/issue-41-spec-v0.1.0 -- test/issue-66-adapters.test.js => 'test/issue-66-adapters.test.js | 440 +++++++++++++++++++++++++++++++++++++++++' (status A).
```

</details>

#### F120 — Comment defends the source-scraping helpers with a claim that does not hold

File: test/issue-68-agents.test.js
Severity: nit
anchor: // this test fails loudly (not silently) if the array is ever renamed away

- Line: 48 · Lens: deslop · Kind: slop · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed at lines 46-49 and the companion at 57-59. The guard is `assert.ok(match, ...)` at line 53, which fires only if the identifier `requiredFields` vanishes entirely; any change keeping the name but altering the shape (double quotes, a spread, fields composed from another array) yields a shorter list and a green test, so 'fails loudly (not silently)' overstates the guarantee. The 'Read-only text scan, no new exports required' note likewise argues for the technique rather than stating what is checked. Neither comment encodes a non-obvious constraint a tool or reader depends on, so neither is load-bearing.

**Fix.** Once the vocabulary and field list are pinned to explicit expected values (see the thermo findings), delete both comment blocks -- the assertions then state the contract themselves and need no defence.

<details><summary>Proof of in-diff scope</summary>

```
git diff --unified=0 origin/main...feat/issue-41-spec-v0.1.0 -- test/issue-68-agents.test.js => 'test/issue-68-agents.test.js | 157 +++++++++++++++' (status A).
```

</details>

### tests-postwrite-install

6 real, 0 refuted or already-correct.

#### F121 — The four obligations fixtures are dead files: no test reads test/fixtures at all

File: test/fixtures/obligations/runtime.md
Severity: major
anchor: type: Attested Computation

- Line: 2 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`
- Spans: test/fixtures/obligations/runtime.md, test/fixtures/obligations/generated-by.md, test/fixtures/obligations/human-prefix.md, test/fixtures/obligations/source-resource.md

Verified. `grep -rn fixtures --exclude-dir=node_modules --exclude-dir=.git` returns only prose hits (README.md:155,228; docs/research/*; test/issue-78.test.js:2,3,30,33 comments; test/issue-55.test.js:270; test/issue-46.test.js:10; test/issue-68.test.js:300) — not one path reference to test/fixtures. test/issue-78.test.js builds every obligation case inline: line 199 `function preWriteBlock(t, code, set)` writes note.md itself and lines 227-243 drive TYPE_MISSING, SOURCE_RESOURCE_MISSING, GENERATED_BY_MISSING, RUNTIME_MISSING, HUMAN_PREFIX_MISSING through it. The four .md files under test/fixtures/obligations are loaded by nothing and mirror exactly those check names, so they read as the authority for behaviour they do not gate.

**Fix.** Delete the four files (test/fixtures/obligations/{generated-by,human-prefix,runtime,source-resource}.md, and the now-empty directories). No test seam changes: issue-78's inline writes at the wrapper boundary already cover all five codes.

**Validator note.** Confirmed: files added by this branch (status A, diff is the proof) and referenced by nothing. Kept the delete branch of the reviewer's fix and dropped the alternative wiring branch — wiring them in would add work for zero coverage gain, and constraint 2 keeps the tested boundary at the wrapper process where issue-78 already sits.

<details><summary>Proof of in-diff scope</summary>

```
test/fixtures/obligations/runtime.md | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/test/fixtures/obligations/runtime.md b/test/fixtures/obligations/runtime.md
new file mode 100644
index 0000000..6e09b5e
--- /dev/null
+++ b/test/fixtures/obligations/runtime.md
@@ -0,0 +1,4 @@
+---
+type: Attested Computation
+---
+# Missing runtime
```

</details>

#### F122 — The OpenCode uninstall test passes vacuously when install does nothing

File: test/issue-105.test.js
Severity: major
anchor:   assert.deepEqual(fs.readdirSync(root), ['opencode.json']);

- Line: 119 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Verified at test/issue-105.test.js:111-121. `runCli(['install','opencode',targetDir]);` and `runCli(['uninstall','opencode',targetDir]);` both discard their result, and the only assertions are absence assertions (line 119 readdir equals ['opencode.json']; line 120 the file's bytes are unchanged). runCli JSON.parses stdout, so only a non-JSON crash would surface; an `ok:false` response (INVALID_ARGS, ADAPTER_CLI_FAILURE, a manifest regression) leaves nothing created, readdir still yields ['opencode.json'], and the test still passes. The one test asserting uninstall removes the plugins/ directory it created never proves that directory existed. Every other install-driving test in this file (lines 55-58, 72-73, 86) asserts response.ok or reads an installed artifact.

**Fix.** Before uninstalling, bind the install result and prove the artifact: `const installed = runCli(['install','opencode',targetDir]); assert.equal(installed.response.ok, true);` then `const pluginEntry = manifest().installs.find((entry) => entry.source === 'plugin.js'); assert.equal(fs.existsSync(path.join(root, pluginEntry.target)), true);` and bind the uninstall too with `assert.equal(removed.response.ok, true)`. Then the two absence assertions carry meaning.

**Validator note.** Confirmed; whole file added by this branch, so nothing here is pre-existing. Constraint 7 does not apply — this is not about the readiness claim or the test count, only about an assertion that cannot fail. Reviewer's fix tightened to name manifest()/pluginEntry, which the test body does not yet have in this scope.

<details><summary>Proof of in-diff scope</summary>

```
diff --git a/test/issue-105.test.js b/test/issue-105.test.js
new file mode 100644
index 0000000..2eed3fe
--- /dev/null
+++ b/test/issue-105.test.js
@@ -0,0 +1,121 @@
+/*
+ * Issue #105 — the accepted OpenCode installation and configuration
+ * contract (decision on #85): the installed plugin lands one level below
+ * OpenCode's native `plugins/` directory under an OKF-specific filename,
+ * the installer never creates/replaces/merges/removes `opencode.json`,
+ * `opencode.jsonc`, or the legacy global `config.json`, and OpenCode
```

</details>

#### F123 — temporaryRoot/runCli/manifest are copy-pasted per test file while test-support/ already exists

File: test/issue-105.test.js
Severity: minor
anchor: function runCli(args) {

- Line: 36 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`
- Spans: test/issue-105.test.js, test/issue-96.test.js, test/issue-97-delegation-narrowing.test.js, test-support/snapshot.js

Confirmed at the claimed location. runCli is byte-identical in test/issue-105.test.js:36-39 and test/issue-96.test.js:56-59; test/issue-66-adapters.test.js:45-50 is the same wrapper plus two extra stdout-shape assertions. temporaryRoot appears three times (issue-105:22 with a fixed prefix, issue-96:16 and issue-66-adapters:18 with a prefix parameter), and manifest exists in a no-arg form (issue-105:28) and a harness-arg form (issue-97:19, issue-66-adapters:24). test-support/snapshot.js exists for exactly this reason and documents why it lives outside test/.

**Fix.** Move temporaryRoot(t, prefix), runCli(args) and manifest(harness) into test-support/ next to snapshot.js (e.g. test-support/adapter-cli.js) and require them from issue-96, issue-97, issue-105 and issue-66-adapters. No new seam is introduced: these stay test helpers around the same wrapper-process boundary.

**Validator note.** Not refuted by constraint 2: test-support/ is a test-only helper module, not a new tested contract boundary, and snapshot.js already set the precedent. One correction to the detail: the issue-66-adapters copy is near-identical, not byte-identical, and issue-105's runCli is not a divergence in JSON.parse behaviour (issue-96's is identical) — the divergence is the stdout single-line assertion that only issue-66-adapters makes.

#### F124 — The no-agents assertion only checks a leading 'agents/', not any agents path segment

File: test/issue-97-delegation-narrowing.test.js
Severity: minor
anchor:       assert.ok(!entry.source.startsWith('agents/'), `${harness}:${entry.source}`);

- Line: 33 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed, but at line 33 (source) and 34 (target), not line 25. The test name at line 29 is 'no adapter manifest installs entry names an agents/ path', while startsWith only rejects a top-level prefix; a nested `.claude-plugin/agents/okf-delegate.md` or `plugins/agents/...` target would pass. `grep -rn agents adapters/*/manifest.json` currently returns nothing, so the assertion holds today and the gap is purely in what it would catch tomorrow — which is the whole point of the guard issue #97 adds.

**Fix.** Check path segments instead of the prefix: for both entry.source and entry.target, assert `entry.source.split('/').includes('agents') === false` (same for target), keeping the per-harness message.

**Validator note.** Not refuted by constraint 6: it forbids reporting the unreachable delegated path as a bug, and this finding does not — it strengthens the assertion that records the narrowing. Location corrected from line 25 to line 33.

#### F125 — Snapshot-and-filter used to compare one file whose exact bytes the test just wrote

File: test/issue-105.test.js
Severity: nit
anchor:   const before = snapshot(root).filter(([name]) => name === 'opencode.json');

- Line: 114 · Lens: deslop · Kind: slop · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed at line 114 (and the matching after-comparison at line 120). Line 112 writes '{"unrelated": true}\n' itself, and line 119 already asserts the directory holds nothing else, so recursing the whole tree and filtering to a single tuple twice is heavier than the check needs. Nothing else in the test consumes `before`.

**Fix.** Replace the before/after snapshot pair with `assert.equal(fs.readFileSync(path.join(root, 'opencode.json'), 'utf8'), '{"unrelated": true}\n')` after the uninstall, drop the `before` variable, and drop the now-unused `snapshot` require at line 16 if no other test in this file uses it.

**Validator note.** Confirmed; snapshot() is used nowhere else in issue-105.test.js, so the fix must also remove the require on line 16 — added that to the fix. It remains used by test/issue-96.test.js, so test-support/snapshot.js stays.

#### F126 — preWriteBlock returns a root no caller uses

File: test/issue-78.test.js
Severity: nit
anchor:   return root;

- Line: 210 · Lens: deslop · Kind: slop · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed: `return root;` is at test/issue-78.test.js:210, the last statement of preWriteBlock (declared line 199). All five call sites (lines 227, 231, 235, 239, 243) invoke it as a bare statement, e.g. `preWriteBlock(t, 'TYPE_MISSING', { type: '' });`, so the value is never bound. Not load-bearing.

**Fix.** Drop the `return root;` line.

**Validator note.** Confirmed; the other `return root;` in this file (line 72, in the bundle() helper) IS load-bearing and must stay.

### docs-public-and-proof

7 real, 1 refuted or already-correct.

#### F127 — Orphaned, unattributed third-party talk transcript committed under docs/

File: docs/transcript.md
Severity: major
anchor: [music] >> Yes, so hi everyone. My name is Philip.

- Line: 1 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed at the claimed site. docs/transcript.md is 47 lines (48 in-diff, the last line has no trailing newline — `tail -c` ends at `>> [applause]` with no 0x0a), raw ASR text of a Google DeepMind speaker's talk on skill evals, with no title, heading, speaker/event attribution, source URL or date. Nothing references it: `grep -rn transcript` over the tree hits only docs/research/* prose about other videos and the word "transcript" in unrelated research notes — zero hits for `transcript.md`, and okf/ (index.md, releases/) does not index it. Every neighbour under docs/research/ carries an explicit **Evidence source:** line (e.g. lightweight-durable-context.md:57), which this file does not.

**Fix.** Delete docs/transcript.md. If a claim on this branch actually rests on that talk, put the note under docs/research/ in the shape of its neighbours: title, speaker, event, date, source URL, and the extracted findings — not the raw ASR text — and link it from the document that cites it.

<details><summary>Proof of in-diff scope</summary>

```
$ git diff --unified=0 origin/main...feat/issue-41-spec-v0.1.0 -- docs/transcript.md
diff --git a/docs/transcript.md b/docs/transcript.md
new file mode 100644
index 0000000..ea6b177
--- /dev/null
+++ b/docs/transcript.md
@@ -0,0 +1,48 @@
+[music] >> Yes, so hi everyone. My name is Philip.
...
 docs/transcript.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 48 insertions(+)
```

</details>

#### F129 — Zero-dependency rule is stated unqualified, and the branch itself contains a counterexample

File: AGENTS.md
Severity: minor
anchor: - **Zero dependencies.** Nothing outside the Node.js standard library.

- Line: 7 · Lens: docs · Kind: docs · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed at AGENTS.md:7 (verbatim). The branch adds eval/package.json pinning `@flue/runtime` 2.0.3 and eval/package-lock.json (118 `"resolved"` entries). The recorded design permits it: docs/spec/okf-agent-skills-v0.1.0-completion.md:347-349 — "A `package.json` MUST NOT become required to install or run the suite; if one is added for a development-only convenience, CI MUST still pass without it" — and eval/README.md:3-4 says the slice "is development tooling. It is not part of the shipped product." There is no root package.json (verified absent), so the suite does run without one. Does NOT conflict with hard constraint 1: the constraint is itself scoped to "the shipped runtime, skills, or adapters", and the fix adds no package or lockfile entry.

**Fix.** Scope the rule to what it actually protects: "Zero dependencies. The shipped runtime, skills, and adapters use nothing outside the Node.js standard library, and the deterministic suite runs without any package.json. The non-gating eval/ slice is the one development-only exception."

#### F130 — README restates the OpenCode deny-rule JSON that adapters/opencode/config.json already owns

File: README.md
Severity: minor
anchor: the effective `permission.skill: deny` rules for `okf`, `okf-read`,

- Line: 126 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`
- Spans: README.md, adapters/opencode/config.json

Confirmed. README.md:126 is the anchor; the pasted JSON block runs README.md:133-144 and is byte-identical in content to adapters/opencode/config.json (same five keys, all `"deny"`). test/issue-66-adapters.test.js:150-153 reads adapters/opencode/config.json and asserts each of the five skills is `deny`; test/issue-105.test.js:80-88 asserts the manifest next_action names every skill in that config so the two copies cannot drift. The README copy has no such guard. Not refuted by hard constraint 7 — that constraint preserves the manual-merge contract, which the proposed fix keeps.

**Fix.** Replace the inline JSON with a pointer to the owned copy: tell the reader to merge the rules in `adapters/opencode/config.json` into their own `opencode.json`/`opencode.jsonc`, keeping the placement and last-matching-rule note.

#### F131 — Supported-versions statement contradicts the README's tag-pinned install commands

File: SECURITY.md
Severity: minor
anchor: No version is tagged yet. Until `v0.1.0` ships, only the `main` branch

- Line: 11 · Lens: docs · Kind: docs · Verdict: `confirmed` · Disposition: `fix-as-proposed`
- Spans: SECURITY.md, README.md

Confirmed. SECURITY.md:11-12 reads as quoted (whole file is 12 lines, added on this branch). `git tag` is empty. README.md:28 instructs `npx skills add 'artemVeduta/okf-agent-skills#v0.1.0'` and README.md:61 `git clone --branch v0.1.0 https://github.com/artemVeduta/okf-agent-skills.git`, so the shipped pair tells a reader to install a ref the security policy says does not exist, and the statement goes stale the moment the tag lands.

**Fix.** Make the supported-version statement forward-compatible with the release it ships in: "`v0.1.0` is the supported release; only its tag and `main` receive fixes." Do not change the install commands — the specification fixes their exact form.

#### F132 — Self-quoted diffstat describes an older 140-line copy of this 149-line file

File: docs/ci-gate-proof.md
Severity: nit
anchor: docs/ci-gate-proof.md | 140 ++++++++++++++++++++++++++++++++++++++++++++++++++

- Line: 133 · Lens: docs · Kind: docs · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed. The quoted stat sits at docs/ci-gate-proof.md:133 under the `git diff --stat 34be353..origin/ci-proof/issue-107` block at line 129, and the committed file is 149 lines (`wc -l` = 149). A reader comparing 140 against the file in front of them cannot tell which number is stale.

**Fix.** Add one sentence next to the stat: the `ci-proof/issue-107` branch carries the original 140-line copy of this file, and the copy on the release branch was extended afterwards — so the diffstat is evidence about that branch, not about the file as it now stands.

#### F133 — Self-congratulatory meta-sentence introducing Limitations

File: README.md
Severity: nit
anchor: This release discloses the following plainly, rather than leaving them for

- Line: 199 · Lens: deslop · Kind: slop · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Sanity check passed: the sentence is still at README.md:199-200, directly under `## Limitations` at line 197, and it is not load-bearing — it carries no fact a reader needs; the heading plus the bolded items ("**Support ceiling — provisional.**" ...) do the work. It is prose about the prose.

**Fix.** Delete the sentence and let the "## Limitations" heading introduce the list directly.

#### F134 — Development section both delegates the suite command and repeats it

File: README.md
Severity: nit
anchor: node --test "test/*.test.js"

- Line: 248 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed. README.md:243 says "See [`CONTRIBUTING.md`](CONTRIBUTING.md) for how to run the suite ..." and README.md:247-249 prints the command anyway. CONTRIBUTING.md:5 owns the same command and CONTRIBUTING.md:8 adds the context the README copy lacks: "Do not use or document `node --test test/`; it misses files."

**Fix.** Drop the trailing code block from the Development section and keep the CONTRIBUTING.md reference, or keep the command in the README and delete the delegation sentence — one of the two, not both.

### docs-research

11 real, 9 refuted or already-correct.

#### F137 — OpenCode section documents an installs list the shipped manifest does not have

File: docs/research/adapter-install-targets.md
Severity: major
anchor: config.json   -> config.json

- Line: 136 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`
- Spans: docs/research/adapter-install-targets.md, adapters/opencode/manifest.json

Verified at the claimed site. Line 132-138 of the record quotes an installs list of `config.json -> config.json`, `plugin.js -> plugin.js`, `manifest.json -> manifest.json`, and lines 139-156 build a "no single target directory satisfies both discovery rules" conclusion on it. The shipped `adapters/opencode/manifest.json` (also new on this branch) has exactly two installs: `{ "source": "plugin.js", "target": "plugins/okf-agent-skills.js" }` and `{ "source": "manifest.json", "target": "manifest.json" }`. There is no `config.json` install at all, and `plugin.js` already lands one level deep in `plugins/`, which is precisely what the record's own evidence (`packages/opencode/src/config/plugin.ts`, glob `{plugin,plugins}/*.{ts,js}`) says is required. So the record's central conflict analysis and its summary-table row at line 197 (`opencode | *(none — recorded gap)* | n/a`) both contradict the manifest they claim to cross-check, and this is the D6 decision record a reader will use.

**Fix.** Re-run the cross-check against the current `adapters/opencode/manifest.json`: quote its real two-entry installs list, drop the `config.json` conflict analysis (no such install exists), and state the directory that `plugins/okf-agent-skills.js` plus `manifest.json` resolve under (the global config directory, or a project `.opencode/`). Update the summary-table row from "(none — recorded gap)" to the verified directory, and keep only the genuinely open item (the manual permission merge named in the manifest's `next_action`).

**Validator note.** Confirmed against both files. Hard constraint 7 does not refute this: the finding is about a stale quoted installs list, not about the missing readiness claim or the manual permission merge, and its fix explicitly retains the permission-merge item. When applying, state only the verified install directory — do not add a readiness or "adapter verified working" claim, which decision 85 withholds.

<details><summary>Proof of in-diff scope</summary>

```
$ git -C /Users/artem.veduta/proj/okf-agent-skills diff --unified=0 origin/main...feat/issue-41-spec-v0.1.0 --stat -- docs/research/adapter-install-targets.md adapters/opencode/manifest.json
 adapters/opencode/manifest.json          |  16 +
 docs/research/adapter-install-targets.md | 197 +++++++

diff --git a/adapters/opencode/manifest.json b/adapters/opencode/manifest.json
new file mode 100644
--- /dev/null
+++ b/adapters/opencode/manifest.json
@@ -0,0 +1,16 @@
+  "installs": [
+    { "source": "plugin.js", "target": "plugins/okf-agent-skills.js" },
+    { "source": "manifest.json", "target": "manifest.json" }
+  ]

diff --git a/docs/research/adapter-install-targets.md b/docs/research/adapter-install-targets.md
new file mode 100644
--- /dev/null
+++ b/docs/research/adapter-install-targets.md
@@ -0,0 +1,197 @@
(both files status A; whole-file addition is the scope proof, no blame run)
```

</details>

#### F138 — Recorded v0.1.0 sequence forbids exactly what this branch ships in eval/

File: docs/research/agent-skill-evaluation-automation.md
Severity: major
anchor: 4. Do not add Flue, Langfuse, a judge model, or evaluation CI.

- Line: 184 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`
- Spans: docs/research/agent-skill-evaluation-automation.md, eval/package.json, docs/flue-eval-results.md

Verified. Line 184 reads "Do not add Flue, Langfuse, a judge model, or evaluation CI." under `### v0.1.0`, and Decision Inputs at lines 16-20 defer any repository-local runner to `v0.2.0` with a sketched layout of `eval/cases.json` + `eval/run.mjs` (lines 113-117). The same branch ships a tracked `eval/` slice: `eval/package.json` with `"@flue/runtime": "2.0.3"`, `eval/package-lock.json`, `eval/run.js`, `eval/cases/activation.eval.js`, `eval/cases/wrapper-contract.eval.js`, `eval/lib/*`, plus `docs/flue-eval-results.md` recording two scored runs. The accepted outcome is a non-gating, development-only Flue slice; this record states the opposite decision, so the release carries two contradictory statements of the same decision.

**Fix.** Bring the record in line with the accepted outcome recorded in `docs/flue-eval-results.md` and `eval/README.md`: replace the "Do not add Flue" step and the `v0.2.0`-only Decision Inputs with the decision actually taken (a non-gating, development-only Flue slice under `eval/`, outside `node --test "test/*.test.js"`), and replace the speculative `cases.json`/`run.mjs` sketch with the shipped `eval/` layout. Keep the evidence about the prompt-serialized eval path, which is still the reason the slice is non-gating.

**Validator note.** Confirmed. Hard constraint 1 (zero dependencies) does not refute it: the constraint governs the shipped runtime, skills, and adapters, and `eval/` is tracked development-only tooling already on the branch with its own recorded results file. The fix records an outcome that shipped; it does not propose adding a dependency, and must not touch `eval/` itself.

<details><summary>Proof of in-diff scope</summary>

```
$ git -C /Users/artem.veduta/proj/okf-agent-skills diff --unified=0 origin/main...feat/issue-41-spec-v0.1.0 --stat -- docs/research/agent-skill-evaluation-automation.md eval/package.json
 docs/research/agent-skill-evaluation-automation.md | 232 ++++++++
 eval/package.json                                  |  13 +

diff --git a/eval/package.json b/eval/package.json
new file mode 100644
--- /dev/null
+++ b/eval/package.json
@@ -0,0 +1,13 @@
+  "dependencies": {
+    "@flue/runtime": "2.0.3"
+  }

diff --git a/docs/research/agent-skill-evaluation-automation.md b/docs/research/agent-skill-evaluation-automation.md
new file mode 100644
--- /dev/null
+++ b/docs/research/agent-skill-evaluation-automation.md
@@ -0,0 +1,232 @@
(both files status A; whole-file addition is the scope proof, no blame run)
```

</details>

#### F145 — Decision input attributes skills-autoresearch-flue's prompt-mounted eval path to "Flue"

File: docs/research/agent-skill-evaluation-automation.md
Severity: minor
anchor: - Do not use Flue as the native harness evaluator. Its current eval path puts

- Line: 19 · Lens: docs · Kind: docs · Verdict: `fixed-with-deviation` · Disposition: `fix-with-deviation` · Re-graded major → minor
- Spans: eval/agent.js, eval/cases/activation.eval.js, eval/lib/fixture.js, docs/research/langfuse-coding-agent-skill-evaluation.md

Confirmed in part. Line 19-20 reads "Do not use Flue as the native harness evaluator. Its current eval path puts skill text in a model prompt." The cited behaviour belongs to the third-party alpha harness, not the Flue framework: this document's own §Flue (line 134-140) opens "The examined project identifies itself as an alpha skills autoresearch harness" with researcher/producer/judge phases and Varlock/1Password, and docs/research/langfuse-coding-agent-skill-evaluation.md separates "### Official Flue framework" (line 469) from "### `skills-autoresearch-flue`" (line 495). So the bullet names the wrong project — a genuine factual conflation, not a dated observation. The second half of the reviewer's claim carries much less weight: the bullet's substantive assertion ("It does not test native skill discovery") is still true of the shipped eval/ slice, which mounts skills at `<fixture>/.agents/skills/<name>` (eval/lib/fixture.js:36-39) for Flue's own workspace discovery, not for Claude Code, Codex, or OpenCode. Severity revised down to minor because only the project label is wrong; the guidance it carries still holds.

**Fix.** Rename the subject only: "- Do not use `skills-autoresearch-flue` as the native harness evaluator. Its alpha eval path serializes candidate skill text into the producer prompt; it does not test native skill discovery." Optionally add one bullet of current state: "- The Flue framework itself is used only for the non-gating activation eval in `eval/` (one dependency, `@flue/runtime`), which exercises Flue's own workspace-skill discovery, not native Claude Code, Codex, or OpenCode discovery." Do not restate this as an endorsement of Flue as a native-harness evaluator.

**Validator note.** Confirmed narrowly: the project-name conflation is real and verified against this document's own §Flue and the langfuse record's two separate sections. The "branch now uses Flue so the bullet is false" half is not a defect — the bullet's claim about native discovery remains true of eval/. Fix rewritten to the rename only.

<details><summary>Proof of in-diff scope</summary>

```
diff --git a/docs/research/agent-skill-evaluation-automation.md b/docs/research/agent-skill-evaluation-automation.md
new file mode 100644
index 0000000..c4a3a54
--- /dev/null
+++ b/docs/research/agent-skill-evaluation-automation.md
```

</details>

#### F146 — Options-table "Flue harness" row describes skills-autoresearch-flue, not the Flue framework

File: docs/research/agent-skill-evaluation-automation.md
Severity: minor
anchor: | Flue harness | No, not for this use | Its own generated phase workspaces | Strong loop support | Separate system and dependency set | Anthropic plus its secret tools | High |

- Line: 84 · Lens: docs · Kind: docs · Verdict: `fixed-with-deviation` · Disposition: `fix-with-deviation`
- Spans: eval/agent.js, eval/.env.example

Confirmed on the labelling point only. Line 84's cells ("Its own generated phase workspaces", "Anthropic plus its secret tools", cost "High") match this document's §Flue description of the alpha autoresearch harness with Varlock and 1Password, not the Flue framework — the same conflation as line 19, and the langfuse record keeps the two projects in separate sections. The reviewer's cost argument does not stand on its own: the row's High cost is accurate for the autoresearch harness it actually describes, and the row's "No, not for this use" native-discovery cell is still true of eval/, which mounts skills for Flue's own workspace discovery. So the defect is the row's name, not its cells.

**Fix.** Rename the row label to `skills-autoresearch-flue` and leave every cell as-is. Do not add a second row describing the shipped `eval/` slice: this table compares candidate options as of the research date, and grafting current branch state into it makes the table's other rows inconsistent. Cross-link the shipped slice from a dated note instead if desired.

**Validator note.** Confirmed as a mislabel only; the reviewer's "wrong cost" half is refuted because the cells correctly describe the harness the row actually covers. Fix narrowed to the rename.

<details><summary>Proof of in-diff scope</summary>

```
diff --git a/docs/research/agent-skill-evaluation-automation.md b/docs/research/agent-skill-evaluation-automation.md
new file mode 100644
index 0000000..c4a3a54
--- /dev/null
+++ b/docs/research/agent-skill-evaluation-automation.md
```

</details>

#### F147 — Third record restates the two dedicated records without new evidence

File: docs/research/agent-skill-evaluation-automation.md
Severity: minor
anchor: ### Flue

- Line: 134 · Lens: deslop · Kind: slop · Verdict: `confirmed` · Disposition: `fix-as-proposed`
- Spans: docs/research/agent-skill-evaluation-automation.md

Text is present as described: `### Flue` at line 134 and `### Langfuse` at line 155 restate verdicts that `docs/research/flue-skill-evaluation.md` (259 lines) and `docs/research/langfuse-coding-agent-skill-evaluation.md` (623 lines) establish with their own citations. The duplication is where both the `skills-autoresearch-flue` name collision and the stale "do not add Flue" step entered the set. The prose is narrative, not tool-consumed, so it is not load-bearing.

**Fix.** Cut both sections down to one sentence each stating the verdict and linking to the dedicated record (`./flue-skill-evaluation.md`, `./langfuse-coding-agent-skill-evaluation.md`), and delete the duplicated dependency lists, package names, and citation footnotes those records own (R15-R22 at lines 225-232, which nothing else in this record references).

**Validator note.** Confirmed present and not load-bearing. Checked that R15-R22 are referenced only inside these two sections (lines 137-166), so removing the sections leaves no dangling reference markers. Sequence with the name-collision finding at line 136 so the rename is not done on text about to be deleted.

<details><summary>Proof of in-diff scope</summary>

```
docs/research/agent-skill-evaluation-automation.md is status A (whole-file addition).
```

</details>

#### F148 — "Flue" names two different projects across the research set

File: docs/research/agent-skill-evaluation-automation.md
Severity: minor
anchor: The examined project identifies itself as an alpha skills autoresearch harness.

- Line: 136 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`
- Spans: docs/research/agent-skill-evaluation-automation.md, docs/research/flue-skill-evaluation.md, docs/research/langfuse-coding-agent-skill-evaluation.md

Confirmed at line 136 under the `### Flue` heading at line 134: the section describes the alpha `skills-autoresearch-flue` project (Node.js 24, pnpm, Varlock/1Password, producer prompt serialization), and its citations R15-R19 (lines 225-229) name `skills-autoresearch-flue` explicitly. `docs/research/flue-skill-evaluation.md` uses "Flue" for the withastro framework at tag `v2.0.3` (commit `bf86b87`), and `docs/research/langfuse-coding-agent-skill-evaluation.md` splits the two under `### Official Flue framework` (line 469) and `### skills-autoresearch-flue` (line 495). The Decision Inputs line at 19-20, "Do not use Flue as the native harness evaluator. Its current eval path puts skill text in a model prompt", therefore reads as attributing the alpha harness's limitation to `@flue/runtime`, the framework `eval/` depends on.

**Fix.** Rename this section and every reference in it to `skills-autoresearch-flue`, and reserve "Flue" for the `@flue/runtime` framework. Restate the Decision Inputs line so the prompt-serialization limitation is attributed to the alpha harness, and state separately what the framework itself does or does not observe.

**Validator note.** Confirmed; location correct. Overlaps with the deslop finding at line 134 — if that section is collapsed to one linking sentence, apply the rename to the surviving sentence and to the Decision Inputs line rather than twice.

<details><summary>Proof of in-diff scope</summary>

```
All three files are status A on feat/issue-41-spec-v0.1.0 (whole-file additions).
```

</details>

#### F149 — In-repo citations pin line numbers that are already wrong

File: docs/research/flue-skill-evaluation.md
Severity: minor
anchor: [^local-completion]: This repository, [`docs/spec/okf-agent-skills-v0.1.0-completion.md`, lines 334-340](../spec/okf-agent-skills-v0.1.0-completion.md#repository-setup).

- Line: 258 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed. `### Repository setup` is at line 343 of `docs/spec/okf-agent-skills-v0.1.0-completion.md` (484 lines), not 334-340; the sibling footnote at line 257 cites "lines 363-394" for Testing Decisions, whose heading is at line 372. Both ranges point a reader at the wrong text today, and they are pinned into a document this same branch keeps editing.

**Fix.** Drop the line ranges from the two in-repo footnotes and cite the heading anchors only (`#repository-setup`, `#testing-decisions`, `#acceptance-evidence`), which survive edits. Keep line ranges only for the external, commit-pinned sources where the commit hash freezes them.

**Validator note.** Confirmed by grepping the target headings: 343 and 372, versus the cited 334-340 and 363-394. Note the `#acceptance-evidence` anchor in footnote 257 points into docs/spec/okf-agent-skills-v0.1.0.md, not the completion spec — keep each anchor with its own file when editing.

<details><summary>Proof of in-diff scope</summary>

```
docs/research/flue-skill-evaluation.md is status A (whole-file addition).
```

</details>

#### F151 — Frozen 241-test figure is stale as a release criterion

File: docs/research/langfuse-coding-agent-skill-evaluation.md
Severity: minor
anchor: signal. Do not make it a required `v0.1.0` check. Keep the existing 241-test

- Line: 562 · Lens: docs · Kind: docs · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed. Line 562-563 reads "Keep the existing 241-test offline suite as the release authority" — a present-tense release criterion pinned to a fixed count, unlike the measurement at line 57 which pins itself to commit 54feaf0 ("It completed with 241 tests, 241 ...") and is fine. The suite has grown well past 241 on this branch (259 top-level `test(` declarations across test/*.test.js), and hard constraint 7 states directly that frozen counts are superseded and the live criterion is "pass count equals test count, zero fail". So the frozen figure is wrong as an authority.

**Fix.** Change line 562-563 to "Keep the offline `node --test \"test/*.test.js\"` suite as the release authority, with the pass count equal to the test count and zero failures." Leave the commit-pinned 241-test measurement at line 57 unchanged.

**Validator note.** Confirmed. Exact count not re-measured by running the suite (read-only mode); staleness proven by the 259 test declarations in test/*.test.js and by hard constraint 7's own statement that frozen figures are superseded. The fix avoids naming any new number, so it does not depend on the exact total.

<details><summary>Proof of in-diff scope</summary>

```
diff --git a/docs/research/langfuse-coding-agent-skill-evaluation.md b/docs/research/langfuse-coding-agent-skill-evaluation.md
new file mode 100644
index 0000000..1488a8a
--- /dev/null
+++ b/docs/research/langfuse-coding-agent-skill-evaluation.md
```

</details>

#### F152 — Citation claims four locations were quoted; the body quoted two

File: docs/research/adapter-install-targets.md
Severity: nit
anchor: the four standalone locations quoted above:

- Line: 102 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed. The Codex body (lines 58-84) names `~/.codex/hooks.json` (with the `$CODEX_HOME` variant) and `<repo>/.codex/hooks.json`, and never mentions either `config.toml`. The citation at line 102 says "the four standalone locations quoted above" and then lists both `config.toml` paths.

**Fix.** Change the citation to "the standalone hook locations named above, plus the two `config.toml` layers Codex reads from the same directories", or add the two `config.toml` paths to the body where the locations are first enumerated.

**Validator note.** Confirmed; location correct.

<details><summary>Proof of in-diff scope</summary>

```
docs/research/adapter-install-targets.md is status A (whole-file addition).
```

</details>

#### F153 — Same Codex page cited under two different URLs

File: docs/research/agent-skill-evaluation-automation.md
Severity: nit
anchor: - **R5.** OpenAI, [Codex non-interactive mode](https://developers.openai.com/codex/noninteractive).

- Line: 215 · Lens: thermo · Kind: quality · Verdict: `confirmed` · Disposition: `fix-as-proposed`
- Spans: docs/research/agent-skill-evaluation-automation.md, docs/research/langfuse-coding-agent-skill-evaluation.md

Confirmed textually. Line 215 cites `developers.openai.com/codex/noninteractive`; `docs/research/langfuse-coding-agent-skill-evaluation.md` cites the same page as `developers.openai.com/codex/non-interactive-mode` at lines 286 and 604. `docs/research/adapter-install-targets.md` separately records that `developers.openai.com/codex/...` paths redirect to `learn.chatgpt.com`. The two slugs cannot both be the page's path, so one record carries a dead citation for a fact both rely on. Not verified over the network (read-only, no fetch performed) — the internal inconsistency is the defect.

**Fix.** Verify the slug once and use the same URL in both records (and note the `learn.chatgpt.com` redirect the way the adapter record already does).

**Validator note.** Confirmed as an internal citation inconsistency; the claim that one slug is dead is inferred, not network-verified, so the fix must start by checking which URL resolves.

<details><summary>Proof of in-diff scope</summary>

```
Both files are status A (whole-file additions).
```

</details>

#### F154 — Same recommendation stated four times in one record

File: docs/research/langfuse-coding-agent-skill-evaluation.md
Severity: nit
anchor: ## Final recommendation

- Line: 565 · Lens: deslop · Kind: slop · Verdict: `confirmed` · Disposition: `fix-as-proposed`

Confirmed at line 565. The same verdict — small local Node.js runner first, Langfuse as an optional later sink, nothing in the release gate, Flue not adopted — appears in `## Executive conclusion` (line 17, with its five numbered steps), as the `**Inference:**` closing `## Comparison with a small local harness` (line 448 table), across `### Phase 1`-`### Phase 4` (lines 532-563), and again here. Narrative prose, nothing consumes it.

**Fix.** Keep the executive conclusion and the phased sequence, which say different things (verdict versus order of work). Delete this closing section and the duplicated inference at the end of the comparison table.

**Validator note.** Confirmed present and not load-bearing. One caveat for the fix: `## Final recommendation` is the only place that records the Flue non-adoption verdict and the Cloud-Hobby-versus-self-host data-policy point; fold those two sentences into the executive conclusion before deleting the section, or evidence is lost rather than deduplicated. Also note line 563's "241-test offline suite" figure inside the Phase 4 text — hard constraint 7 forbids treating a changed test count as a defect, so leave it alone.

<details><summary>Proof of in-diff scope</summary>

```
docs/research/langfuse-coding-agent-skill-evaluation.md is status A (whole-file addition).
```

</details>

## Confirmed slop

- **F003** `scripts/lib/presence.js:50` — Defensive typeof guard on an injected service that always provides isFile
- **F006** `scripts/lib/validation.js:562` — Comma-operator side effect hidden inside an argument
- **F009** `scripts/lib/admission.js:106` — ~300-character single-line object literals in a file that otherwise formats objects one field per line
- **F010** `scripts/lib/validation.js:17` — Bare 'path' import where the rest of scripts/lib uses the node: prefix
- **F016** `scripts/lib/navigation.js:32` — safeExists distrusts the injected service that routing.js calls bare
- **F017** `scripts/lib/navigation.js:186` — Empty catch hides a realpath failure that guardPath reports as unobservable
- **F018** `scripts/lib/navigation.js:272` — Several helpers return fields no caller reads
- **F019** `scripts/lib/navigation.js:287` — routeScope guards against a route shape locate never returns
- **F024** `scripts/lib/navigation.js:441` — The empty-candidate short circuit in search sits after the loop it should skip
- **F031** `scripts/lib/protocol.js:85` — Validation for a 'route' operation that does not exist anywhere in the suite
- **F037** `scripts/lib/runtime.js:139` — appendDerivative parses the frontmatter three times, but only the middle call is redundant
- **F038** `scripts/lib/runtime.js:147` — filter(Boolean) guards a case executeBounded cannot reach
- **F039** `scripts/lib/services.js:8` — Bare 'fs'/'path' requires in the one module that departs from the node: prefix
- **F047** `adapters/opencode/plugin.js:6` — Header comment restates another module's design essay, with an unreflowed patched-in sentence
- **F048** `scripts/lib/adapters.js:10` — Unreflowed sentence appended to the module header comment
- **F049** `scripts/lib/adapters.js:29` — Second, inconsistent derivation of the scripts directory
- **F057** `skills/okf-read/SKILL.md:59` — Procedure step 3 restates the Navigation path section in full
- **F060** `skills/okf-lifecycle/SKILL.md:12` — The "never an adapter or a hook" rule is restated three times in one 66-line file
- **F062** `eval/lib/wrapper-contract.check.js:3` — Header comment hardcodes five test filenames from another directory
- **F067** `eval/lib/report.check.js:1` — "Red-before-green seam" narrates how the file was authored, not what it does
- **F069** `eval/run.js:31` — Session anecdote embedded in a source comment
- **F078** `docs/spec/okf-agent-skills-v0.1.0-completion.md:475` — A paragraph that argues with an imagined objection instead of stating a decision
- **F081** `test/issue-46.test.js:10` — Top-level assertion that a stdlib function exists
- **F083** `test/issue-46.test.js:43` — runWrapper swallows a JSON parse failure
- **F084** `test/issue-46.test.js:164` — notEqual on a value the previous line already pinned exactly
- **F085** `test/issue-46.test.js:214` — Assertions that restate assertions three lines above
- **F091** `test/issue-47.test.js:10` — Unused writeWrapper binding, and a manifest written twice in one test
- **F094** `test/issue-50.test.js:20` — Retired vocabulary words are split and re-joined to dodge a self-match that cannot happen
- **F099** `test/issue-49.test.js:109` — Banned-word check uses an asymmetric lookbehind/lookahead over a string-replaced copy of stdout
- **F102** `test/issue-50.test.js:149` — assertFinding re-asserts the code it searched by and the label set membership twice
- **F103** `test/issue-50.test.js:212` — notDeepEqual against [] is subsumed by the assert.equal(..., undefined) on the next line
- **F109** `test/issue-51-policy.test.js:102` — Second test wraps its index fixture in a Buffer and compares Buffers, unlike the first test's string
- **F111** `test/issue-53.test.js:168` — Two statements crammed onto one line, inconsistent with the rest of the file
- **F116** `test/issue-68.test.js:63` — Empty catch swallows a parse failure that every caller then dereferences
- **F117** `test/issue-72.test.js:28` — Comment states a premise the branch contradicts: SKILL.md files do ship
- **F120** `test/issue-68-agents.test.js:48` — Comment defends the source-scraping helpers with a claim that does not hold
- **F125** `test/issue-105.test.js:114` — Snapshot-and-filter used to compare one file whose exact bytes the test just wrote
- **F126** `test/issue-78.test.js:210` — preWriteBlock returns a root no caller uses
- **F133** `README.md:199` — Self-congratulatory meta-sentence introducing Limitations
- **F147** `docs/research/agent-skill-evaluation-automation.md:134` — Third record restates the two dedicated records without new evidence
- **F154** `docs/research/langfuse-coding-agent-skill-evaluation.md:565` — Same recommendation stated four times in one record

## Documentation conformance

- **F052** (major) `okf/index.md:3` — The shipped bundle root declares knowledge-only for a code-backed repository, disabling the code-recoverable gate
- **F053** (major) `skills/okf-read/SKILL.md:31` — The documented `resolve` request shape crashes the wrapper instead of returning a result
- **F054** (major) `skills/okf-write/SKILL.md:46` — The performed-effects list names six effects the v0.1.0 runtime refuses with UNSUPPORTED_INPUT
- **F055** (minor) `okf/releases/index.md:1` — The shipped bundle's directory index has no frontmatter, so the repository's own enumerate degrades
- **F056** (minor) `skills/okf-lifecycle/SKILL.md:51` — INVALID_SCOPE arrives as `blocked`, not as `abstained`
- **F058** (minor) `skills/okf-write/SKILL.md:28` — `set` is documented as the way to change a status, which the runtime blocks
- **F059** (minor) `skills/okf-write/SKILL.md:36` — ROOT_DECLARATION_NOT_EXACT is a finding code, never a `data.code`
- **F070** (blocker) `docs/spec/okf-agent-skills-v0.1.0.md:3651` — Four rows the completion spec records decisions for are still `Open` in the open-item table
- **F071** (major) `docs/spec/okf-agent-skills-v0.1.0-completion.md:20` — The completion spec's Problem Statement asserts, in the present tense, a repository state this branch has already changed
- **F072** (major) `docs/spec/okf-agent-skills-v0.1.0.md:2763` — A new MUST fixes the shared module set at five modules; fifteen ship
- **F073** (major) `docs/spec/okf-agent-skills-v0.1.0.md:3669` — The undefined-terms gap list is stale: this branch defines every term it lists
- **F076** (minor) `docs/spec/okf-agent-skills-v0.1.0.md:3376` — Two v0.1.0-reachable operation-map rows still name "guarded write checks" after the same pass removed it from their siblings
- **F077** (minor) `docs/spec/okf-agent-skills-v0.1.0.md:3404` — The D7 ownership table's Runtime module column is the constant `scripts/lib/runtime.js`
- **F129** (minor) `AGENTS.md:7` — Zero-dependency rule is stated unqualified, and the branch itself contains a counterexample
- **F131** (minor) `SECURITY.md:11` — Supported-versions statement contradicts the README's tag-pinned install commands
- **F132** (nit) `docs/ci-gate-proof.md:133` — Self-quoted diffstat describes an older 140-line copy of this 149-line file
- **F145** (minor) `docs/research/agent-skill-evaluation-automation.md:19` — Decision input attributes skills-autoresearch-flue's prompt-mounted eval path to "Flue"
- **F146** (minor) `docs/research/agent-skill-evaluation-automation.md:84` — Options-table "Flue harness" row describes skills-autoresearch-flue, not the Flue framework
- **F151** (minor) `docs/research/langfuse-coding-agent-skill-evaluation.md:562` — Frozen 241-test figure is stale as a release criterion

## Refuted and already-correct

These cost one line each here instead of a fixer's skip.

#### F011 — The bundle-root index.md is read and parsed up to four times per postWrite

File: scripts/lib/validation.js
Severity: nit
anchor:     const root = checkRoot(bundleRoot, services);

- Verdict: `refuted` · Disposition: `refute-skip` · Lens: thermo

The current code is already acceptable: the redundant reads are real but harmless, the claimed four-reads-per-postWrite count is wrong (three), and the proposed refactor is larger than the problem.

#### F012 — 961-line module bundles a YAML implementation, the write gates, the read validator and the review/trust-tier logic

File: scripts/lib/validation.js
Severity: nit
anchor: module.exports = { evaluate, evaluateCreate, evaluateReview, parseFrontmatter, parseYAML, serializeFrontmatter, postWrite, projectMode, validateRead };

- Verdict: `refuted` · Disposition: `refute-skip` · Lens: thermo

Finding itself is refuted, not the observation: the file really is 961 lines, but hard constraint 2 forbids proposing new seams, and the split is a no-behavior-change reorg whose only beneficiary does not exist yet.

#### F035 — Request allowlist admits three top-level fields nothing sends and nothing reads

File: scripts/lib/protocol.js
Severity: nit
anchor: const requestKeys = ['protocol', 'skill', 'operation', 'payload', 'task_kind', 'scope', 'target', 'settings', 'invocation', 'brief'];

- Verdict: `refuted` · Disposition: `refute-skip` · Lens: deslop

The finding itself is refuted: the code is deliberate and contract-tested, not dead. Deleting the entries would break test/issue-48.test.js:344 ('accepts every allowed top-level request key'), which sends all three fields through the wrapper seam and requires them to be accepted.

#### F044 — One module holds two unrelated responsibilities with disjoint consumers

File: scripts/lib/adapters.js
Severity: minor
anchor:  * Also the one at-most-once orientation claim cycle every harness adapter

- Verdict: `refuted` · Disposition: `refute-skip` · Lens: thermo

Finding refuted on YAGNI/churn grounds, not because the description is wrong: the two halves really do live in one file, but the current code is acceptable and the proposed split is unrequested restructuring that would rename a require path the installed adapter copy resolves at runtime. It also conflicts with the fix accepted for the harness-enum finding.

#### F110 — A gate test opens by asserting that its own local helper works

File: test/issue-51-policy.test.js
Severity: nit
anchor:   assert.equal(hasForbiddenMechanic('const followSuccessor = true;', 'follow'), true);

- Verdict: `refuted` · Disposition: `refute-skip` · Lens: deslop

Finding itself is refuted: the current code is right. The scan loop is vacuously true (no forbidden term occurs anywhere in scripts/), so this positive assertion is the sole guard against the regex silently passing everything — it is load-bearing, and the deslop lens explicitly refutes load-bearing targets.

#### F128 — README claims each installed skill carries its own copy of the runtime; the shipped layout is a symlink

File: README.md
Severity: major
anchor: its own copy of the runtime under `<skill-root>/scripts/`, and each `SKILL.md`

- Verdict: `refuted` · Disposition: `refute-skip` · Lens: docs

Finding refuted, not merely out of scope: the current README text is correct and the proposed fix would have made it wrong. defectReal=false because the documented property (installed skill independently executable, own runtime) is exactly what test/issue-96.test.js:21-24,75 encodes.

#### F135 — OpenCode "no addressable target directory" gap was closed later on this branch

File: docs/research/adapter-install-targets.md
Severity: major
anchor: **No single recommended target directory closes the gap. This is a recorded, evidenced,

- Verdict: `refuted` · Disposition: `refute-skip` · Lens: docs

REFUTED by hard constraint 7 (OpenCode recorded as an evidence gap in this research record is correct and must not be reported as a defect; decision 85 ships the adapter without a readiness claim). The manifest observation is accurate but the finding's thesis and fix conflict with the recorded decision.

#### F136 — Quoted OpenCode installs list does not match the shipped manifest

File: docs/research/adapter-install-targets.md
Severity: major
anchor: **The conflict.** `adapters/opencode/manifest.json`'s `installs` list places `config.json`,

- Verdict: `refuted` · Disposition: `refute-skip` · Lens: docs

REFUTED by hard constraint 7. The quoted installs list is indeed stale against adapters/opencode/manifest.json, but the finding reports the recorded OpenCode evidence gap as a defect and its fix removes it, which the recorded decision forbids.

#### F139 — Recommendation says the Flue cost is too high before v0.1.0; the branch adopted it before v0.1.0

File: docs/research/flue-skill-evaluation.md
Severity: major
anchor: and a new agent application. This cost is too high for one supplemental smoke

- Verdict: `refuted` · Disposition: `refute-skip` · Lens: docs

REFUTED: the document is already correct — it explicitly permits optional non-gating Flue developer tooling two lines later, and the cost judgement is a declined recommendation, not a false claim about the branch.

#### F140 — The record never states the in-process option it is cited as justifying, and mis-states its transcript capability

File: docs/research/flue-skill-evaluation.md
Severity: major
anchor: better than the existing manual real-harness run for release evidence.

- Verdict: `refuted` · Disposition: `refute-skip` · Lens: docs

REFUTED: the document is already correct. The criticized sentence is scoped to the `flue run --json` CLI variant, and the in-process `start()`/`init()` API is documented at lines 55 and 77 — the reviewer misread the subsection's scope.

#### F141 — "No package.json for the first evaluation slice" is falsified by eval/

File: docs/research/langfuse-coding-agent-skill-evaluation.md
Severity: major
anchor: **Recommendation:** Do not add a `package.json`, Langfuse SDK, OpenTelemetry

- Verdict: `refuted` · Disposition: `refute-skip` · Lens: docs

REFUTED: declined recommendation scoped to the Langfuse runner/publisher, not an assertion about what the repository contains. Every operative clause of the recommendation still holds.

#### F142 — Final recommendation says not to adopt Flue; the branch adopted it

File: docs/research/langfuse-coding-agent-skill-evaluation.md
Severity: major
anchor: of the shipped runtime and the deterministic release gate. Do not adopt Flue

- Verdict: `refuted` · Disposition: `refute-skip` · Lens: docs

REFUTED: the document is already correct — the reviewer dropped the "for this task" scope, which limits the clause to driving the three native CLIs, something eval/ still does not do.

#### F143 — Summary table still reports the closed OpenCode gap

File: docs/research/adapter-install-targets.md
Severity: minor
anchor: | opencode    | *(none — recorded gap)*     | n/a         | Verified evidence found; the manifest's flat file layout is not fully addressable by any one directory. See above. |

- Verdict: `refuted` · Disposition: `refute-skip` · Lens: docs

REFUTED by hard constraint 7 — the summary row records the same OpenCode evidence gap the constraint declares correct.

#### F144 — Repository-local runner is deferred to v0.2.0 but shipped in v0.1.0

File: docs/research/agent-skill-evaluation-automation.md
Severity: minor
anchor: - For `v0.2.0`, add an optional repository-local runner only if repeated manual

- Verdict: `refuted` · Disposition: `refute-skip` · Lens: docs

REFUTED: declined recommendation plus a hypothetical layout sketch. The document is already correct on its own terms.

#### F150 — Comparison table records Flue as "Not required"

File: docs/research/langfuse-coding-agent-skill-evaluation.md
Severity: minor
anchor: | Best use now | Required base | Optional second layer | Not required | Research only |

- Verdict: `refuted` · Disposition: `refute-skip` · Lens: docs

REFUTED: the document is already correct — eval/ is non-gating optional tooling, which is what "Not required" says; and the neighbouring cell is a recommendation the project has not yet acted on.

## Fix lanes

- **L1** — chunks docs-public-and-proof · 7 findings · 6 owned file paths
- **L2** — chunks docs-research, eval-harness · 20 findings · 17 owned file paths
- **L3** — chunks docs-spec-and-context · 9 findings · 3 owned file paths
- **L4** — chunks runtime-core, skills-and-bundle · 23 findings · 11 owned file paths
- **L5** — chunks runtime-navigation, runtime-validation · 22 findings · 8 owned file paths
- **L6** — chunks tests-adapters-delegation, tests-lifecycle-review-router, tests-postwrite-install, tests-read-navigation, tests-write-gate · 47 findings · 49 owned file paths
- **L7** — chunks wrappers-adapters-ci · 11 findings · 7 owned file paths
