Skip to content
This repository was archived by the owner on Oct 6, 2026. It is now read-only.

fix: the harness works outside the author's machine (spec 024, closed) - #3

Merged
prodelt merged 3 commits into
mainfrom
claude/harness-analysis-optimization-z5xtwm
Sep 4, 2026
Merged

prodelt merged 3 commits into
mainfrom
claude/harness-analysis-optimization-z5xtwm

Conversation

@prodelt

@prodelt prodelt commented Sep 4, 2026 •

Copy link
Copy Markdown
Owner

Summary

The first audit of this harness run on Linux rather than Windows. On Linux and macOS — the only server platform, the one the harness is declared a core for — none of the three links in its own promise held, and none of the three reported a failure. Spec 024 is closed: ten defects, all reproduced, each fix shipping the regression that was missing.

before after
node tools/elt-oracle-runner.js --full 110/112, exit 1 118/118, exit 0
node tools/elt.js oracle exit 1 (0s), no reason printed full run, real exit code
elt commit with the shipped hook enabled impossible by construction passes
a suite run inside the gated project wrote 5 fabricated ctx7 proofs hermetic: tree byte-identical
a hung judge in background verify exit 0, no run-log, no queue row non-zero, terminal record written
a refusal read by an unattended agent exit 4 for four different causes a stable reason slug per cause
.harness/harness.json 8 of 33 fields validated every field typed; typos rejected

Why it could not work outside Windows

  • T001 (D29) — the platform is chosen, not assumed. The shipped config carried shell: "powershell"; sh() spawned it unchecked, and on ENOENT spawnSync returns status: null, which became an ordinary red oracle. The shell dispatcher existed in five copies, so a fix in one never reached the others. Now one module (tools/shell-run.js), default from process.platform, a missing interpreter names itself and the field to edit, and shell enters config validation.
  • T002 (D33) — a hung judge fails closed. timer.unref() in withJudgeTimeout defeated the guard's entire purpose: an unref'd timer does not hold the event loop, so with a judge that never returns Node drained the loop and exited before the timeout fired — try/finally never ran and the process returned zero. The same line hangs the guard's own regression and cancelled 13 tests after it. The benchmark grader got a preflight: a missing pytest is no longer indistinguishable from a wrong solution.
  • T005 (D32) — the gate lock actually hangs. .githooks/pre-commit was committed at mode 100644; git prints hint: ... not set as executable and commits anyway.

Why "green" did not mean "checked"

  • T003 (D30) — the tree proof is computed from content, not from git's output format. treeHash() hashed git status --porcelain lines verbatim, status column included. That column changes on staging (" M f" → "M f", "?? g" → "A g"), and elt commit runs git add -A exactly between minting the proof and re-checking it in the hook — so it could never match. Second half of the same root: .trim() on the whole stream ate the leading space of the first line, and line.slice(3) then truncated the path.
  • T004 (D31) — the oracle cache stopped being evidence. computeEntry was called without scanDirs while TEST_ROOTS has three entries, so editing bin/l0.js never moved the key of bin/l0.test.js — sabotaging the L0 entry point printed 65/65 passed in 2.4s. The cache also lived in a gitignored .harness/, invisible to treeHash. Now the roots are shared, RUNNER_VERSION covers the selection rules, the cache moved to .git/elt/, and the proof carries ran/cached — ran: 0 on a non-empty selection is refused as an absence of checking.
  • T006 (D34) — the suite stopped issuing permits to itself. The live ctx7 ledger was 75 rows, 100% fabricated, refreshed on every oracle run so the 30-day freshness window never expired, and substring matching let them cover ai, mcp, agents, playwright, cli and vercel.
  • T010 (D38) — three checks that could not fire. Two filtered the review queue on a resolved field nothing writes (rows close with closedAt) — in graph-state a settled row could block elt cutover forever. The --push guard read commitHash from certificates that carry commit; a naive rename would have flipped it to always-block, since the certificate stores a short sha and the check used the full one.

Why an outside developer, or a server agent, could not use it

  • T007 (D35) — package.json now exists, with engines, npm test, and the version as a fifth source in version-check (the root of D28).
  • T009 (D37) — a refusal is now readable by a machine. judge-invoke.js dropped r.reason, so reasons arrived empty on failure; judgeLog reached elt.js and went only to the run-log. The real cause lived in a file nothing printed — on a server that is --dangerously-skip-permissions cannot be used with root/sudo privileges. And exit code 4 covered 44 of 55 refusals. Renumbering would break drivers that branch on === 4, so the discriminator became a slug: die(msg, code, reason) plus {ok, code, reason, message} under --json or ELT_JSON=1. --json now covers commit, gate and judge run. Human output is unchanged byte for byte, and a regression asserts that.
  • T008 (D36) — the config is a contract. Eight of thirty-three fields were validated; the rest did not reject a typo, they changed the gate: redProof: "OFF" kept the circuit on, specApproval: "no" kept the approval gate on, batch: "three" silently became 3. Types are errors now; unknown keys are warnings until the next minor (SCHEMA_VERSION), because retired fields sit in existing configs. The schema was written from the code, which paid off immediately — its first version rejected working configs on branchPolicy, where an invented current disagreed with the real feature | none.

