Commit Graph

390 Commits

Author SHA1 Message Date
Дмитрий e3a1c84ea9 fix(m5): F-6 — нижняя граница времени в floor-decide approvalOpen
approvalOpen считал свежим одобрение с будущим ts (now - ts < 0 <= window) — часовой
сдвиг/подлог открывал дверь владельца. Добавлена нижняя граница now - ts >= 0: свежесть =
ts в прошлом И в пределах окна.

Аудит Машины 5 (объектив корректность). TDD RED->GREEN. Регрессия tools-only 2789 + 2 skip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 16:18:44 +03:00
Дмитрий ce48f3e9b5 fix(m5): F-3 — манифест floor-manifest-check проверяет весь защитный контур
DEFAULT_REQUIRED_HOOKS проверял только enforce-floor — owner мог зарегистрировать пол,
забыть верховную стену / exfil-стражей и получить зелёный «protected». Расширено до
security-load-bearing набора: enforce-floor + enforce-supreme-gate + normative-content
+ read-path-deny + mcp-classification. «Пол подтверждён» теперь = весь контур. WARN-only
(Δ8 — сигнал, не блок); owner может передать иной requiredHooks.

Аудит Машины 5 (объектив sharp-edges). TDD RED->GREEN. Регрессия tools-only 2789 + 2 skip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 16:18:26 +03:00
Дмитрий 37ce771eb1 fix(m5): F-1 — убрать мёртвый контент-скан в enforce-read-path-deny
decide() гейтил по content в ветке, недостижимой в проде: enforce-read-path-deny —
PreToolUse(Read)-хук, main() не передавал content, а контента до чтения нет. Ветка
+ импорт scanSecrets убраны — decide() гейтит строго по пути (path-deny). Реальный
exfil (исходящий payload) закрыт живым enforce-mcp-classification.scanEgress; чтение
секрета само по себе не вынос.

Аудит Машины 5 (объектив sharp-edges + agentic-actions-auditor). TDD RED->GREEN.
Регрессия tools-only 2789 passed + 2 skip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 16:18:05 +03:00
Дмитрий a0a73d8d3a feat(m5): Пакет 8 — Δ3 честный двухтакт reconcile (8.1 пред-запись + 8.2 реконсилер)
Δ3: убрано обещание «атомарно на исполнении» (PreToolUse не видит факт). Достижимый максимум —
два такта:
- 8.1 (runGate): пред-запись НАМЕРЕНИЯ в журнал ДО allow. Журнал вернул false ИЛИ бросил →
  стена НЕ разрешает (block), указатель не двигается («нет записи → нет действия», явно).
  Backward-compat: push → length (truthy) = успех; только явный false/throw → block.
- 8.2 (enforce-reconcile.mjs, новый): PostToolUse-сверка. reconcileAction — исполненное
  действие без пред-записи → action-without-record (возможен обход). findOrphanIntents —
  пред-записи без исполнения → record-without-action. WARN-уровень (не блок: PreToolUse-пол
  уже отработал, PostToolUse не отменяет исполненное). Чистые функции + fail-quiet I/O main.

+2 (supreme-gate runGate) +5 (reconcile) тестов. Полная tools-only регрессия 2788 + 2 skip
(0 регрессий). Машина 5 (Пакеты 5-8) собрана полностью.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 15:29:48 +03:00
Дмитрий 47b61b22bc feat(m5): 7.6 инвариант разделения дисциплина/защита (Пакет 7, Блок 4.6)
Структурный guard-инвариант: модули защиты-пола GREEN (criterion-green, floor-signer)
не несут override-вокабуляра — мутация P18 + неподделываемый по-критерийный GREEN (Пакет 5)
это ЗАЩИТА (без override). tdd-gate остаётся ДИСЦИПЛИНОЙ (fail-open + override) в ОТДЕЛЬНОМ
хуке. Перенос мутации/GREEN-пола под override упадёт здесь. +positive control (не вакуумен).
7.7 (неподделываемый GREEN/RED) — закрыт Пакетами 3+5.

Guard-тест (prod-код не пишется; концепт реализован Пакетом 5). +3 теста.
Полная tools-only регрессия 2781 passed + 2 skip (0 регрессий). Пакет 7 завершён.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 15:24:52 +03:00
Дмитрий 1a2ead4704 feat(m5): 7.5 parallel-session-lock pid-liveness (Пакет 7, дыра 5, Блок 4.5)
Закрыта дыра 5: свежий (не-stale) лок МЁРТВОГО держателя блокировал на весь TTL.
- isProcessAlive(pid): process.kill(pid,0) — ESRCH→мёртв, EPERM→жив; невалидный pid→мёртв.
- acquire +isPidAlive (инъектируемый): перехват, если держатель не жив (помимо stale/same-session).
  Без isPidAlive — backward-compat (старое поведение).
