Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
9e90abd
docs(experiments): audit published numbers, retract three claims
Oct 6, 2026
5222967
experiments(crystal-delivery): freeze act list and ordering rule
Oct 6, 2026
6478657
experiments(crystal-delivery): D8 results - both changes behave as do…
Oct 6, 2026
897186e
docs(experiments): Exp 28 - D8 delivery check with maximal re-verific…
Oct 6, 2026
ee42297
docs(experiments): Exp 29 - trap bundle audit, design clean, headline…
Oct 6, 2026
a724ffe
feat(gates): delivery telemetry out of temp with per-evaluation decis…
Oct 6, 2026
070b8f7
docs(experiments): Exp 30 second wave - D9 telemetry, red team fixes,…
Oct 6, 2026
8f70f07
experiments(note-tone): freeze M4 voices and red-team, smoke scope only
Oct 6, 2026
0f88778
feat(gates): MSCODEBASE_ADVISORY_TEXT override for controlled voice e…
Oct 6, 2026
d9725ba
fix(gates): loader-safe plugin entry, length-matched M4 voices amendment
Oct 6, 2026
6196ea8
fix(gates): entry exports only default, helpers moved out of loader path
Oct 6, 2026
a762679
fix(gates): hooks never throw on malformed input, loader-proof test
Oct 6, 2026
7f7d736
fix(gates): helpers out of plugin dir, loader auto-loads every file
Oct 6, 2026
009f51a
experiments(note-tone): probe2 loader fixed, gate cli needs explicit …
Oct 6, 2026
7730211
experiments(note-tone): smoke 5 runs, rig works, no verdicts, measure…
Oct 7, 2026
4342259
fix(stale-detector): exclude frozen/ evidence from version drift
Oct 8, 2026
7d1913b
experiments(note-tone): reachability probe, 4 runs, scaling blocked
Oct 8, 2026
ac5945a
fix(gate): gate commits that use git global options
Oct 8, 2026
cb729d5
experiments(note-tone): v4 choice design, note steers agent 1.000 vs …
Oct 10, 2026
eb22241
experiments(note-tone): v5 phrasing contrast, no detectable effect
Oct 10, 2026
6bf42c6
experiments(tom-e1e2): independent re-derivation, 25/25 hashes, 400/4…
Oct 10, 2026
bfa8f80
experiments(tom-e1e2): deterministic delivery+order check, 30/30
Oct 10, 2026
a248c35
experiments(tom-e1e2): red team, 5 findings, history ledger
Oct 10, 2026
ef9d404
experiments: corrections after self-audit, overlap 416/416
Oct 10, 2026
0658efb
experiments(tom-e1e2): red team-2, five blocks from the TZ
Oct 10, 2026
5bf53ab
experiments(tom-e1e2): redteam-2 corrections after self-audit
Oct 10, 2026
78c5fad
experiments(note-tone): preserve full-30 raw evidence and leftover rigs
Oct 10, 2026
29bed60
experiments(tom-e1e2): red team-3 vectors + independent verification
Oct 10, 2026
7450c56
experiments(replication): harness fixes from pilot
Oct 10, 2026
d90015b
fix(rigs): kill process tree on timeout + incident diary
Oct 10, 2026
48a1b06
experiments(replication): sidefx merge, t3a exclusion, kill-tree
Oct 10, 2026
6cd1432
experiments(replication): R1 null, 44 runs, HELP 0.600 vs SHAM 0.529
Oct 10, 2026
1110d5c
experiments(behavior): #2-#5 results, classifier fix, artifact rule
Oct 10, 2026
5d8ad6f
experiments(replication): prohibitive arm, no effect, circuit closed
Oct 10, 2026
25864c5
experiments(note-tone): backfill artifact verdicts into arm/phrase rows
Oct 10, 2026
439e709
experiments(replication): all 68 run artifacts
Oct 10, 2026
578d057
docs(tom-e1e2): complete documentation, index, Tom comment draft
Oct 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
176 changes: 104 additions & 72 deletions .opencode/plugin/mscodebase_gate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,124 +9,156 @@
// (объективный) и явный MSCODEBASE_ISOLATION_GATE=block.
//
// Переключатели (env): MSCODEBASE_PY, MSCODEBASE_STALE_GATE=block|off,
// MSCODEBASE_ISOLATION_GATE=advisory|block|off, MSCODEBASE_GATE_LOG.
// MSCODEBASE_ISOLATION_GATE=advisory|block|off,
// MSCODEBASE_GATE_LOG (явный путь к JSONL),
// MSCODEBASE_ADVISORY_TEXT (M4 override: фиксированный текст
// advisory-заметки вместо вывода гейта; additive, default-off),
// MSCODEBASE_DATA_DIR / BASE_INDEX_DIR (корень данных для пути по умолчанию).
// Хук никогда не роняет хост: любые сбои → лог, без throw (кроме намеренного блока).
//
// Телеметрия: на КАЖДУЮ оценку гейта — РОВНО ОДНА JSON-строка в gate_telemetry.jsonl
// (data dir проекта, см. src/core/artifact_paths.get_gate_log_path — НЕ %TEMP%):
// {t, gate: stale|isolation, mode, act, drift|count, delivered, reason, restraint?, latency_ms, refusal_len?}
// reason ∈ fired_block | fired_advisory | suppressed_restraint | suppressed_zero_findings
// | skipped_off | error_unavailable | error_exception.
// Правило: та же ветка, что РЕШАЕТ (throw/stash/suppress), и ПИШЕТ строку —
// второго кодового пути нет (комментарий у каждой эмиссии называет зеркалимое решение).
// В лог НЕ пишутся тела findings/выводы CLI (только refusal_len) — секретоподобный
// текст в принципе не попадает в файл (вывод CLI и так проходит redact.py на стороне src/cli.py).
//
// ВАЖНО: вызов python — через node:child_process, НЕ через `$` из контекста плагина: в opencode
// 1.18.23 контекст фабрики не содержит `$` (API дрейфанул) → `TypeError: $ is not a function`.
//
// LOADER CONTRACT (opencode ≥1.16): legacy plugin loader итерирует
// Object.values(module) и ВЫЗЫВАЕТ каждый function-экспорт как фабрику плагина
// (см. opencode issues #41234/#31575/#42451). Поэтому этот входной модуль обязан
// экспортировать ТОЛЬКО default-фабрику — никаких других runtime-экспортов
// (включая const). Все хелперы живут в ../../tools/gate_helpers.ts; тесты хелперов
// импортируют gate_helpers.ts напрямую, а loader-safety тест импортирует ЭТОТ
// модуль и проверяет Object.keys(entry) === ["default"].