Coverage 112 → 118. Full evidence in specs/024-harness-scale-readiness/audit.md (E1–E22); D29–D38 closed and D39–D41 plus the D36 remainder opened in docs/DEFECTS.md.

Test plan

Measured on Linux, Node 22.22.2, clean tree. CI ran the same suite green on windows-latest and ubuntu-latest for every commit on this branch.

node tools/elt-oracle-runner.js --full   # 118/118 passed in 49.3s — exit 0
node tools/gen-agents-md.js --check      # копии совпадают с CLAUDE.md — exit 0
node bin/doctor.js                       # exit 0
node tools/version-check.js              # ok — 5.0.1 (five sources)

Hermeticity verified separately: git status --porcelain -uall hashed before and after a full run is byte-identical, and .harness/ctx7-proof.jsonl is no longer created.

The branch's smallest regressions:

  • tools/elt-tree-hash.test.js — discriminating, verified by reverting the fix: 3 of its 5 assertions go red on the pre-024 elt.js. The other two guard against over-fixing — a real content change after the proof must still be refused.

  • tools/shell-run.test.js — a missing interpreter is named rather than reported as a red oracle; the shipped config carries no hard powershell; no second dispatcher copy may reappear.

  • tools/supply-exec-bit.test.js — every file under .githooks/ is executable in git's index (checked there, not on the filesystem, so it holds on Windows too).

  • tools/elt-oracle-cache.test.js — editing a source under bin/ or benchmarks/ moves its test's cache key; SCAN_DIRS and TEST_ROOTS may not diverge again.

  • tools/elt-commit-proof.test.js — a proof with ran: 0 is refused on both the plain and the trusted gate path; an old-schema proof is refused by name.

  • tools/context7-cli.test.js — an injected runner mints no proof; coverage matches package names exactly, and the free-text query no longer counts at all.

  • tools/elt-machine-readable.test.js — two refusals sharing exit 4 are told apart by slug; ELT_JSON=1 works without the flag; without --json stdout stays empty and stderr is unchanged; a dead judge prints the provider log.

  • tools/elt-dead-checks.test.js — each of the three revived checks is asserted to fire and not to fire where it shouldn't.

  • tools/harness-schema.test.js — the exact config that used to validate is now rejected; retired fields are named as retired; the repo's own config passes with zero warnings.

  • tools/review-runtime.test.js — a sixth lens no longer kills the judge, while a missing declared lens still does.

  • full suite exits 0

  • instruction drift check exits 0

  • the smallest regression for this branch exists (or the change is trivial)

🤖 Generated with Claude Code

https://claude.ai/code/session_01ADgXkZxYt9KBYyjnPGCQeF

prodelt and others added 2 commits September 4, 2026 05:46
Первый аудит харнеса, сделанный НЕ на Windows. На Linux и macOS — единственной
серверной платформе, ради которой харнес заявлен ядром для автоматических
агентов, — не работало ни одно из трёх звеньев обещания «не пускать
непроверенное в main», и ни одно не сообщало об отказе.

Замер до/после (Linux, Node 22.22.2):

  elt-oracle-runner --full     110/112 exit 1  ->  115/115 exit 0
  node tools/elt.js oracle     exit 1 (0s)     ->  полный прогон, настоящий код
  elt commit под своим хуком   непроходим      ->  проходит
  прогон сьюта в дереве        +5 фиктивных    ->  герметичен
                               ctx7-пруфов

T001 (D29) Платформа выбирается, а не предполагается. В поставочном конфиге
стоял shell:"powershell", sh() спавнил его без проверки, а ENOENT давал
status:null -> обычный красный оракул. Диспетчер шелла был скопирован пять раз;
теперь он один (tools/shell-run.js), дефолт из process.platform, отсутствующий
интерпретатор называет себя, поле shell входит в валидацию конфига.

T002 (D33) Зависший судья фейлится закрыто. timer.unref() в withJudgeTimeout
отменял весь смысл гарда: таймер не держал event loop, Node выходил ДО его
срабатывания, try/finally не выполнялся — ни run-log, ни строки очереди, — а
процесс возвращал НОЛЬ. Она же виснет в собственной регрессии гарда и уносила
13 тестов. Грейдер замера получил preflight: отсутствие pytest больше не
неотличимо от неверного решения.

T003 (D30) Пруф о дереве считается от содержимого, а не от формы вывода git.
treeHash хешировал колонку статуса, которую меняет индексация, а elt commit
делает git add -A ровно между записью пруфа и сверкой в хуке — совпадения не
было никогда. Плюс .trim() всего потока съедал ведущий пробел первой строки, и
slice(3) отъедал символ от пути.