- runAcquireDecision +isPidAlive (default isProcessAlive) → acquire. Живой держатель → блок;
  мёртвый → перехват. Хук остаётся fail-open (availability, не защитный пол).

+5 (pure) +1 (wrapper) тестов; существующий «fresh lock» тест уточнён на живого держателя.
parallel-session-lock + enforce 27/27.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 15:23:15 +03:00
Дмитрий a0e8608fec feat(m5): 7.4 workflow-gate inline-script + keychain-паттерны (Пакет 7, C5, Блок 4.4)
Закрыта дыра C5 (inline `script` был вне охвата → полный обход workflow-гейта):
- decide: inline `script` гейтится как scriptPath — скан DANGEROUS_PATTERNS + одобрение по
  sha256 контента (approve_workflow_script, 5-мин окно). Без одобрения → block.
- DANGEROUS_PATTERNS +keytar +get/set/delete/findPassword|Credentials(Sync) — контроллер
  не напишет workflow, читающий ключ подписанта/судьи из OS-keychain (действует и для scriptPath).
- main: при inline берёт content/sha256 из tool_input.script напрямую.
NB: regex-детектор eval()/child_process — паттерн-матчер, не вызов (header-коммент L19-21).

+5 тестов. enforce-workflow-gate 10/10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 15:18:46 +03:00
Дмитрий 3bb071aa30 feat(m5): 7.3 mcp-classification +egress-скан исходящего (Пакет 7, Блок 4.3)
scanEgress(toolInput): даже разрешённый классификатором MCP-вызов не должен выносить
наружу — block при: секрет в аргументах (общий secret-scan, анти-дрейф с 7.1), data:base64
URI / file:// схема / IP-литерал URL (обход DNS/allowlist), раздутый payload (> maxBytes).
exfil-схемы узко-таргетированы (data:...;base64, и file://) — обычный текст не ловят.
decide: после allow-классификации запускает scanEgress → block с reason «egress: …».

+8 тестов. enforce-mcp-classification 10/10. Дыра «что уходит наружу» закрыта.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 15:15:49 +03:00
Дмитрий c73fc9209a feat(m5): 7.2 normative-content детерм-only + fail-CLOSE (Пакет 7, H3, Блок 4.2)
Закрыта дыра H3 (LLM fail-open / wrapper fail-quiet как escape):
- isProtectiveNormativePath: защитное подмножество CLAUDE.md/память/Pravila.
- decide +protectiveSubset: деградация судьи, который БЫЛ активен (budget_exhausted —
  вектор обхода: исчерпать бюджет чтобы пройти), → fail-CLOSE для защитного подмножества.
  Деградация no_api_key (судья выключен — дефолт) → детерм-only флор (fail-open), БЕЗ
  over-block легитимного claude-md-management в обычном режиме (reason-based различение).
- main() catch → fail-CLOSE (block) вместо fail-quiet; передаёт protectiveSubset.
- «Память = совет, НИКОГДА не разрешение» — детерм-слои (recovery/fake-rule/suspicious)
  держат всегда, до судьи.

Backward-compat: существующий fail-open-degraded тест (без protectiveSubset) сохранён.
+9 тестов. enforce-normative-content-rules 25/25.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 15:13:02 +03:00
Дмитрий 94d07e29aa feat(m5): 7.1 read-path-deny +контент-скан секретов + общий secret-scan (Пакет 7, Блок 4)
Новый общий secret-scan.mjs (анти-дрейф — один источник секрет-паттернов на 7.1 read-выдачу
и 7.3 egress): scanSecrets(text) → {found, hits}. Секрет-подмножество (не PII): PEM private
keys, токены провайдеров (AWS/GitHub/OpenAI/Slack/Sentry/Yandex/JWT/Bearer, regex согласованы
с observer-pii-filter), строки подключения с кредами (scheme://user:pass@). Чистая, без /g.

enforce-read-path-deny.decide расширен опциональным content: путь-деналист — грубый пре-фильтр;
если выдача Read содержит секрет (даже из не-protected пути) → block (fail-CLOSE). Активируется
PostToolUse-обёрткой (content); PreToolUse path-слой backward-compat не тронут.

+9 (secret-scan) +4 (read-path-deny) тестов. Дыра 6 (read без контента) закрыта.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 15:06:53 +03:00
Дмитрий e510037484 feat(m5): Блок 5 — сквозные инварианты N5/Δ9 + манифест-WARN Δ8 (Пакет 6)
Guard/инвариант-тесты над существующим кодом (новый prod-код только в 6.5) +
SessionStart-самопроверка регистрации. Все non-vacuous (positive controls).

6.1 (N5) — строгая проверка пола: поведенческие инварианты запирают фиксы аудита
  M1-M4 (finalGate снимает вето ТОЛЬКО на floorBlocked===false; runGateLadder не
  проходит на ok!==true / undefined / throw) + структурный grep по judge-orchestrator
  (центр «по всем полам разом»). Scope grep'а сужен до orchestrator — токенайзерные
  .ok в floor-decide НЕ floor-вердикты (избегаем ложного срабатывания).
6.2 (Δ9-а) — анти-усыхание floor-набора: снимок 11 команд, обязанных оставаться
  floor:true; удаление строки FLOOR_RE без ADR флипнёт одну → CI краснеет. +control.
6.3 (Δ9-б) — единственный источник DESTRUCTIVE_RE: уже в seed Пакета 1 (подтверждено).
6.4 (escape≠protection) — floor-хуки (enforce-floor/floor-decide) не ссылаются на
  override-вокабуляр; перенос защиты под override падает здесь. +control.
6.5 (Δ8) — floor-manifest-check.mjs (новый): SessionStart читает settings.json,
  cry-WARN при отсутствии регистрации пол-хука. НЕ блок (проблема черепах: «пол стоит»
  = сигнал, не гарантия). Чистое ядро collectHookCommands/checkManifest (settings
  инъектируется, битые секции не бросают) + fail-quiet I/O main (exit 0).

+27 тестов (21 инвариант + 6 манифест). Полная tools-only регрессия 2741 + 2 skip
(0 регрессий). Активация манифест-хука в settings.json — шаг владельца.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 15:01:25 +03:00
Дмитрий e2ebc920ce feat(m5): criterion-green — по-критерийный производитель GREEN + мутация P18 (Пакет 5, 5.6)
Новый чистый модуль criterion-green.mjs. produceGreen эмитит подписанный подписантом
green ТОЛЬКО при настоящем зелёном прогоне: оба условия обязательны —
- testPassed (тест шага реально прошёл);
- mutationKilled (P18: сломали код → тест обязан покраснеть; выжил → не проверяет → не green).
Иначе green:false с причиной (test-not-passed / mutation-survived / no-signer-key) — fail-CLOSE.
Подпись — над тройкой {criterion_id, code_fingerprint, occurrence}; green/coverage — поля.
codeFingerprint(fileContents) — Δ2 детерминированный отпечаток изменённых файлов+тестов
(canonicalJson сортит ключи; правка файла → другой отпечаток → green аннулируется).
Чистый: факты прогона + ключ инъектируются (fs/исполнение тестов — живая обёртка владельца).

+7 тестов (вкл. интеграцию: произведённый green проходит весь критерий-гейт 5.5).
Полная tools-only регрессия 2714 passed + 2 skip (0 регрессий).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 12:54:22 +03:00
Дмитрий 5ad4688f77 feat(m5): критерий-гейт = лесенка из 4 шагов (Пакет 5, 5.5, Δ6)
Δ6: НЕ плодим criterionFullyProven — критерий-гейт = РОВНО 4 проверки поверх
существующей runGateLadder. criterionGateSteps(input) → [criteria-from-sealed-plan,
criteria-green-matched, fingerprint-fresh, green-signatures-valid] (И-семантика:
id ∈ печать → green-присутствие → свежесть отпечатка Δ2 → подпись подписанта Δ5).
runCriterionGate гоняет их через runGateLadder (короткое замыкание на первом провале).
Структурный тест «ровно эти 4 шага в порядке» — лекарство «забыл шаг». End-to-end:
валидный сценарий (signGreen) проходит; по одному провалу на каждый шаг.

+7 тестов (2 структура + 5 И-семантика). Полная tools-only регрессия 2707 + 2 skip
(0 регрессий; import judge-gate-floor, цикла нет).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 12:51:11 +03:00
Дмитрий 052703f4d0 feat(m5): greenSignaturesValid — подпись подписанта на green (Пакет 5, 5.4, Δ5)
Δ5: подлинность green = подпись ПОДПИСАНТА, не совпадение id (id = целостность).
greenSignaturesValid реконструирует подписанную тройку {criterion_id,
code_fingerprint, occurrence} из green-run и проверяет verifyGreen (floor-signer).
Синергия с 5.3: подмена отпечатка для прохода свежести ломает подпись здесь
(отпечаток входит в подписанную тройку). Чистая, fail-CLOSE (нет ключа/sig →
unsigned). Красные прогоны подписи не требуют (их ловит criteriaGreenMatched).

По авторитетному Δ6 — ОТДЕЛЬНЫЙ шаг 4 лесенки критерий-гейта.

+5 тестов. judge-gate-floor 37/37 (аддитивно; import floor-signer, цикла нет).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 12:48:13 +03:00
Дмитрий 05527182fa feat(m5): fingerprintFresh — свежесть отпечатка green (Пакет 5, 5.3, Δ2)
Δ2: зелёный прогон засчитывается только если его code_fingerprint совпадает с
текущим (изменённые файлы шага + тесты; currentFingerprints инъектируется живым
гейтом). Правка файла после прогона → расхождение → green аннулирован (stale).
Чистая функция, fail-CLOSE (нет текущего отпечатка ≠ записанному → stale). Красные
прогоны не проверяются (их «не-зелёность» ловит criteriaGreenMatched).

По авторитетному Δ6 — ОТДЕЛЬНЫЙ шаг лесенки критерий-гейта (не внутрь
criteriaGreenMatched, которая остаётся «green-присутствием», Δ6 шаг 2).

+5 тестов. judge-gate-floor 32/32 (чисто аддитивно).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 12:45:12 +03:00
Дмитрий 4e69a3bc0c feat(m5): механическая значимость шага в k5CriterionCheck (Пакет 5, 5.2, C3/F18)
Значимость шага больше НЕ из контроллер-флагов trivial/significant (их подделка
разжаловала значимый шаг и обнуляла K5). Новый isSignificantStep(step):
- object разрушителен (classifyDestructive.suspicious) → значим, перевешивает op
  (анти-обход «op:Read, object: rm -rf»);
- op НЕ из READONLY_OPS (Read/Grep/Glob/LS/NotebookRead) → значим (мутирующий/
  неизвестный/пустой op → сомнение → значим, fail-CLOSE);
- чистое чтение → не значим, критерий не требуется.
k5CriterionCheck вызывает isSignificantStep вместо trivial/significant-скипов.

+7 тестов. Полная tools-only регрессия 2690 passed + 2 skip (0 регрессий).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 12:42:36 +03:00
Дмитрий be3966dbad feat(m5): sealed criterion_id в plan-lock.freezePlan (Пакет 5, 5.1, Δ5)
Каждый шаг получает детерминированный criterion_id = sha256(canonicalJson
содержимого шага без criterion_id) ДО planId/печати → id запечатан подписью плана.
- stepCriterionId (чистая, идемпотентная: criterion_id исключён из хеша).
- sealedCriterionIds(frozenPlan) — вход для criteriaFromSealedPlan (Гейт-2, F3/F9).
- id = ЦЕЛОСТНОСТЬ, не подлинность (Δ5): подмена object/op меняет id; подмена id
  после печати ломает verifyFrozenPlan; подлинность зелёного даёт подпись подписанта (5.4).

+7 тестов. Полная tools-only регрессия 2683 passed + 2 skip (0 регрессий).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 12:38:59 +03:00
Дмитрий 478a9fd8d4 feat(m5): запрет env-fallback ключа + сужение node-whitelist (Пакет 4, Блок 2 RISK, Δ4)
4.1 env-fallback УБРАН (receipt-key-config + judge-key-config):
- resolveReceiptKey/resolveJudgeKey читают ТОЛЬКО keychain; ROUTER_MENTOR_*_KEY env
  больше не читается (анти-казуальная утечка через env/логи; шов под HSM). `env` убран
  из сигнатуры (extra-проп игнорируется); KEY_ENV сохранён в _internals (domain-тест R-31).
- Критерий: env задан, keychain пуст → null.

4.2 node-whitelist сужен (enforce-router-gate, RISK):
- широкое `node <любой-файл>` правило удалено из SAFE_EXACT; новая ветвь classifyWhitelist
  через nodeScriptAllowed: allow только плоский tools/<file>.{mjs,cjs,js} + vitest-runner
  + node --version/-v; иначе default-deny. Инспекция ТОКЕНА пути (shell-quote сохраняет
  бэкслеши Windows-пути — инвариант запиннен тестом), нормализация \\ → /.
- verify (живой): после правки `node vitest.mjs` отработал по всем 147 тест-файлам —
  тул-чейн не сломан (canary). Гейт жив в сессии (powershell+router фаеры подтвердили).

Δ4 честно (§7): сужение — анти-казуальная мера, НЕ полный close. Остаток «контроллер
commit'ит разрешённый tools-скрипт и запускает» принят; file-watcher блокирует
session-edited до commit. Полная изоляция ключа — только HSM/YubiKey (v4.2).

TDD RED-GREEN per задача; tools-only регрессия 2677 passed + 2 skip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 12:19:02 +03:00
Дмитрий 3795fcfe0a feat(m5): floor-signer + RECEIPT_DOMAINS.M5_GREEN + occurrence (Пакет 3, Блок 2)
- receipt-sign.mjs: +домен M5_GREEN (отдельный, не default; cross-domain тест)
- floor-signer.mjs (новый): signGreen/verifyGreen/acceptGreen — подпись по-критерийного
  GREEN в домене M5_GREEN, occurrence монотонный (анти-пере-зачёт одного green на 2
  критерия), подмена criterion_id/occurrence ломает HMAC → bad-signature.
- Δ4 честно: подписант = анти-казуальная утечка ключа + шов под HSM, НЕ защита ключа
  от мотивированного контроллера (node tools/X.mjs до Пакета 4); см. spec §7/§10.
- Чистые функции (ключ-аргумент), как receipt-sign.mjs; процесс-обёртка/резолв ключа —
  Пакет 4+; потребитель держит lastOccurrence — Пакет 5.
- TDD RED-GREEN; tools-only регрессия 2666 passed + 2 skip (+17).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 11:59:56 +03:00
Дмитрий 2cb3da88c5 feat(m5): Пакет 2 — несущий пол (floor-decide + enforce-floor + дверь Δ1 + Δ7 + C4)
Блок 1 Машины 5: вето-до-плана на необратимое, независимо от членства в плане.

- tools/floor-decide.mjs — чистое ядро: Bash floor (classifyDestructive whole-string +
  посегментно tokenizeBash — кавычки/chaining нейтрализованы) + tool-agnostic запись
  (P10-a: .env/ключ/cert + ~/.claude/runtime, fail-CLOSED на normalize-throw).
- Дверь владельца Δ1 — read-only approve_git_operation (exact+5мин окно, НЕ consume).
- tools/enforce-floor.mjs — обёртка matcher '*' (регистрация — шаг владельца, ОТДЕЛЬНО
  от стены М2), loadApprovedGitOps read-only, fail-CLOSED, НЕ импортирует plan-lock.
- C4: migrate:fresh/refresh/reset убраны из router-gate whitelist → default-deny даже
  без floor-хука (SPOF-защита); bare migrate + migrate:rollback остаются.
- Δ7: enforce-supreme-gate.decide на allow-пути зовёт classifyDestructive(...).floor —
  разрушительный in-plan шаг НЕ продвигает указатель (стена не благословляет снос).
- Атака-линза: закрыт P10-a-пробел (MCP-writer в .env floor бы пропустил).

Audit-context вскрыл расхождения план↔код (задокументированы в floor-decide JSDoc):
writer approval НЕ подписывает (интегрити = protected-path side-channel, не HMAC);
F5-гонка мнимая (loadApprovedGitOps read-only+window, не consume); force-push доп-блок
shell-content GIT_HARD (дверь для него мут — защита-в-глубину). Дверь = шов под М6.

Регрессия tools-only: 2649 passed + 2 skip (+41). Residual: node-whitelist hole
для записи в runtime (Пакет 4 сужает); base64-обфускация floor (~0.5%, М6).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 11:42:58 +03:00
Дмитрий af3e11b6cd feat(m5): classifyDestructive двухуровневый + rewire a2CaseSelect/detectHighRisk (§4, N1)
Пакет 1 Машины 5 (роутер-наставник, пол). Единый источник разрушительности
classify-destructive.mjs: floor (точный необратимый набор, hard-block) + suspicious
(грубый набор для голосов судьи), инвариант floor => suspicious.

- N1: голый migrate/migrate:rollback/migrate --force => suspicious, НЕ floor (деплой не ломается).
- rewire a2CaseSelect (M4) и detectHighRisk (M3) на classifyDestructive.suspicious;
  оба локальных DESTRUCTIVE_RE удалены (Δ9-б — единственный источник).
- Δ9(б) seed CI-инвариант m5-floor-invariants.test.mjs (positive-control, не вакуумный).
- sharp-edges (Step 1.9): floor force-push выровнен с каноном shell-content — закрыт
  обход кавычками git push "--force" (длинные флаги без обязательного \s; -f/+ с \s).
- parity к двум прежним regex сохранён (format/db:wipe/force-push-литерал).

Регрессия tools-only: 2608 passed + 2 skip (+48). Residuals (chaining/reset-quote)
переданы Пакету 2 (tokenizeBash посегментно).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 11:21:38 +03:00
Дмитрий ae81a52a99 fix(router-mentor): third audit (correctness lens) — close 3 M4 fail-open/crash holes
Третий построчный аудит машин 1-4 свежим объективом (корректность логики /
реальные баги — НЕ понимание, НЕ грабли; это были два прошлых прохода).
4 читающих под-агента code-analyzer. M1/M2/M3 — багов ядра нет (подтверждено).
M4 (судья, инертен; код должен быть верен и при включении): 3 реальные дыры по TDD.

M4:
- judge-engine.mjs runJudge: (raw.objections||[]).filter((o)=>o.verdict) падал на
  objections=[null] (o.verdict на null) и на не-массиве (.filter is not a function).
  || гасит только falsy. Краш ломал вердикт; в инертной обёртке выброс уходил в
  catch→block:false = fail-open. Fix: Array.isArray(...)?...:[] + (o && o.verdict).
- judge-verdict-slots.mjs: String(raw).trim().length скрывал не-строки — слот {}
  давал '[object Object]' (длина 15) и проходил как содержательный (мусорный
  объект/массив штамповал форму вердикта). Fix: слот обязан быть строкой
  (typeof raw !== 'string' → trivial). Мягкий fail-open формы закрыт.
- judge-orchestrator.mjs runGateLadder: step.run() без try/catch пробрасывал
  исключение упавшего шага пола вместо «пол не пройден» → решение неопределённо
  (в обёртке catch→block:false = fail-open). Fix: бросок шага = passed:false
  (fail-closed → блок), последующие не запускаются. Чистый модуль теперь сам
  гарантирует безопасную сторону, не полагаясь на обёртку.

Регрессия tools-only 2560 passed + 2 skip (+5 TDD-тестов, 0 регрессий).

Осознанно НЕ менялось (без призраков):
- M1 verifyChain без 3-го арг = нарушение контракта вызова, не валидный вход.
- M2 node-в-цепочке = то же разрешение, что одиночный node (контракт, тест L53);
  readonly-git-в-цепочке блок = осознанный default-deny (fail-safe).
- M3 defer уже защищён G-фиксом (if e.status!=='pending' return e — ДО defer);
  N3 stale-комментарий (код строже докстринга).
- M4-C DESTRUCTIVE_RE иллюстративен (divergence всё равно судится; разрушительный
  bash режется полом M2/M5 до судьи); M4-D slop-counter↔logVerdict — live-wiring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 08:11:12 +03:00
Дмитрий c4f63e36b5 fix(router-mentor): sharp-edges audit M1-M4 — close 8 misuse-resistance holes
Второй аудит машин 1-4 другим объективом (sharp-edges: устойчивость к
неправильному применению / мягкие умолчания / совпадение по пустоте-подстроке).
Криптоядра здоровы (подтверждено). 8 реальных дыр закрыты по TDD:

M3:
- coverage-machine F-1: покрытие считалось по двусторонней ПОДСТРОКЕ — produces
  "a" покрывал запрос "audit-rls-policy" (ложное «всё покрыто»). Новый tokensCover:
  точное равенство ИЛИ подмножество слов по границам. coveringSkill + coverageRegistry.
- router-engine F-8: confidence не проверялся на диапазон — 5/Infinity проходили как
  «уверен» (обход воздержания 5.2), -3 как принуд. abstain. validateTrace: [0,1] finite.
- round-control C: пустой roundKey="" активировал managed-режим (!= null) → все сессии
  делили один счётчик-бакет. Теперь managed требует непустую строку.
- router-learning-queue G: повторное approve уже-решённого id повторно клало запись в
  фонд (дубль). applyApprovalBatch: переводит только status==='pending'.

M2:
- plan-lock F5: шаг с пустым object был джокером (object:'' матчил действие, чей путь
  не извлёкся → object''). actionMatchesStep: пустой object шага не матчит ничего.

M4 (инертна; чистые fail-closed правки кода, корректны и при включении):
- judge-slop-counter H: битый/null вердикт в списке ронял счёт (v.missing на null).
  Теперь не крашит, считается халтурой (безопасная сторона).
- judge-engine J: consensusDecision на пустом/битом списке дрейфовал к GO. Теперь GO
  только если есть голоса И каждый чистый GO; иначе NO-GO (fail-closed для hard-risk).
- judge-orchestrator K: finalGate снимал вето пола на любой falsy floorBlocked
  (undefined от упавшей проверки = fail-open). Теперь снять может только явный false.

Регрессия tools-only 2555 passed + 2 skip (+15 TDD-тестов, 0 регрессий).

Осознанно НЕ менялось (без призраков):
- M1 receipt-sign domain default '' / разделитель пробел — backward-compat контракт
  (тест 18-19), инъективен на enum-доменах без пробелов.
- M1 action-journal атомарность записи головы + битая .jsonl строка — fail-closed
  (битьё → verifyChain ok:false → стена блокирует); чистого behavioral-теста нет.
- M3 round-control requiredSkills=[] — контракт вызывающего (пустой = не требуется).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 06:24:21 +03:00
Дмитрий c13a1a56a1 fix(router-mentor): close sessionId path-traversal class across M1-M4
Аудит M1-M4 (audit-context-building) нашёл непоследовательность guard формы
sessionId: N3-фикс защитил только action-journal.paths() (M1), а 4 sibling-
строителя пути из event.session_id (недоверенный источник) остались без проверки.

Единый экспорт assertSafeSessionId (action-journal.mjs, переиспользует SESSION_ID_RE
N3) применён во всех точках машин:
- M1 action-journal.paths() — рефактор на общий guard (поведение N3 сохранено)
- M4 judge-subrun-journal.paths() — guard добавлен (канал прилежности судьи F1)
- M2 plan-lock.planPath + artifactPath — guard добавлен
- M2 enforce-supreme-gate — экспортируемый guarded stepStatePath, применён в main()

TDD RED-GREEN на каждом файле. Регрессия tools-only 2540 passed + 2 skip (+10).
Серьёзность класса — низкая / защита-в-глубину (sessionId harness-controlled),
закрыт ради консистентности (был 1 из 5).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 05:41:07 +03:00
Дмитрий ded7525be7 fix(m1-foundation): verifyChain fail-closed на битой записи + sessionId path-guard
Аудит Машины 1 (audit-context-building), 2 находки low/defense-in-depth:
N2 — verifyChain больше не бросает TypeError на структурно-битой записи
(null / не-объект / массив, приходит из порченого .jsonl через loadJournal):
guard !e||typeof!=='object'||Array → {ok:false, brokenAt:null}.
N3 — paths() валидирует sessionId (/^[A-Za-z0-9_-]+/) до склейки пути
журнала → throw на ../ / \ . : закрывает path-traversal (fail-closed,
supreme-gate ловит внешним try/catch → block).

N1 (keytar getPasswordSync inert) и N4 (verifyChain/анти-откат — контракт M4)
не трогались: N1 — верное зеркало judge-key-config/llm-judge-config (env —
рабочий путь, fail-closed цел); N4 — deferred межмашинный дизайн.

TDD RED→GREEN, +7 тестов. Регрессия tools-only 2530 passed + 2 skip, 0 регрессий.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 05:03:08 +03:00
Дмитрий 1739b1de08 fix(m2): close supreme-wall holes F-A/F-B/F-C (audit 2026-06-07)
F-A (HIGH): Bash green-pass via reading-chain reason collapse — chain
  <reader> && <whitelisted-mutator> (composer pint / php artisan migrate:fresh
  / pest / npm test / node <script>) bypassed the wall. isObserveOnly now
  re-tokenizes and requires EVERY segment be a true reader (READING_CMDS) or
  a single readonly-git, not trusting the collapsed 'reading' reason.
F-B (minor): observe-only no longer choked when plan present but artifact
  missing/invalid (decideMode honors isObserveOnly; finding-9 invariant).
F-C (low): closed-door (C-5) ref-check moved out of the artifact_id guard —
  a step with ref must resolve in a sealed artifact even if plan has no
  artifact_id. TDD: RED proven per fix; full tools regression 2523 GREEN.
2026-06-07 04:33:17 +03:00
Дмитрий a29f8bdc10 feat(m2): export READING_CMDS as single source of true readers (F-A prep) 2026-06-07 04:32:56 +03:00
Дмитрий 157a52efae fix(m3): learning-queue — reject > approve при конфликте id (hard-rule без явного да НИКАК)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 04:00:06 +03:00
Дмитрий a6b61f5a62 fix(m3): skill-contract — neutrality сканирует все строковые поля inherent+skill (аудит F4)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 03:56:16 +03:00
Дмитрий 6525e44173 fix(m3): coverage-machine — пустой токен не покрывает запрос (аудит F3)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 03:56:10 +03:00
Дмитрий 3446f9e5dc fix(m3): round-control — managed-терминатор по roundKey, авто-инкремент круга (аудит F2)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 03:56:03 +03:00
Дмитрий 1e06b8ddfe fix(m3): router-engine — chosen обязан быть среди candidates (аудит F1)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 03:55:52 +03:00
Дмитрий e423cd70a1 fix(m4): audit closures — goal anchor + card guard + gate-1 coverage gate + sealed criteria + A2 router/packaging + reversibility doubt-blocks 2026-06-07 03:15:56 +03:00
Дмитрий 2683d22d5e feat(m4): judge — discipline floor + seal channel + gate floor + engine + orchestration + postfactum evaluator + inert hook wrapper 2026-06-06 03:07:38 +03:00
Дмитрий 7ee388a061 fix(m3): закрытие находок аудита — гард R4 + терминатор + footguns; F2/F3 как честный остаток 2026-06-05 17:32:45 +03:00
Дмитрий 11024823c4 feat(m3): граф зависимостей решений (#2) + дисциплина доменного навыка (#3) 2026-06-05 17:10:21 +03:00
Дмитрий ed720d11e4 feat(m3): разговорная фаза 2026-06-05 — вход роутера + контроль + выходная верность + честные контракты 2026-06-05 14:52:45 +03:00
Дмитрий e362fd24a6 fix(m2): указатель шага подписан (HMAC step-ptr) — подмена ptr → сброс (R-19) 2026-06-05 06:23:35 +03:00
Дмитрий 527acd7cf3 fix(brain): доменное разделение подписи — план/артефакт/журнал/расписка не взаимозаменяемы (R-31/J6) 2026-06-05 06:21:19 +03:00
Дмитрий 109e35fbc8 fix(m2): session_id из stdin-события, не из env — сессии больше не слипаются (R-28/J3) 2026-06-05 06:09:45 +03:00
Дмитрий d138bd5f0e fix(m2): указатель шага привязан к plan_id — перепечать сбрасывает указатель (R-27/J2) 2026-06-05 04:21:05 +03:00
Дмитрий 86b48a10ef fix(m2): decide самодостаточно проверяет печать артефакта (защита-в-глубину) (F2) 2026-06-05 04:05:22 +03:00
Дмитрий ee7e483e29 fix(m2): actionOf — поля объекта выровнены с B4 (ловит MCP filename/uri/destination) (F1) 2026-06-05 04:04:09 +03:00
Дмитрий 27a7cc9a18 fix(m1): extractPath — расширены path-поля (ловит MCP filename/uri/destination) (B4) 2026-06-05 03:42:40 +03:00
Дмитрий 558f0a4253 fix(m1): seq+ts входят в chain_hash журнала — подмена метаданных ломает цепь (B2) 2026-06-05 03:41:38 +03:00
Дмитрий 317682b59c feat(m3-d): контракты + look-ahead в промпт роутера + проброс runRouter (фикс-2) 2026-06-05 03:24:49 +03:00
Дмитрий 929ad55f2c feat(m3-d): нюх 5.3 + интервьюер 4.4 в промпт роутера (фикс-3) 2026-06-05 03:23:11 +03:00
Дмитрий cb01a482d0 feat(m3-a): checkContractNeutrality — G1 страж нейтральности этикетки (фикс-5, опц.) 2026-06-05 03:22:07 +03:00
Дмитрий 688ff61da2 feat(m3-a): dispatchContract — G3 детерминированная диспетчеризация точно|мягко (фикс-4) 2026-06-05 03:20:55 +03:00
Дмитрий 0c07530195 fix(m3-b): resolveNode заземляет skill-ref по префиксу (superpowers:X -> #19) — фикс-1 2026-06-05 03:18:26 +03:00