import type { Plugin } from "@opencode-ai/plugin"
import { appendFileSync, mkdirSync } from "node:fs"
import { execFile } from "node:child_process"
import { dirname, join } from "node:path"
import { tmpdir } from "node:os"
import { dirname } from "node:path"
import {
resolveGateLog,
makeRow,
parseStale,
parseIsolation,
decideStale,
decideIsolation,
buildAdvisoryNote,
isolationRowExtra,
cli,
actOf,
isGitCommit,
} from "../../tools/gate_helpers.ts"
import type { GateRow } from "../../tools/gate_helpers.ts"

// Module-local config + stash (identical lines to the pre-split module):
// the entry must export ONLY default, so values the factory closes over live here,
// not as helper exports (opencode ≥1.16 legacy loader would invoke function
// exports and choke on non-function ones — issues #41234/#31575).
// Project-local venv by default; MSCODEBASE_PY overrides. No machine-specific path.
const LOG = process.env.MSCODEBASE_GATE_LOG ?? join(tmpdir(), "opencode", "mscodebase_gate.log")
const STALE_MODE = process.env.MSCODEBASE_STALE_GATE ?? "block"
const ISO_MODE = process.env.MSCODEBASE_ISOLATION_GATE ?? "advisory"

const STALE_ARGS = "{}"
const ISO_ARGS = '{"action":"isolation","kwargs":{"restraint":true}}'

function log(rec: Record<string, unknown>) {
try {
mkdirSync(dirname(LOG), { recursive: true })
appendFileSync(LOG, JSON.stringify({ t: Date.now(), ...rec }) + "\n")
} catch {
// never break the host
}
}

function cli(directory: string, tool: string, argsJson: string): Promise<string> {
const py = process.env.MSCODEBASE_PY ?? join(directory, "venv", "Scripts", "python.exe")
return new Promise((resolve) => {
try {
execFile(
py,
["-m", "src.cli", tool, argsJson],
{ cwd: directory, timeout: 90000, windowsHide: true, maxBuffer: 4 * 1024 * 1024 },
(_err, stdout) => resolve(stdout ?? ""),
)
} catch (e) {
log({ kind: "cli_spawn_error", err: String(e).slice(0, 200) })
resolve("")
}
})
}

function staleDrift(out: string): number {
const m = /Total drift instances:\s*(\d+)/i.exec(out)
return m ? parseInt(m[1], 10) : 0
}

function isolationCount(out: string): number {
const r = /removed_last_caller:\s*(\d+)/i.exec(out)
const n = /new_orphan:\s*(\d+)/i.exec(out)
return (r ? parseInt(r[1], 10) : 0) + (n ? parseInt(n[1], 10) : 0)
}

function isolationSuppressed(out: string): boolean {
return /deliver:\s*✗/i.test(out) || /deliver:\s*false/i.test(out)
}

const stash = new Map<string, string>()

export default (async ({ directory }) => {
log({ kind: "init", directory, stale: STALE_MODE, isolation: ISO_MODE })
const LOG = resolveGateLog(directory ?? "")
const emit = (rec: Record<string, unknown>) => {
try {
mkdirSync(dirname(LOG), { recursive: true })
// единый конструктор строки (makeRow): вся телеметрия идёт через эту точку
appendFileSync(LOG, JSON.stringify(makeRow(rec as GateRow)) + "\n")
} catch {
// never break the host
}
}
emit({ kind: "init", directory, stale: STALE_MODE, isolation: ISO_MODE, log: LOG })
return {
"tool.execute.before": async (input: any, output: any) => {
// Контракт хоста: хук никогда не бросает. opencode ≥1.18 вызывает хуки с
// формами входа, которые мы не контролируем (undefined input/output) —
// непроверенный доступ ронял весь сервер (M4 smoke, попытки 2–3).
if (!input || typeof input !== "object" || !output || typeof output !== "object") return
if (input.tool !== "bash") return
const cmd = String(output.args?.command ?? "")
if (!/\bgit\s+commit\b/.test(cmd)) return
// Tokenizer, not /\bgit\s+commit\b/: global options such as `git -C <dir> commit`
// are the same command and must be gated too (M4 full-30 run 11 slipped through).
if (!isGitCommit(cmd)) return
const act = actOf(output)

// ── Gate 1: stale_detector (block) ──
if (STALE_MODE !== "off") {
// ── Gate 1: stale_detector ──
if (STALE_MODE === "off") {
// mirrors the skip branch: gate off → no CLI call, nothing delivered
emit({ gate: "stale", mode: STALE_MODE, act, drift: 0, delivered: false, reason: "skipped_off", latency_ms: 0 })
} else {
const t0 = Date.now()
try {
const out = await cli(directory, "stale_detector", STALE_ARGS)
const drift = staleDrift(out)
log({ kind: "stale", drift, out: out.slice(0, 400) })
if (STALE_MODE === "block" && drift > 0) {
log({ kind: "stale_block", drift })
throw new Error(
`CRYSTAL-GATE: ${drift} doc-drift instance(s). Docs assert a version the code no longer is.\n${out.slice(0, 500)}`,
const parsed = parseStale(out)
const latency_ms = Date.now() - t0
if (STALE_MODE === "block" && parsed.drift > 0) {
const err = new Error(
`CRYSTAL-GATE: ${parsed.drift} doc-drift instance(s). Docs assert a version the code no longer is.\n${out.slice(0, 500)}`,
)
// mirrors the throw below: refusal length, never the text
emit({ gate: "stale", mode: STALE_MODE, act, drift: parsed.drift, delivered: true, reason: "fired_block", refusal_len: String(err).length, latency_ms })
throw err
}
// mirrors the fall-through branch: no block → nothing delivered, reason from the same parse
const d = decideStale(STALE_MODE, parsed, false)
emit({ gate: "stale", mode: STALE_MODE, act, drift: parsed.drift, delivered: d.delivered, reason: d.reason, latency_ms })
} catch (e) {
if (String(e).includes("CRYSTAL-GATE")) throw e
log({ kind: "stale_error", err: String(e).slice(0, 200) })
// mirrors the swallow-and-continue branch: host never breaks on gate errors
emit({ gate: "stale", mode: STALE_MODE, act, drift: 0, delivered: false, reason: "error_exception", latency_ms: Date.now() - t0 })
}
}

// ── Gate 2: graph isolation (advisory/block) ──
if (ISO_MODE !== "off") {
// ── Gate 2: graph isolation ──
if (ISO_MODE === "off") {
// mirrors the skip branch: gate off → no CLI call, nothing delivered
emit({ gate: "isolation", mode: ISO_MODE, act, count: 0, delivered: false, reason: "skipped_off", latency_ms: 0 })
} else {
const t0 = Date.now()
try {
const out = await cli(directory, "graph_query", ISO_ARGS)
const count = isolationCount(out)
log({ kind: "isolation", count, out: out.slice(0, 400) })
if (count > 0) {
if (ISO_MODE === "block") {
log({ kind: "isolation_block", count })
throw new Error(`GRAPH-GATE (block): this commit isolates ${count} graph node(s).\n${out.slice(0, 500)}`)
}
if (!isolationSuppressed(out)) stash.set(input.callID, out)
const parsed = parseIsolation(out)
const latency_ms = Date.now() - t0
if (parsed.count > 0 && ISO_MODE === "block") {
const err = new Error(`GRAPH-GATE (block): this commit isolates ${parsed.count} graph node(s).\n${out.slice(0, 500)}`)
// mirrors the throw below: refusal length, never the text
emit({ gate: "isolation", mode: ISO_MODE, act, count: parsed.count, delivered: true, reason: "fired_block", refusal_len: String(err).length, ...(parsed.restraint ? { restraint: parsed.restraint } : {}), latency_ms })
throw err
}
// mirrors the stash/suppress/fall-through branch below: the SAME parsed values feed the row
if (parsed.count > 0 && ISO_MODE === "advisory" && !parsed.suppressed) stash.set(input.callID, out)
const d = decideIsolation(ISO_MODE, parsed, false)
emit({ gate: "isolation", mode: ISO_MODE, act, count: parsed.count, delivered: d.delivered, reason: d.reason, ...(parsed.restraint ? { restraint: parsed.restraint } : {}), ...isolationRowExtra(out, parsed, ISO_MODE), latency_ms })
} catch (e) {
if (String(e).includes("GRAPH-GATE")) throw e
log({ kind: "isolation_error", err: String(e).slice(0, 200) })
// mirrors the swallow-and-continue branch: host never breaks on gate errors
emit({ gate: "isolation", mode: ISO_MODE, act, count: 0, delivered: false, reason: "error_exception", latency_ms: Date.now() - t0 })
}
}
},

"tool.execute.after": async (input: any, output: any) => {
if (ISO_MODE !== "advisory") return
if (!input || typeof input !== "object" || !output || typeof output !== "object") return
const cached = stash.get(input.callID)
if (!cached) return
stash.delete(input.callID)
log({ kind: "isolation_advisory", len: cached.length })
output.output =
(output.output ?? "") +
`\n\n✦ GRAPH-GATE (advisory): this commit isolates graph node(s).\n${cached.slice(0, 500)}`
// без лог-строки: fired_advisory уже записана в before (одна оценка — одна строка)
const note = buildAdvisoryNote(cached, ISO_MODE)
if (!note) return // тихий гейт молчит и с включённым override: доставка не выдумывается
output.output = (output.output ?? "") + note.text
},
}
}) satisfies Plugin
Loading
Loading