T004 (D31) Кэш оракула перестал быть доказательством. computeEntry звался без
scanDirs при трёх корнях: правка bin/l0.js не сдвигала ключ bin/l0.test.js.
Сам кэш лежал в гитигнорённом .harness/, невидимом для treeHash. Теперь корни
общие, RUNNER_VERSION включает правила выборки, кэш в .git/elt/, пруф несёт
ran/cached, и ran:0 при непустой выборке отвергается гейтом.

T005 (D32) Замок гейта висит по-настоящему: .githooks/pre-commit был
закоммичен режимом 100644, git печатал hint и коммитил.

T006 (D34) Сьют перестал выписывать разрешения сам себе: живой ctx7-реестр был
75 строк 100% фабрикации, и подстрочный матч покрывал ими ai, mcp, agents,
playwright, cli, vercel.

T007 (D35) package.json появился: engines, npm test, version пятым источником
version-check (корень D28).

Охват 112 -> 115: три регрессии на дыры, у которых её не было. elt-tree-hash
краснеет на трёх утверждениях из пяти на коде до спеки.

Открыто и записано: D36-D41 (docs/DEFECTS.md), доказательства —
specs/024-harness-scale-readiness/audit.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADgXkZxYt9KBYyjnPGCQeF
Харнес заявлен ядром в том числе для агентов на автоматических серверах.
У такого агента ровно два канала — код выхода и stdout, — и оба молчали
о причине.

D37, три независимых потери причины:

1. judge-invoke.js:68 не пробрасывал r.reason: при runOk:false поле reasons
   уезжало ПУСТЫМ, и наружу шло verdict:dead, reasons:["judge dead"].
2. elt.js получал judgeLog и писал его только в run-log. На экране оставался
   exit 4 без причины; настоящая строка лежала в логе провайдера, который
   никто не открывал. Живой случай на сервере под root:
   "--dangerously-skip-permissions cannot be used with root/sudo privileges".
3. Код 4 стоял на 44 отказах из 55: "задача не найдена", "спека не подписана",
   "пруф протух" и "судья мёртв" приезжали одним числом.

Переномеровать 44 места нельзя — существующие драйверы сверяются с === 4.
Поэтому различителем стал slug, а не код:

  die(msg, code, reason)  ->  {"ok":false,"code":4,"reason":"spec-unapproved",
                               "message":"..."}

Конверт печатается под --json или ELT_JSON=1 (канал для хуков и
harness.json.oracle, куда argv не дописать). Текстовый вывод не изменился
ни на байт — конверт это добавка, а не замена, и регрессия это утверждает.

--json появился у commit, gate и judge run — ровно тех трёх команд, вокруг
которых крутится автоматика и у которых его не было.

Судья-мертвец теперь печатает машинную причину, путь к логу провайдера и его
хвост. Таблица кодов и причин — новый раздел "Running it headless" в
docs/USAGE.md, там же список того, что вообще работает без agent CLI.

Охват 115 -> 116: tools/elt-machine-readable.test.js.
Полный оракул на Linux: 116/116, exit 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADgXkZxYt9KBYyjnPGCQeF
@prodelt

This comment was marked as resolved.

… T010)

Три места, каждое из которых выглядело работающей защитой (D38).

1. Очередь ревью фильтровалась по полю `resolved`, которого не пишет НИКТО:
   строки закрываются меткой `closedAt` (elt review close). `unresolvedReview`
   в `elt run` не убывал никогда; в graph-state.js разобранная строка навсегда
   оставалась неоднозначностью и могла навсегда заблокировать `elt cutover`.

2. Гард `--push` читал у сертификата `commitHash` — поле только РЕЛИЗНЫХ
   сертификатов (certification.js:224); батчевые несут `commit` (:162), поэтому
   условие было всегда ложным и привязка к коммиту не проверялась ни разу.
   Наивное переименование переключило бы гард в "всегда блокировать": в
   сертификат едет КОРОТКИЙ sha (rev-parse --short), а сверка брала полный.
   Сравнение стало префиксным по длине записанного — короткий против полного,
   полный против полного, чужой блокируется.

3. review-runtime требовал РОВНО пять линз (lenses.length !== LENS_NAMES.length),
   поэтому ШЕСТАЯ давала тот же dead, что и недостача. Форк, добавивший
   собственную линзу, получал мёртвого судью на КАЖДОМ слайсе — единственная
   объявленная точка расширения ломала харнес целиком. Набор закрыт снизу:
   все объявленные обязаны присутствовать, лишние работают наравне. Недостача
   по-прежнему смертельна и теперь называет имена, а не только количество.

Охват 116 -> 117: tools/elt-dead-checks.test.js плюс новый кейс в
review-runtime.test.js. Каждый тест утверждает и что защита срабатывает, и что
она не срабатывает там, где не должна.

Полный оракул на Linux: 117/117, exit 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADgXkZxYt9KBYyjnPGCQeF
@prodelt
prodelt merged commit 334c31b into main Sep 4, 2026
4 checks passed
@prodelt prodelt changed the title fix: the harness works outside the author's machine (spec 024, T001-T007) fix: the harness works outside the author's machine (spec 024, closed) Sep 4, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant