diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1b908f5a..34020a8d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,6 +47,9 @@ jobs: - name: Verify Go tests run: go test ./... + - name: Verify capability runner tests + run: npm run test:capability-runners + - name: Verify CGO-free build run: CGO_ENABLED=0 go build ./... diff --git a/cli/scripts/capability-runner-utils.mjs b/cli/scripts/capability-runner-utils.mjs new file mode 100644 index 00000000..330ce0e9 --- /dev/null +++ b/cli/scripts/capability-runner-utils.mjs @@ -0,0 +1,43 @@ +import { randomBytes } from "node:crypto"; +import { mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; + +const requiredOptions = ["client", "expected-version", "receipt"]; +const safeVersionPattern = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}$/; + +export function parseRunnerArgs(argv) { + const values = {}; + for (let index = 0; index < argv.length; index += 2) { + const option = argv[index]; + const value = argv[index + 1]; + if (!option?.startsWith("--") || !requiredOptions.includes(option.slice(2))) throw new Error(`unknown option ${option ?? ""}`); + const name = option.slice(2); + if (Object.hasOwn(values, name)) throw new Error(`duplicate option --${name}`); + if (value === undefined || value.startsWith("--")) throw new Error(`option --${name} requires a value`); + values[name] = value; + } + for (const name of requiredOptions) if (!Object.hasOwn(values, name)) throw new Error(`missing required option --${name}`); + if (values.client === "" || values.client.startsWith("-") || /[\0\r\n]/.test(values.client)) throw new Error("--client must be a safe executable name or path"); + if (!safeVersionPattern.test(values["expected-version"])) throw new Error("--expected-version must be an exact safe identity"); + if (values.receipt === "" || /[\0\r\n]/.test(values.receipt) || !values.receipt.endsWith(".json")) throw new Error("--receipt must be a safe JSON path"); + return { + client: values.client, + expectedVersion: values["expected-version"], + receiptPath: resolve(values.receipt), + }; +} + +export function publishReceiptIfSuccessful(receiptPath, receipt, successful) { + if (!successful) return false; + const directory = dirname(receiptPath); + mkdirSync(directory, { recursive: true }); + const temporaryPath = `${receiptPath}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`; + try { + writeFileSync(temporaryPath, `${JSON.stringify(receipt, null, 2)}\n`, { encoding: "utf8", flag: "wx", mode: 0o644 }); + renameSync(temporaryPath, receiptPath); + } catch (error) { + rmSync(temporaryPath, { force: true }); + throw error; + } + return true; +} diff --git a/cli/scripts/u8-cursor-smoke.mjs b/cli/scripts/preflight-cursor-agent-context.mjs similarity index 80% rename from cli/scripts/u8-cursor-smoke.mjs rename to cli/scripts/preflight-cursor-agent-context.mjs index d3b1f4c3..38140ebb 100644 --- a/cli/scripts/u8-cursor-smoke.mjs +++ b/cli/scripts/preflight-cursor-agent-context.mjs @@ -1,21 +1,19 @@ #!/usr/bin/env node import { createHash } from "node:crypto"; -import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { existsSync, readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { dirname, join, resolve } from "node:path"; import { spawnSync } from "node:child_process"; +import { parseRunnerArgs, publishReceiptIfSuccessful } from "./capability-runner-utils.mjs"; const scriptDir = dirname(fileURLToPath(import.meta.url)); const repoRoot = resolve(scriptDir, "../.."); -const researchDir = join(repoRoot, "docs/changes/20260710-journal-reliability-foundation/research"); -const evidencePath = join(researchDir, "u8-cursor-agent-candidate-preflight.json"); -const expectedVersion = "2026.05.09-0afadcc"; const platform = `${process.platform}-${process.arch}`; const candidateHooksPath = "dist/cursor/hooks.json"; const candidateBinaryPath = `bin/native/${platform}/loaf`; -export function classifyCursorPreflight(versionOutput, helpOutput) { +export function classifyCursorPreflight(versionOutput, helpOutput, expectedVersion) { const observedVersion = versionOutput.trim(); const exactVersion = observedVersion === expectedVersion; const noSessionPersistence = helpOutput.includes("--no-session-persistence"); @@ -66,18 +64,20 @@ function candidateArtifacts() { }; } -function main() { - mkdirSync(researchDir, { recursive: true }); +function main(argv = process.argv.slice(2)) { + const { client, expectedVersion, receiptPath } = parseRunnerArgs(argv); const timestamp = new Date().toISOString(); const buildGo = run("npm", ["run", "build:go"], repoRoot); if (buildGo.status !== 0) throw new Error("candidate Go build failed"); const buildCursor = run("bin/loaf", ["build", "--target", "cursor"], repoRoot); if (buildCursor.status !== 0) throw new Error("candidate Cursor target build failed"); const artifacts = candidateArtifacts(); - const version = run("cursor-agent", ["--version"], repoRoot); - const help = run("cursor-agent", ["--help"], repoRoot); + const version = run(client, ["--version"], repoRoot); + const help = run(client, ["--help"], repoRoot); if (version.status !== 0 || help.status !== 0) throw new Error("installed cursor-agent version/help preflight failed"); - const preflight = classifyCursorPreflight(version.stdout, help.stdout); + const preflight = classifyCursorPreflight(version.stdout, help.stdout, expectedVersion); + if (!preflight.exactVersion) throw new Error(preflight.blocker); + if (preflight.noSessionPersistence) throw new Error("installed cursor-agent exposes --no-session-persistence, but a model-visible isolated smoke is not implemented"); const smoke = { evidence_version: 2, timestamp, @@ -102,8 +102,15 @@ function main() { blocker: preflight.blocker, retry_condition: "Rerun after the installed CLI exposes a documented no-session-persistence mode or after an explicitly approved isolated session-store boundary is available.", }; - writeFileSync(evidencePath, `${JSON.stringify(smoke, null, 2)}\n`); - process.stdout.write(`${JSON.stringify({ evidence_path: "docs/changes/20260710-journal-reliability-foundation/research/u8-cursor-agent-candidate-preflight.json", status: "unavailable", reason: smoke.blocker }, null, 2)}\n`); + publishReceiptIfSuccessful(receiptPath, smoke, true); + process.stdout.write(`${JSON.stringify({ receipt: receiptPath, status: "safely-refused", reason: smoke.blocker }, null, 2)}\n`); } -if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) main(); +if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + try { + main(); + } catch (error) { + process.stderr.write(`${error instanceof Error ? error.message : "Cursor agent context preflight failed"}\n`); + process.exitCode = 1; + } +} diff --git a/cli/scripts/u8-cursor-smoke.test.mjs b/cli/scripts/preflight-cursor-agent-context.test.mjs similarity index 69% rename from cli/scripts/u8-cursor-smoke.test.mjs rename to cli/scripts/preflight-cursor-agent-context.test.mjs index 28c43fed..ef9d4b31 100644 --- a/cli/scripts/u8-cursor-smoke.test.mjs +++ b/cli/scripts/preflight-cursor-agent-context.test.mjs @@ -1,9 +1,9 @@ import test from "node:test"; import assert from "node:assert/strict"; -import { classifyCursorPreflight } from "./u8-cursor-smoke.mjs"; +import { classifyCursorPreflight } from "./preflight-cursor-agent-context.mjs"; test("classifies an unsafe Cursor preflight without claiming execution", () => { - const preflight = classifyCursorPreflight("2026.05.09-0afadcc\n", "Usage: agent\n"); + const preflight = classifyCursorPreflight("candidate-42\n", "Usage: agent\n", "candidate-42"); assert.equal(preflight.exactVersion, true); assert.equal(preflight.noSessionPersistence, false); assert.equal(preflight.smokeExecuted, false); @@ -11,7 +11,7 @@ test("classifies an unsafe Cursor preflight without claiming execution", () => { }); test("rejects an unexpected installed version before any smoke", () => { - const preflight = classifyCursorPreflight("3.11.19\n", "--no-session-persistence\n"); + const preflight = classifyCursorPreflight("unexpected\n", "--no-session-persistence\n", "candidate-42"); assert.equal(preflight.exactVersion, false); assert.equal(preflight.noSessionPersistence, true); assert.equal(preflight.smokeExecuted, false); diff --git a/cli/scripts/u8-claude-smoke.mjs b/cli/scripts/smoke-claude-code-startup.mjs similarity index 74% rename from cli/scripts/u8-claude-smoke.mjs rename to cli/scripts/smoke-claude-code-startup.mjs index 191b94d3..724a8402 100644 --- a/cli/scripts/u8-claude-smoke.mjs +++ b/cli/scripts/smoke-claude-code-startup.mjs @@ -1,18 +1,18 @@ #!/usr/bin/env node import { createHash, randomBytes } from "node:crypto"; -import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; import { fileURLToPath } from "node:url"; -import { dirname, join, relative, resolve } from "node:path"; +import { dirname, join, relative, resolve, sep } from "node:path"; import { spawnSync } from "node:child_process"; +import { tmpdir } from "node:os"; +import { parseRunnerArgs, publishReceiptIfSuccessful } from "./capability-runner-utils.mjs"; const scriptDir = dirname(fileURLToPath(import.meta.url)); const repoRoot = resolve(scriptDir, "../.."); -const researchDir = join(repoRoot, "docs/changes/20260710-journal-reliability-foundation/research"); -const evidencePath = join(researchDir, "u8-claude-code-2.1.218-candidate-smoke.json"); -const expectedVersion = "2.1.218"; const platform = `${process.platform}-${process.arch}`; const candidatePluginPath = "plugins/loaf"; +const markerPattern = /^LOAF_CLAUDE_STARTUP_SMOKE_[A-F0-9]{12}$/; export function parseClaudeStreamOutput(raw, marker) { const events = raw.split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line)); @@ -65,12 +65,21 @@ function sha256(path) { return createHash("sha256").update(readFileSync(path)).digest("hex"); } -function main() { - mkdirSync(researchDir, { recursive: true }); - const marker = `LOAF_U8_SMOKE_${randomBytes(6).toString("hex").toUpperCase()}`; +function main(argv = process.argv.slice(2)) { + const { client, expectedVersion, receiptPath } = parseRunnerArgs(argv); + const marker = `LOAF_CLAUDE_STARTUP_SMOKE_${randomBytes(6).toString("hex").toUpperCase()}`; + if (!markerPattern.test(marker)) throw new Error("generated marker does not match the required format"); const timestamp = new Date().toISOString(); - const tempRoot = mkdtempSync(join(repoRoot, ".u8-claude-smoke-")); - const cleanup = () => rmSync(tempRoot, { recursive: true, force: true }); + const tempRoot = mkdtempSync(join(tmpdir(), "loaf-claude-code-startup-smoke-")); + if (resolve(tempRoot).startsWith(`${repoRoot}${sep}`)) throw new Error("disposable Claude Code smoke root must be outside the repository"); + const cleanup = () => { + try { + rmSync(tempRoot, { recursive: true, force: true, maxRetries: 3, retryDelay: 50 }); + } catch { + return false; + } + return !existsSync(tempRoot); + }; for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) { process.once(signal, () => { cleanup(); @@ -85,14 +94,16 @@ function main() { const candidatePlugin = join(repoRoot, candidatePluginPath); const candidateBinary = join(candidatePlugin, "bin", "loaf"); let smoke; + let failure; + let cleanupSucceeded = false; try { const buildGo = run("npm", ["run", "build:go"], repoRoot); if (buildGo.status !== 0) throw new Error("candidate Go build failed"); const buildClaude = run("bin/loaf", ["build", "--target", "claude-code"], repoRoot); if (buildClaude.status !== 0) throw new Error("candidate Claude build failed"); if (!existsSync(candidateBinary)) throw new Error("candidate plugin binary is missing"); - const version = run("claude", ["--version"], repoRoot); - if (version.status !== 0 || !claudeVersionMatches(version.stdout, expectedVersion)) throw new Error("installed Claude version is not 2.1.218"); + const version = run(client, ["--version"], repoRoot); + if (version.status !== 0 || !claudeVersionMatches(version.stdout, expectedVersion)) throw new Error(`installed Claude Code version does not match ${expectedVersion}`); if (run("git", ["init", "-q"], disposableRepo).status !== 0) throw new Error("disposable Git initialization failed"); const candidateEnv = { LOAF_DB: dbPath }; if (run(candidateBinary, ["state", "init", "--json"], disposableRepo, candidateEnv).status !== 0) throw new Error("isolated Loaf state initialization failed"); @@ -104,7 +115,7 @@ function main() { "--include-hook-events", "--output-format", "stream-json", "-p", "Reply with exactly the unique marker present in Loaf continuity context, and nothing else.", ]; - const claude = run("claude", [ + const claude = run(client, [ "--plugin-dir", candidatePlugin, "--strict-mcp-config", "--mcp-config", '{"mcpServers":{}}', "--no-session-persistence", "--setting-sources", "", "--tools", "", "--include-hook-events", "--output-format", "stream-json", "-p", "Reply with exactly the unique marker present in Loaf continuity context, and nothing else.", @@ -139,23 +150,21 @@ function main() { }; if (claude.status !== 0 || claude.stderr.length !== 0 || !parsed.hookObservation.additional_context_marker || !parsed.assistantMarkerMatch) throw new Error("model-visible marker smoke did not pass"); } catch (error) { - smoke ??= { - evidence_version: 2, timestamp, target: "claude-code", surface: "cli", version: expectedVersion, platform, - installed_mode: "plugin-dir", context_mode: "startup", adapter: "claude-session-start-v1", mode: "explicit-plugin-dir", - invocation: { command: "claude", args: [], cwd: "" }, setup: [], candidate_plugin_path: candidatePluginPath, - exit_code: 1, stderr_empty: false, model_visible_marker_observed: false, assistant_marker_match: false, marker, - hook_observation: { event_name: "", native_json: false, hook_event_name: "", additional_context_marker: false }, - candidate_artifacts: { hooks_path: "plugins/loaf/hooks/hooks.json", hooks_sha256: "", native_binary_path: "", native_binary_sha256: "" }, - failure_reason: error instanceof Error ? error.message : "smoke failed", - }; - smoke.failure_reason = error instanceof Error ? error.message : "smoke failed"; - smoke.exit_code = smoke.exit_code ?? 1; + failure = error; } finally { - cleanup(); + cleanupSucceeded = cleanup(); } - writeFileSync(evidencePath, `${JSON.stringify(smoke, null, 2)}\n`); - process.stdout.write(`${JSON.stringify({ evidence_path: "docs/changes/20260710-journal-reliability-foundation/research/u8-claude-code-2.1.218-candidate-smoke.json", exit_code: smoke.exit_code, assistant_marker_match: smoke.assistant_marker_match }, null, 2)}\n`); - if (smoke.exit_code !== 0 || !smoke.assistant_marker_match) process.exitCode = 1; + if (failure) throw failure; + if (!cleanupSucceeded) throw new Error("disposable Claude Code smoke cleanup failed"); + publishReceiptIfSuccessful(receiptPath, smoke, true); + process.stdout.write(`${JSON.stringify({ receipt: receiptPath, exit_code: smoke.exit_code, assistant_marker_match: smoke.assistant_marker_match, cleanup_succeeded: true }, null, 2)}\n`); } -if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) main(); +if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + try { + main(); + } catch (error) { + process.stderr.write(`${error instanceof Error ? error.message : "Claude Code startup smoke failed"}\n`); + process.exitCode = 1; + } +} diff --git a/cli/scripts/smoke-claude-code-startup.test.mjs b/cli/scripts/smoke-claude-code-startup.test.mjs new file mode 100644 index 00000000..76f3ed3b --- /dev/null +++ b/cli/scripts/smoke-claude-code-startup.test.mjs @@ -0,0 +1,58 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { claudeVersionMatches, parseClaudeStreamOutput } from "./smoke-claude-code-startup.mjs"; +import { parseRunnerArgs, publishReceiptIfSuccessful } from "./capability-runner-utils.mjs"; + +test("parses native SessionStart output and exact assistant marker", () => { + const marker = "LOAF_CLAUDE_STARTUP_SMOKE_ABCDEF123456"; + const raw = [ + JSON.stringify({ type: "system", subtype: "hook_response", hook_event: "SessionStart", output: JSON.stringify({ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: `digest ${marker}` } }) }), + JSON.stringify({ type: "assistant", message: { content: [{ type: "text", text: marker }] } }), + JSON.stringify({ type: "result", result: marker }), + ].join("\n"); + const parsed = parseClaudeStreamOutput(raw, marker); + assert.equal(parsed.hookObservation.native_json, true); + assert.equal(parsed.hookObservation.additional_context_marker, true); + assert.equal(parsed.assistantMarkerMatch, true); +}); + +test("rejects a stream without SessionStart hook response", () => { + assert.throws(() => parseClaudeStreamOutput(JSON.stringify({ type: "result", result: "no hook" }), "marker"), /SessionStart hook response/); +}); + +test("requires the exact Claude Code version token", () => { + assert.equal(claudeVersionMatches("9.8.7 (Claude Code)\n", "9.8.7"), true); + assert.equal(claudeVersionMatches("9.8.70 (Claude Code)", "9.8.7"), false); + assert.equal(claudeVersionMatches("Claude Code 9.8.7", "9.8.7"), false); +}); + +test("requires one safe value for every runner option", () => { + const parsed = parseRunnerArgs(["--client", "/opt/claude", "--expected-version", "9.8.7", "--receipt", "proof.json"]); + assert.equal(parsed.client, "/opt/claude"); + assert.equal(parsed.expectedVersion, "9.8.7"); + assert.ok(parsed.receiptPath.endsWith("proof.json")); + assert.throws(() => parseRunnerArgs(["--client", "claude"]), /missing required option/); + assert.throws(() => parseRunnerArgs(["--client", "claude", "--client", "other", "--expected-version", "9.8.7", "--receipt", "proof.json"]), /duplicate option/); + assert.throws(() => parseRunnerArgs(["--unknown", "value", "--client", "claude", "--expected-version", "9.8.7", "--receipt", "proof.json"]), /unknown option/); + assert.throws(() => parseRunnerArgs(["--client", "claude\nunsafe", "--expected-version", "9.8.7", "--receipt", "proof.json"]), /safe executable/); + assert.throws(() => parseRunnerArgs(["--client", "claude", "--expected-version", "9.8.7 unsafe", "--receipt", "proof.json"]), /exact safe identity/); + assert.throws(() => parseRunnerArgs(["--client", "claude", "--expected-version", "9.8.7", "--receipt", "proof.txt"]), /safe JSON path/); +}); + +test("publishes success atomically and preserves an existing receipt on failure", () => { + const root = mkdtempSync(join(tmpdir(), "loaf-capability-receipt-test-")); + const receiptPath = join(root, "receipt.json"); + try { + writeFileSync(receiptPath, "existing\n"); + assert.equal(publishReceiptIfSuccessful(receiptPath, { status: "failed" }, false), false); + assert.equal(readFileSync(receiptPath, "utf8"), "existing\n"); + assert.equal(publishReceiptIfSuccessful(receiptPath, { status: "passed" }, true), true); + assert.deepEqual(JSON.parse(readFileSync(receiptPath, "utf8")), { status: "passed" }); + assert.deepEqual(readdirSync(root), ["receipt.json"]); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/cli/scripts/u8-codex-smoke.mjs b/cli/scripts/smoke-codex-startup.mjs similarity index 79% rename from cli/scripts/u8-codex-smoke.mjs rename to cli/scripts/smoke-codex-startup.mjs index 4c8d9aa7..7af649ba 100644 --- a/cli/scripts/u8-codex-smoke.mjs +++ b/cli/scripts/smoke-codex-startup.mjs @@ -6,15 +6,14 @@ import { fileURLToPath } from "node:url"; import { dirname, join, relative, resolve, sep } from "node:path"; import { spawnSync } from "node:child_process"; import { tmpdir } from "node:os"; +import { parseRunnerArgs, publishReceiptIfSuccessful } from "./capability-runner-utils.mjs"; const scriptDir = dirname(fileURLToPath(import.meta.url)); const repoRoot = resolve(scriptDir, "../.."); -const researchDir = join(repoRoot, "docs/changes/20260710-journal-reliability-foundation/research"); -const evidencePath = join(researchDir, "u8-codex-0.145.0-isolated-smoke.json"); -const expectedVersion = "0.145.0"; const platform = `${process.platform}-${process.arch}`; const candidateHooksPath = "dist/codex/.codex/hooks.json"; const candidateNativeRoot = join(repoRoot, "bin", "native"); +const markerPattern = /^LOAF_CODEX_STARTUP_SMOKE_[A-F0-9]{12}$/; export function codexVersionMatches(output, expectedVersion) { const match = output.match(/(?:^|\s)codex-cli\s+([0-9]+\.[0-9]+\.[0-9]+)(?:\s|$)/); @@ -82,11 +81,6 @@ function nativeBinaryPath() { return path; } -function sanitizeError(error, tempRoot) { - const text = error instanceof Error ? error.message : String(error); - return text.replaceAll(repoRoot, "").replaceAll(tempRoot, "").replaceAll(process.env.HOME ?? "", "").replaceAll(/\s+/g, " ").trim().slice(0, 400); -} - function sanitizedStderr(stderr) { const trimmed = stderr.trim(); if (trimmed === "") return ""; @@ -117,12 +111,20 @@ process.exitCode = result.status ?? 1; chmodSync(wrapperPath, 0o700); } -function main() { - mkdirSync(researchDir, { recursive: true }); - const marker = `LOAF_CODEX_U8_${randomBytes(6).toString("hex").toUpperCase()}`; +function main(argv = process.argv.slice(2)) { + const { client, expectedVersion, receiptPath } = parseRunnerArgs(argv); + const marker = `LOAF_CODEX_STARTUP_SMOKE_${randomBytes(6).toString("hex").toUpperCase()}`; + if (!markerPattern.test(marker)) throw new Error("generated marker does not match the required format"); const timestamp = new Date().toISOString(); - const tempRoot = mkdtempSync(join(tmpdir(), "loaf-u8-codex-smoke-")); - const cleanup = () => rmSync(tempRoot, { recursive: true, force: true }); + const tempRoot = mkdtempSync(join(tmpdir(), "loaf-codex-startup-smoke-")); + const cleanup = () => { + try { + rmSync(tempRoot, { recursive: true, force: true, maxRetries: 3, retryDelay: 50 }); + } catch { + return false; + } + return !existsSync(tempRoot); + }; for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) { process.once(signal, () => { cleanup(); @@ -134,8 +136,10 @@ function main() { const codexHome = join(tempRoot, "codex-home"); const stateDir = join(tempRoot, "state"); const dbPath = join(stateDir, "loaf.sqlite"); - const candidateBinary = nativeBinaryPath(); + let candidateBinary; let smoke; + let failure; + let cleanupSucceeded = false; try { mkdirSync(disposableRepo, { recursive: true, mode: 0o700 }); mkdirSync(codexHome, { recursive: true, mode: 0o700 }); @@ -146,9 +150,9 @@ function main() { if (buildGo.status !== 0) throw new Error("candidate Go build failed"); const buildCodex = run("bin/loaf", ["build", "--target", "codex"], repoRoot); if (buildCodex.status !== 0) throw new Error("candidate Codex build failed"); - if (!existsSync(candidateBinary)) throw new Error("candidate native binary is missing"); - const version = run("codex", ["--version"], repoRoot); - if (version.status !== 0 || !codexVersionMatches(version.stdout, expectedVersion)) throw new Error("installed Codex version is not 0.145.0"); + candidateBinary = nativeBinaryPath(); + const version = run(client, ["--version"], repoRoot); + if (version.status !== 0 || !codexVersionMatches(version.stdout, expectedVersion)) throw new Error(`installed Codex version does not match ${expectedVersion}`); if (run("git", ["init", "-q"], disposableRepo).status !== 0) throw new Error("disposable Git initialization failed"); const authPath = join(process.env.CODEX_HOME ?? join(process.env.HOME ?? "", ".codex"), "auth.json"); if (!existsSync(authPath)) throw new Error("installed Codex auth.json is unavailable"); @@ -164,11 +168,11 @@ function main() { hooks: group.hooks.map((hook) => ({ ...hook, command: hook.command.replace("{{LOAF_EXECUTABLE}} journal context --from-hook --codex-hook", hookCommand) })), })); writeFileSync(join(codexHome, "hooks.json"), `${JSON.stringify(sourceHooks, null, 2)}\n`, { mode: 0o600 }); - const candidateEnv = { CODEX_HOME: codexHome, LOAF_DB: dbPath, RUST_LOG: "off" }; + const candidateEnv = { CODEX_HOME: codexHome, LOAF_DB: dbPath }; if (run(candidateBinary, ["state", "init", "--json"], disposableRepo, candidateEnv).status !== 0) throw new Error("isolated Loaf state initialization failed"); if (run(candidateBinary, ["journal", "log", `discover(smoke): ${marker}`], disposableRepo, candidateEnv).status !== 0) throw new Error("isolated journal marker write failed"); const codexArgs = ["exec", "--ephemeral", "--ignore-rules", "--dangerously-bypass-hook-trust", "--sandbox", "read-only", "--json", "-C", "", "Return exactly the unique marker supplied by SessionStart context, and nothing else."]; - const codex = run("codex", [...codexArgs.slice(0, -2), disposableRepo, codexArgs.at(-1)], disposableRepo, candidateEnv); + const codex = run(client, [...codexArgs.slice(0, -2), disposableRepo, codexArgs.at(-1)], disposableRepo, candidateEnv); const parsed = parseCodexJSONL(codex.stdout, marker); if (!existsSync(observationPath)) throw new Error("Codex hook observation file was not written"); if ((statSync(wrapperPath).mode & 0o777) !== 0o700) throw new Error("Codex hook observation wrapper is not mode 0700"); @@ -186,7 +190,7 @@ function main() { adapter: "codex-session-start-v1", mode: "isolated-codex-home", invocation: { command: "codex", args: codexArgs, cwd: "" }, - setup: ["build candidate Go binary and Codex target", "create disposable Git repository", "create isolated CODEX_HOME with hooks enabled", "copy installed auth.json into isolated CODEX_HOME with mode 0600", "initialize absolute disposable LOAF_DB", "write random marker to isolated journal", "observe hook stdout through a mode-0700 disposable wrapper and mode-0600 file", "disable Codex tracing logs (RUST_LOG=off) so backend transport noise cannot contaminate the stderr cleanliness gate"], + setup: ["build candidate Go binary and Codex target", "create disposable Git repository", "create isolated CODEX_HOME with hooks enabled", "copy installed auth.json into isolated CODEX_HOME with mode 0600", "initialize absolute disposable LOAF_DB", "write random marker to isolated journal", "observe hook stdout through a mode-0700 disposable wrapper and mode-0600 file"], exit_code: codex.status, stderr_empty: codex.stderr.length === 0, stderr: sanitizedStderr(codex.stderr), @@ -208,19 +212,21 @@ function main() { }; if (codex.status !== 0 || !observedHook.additionalContextMarker || !parsed.assistantMarkerMatch || smoke.stderr === "unexpected stderr") throw new Error("model-visible Codex marker smoke did not pass"); } catch (error) { - smoke ??= { - evidence_version: 2, timestamp, target: "codex", surface: "cli", version: expectedVersion, platform, installed_mode: "isolated-codex-home", context_mode: "startup", adapter: "codex-session-start-v1", mode: "isolated-codex-home", - invocation: { command: "codex", args: [], cwd: "" }, setup: [], exit_code: 1, stderr_empty: false, stderr: "", model_visible_marker_observed: false, assistant_marker_match: false, marker, - hook_observation: { event_name: "", native_json: false, hook_event_name: "", additional_context_marker: false }, candidate_artifacts: { hooks_path: candidateHooksPath, hooks_sha256: "", native_binary_path: "", native_binary_sha256: "" }, - }; - smoke.failure_reason = sanitizeError(error, tempRoot); - smoke.exit_code = smoke.exit_code ?? 1; + failure = error; } finally { - cleanup(); + cleanupSucceeded = cleanup(); } - writeFileSync(evidencePath, `${JSON.stringify(smoke, null, 2)}\n`); - process.stdout.write(`${JSON.stringify({ evidence_path: "docs/changes/20260710-journal-reliability-foundation/research/u8-codex-0.145.0-isolated-smoke.json", exit_code: smoke.exit_code, assistant_marker_match: smoke.assistant_marker_match }, null, 2)}\n`); - if (smoke.exit_code !== 0 || !smoke.assistant_marker_match) process.exitCode = 1; + if (failure) throw failure; + if (!cleanupSucceeded) throw new Error("disposable Codex smoke cleanup failed"); + publishReceiptIfSuccessful(receiptPath, smoke, true); + process.stdout.write(`${JSON.stringify({ receipt: receiptPath, exit_code: smoke.exit_code, assistant_marker_match: smoke.assistant_marker_match, cleanup_succeeded: true }, null, 2)}\n`); } -if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) main(); +if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + try { + main(); + } catch (error) { + process.stderr.write(`${error instanceof Error ? error.message : "Codex startup smoke failed"}\n`); + process.exitCode = 1; + } +} diff --git a/cli/scripts/u8-codex-smoke.test.mjs b/cli/scripts/smoke-codex-startup.test.mjs similarity index 81% rename from cli/scripts/u8-codex-smoke.test.mjs rename to cli/scripts/smoke-codex-startup.test.mjs index 926c4fa7..0ac82610 100644 --- a/cli/scripts/u8-codex-smoke.test.mjs +++ b/cli/scripts/smoke-codex-startup.test.mjs @@ -1,9 +1,9 @@ import test from "node:test"; import assert from "node:assert/strict"; -import { codexVersionMatches, parseCodexHookObservation, parseCodexJSONL, shellQuote } from "./u8-codex-smoke.mjs"; +import { codexVersionMatches, parseCodexHookObservation, parseCodexJSONL, shellQuote } from "./smoke-codex-startup.mjs"; test("parses native SessionStart marker and exact assistant marker", () => { - const marker = "LOAF_CODEX_U8_ABCDEF123456"; + const marker = "LOAF_CODEX_STARTUP_SMOKE_ABCDEF123456"; const raw = [ JSON.stringify({ type: "item.completed", item: { type: "command_execution", output: JSON.stringify({ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: `digest ${marker}` } }) } }), JSON.stringify({ type: "item.completed", item: { type: "agent_message", text: marker } }), @@ -15,7 +15,7 @@ test("parses native SessionStart marker and exact assistant marker", () => { }); test("does not treat a guessed assistant marker as native hook evidence", () => { - const marker = "LOAF_CODEX_U8_ABCDEF123456"; + const marker = "LOAF_CODEX_STARTUP_SMOKE_ABCDEF123456"; const raw = JSON.stringify({ type: "item.completed", item: { type: "agent_message", text: marker } }); const parsed = parseCodexJSONL(raw, marker); assert.equal(parsed.hookObservation.native_json, false); @@ -23,14 +23,14 @@ test("does not treat a guessed assistant marker as native hook evidence", () => }); test("validates the exact native hook stdout observation", () => { - const marker = "LOAF_CODEX_U8_ABCDEF123456"; + const marker = "LOAF_CODEX_STARTUP_SMOKE_ABCDEF123456"; const observed = JSON.stringify({ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: `digest ${marker}` } }); const parsed = parseCodexHookObservation(observed, marker); assert.deepEqual(parsed, { eventName: "SessionStart:startup", nativeJSON: true, hookEventName: "SessionStart", additionalContextMarker: true }); }); test("rejects extra fields in the native hook stdout observation", () => { - const marker = "LOAF_CODEX_U8_ABCDEF123456"; + const marker = "LOAF_CODEX_STARTUP_SMOKE_ABCDEF123456"; const observed = JSON.stringify({ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: marker, unexpected: true } }); const parsed = parseCodexHookObservation(observed, marker); assert.equal(parsed.nativeJSON, false); @@ -46,7 +46,7 @@ test("rejects malformed JSONL", () => { }); test("requires the exact Codex CLI version token", () => { - assert.equal(codexVersionMatches("codex-cli 0.145.0\n", "0.145.0"), true); - assert.equal(codexVersionMatches("codex-cli 0.145.00\n", "0.145.0"), false); - assert.equal(codexVersionMatches("other-cli 0.145.0\n", "0.145.0"), false); + assert.equal(codexVersionMatches("codex-cli 9.8.7\n", "9.8.7"), true); + assert.equal(codexVersionMatches("codex-cli 9.8.70\n", "9.8.7"), false); + assert.equal(codexVersionMatches("other-cli 9.8.7\n", "9.8.7"), false); }); diff --git a/cli/scripts/u8-opencode-smoke.mjs b/cli/scripts/smoke-opencode-request-context.mjs similarity index 78% rename from cli/scripts/u8-opencode-smoke.mjs rename to cli/scripts/smoke-opencode-request-context.mjs index b0cb0ce8..789c710f 100644 --- a/cli/scripts/u8-opencode-smoke.mjs +++ b/cli/scripts/smoke-opencode-request-context.mjs @@ -1,21 +1,19 @@ #!/usr/bin/env node import { createHash, randomBytes } from "node:crypto"; -import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs"; +import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs"; import { fileURLToPath, pathToFileURL } from "node:url"; -import { basename, delimiter, dirname, join, resolve, sep } from "node:path"; +import { dirname, join, resolve, sep } from "node:path"; import { spawnSync } from "node:child_process"; import { tmpdir } from "node:os"; +import { parseRunnerArgs, publishReceiptIfSuccessful } from "./capability-runner-utils.mjs"; const scriptDir = dirname(fileURLToPath(import.meta.url)); const repoRoot = resolve(scriptDir, "../.."); -const researchDir = join(repoRoot, "docs/changes/20260710-journal-reliability-foundation/research"); -const evidencePath = join(researchDir, "u8-opencode-1.18.4-isolated-smoke.json"); -const expectedVersion = "1.18.4"; const platform = `${process.platform}-${process.arch}`; const candidateHooksPath = "dist/opencode/plugins/hooks.ts"; const candidateNativePath = `bin/native/${platform}/loaf`; -const markerPattern = /^LOAF_OPENCODE_U8_[A-F0-9]{12}$/; +const markerPattern = /^LOAF_OPENCODE_REQUEST_SMOKE_[A-F0-9]{12}$/; const prompt = "Reply with exactly the unique marker present in Loaf continuity context, and nothing else."; const setupSteps = [ "build candidate Go binary and OpenCode target", @@ -25,10 +23,10 @@ const setupSteps = [ "observe candidate hook stdout through a mode-0700 wrapper and mode-0600 file", ]; -const safeEnvironmentKeys = ["LANG", "LC_ALL", "PATH", "TERM", "TZ", "XDG_CACHE_HOME"]; +const safeEnvironmentKeys = ["LANG", "LC_ALL", "PATH", "TERM", "TZ"]; -export function opencodeVersionMatches(output, expected = expectedVersion) { - return output.trim() === expected; +export function opencodeVersionMatches(output, expectedVersion) { + return output.trim() === expectedVersion; } export function collectTextValues(value, texts = []) { @@ -100,31 +98,9 @@ function sha256(path) { return createHash("sha256").update(readFileSync(path)).digest("hex"); } -export function resolveExecutable(command) { - for (const directory of (process.env.PATH ?? "").split(delimiter)) { - if (!directory) continue; - const candidate = resolve(directory, command); - try { - const info = statSync(candidate); - if (info.isFile() && (info.mode & 0o111) !== 0) { - // Multiplexer shims (mise et al.) realpath to a differently named dispatcher binary that keys off argv[0], which realpath destroys; skip them so the smoke runs the real standalone binary under the isolated env. - const resolved = realpathSync(candidate); - if (basename(resolved) === command) return resolved; - } - } catch { - // Continue through the explicit PATH allowlist. - } - } - throw new Error(`installed ${command} executable is unavailable`); -} - -export function buildEnvironment() { +function buildEnvironment() { const env = {}; for (const key of safeEnvironmentKeys) if (process.env[key] !== undefined) env[key] = process.env[key]; - // mise-shimmed toolchains resolve user env templates that may reference XDG dirs, so builds need them passed through; the disposable harness env stays fully isolated. - for (const key of ["XDG_CACHE_HOME", "XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_STATE_HOME"]) { - if (process.env[key] !== undefined) env[key] = process.env[key]; - } if (process.env.HOME !== undefined) env.HOME = process.env.HOME; if (process.env.USERPROFILE !== undefined) env.USERPROFILE = process.env.USERPROFILE; return env; @@ -198,7 +174,7 @@ process.exitCode = result.status ?? 1; chmodSync(wrapperPath, 0o700); } -function baseReceipt(marker, invocation, artifacts) { +function baseReceipt(marker, invocation, artifacts, expectedVersion) { return { evidence_version: 2, timestamp: new Date().toISOString(), @@ -226,11 +202,11 @@ function baseReceipt(marker, invocation, artifacts) { }; } -function main() { - mkdirSync(researchDir, { recursive: true }); - const marker = `LOAF_OPENCODE_U8_${randomBytes(6).toString("hex").toUpperCase()}`; +function main(argv = process.argv.slice(2)) { + const { client, expectedVersion, receiptPath } = parseRunnerArgs(argv); + const marker = `LOAF_OPENCODE_REQUEST_SMOKE_${randomBytes(6).toString("hex").toUpperCase()}`; if (!markerPattern.test(marker)) throw new Error("generated marker does not match the required format"); - const tempRoot = mkdtempSync(join(tmpdir(), "loaf-u8-opencode-smoke-")); + const tempRoot = mkdtempSync(join(tmpdir(), "loaf-opencode-request-context-smoke-")); chmodSync(tempRoot, 0o700); if (resolve(tempRoot).startsWith(`${repoRoot}${sep}`)) throw new Error("disposable OpenCode smoke root must be outside the repository"); const disposableRepo = join(tempRoot, "repo"); @@ -238,15 +214,15 @@ function main() { const dbPath = join(stateDir, "loaf.sqlite"); const candidateBinary = join(repoRoot, candidateNativePath); const candidatePlugin = join(repoRoot, candidateHooksPath); - const installedOpenCode = resolveExecutable("opencode"); const invocation = { command: "opencode", args: ["run", "--format", "json", "--model", "opencode/deepseek-v4-flash-free", "--dir", "", prompt], cwd: "", }; let artifacts = { hooks_path: candidateHooksPath, hooks_sha256: "", native_binary_path: candidateNativePath, native_binary_sha256: "" }; - let smoke = baseReceipt(marker, invocation, artifacts); + let smoke = baseReceipt(marker, invocation, artifacts, expectedVersion); let cleanupSucceeded = false; + let failure; const cleanup = () => { for (let attempt = 0; attempt < 5; attempt += 1) { try { @@ -280,8 +256,8 @@ function main() { if (!existsSync(candidateBinary)) throw new Error("candidate native binary is missing"); const pluginURL = pathToFileURL(candidatePlugin).href; const env = disposableEnvironment(tempRoot, dbPath, pluginURL); - const version = run(installedOpenCode, ["--version"], repoRoot, env); - if (version.status !== 0 || !opencodeVersionMatches(version.stdout)) throw new Error("installed OpenCode version is not exactly 1.18.4"); + const version = run(client, ["--version"], repoRoot, env); + if (version.status !== 0 || !opencodeVersionMatches(version.stdout, expectedVersion)) throw new Error(`installed OpenCode version does not match ${expectedVersion}`); if (run("git", ["init", "-q"], disposableRepo, env).status !== 0) throw new Error("disposable Git initialization failed"); if (run(candidateBinary, ["state", "init", "--json"], disposableRepo, env).status !== 0) throw new Error("isolated Loaf state initialization failed"); if (run(candidateBinary, ["journal", "log", `discover(smoke): ${marker}`], disposableRepo, env).status !== 0) throw new Error("isolated journal marker write failed"); @@ -291,7 +267,7 @@ function main() { if ((statSync(wrapperPath).mode & 0o777) !== 0o700) throw new Error("OpenCode hook observation wrapper is not mode 0700"); const smokeEnv = { ...env, PATH: `${tempRoot}:${env.PATH ?? ""}` }; const opencodeArgs = ["run", "--format", "json", "--model", "opencode/deepseek-v4-flash-free", "--dir", disposableRepo, prompt]; - const opencode = run(installedOpenCode, opencodeArgs, disposableRepo, smokeEnv, 600000); + const opencode = run(client, opencodeArgs, disposableRepo, smokeEnv, 600000); if (!existsSync(observationPath)) throw new Error("OpenCode hook observation file was not written"); if ((statSync(observationPath).mode & 0o777) !== 0o600) throw new Error("OpenCode hook observation is not mode 0600"); const observedOutput = readFileSync(observationPath, "utf8"); @@ -313,15 +289,22 @@ function main() { throw new Error("model-visible OpenCode marker smoke did not pass"); } } catch (error) { - smoke.failure_reason = sanitizeError(error, [[repoRoot, ""], [tempRoot, ""], [process.env.HOME ?? "", ""]]); - smoke.exit_code = smoke.exit_code || 1; + failure = error; } finally { cleanup(); smoke.cleanup_succeeded = cleanupSucceeded; } - writeFileSync(evidencePath, `${JSON.stringify(smoke, null, 2)}\n`); - process.stdout.write(`${JSON.stringify({ evidence_path: "docs/changes/20260710-journal-reliability-foundation/research/u8-opencode-1.18.4-isolated-smoke.json", exit_code: smoke.exit_code, assistant_marker_match: smoke.assistant_marker_match, plugin_loaded: smoke.plugin_loaded, root_session_lookup_proven: smoke.root_session_lookup_proven, cleanup_succeeded: smoke.cleanup_succeeded }, null, 2)}\n`); - if (smoke.exit_code !== 0 || !smoke.assistant_marker_match || !smoke.plugin_loaded || !smoke.root_session_lookup_proven || !smoke.cleanup_succeeded) process.exitCode = 1; + if (failure) throw new Error(sanitizeError(failure, [[repoRoot, ""], [tempRoot, ""], [process.env.HOME ?? "", ""]])); + if (!cleanupSucceeded) throw new Error("disposable OpenCode smoke cleanup failed"); + publishReceiptIfSuccessful(receiptPath, smoke, true); + process.stdout.write(`${JSON.stringify({ receipt: receiptPath, exit_code: smoke.exit_code, assistant_marker_match: smoke.assistant_marker_match, plugin_loaded: smoke.plugin_loaded, root_session_lookup_proven: smoke.root_session_lookup_proven, cleanup_succeeded: true }, null, 2)}\n`); } -if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) main(); +if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + try { + main(); + } catch (error) { + process.stderr.write(`${error instanceof Error ? error.message : "OpenCode request-context smoke failed"}\n`); + process.exitCode = 1; + } +} diff --git a/cli/scripts/smoke-opencode-request-context.test.mjs b/cli/scripts/smoke-opencode-request-context.test.mjs new file mode 100644 index 00000000..39bebc03 --- /dev/null +++ b/cli/scripts/smoke-opencode-request-context.test.mjs @@ -0,0 +1,55 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { collectTextValues, modelVisibleProof, opencodeVersionMatches, parseOpenCodeJSONL, sanitizeError, sanitizedStderr } from "./smoke-opencode-request-context.mjs"; + +test("requires the exact OpenCode version token", () => { + assert.equal(opencodeVersionMatches("9.8.7\n", "9.8.7"), true); + assert.equal(opencodeVersionMatches("9.8.70", "9.8.7"), false); + assert.equal(opencodeVersionMatches("v9.8.7", "9.8.7"), false); + assert.equal(opencodeVersionMatches("9.8.7-dev", "9.8.7"), false); +}); + +test("recursively extracts only string values at text keys", () => { + const texts = collectTextValues({ + text: "top", + nested: [{ text: "nested" }, { value: { text: "deep" } }], + notText: "ignored", + }); + assert.deepEqual(texts, ["top", "nested", "deep"]); +}); + +test("parses JSONL recursively and matches an exact trimmed marker", () => { + const marker = "LOAF_OPENCODE_REQUEST_SMOKE_ABCDEF123456"; + const raw = [ + JSON.stringify({ type: "message", parts: [{ type: "text", text: `context ${marker}` }] }), + JSON.stringify({ type: "result", data: { nested: { text: ` ${marker} ` } } }), + ].join("\n"); + assert.equal(parseOpenCodeJSONL(raw, marker).assistantMarkerMatch, true); + assert.equal(parseOpenCodeJSONL(JSON.stringify({ type: "message", text: "wrong" }), marker).assistantMarkerMatch, false); +}); + +test("rejects malformed and non-object JSONL events", () => { + assert.throws(() => parseOpenCodeJSONL("{not-json}", "marker"), SyntaxError); + assert.throws(() => parseOpenCodeJSONL("null", "marker"), SyntaxError); + assert.throws(() => parseOpenCodeJSONL("[]", "marker"), SyntaxError); +}); + +test("assistant text alone is not model-visible proof without hook observation", () => { + assert.equal(modelVisibleProof(true, false), false); + assert.equal(modelVisibleProof(true, true), true); +}); + +test("sanitizes paths and bounds failure text", () => { + const failure = sanitizeError(new Error("failed at /Users/test/.cache/loaf/tmp/repo with\nsecret details"), [ + ["/Users/test/.cache/loaf", ""], + ["/tmp/repo", ""], + ]); + assert.equal(failure, "failed at with secret details"); + assert.ok(failure.length <= 400); + assert.equal(sanitizeError(new Error("\n\t"), []), "smoke failed"); +}); + +test("sanitizes stderr to the receipt contract", () => { + assert.equal(sanitizedStderr("\n"), ""); + assert.equal(sanitizedStderr("credential=secret\n"), "unexpected stderr"); +}); diff --git a/cli/scripts/u8-claude-smoke.test.mjs b/cli/scripts/u8-claude-smoke.test.mjs deleted file mode 100644 index 4e83b4f9..00000000 --- a/cli/scripts/u8-claude-smoke.test.mjs +++ /dev/null @@ -1,26 +0,0 @@ -import test from "node:test"; -import assert from "node:assert/strict"; -import { claudeVersionMatches, parseClaudeStreamOutput } from "./u8-claude-smoke.mjs"; - -test("parses native SessionStart output and exact assistant marker", () => { - const marker = "LOAF_U8_SMOKE_TEST"; - const raw = [ - JSON.stringify({ type: "system", subtype: "hook_response", hook_event: "SessionStart", output: JSON.stringify({ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: `digest ${marker}` } }) }), - JSON.stringify({ type: "assistant", message: { content: [{ type: "text", text: marker }] } }), - JSON.stringify({ type: "result", result: marker }), - ].join("\n"); - const parsed = parseClaudeStreamOutput(raw, marker); - assert.equal(parsed.hookObservation.native_json, true); - assert.equal(parsed.hookObservation.additional_context_marker, true); - assert.equal(parsed.assistantMarkerMatch, true); -}); - -test("rejects a stream without SessionStart hook response", () => { - assert.throws(() => parseClaudeStreamOutput(JSON.stringify({ type: "result", result: "no hook" }), "marker"), /SessionStart hook response/); -}); - -test("requires the exact Claude Code version token", () => { - assert.equal(claudeVersionMatches("2.1.218 (Claude Code)\n", "2.1.218"), true); - assert.equal(claudeVersionMatches("2.1.2180 (Claude Code)", "2.1.218"), false); - assert.equal(claudeVersionMatches("Claude Code 2.1.218", "2.1.218"), false); -}); diff --git a/cli/scripts/u8-opencode-smoke.test.mjs b/cli/scripts/u8-opencode-smoke.test.mjs deleted file mode 100644 index e671d201..00000000 --- a/cli/scripts/u8-opencode-smoke.test.mjs +++ /dev/null @@ -1,100 +0,0 @@ -import test from "node:test"; -import assert from "node:assert/strict"; -import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; -import { delimiter, join } from "node:path"; -import { tmpdir } from "node:os"; -import { buildEnvironment, collectTextValues, modelVisibleProof, opencodeVersionMatches, parseOpenCodeJSONL, resolveExecutable, sanitizeError, sanitizedStderr } from "./u8-opencode-smoke.mjs"; - -test("build environment passes XDG dirs through only when set", () => { - const xdgKeys = ["XDG_CACHE_HOME", "XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_STATE_HOME"]; - const saved = Object.fromEntries(xdgKeys.map((key) => [key, process.env[key]])); - try { - for (const key of xdgKeys) delete process.env[key]; - let env = buildEnvironment(); - for (const key of xdgKeys) assert.equal(key in env, false); - process.env.XDG_CACHE_HOME = "/tmp/u8-test-cache"; - env = buildEnvironment(); - assert.equal(env.XDG_CACHE_HOME, "/tmp/u8-test-cache"); - assert.equal("XDG_CONFIG_HOME" in env, false); - } finally { - for (const key of xdgKeys) { - if (saved[key] === undefined) delete process.env[key]; - else process.env[key] = saved[key]; - } - } -}); - -test("skips multiplexer shims whose realpath basename differs from the command", () => { - const tempRoot = mkdtempSync(join(tmpdir(), "u8-resolve-test-")); - const savedPath = process.env.PATH; - try { - const shimDir = join(tempRoot, "shims"); - const realDir = join(tempRoot, "installs"); - mkdirSync(shimDir, { recursive: true }); - mkdirSync(realDir, { recursive: true }); - const mux = join(tempRoot, "mise"); - writeFileSync(mux, "#!/bin/sh\n", { mode: 0o755 }); - symlinkSync(mux, join(shimDir, "opencode")); - const genuine = join(realDir, "opencode"); - writeFileSync(genuine, "#!/bin/sh\n", { mode: 0o755 }); - process.env.PATH = [shimDir, realDir].join(delimiter); - assert.equal(resolveExecutable("opencode"), realpathSync(genuine)); - process.env.PATH = shimDir; - assert.throws(() => resolveExecutable("opencode"), /unavailable/); - } finally { - process.env.PATH = savedPath; - rmSync(tempRoot, { recursive: true, force: true }); - } -}); - -test("requires the exact OpenCode version token", () => { - assert.equal(opencodeVersionMatches("1.18.4\n"), true); - assert.equal(opencodeVersionMatches("1.18.40"), false); - assert.equal(opencodeVersionMatches("v1.18.4"), false); - assert.equal(opencodeVersionMatches("1.18.4-dev"), false); -}); - -test("recursively extracts only string values at text keys", () => { - const texts = collectTextValues({ - text: "top", - nested: [{ text: "nested" }, { value: { text: "deep" } }], - notText: "ignored", - }); - assert.deepEqual(texts, ["top", "nested", "deep"]); -}); - -test("parses JSONL recursively and matches an exact trimmed marker", () => { - const marker = "LOAF_OPENCODE_U8_ABCDEF123456"; - const raw = [ - JSON.stringify({ type: "message", parts: [{ type: "text", text: `context ${marker}` }] }), - JSON.stringify({ type: "result", data: { nested: { text: ` ${marker} ` } } }), - ].join("\n"); - assert.equal(parseOpenCodeJSONL(raw, marker).assistantMarkerMatch, true); - assert.equal(parseOpenCodeJSONL(JSON.stringify({ type: "message", text: "wrong" }), marker).assistantMarkerMatch, false); -}); - -test("rejects malformed and non-object JSONL events", () => { - assert.throws(() => parseOpenCodeJSONL("{not-json}", "marker"), SyntaxError); - assert.throws(() => parseOpenCodeJSONL("null", "marker"), SyntaxError); - assert.throws(() => parseOpenCodeJSONL("[]", "marker"), SyntaxError); -}); - -test("assistant text alone is not model-visible proof without hook observation", () => { - assert.equal(modelVisibleProof(true, false), false); - assert.equal(modelVisibleProof(true, true), true); -}); - -test("sanitizes paths and bounds failure text", () => { - const failure = sanitizeError(new Error("failed at /Users/test/.cache/loaf/tmp/repo with\nsecret details"), [ - ["/Users/test/.cache/loaf", ""], - ["/tmp/repo", ""], - ]); - assert.equal(failure, "failed at with secret details"); - assert.ok(failure.length <= 400); - assert.equal(sanitizeError(new Error("\n\t"), []), "smoke failed"); -}); - -test("sanitizes stderr to the receipt contract", () => { - assert.equal(sanitizedStderr("\n"), ""); - assert.equal(sanitizedStderr("credential=secret\n"), "unexpected stderr"); -}); diff --git a/config/target-capabilities.json b/config/target-capabilities.json index c74c29cc..ff2e1d6a 100644 --- a/config/target-capabilities.json +++ b/config/target-capabilities.json @@ -20,7 +20,7 @@ "status": "supported", "model_visible": true, "evidence": { - "source": "docs/changes/20260710-journal-reliability-foundation/research/u8-claude-code-2.1.218-candidate-smoke.json", + "source": "docs/changes/20260710-journal-reliability-foundation/research/claude-code-2.1.218-plugin-startup-smoke.json", "level": "installed-smoke", "summary": "Claude Code 2.1.218 startup smoke returned SessionStart additionalContext and the isolated marker was model-visible exactly." } @@ -70,7 +70,7 @@ "durable_identity": "unproven", "child_suppression": "unproven", "evidence": { - "source": "docs/changes/20260710-journal-reliability-foundation/research/u8-claude-code-2.1.207-candidate-smoke.md", + "source": "docs/changes/20260710-journal-reliability-foundation/research/claude-code-2.1.207-plugin-startup-smoke.md", "level": "source", "summary": "The retained Claude smoke covers startup context only and does not prove completion result fidelity or durable identity." } @@ -102,7 +102,7 @@ "model_visible": false, "reason": "Cursor IDE native sessionStart is source-supported, but an installed model-visible smoke is not yet proven for 3.11.19.", "evidence": { - "source": "docs/changes/20260710-journal-reliability-foundation/research/u8-target-capability-survey.md", + "source": "docs/changes/20260710-journal-reliability-foundation/research/target-capability-survey.md", "level": "source", "summary": "Cursor IDE 3.11.19 bundle invokes native new-composer sessionStart delivery with additional_context output." } @@ -113,7 +113,7 @@ "model_visible": false, "reason": "No native Cursor IDE resume context trigger is retained for this exact version.", "evidence": { - "source": "docs/changes/20260710-journal-reliability-foundation/research/u8-target-capability-survey.md", + "source": "docs/changes/20260710-journal-reliability-foundation/research/target-capability-survey.md", "level": "source", "summary": "The retained Cursor source survey classifies resume as outside the native sessionStart surface." } @@ -124,7 +124,7 @@ "model_visible": false, "reason": "No native Cursor IDE compact context trigger is retained for this exact version.", "evidence": { - "source": "docs/changes/20260710-journal-reliability-foundation/research/u8-target-capability-survey.md", + "source": "docs/changes/20260710-journal-reliability-foundation/research/target-capability-survey.md", "level": "source", "summary": "The retained Cursor source survey classifies compact as outside the native sessionStart surface." } @@ -135,7 +135,7 @@ "model_visible": false, "reason": "No native Cursor IDE cloud-session context trigger is retained for this exact version.", "evidence": { - "source": "docs/changes/20260710-journal-reliability-foundation/research/u8-target-capability-survey.md", + "source": "docs/changes/20260710-journal-reliability-foundation/research/target-capability-survey.md", "level": "source", "summary": "The retained Cursor source survey classifies cloud sessions as outside the native sessionStart surface." } @@ -149,7 +149,7 @@ "durable_identity": "unproven", "child_suppression": "unproven", "evidence": { - "source": "docs/changes/20260710-journal-reliability-foundation/research/u8-target-capability-survey.md", + "source": "docs/changes/20260710-journal-reliability-foundation/research/target-capability-survey.md", "level": "source", "summary": "Cursor source evidence does not prove completion result fidelity or durable identity." } @@ -181,7 +181,7 @@ "model_visible": false, "reason": "Cursor Agent native sessionStart is source-supported, but an installed model-visible smoke is not yet proven.", "evidence": { - "source": "docs/changes/20260710-journal-reliability-foundation/research/u8-target-capability-survey.md", + "source": "docs/changes/20260710-journal-reliability-foundation/research/target-capability-survey.md", "level": "source", "summary": "Cursor Agent 2026.05.09-0afadcc retains the same native new-composer sessionStart surface." } @@ -192,7 +192,7 @@ "model_visible": false, "reason": "No native Cursor Agent resume context trigger is retained for this exact version.", "evidence": { - "source": "docs/changes/20260710-journal-reliability-foundation/research/u8-target-capability-survey.md", + "source": "docs/changes/20260710-journal-reliability-foundation/research/target-capability-survey.md", "level": "source", "summary": "The retained Cursor source survey classifies resume as outside the native sessionStart surface." } @@ -203,7 +203,7 @@ "model_visible": false, "reason": "No native Cursor Agent compact context trigger is retained for this exact version.", "evidence": { - "source": "docs/changes/20260710-journal-reliability-foundation/research/u8-target-capability-survey.md", + "source": "docs/changes/20260710-journal-reliability-foundation/research/target-capability-survey.md", "level": "source", "summary": "The retained Cursor source survey classifies compact as outside the native sessionStart surface." } @@ -214,7 +214,7 @@ "model_visible": false, "reason": "No native Cursor Agent cloud-session context trigger is retained for this exact version.", "evidence": { - "source": "docs/changes/20260710-journal-reliability-foundation/research/u8-target-capability-survey.md", + "source": "docs/changes/20260710-journal-reliability-foundation/research/target-capability-survey.md", "level": "source", "summary": "The retained Cursor source survey classifies cloud sessions as outside the native sessionStart surface." } @@ -228,7 +228,7 @@ "durable_identity": "unproven", "child_suppression": "unproven", "evidence": { - "source": "docs/changes/20260710-journal-reliability-foundation/research/u8-target-capability-survey.md", + "source": "docs/changes/20260710-journal-reliability-foundation/research/target-capability-survey.md", "level": "source", "summary": "Cursor Agent source evidence does not prove completion result fidelity or durable identity." } @@ -259,7 +259,7 @@ "status": "supported", "model_visible": true, "evidence": { - "source": "docs/changes/20260710-journal-reliability-foundation/research/u8-codex-0.145.0-isolated-smoke.json", + "source": "docs/changes/20260710-journal-reliability-foundation/research/codex-0.145.0-isolated-startup-smoke.json", "level": "installed-smoke", "summary": "Codex 0.145.0 isolated CODEX_HOME startup smoke observed exact native SessionStart hookSpecificOutput and the random marker was returned by the model." } @@ -271,7 +271,7 @@ "model_visible": false, "reason": "The tagged schema retains SessionStart for resume, but an installed model-visible resume smoke is not yet proven.", "evidence": { - "source": "docs/changes/20260710-journal-reliability-foundation/research/u8-target-capability-survey.md", + "source": "docs/changes/20260710-journal-reliability-foundation/research/target-capability-survey.md", "level": "source", "summary": "Codex 0.144.1 source survey classifies resume as a SessionStart context mode." } @@ -283,7 +283,7 @@ "model_visible": false, "reason": "The tagged schema retains SessionStart for clear, but an installed model-visible clear smoke is not yet proven.", "evidence": { - "source": "docs/changes/20260710-journal-reliability-foundation/research/u8-target-capability-survey.md", + "source": "docs/changes/20260710-journal-reliability-foundation/research/target-capability-survey.md", "level": "source", "summary": "Codex 0.144.1 source survey classifies clear as a SessionStart context mode." } @@ -295,7 +295,7 @@ "model_visible": false, "reason": "The tagged schema retains SessionStart for compact, but an installed model-visible compact smoke is not yet proven.", "evidence": { - "source": "docs/changes/20260710-journal-reliability-foundation/research/u8-target-capability-survey.md", + "source": "docs/changes/20260710-journal-reliability-foundation/research/target-capability-survey.md", "level": "source", "summary": "Codex 0.144.1 source survey classifies compact as a SessionStart context mode; SubagentStart is not a context mode." } @@ -309,7 +309,7 @@ "durable_identity": "unproven", "child_suppression": "unproven", "evidence": { - "source": "docs/changes/20260710-journal-reliability-foundation/research/u8-target-capability-survey.md", + "source": "docs/changes/20260710-journal-reliability-foundation/research/target-capability-survey.md", "level": "source", "summary": "Codex hook source evidence does not prove completion result fidelity or durable identity." } @@ -340,7 +340,7 @@ "status": "supported", "model_visible": true, "evidence": { - "source": "docs/changes/20260710-journal-reliability-foundation/research/u8-opencode-1.18.4-isolated-smoke.json", + "source": "docs/changes/20260710-journal-reliability-foundation/research/opencode-1.18.4-isolated-request-smoke.json", "level": "installed-smoke", "summary": "OpenCode 1.18.4 isolated-XDG request smoke returned the exact random continuity marker through the model-visible plugin system transformation." } @@ -351,7 +351,7 @@ "model_visible": false, "reason": "OpenCode has no native distinct startup signal; request transformation is the only installed model-visible context proof.", "evidence": { - "source": "docs/changes/20260710-journal-reliability-foundation/research/u8-target-capability-survey.md", + "source": "docs/changes/20260710-journal-reliability-foundation/research/target-capability-survey.md", "level": "source", "summary": "OpenCode 1.17.18 exposes request transformation rather than a distinct startup lifecycle signal." } @@ -363,7 +363,7 @@ "model_visible": false, "reason": "The request transformation can deliver context after reopening a session, but an installed model-visible resume smoke is not yet proven.", "evidence": { - "source": "docs/changes/20260710-journal-reliability-foundation/research/u8-target-capability-survey.md", + "source": "docs/changes/20260710-journal-reliability-foundation/research/target-capability-survey.md", "level": "source", "summary": "OpenCode 1.17.18 retains the request transformation surface for context after session reopen." } @@ -375,7 +375,7 @@ "model_visible": false, "reason": "The compaction adapter can append context, but an installed model-visible compact smoke is not yet proven.", "evidence": { - "source": "docs/changes/20260710-journal-reliability-foundation/research/u8-target-capability-survey.md", + "source": "docs/changes/20260710-journal-reliability-foundation/research/target-capability-survey.md", "level": "source", "summary": "OpenCode 1.17.18 retains experimental.session.compacting for compaction context injection." } @@ -389,7 +389,7 @@ "durable_identity": "unproven", "child_suppression": "parentID is available for context but completion identity remains unwired", "evidence": { - "source": "docs/changes/20260710-journal-reliability-foundation/research/u8-target-capability-survey.md", + "source": "docs/changes/20260710-journal-reliability-foundation/research/target-capability-survey.md", "level": "source", "summary": "OpenCode bash exit status is source-proven, but operation success and durable completion identity remain unproven." } @@ -422,7 +422,7 @@ "model_visible": false, "reason": "root-only identity cannot be proven", "evidence": { - "source": "docs/changes/20260710-journal-reliability-foundation/research/u8-target-capability-survey.md", + "source": "docs/changes/20260710-journal-reliability-foundation/research/target-capability-survey.md", "level": "source", "summary": "Amp 0.0.1783873056-g278461 exposes agent.start, but foreground child identity cannot be distinguished from the root turn." } @@ -433,7 +433,7 @@ "model_visible": false, "reason": "Amp has no native distinct startup context signal.", "evidence": { - "source": "docs/changes/20260710-journal-reliability-foundation/research/u8-target-capability-survey.md", + "source": "docs/changes/20260710-journal-reliability-foundation/research/target-capability-survey.md", "level": "source", "summary": "Amp exposes agent.start rather than a distinct startup context signal." } @@ -444,7 +444,7 @@ "model_visible": false, "reason": "Amp has no native distinct resume context signal.", "evidence": { - "source": "docs/changes/20260710-journal-reliability-foundation/research/u8-target-capability-survey.md", + "source": "docs/changes/20260710-journal-reliability-foundation/research/target-capability-survey.md", "level": "source", "summary": "Amp exposes no native resume context signal for this exact version." } @@ -455,7 +455,7 @@ "model_visible": false, "reason": "Amp has no native distinct compact context signal.", "evidence": { - "source": "docs/changes/20260710-journal-reliability-foundation/research/u8-target-capability-survey.md", + "source": "docs/changes/20260710-journal-reliability-foundation/research/target-capability-survey.md", "level": "source", "summary": "The retained Amp source survey records no native compaction surface." } @@ -469,7 +469,7 @@ "durable_identity": "unproven because only thread id and active/background state are available", "child_suppression": "unproven because only thread id and active/background state are available", "evidence": { - "source": "docs/changes/20260710-journal-reliability-foundation/research/u8-target-capability-survey.md", + "source": "docs/changes/20260710-journal-reliability-foundation/research/target-capability-survey.md", "level": "source", "summary": "Amp tool.call/tool.result and generic done/error/cancelled signals do not prove operation success, durable identity, or child suppression." } diff --git a/docs/changes/20260710-journal-reliability-foundation/research/u8-claude-code-2.1.207-candidate-smoke.md b/docs/changes/20260710-journal-reliability-foundation/research/claude-code-2.1.207-plugin-startup-smoke.md similarity index 62% rename from docs/changes/20260710-journal-reliability-foundation/research/u8-claude-code-2.1.207-candidate-smoke.md rename to docs/changes/20260710-journal-reliability-foundation/research/claude-code-2.1.207-plugin-startup-smoke.md index 29c5f38c..a556098d 100644 --- a/docs/changes/20260710-journal-reliability-foundation/research/u8-claude-code-2.1.207-candidate-smoke.md +++ b/docs/changes/20260710-journal-reliability-foundation/research/claude-code-2.1.207-plugin-startup-smoke.md @@ -1,4 +1,4 @@ -# U8 Claude Code candidate smoke +# Claude Code 2.1.207 plugin startup smoke - Date: 2026-07-13 - Surface: Claude Code 2.1.207 on `darwin-arm64` @@ -6,6 +6,6 @@ - Isolation: disposable Git repository and absolute disposable `LOAF_DB`; no global plugin installation, global Loaf database, or persisted Claude session was used. - Candidate: freshly built `plugins/loaf` Claude target. - Result: exit 0 with no stderr. The `SessionStart:startup` hook returned valid Claude-native JSON with `hookSpecificOutput.hookEventName=SessionStart` and an `additionalContext` containing the random marker written only to the isolated journal. The model-visible response returned that marker exactly. -- Reusable command: `node cli/scripts/u8-claude-smoke.mjs` (builds the candidate, creates disposable Git and absolute `LOAF_DB` state, runs the exact smoke, validates the native hook response and assistant marker, cleans disposable state, and writes the receipt). -- Structured receipt: [u8-claude-code-2.1.207-candidate-smoke.json](u8-claude-code-2.1.207-candidate-smoke.json) +- Reusable command: `node cli/scripts/smoke-claude-code-startup.mjs --client claude --expected-version 2.1.207 --receipt docs/changes/20260710-journal-reliability-foundation/research/claude-code-2.1.207-plugin-startup-smoke.json` (builds the candidate, creates disposable Git and absolute `LOAF_DB` state, runs the exact smoke, validates the native hook response and assistant marker, cleans disposable state, and atomically publishes the receipt only after success). +- Structured receipt: [claude-code-2.1.207-plugin-startup-smoke.json](claude-code-2.1.207-plugin-startup-smoke.json) - Evidence level: candidate model-visible smoke for Claude Code 2.1.207 in the explicit candidate-plugin mode. The receipt proves startup only; resume, clear, and compact remain candidate modes. This does not prove the globally installed Loaf plugin. diff --git a/docs/changes/20260710-journal-reliability-foundation/research/u8-claude-code-2.1.218-candidate-smoke.json b/docs/changes/20260710-journal-reliability-foundation/research/claude-code-2.1.218-plugin-startup-smoke.json similarity index 94% rename from docs/changes/20260710-journal-reliability-foundation/research/u8-claude-code-2.1.218-candidate-smoke.json rename to docs/changes/20260710-journal-reliability-foundation/research/claude-code-2.1.218-plugin-startup-smoke.json index 86a82e09..68b50bb9 100644 --- a/docs/changes/20260710-journal-reliability-foundation/research/u8-claude-code-2.1.218-candidate-smoke.json +++ b/docs/changes/20260710-journal-reliability-foundation/research/claude-code-2.1.218-plugin-startup-smoke.json @@ -1,6 +1,6 @@ { "evidence_version": 2, - "timestamp": "2026-07-24T18:30:01.704Z", + "timestamp": "2026-07-24T22:31:44.205Z", "target": "claude-code", "surface": "cli", "version": "2.1.218", @@ -41,7 +41,7 @@ "stderr_empty": true, "model_visible_marker_observed": true, "assistant_marker_match": true, - "marker": "LOAF_U8_SMOKE_31ABBEE4A790", + "marker": "LOAF_CLAUDE_STARTUP_SMOKE_82C9A319C829", "hook_observation": { "event_name": "SessionStart:startup", "native_json": true, diff --git a/docs/changes/20260710-journal-reliability-foundation/research/u8-codex-0.145.0-isolated-smoke.json b/docs/changes/20260710-journal-reliability-foundation/research/codex-0.145.0-isolated-startup-smoke.json similarity index 88% rename from docs/changes/20260710-journal-reliability-foundation/research/u8-codex-0.145.0-isolated-smoke.json rename to docs/changes/20260710-journal-reliability-foundation/research/codex-0.145.0-isolated-startup-smoke.json index 13567dca..0835817f 100644 --- a/docs/changes/20260710-journal-reliability-foundation/research/u8-codex-0.145.0-isolated-smoke.json +++ b/docs/changes/20260710-journal-reliability-foundation/research/codex-0.145.0-isolated-startup-smoke.json @@ -1,6 +1,6 @@ { "evidence_version": 2, - "timestamp": "2026-07-24T18:30:42.555Z", + "timestamp": "2026-07-24T22:32:03.850Z", "target": "codex", "surface": "cli", "version": "0.145.0", @@ -32,15 +32,14 @@ "copy installed auth.json into isolated CODEX_HOME with mode 0600", "initialize absolute disposable LOAF_DB", "write random marker to isolated journal", - "observe hook stdout through a mode-0700 disposable wrapper and mode-0600 file", - "disable Codex tracing logs (RUST_LOG=off) so backend transport noise cannot contaminate the stderr cleanliness gate" + "observe hook stdout through a mode-0700 disposable wrapper and mode-0600 file" ], "exit_code": 0, "stderr_empty": false, "stderr": "Reading additional input from stdin...", "model_visible_marker_observed": true, "assistant_marker_match": true, - "marker": "LOAF_CODEX_U8_8DEF409DF6C0", + "marker": "LOAF_CODEX_STARTUP_SMOKE_A5533AEA314C", "hook_observation": { "event_name": "SessionStart:startup", "native_json": true, diff --git a/docs/changes/20260710-journal-reliability-foundation/research/u8-cursor-agent-candidate-preflight.json b/docs/changes/20260710-journal-reliability-foundation/research/cursor-agent-2026.05.09-0afadcc-isolation-preflight.json similarity index 90% rename from docs/changes/20260710-journal-reliability-foundation/research/u8-cursor-agent-candidate-preflight.json rename to docs/changes/20260710-journal-reliability-foundation/research/cursor-agent-2026.05.09-0afadcc-isolation-preflight.json index 356c9eef..12090d35 100644 --- a/docs/changes/20260710-journal-reliability-foundation/research/u8-cursor-agent-candidate-preflight.json +++ b/docs/changes/20260710-journal-reliability-foundation/research/cursor-agent-2026.05.09-0afadcc-isolation-preflight.json @@ -1,6 +1,6 @@ { "evidence_version": 2, - "timestamp": "2026-07-13T19:08:37.525Z", + "timestamp": "2026-07-14T16:12:24.351Z", "target": "cursor", "surface": "cursor-agent", "version": "2026.05.09-0afadcc", @@ -22,7 +22,7 @@ "hooks_path": "dist/cursor/hooks.json", "hooks_sha256": "7cba16593baab2f9e98c46efaa4b6ab023de08e04ff78c10cb3f48e8ec73fbc0", "native_binary_path": "bin/native/darwin-arm64/loaf", - "native_binary_sha256": "59694569d043f4ef0dfdd8442ad96664dca029cdc31ad3400266f1e266600e72" + "native_binary_sha256": "902e41eea856eacaf172fc9708aa873bb2bc67c360ab00c7ac36070b106855f4" }, "blocker": "installed cursor-agent does not expose --no-session-persistence; refusing a model-visible smoke that could persist session state globally", "retry_condition": "Rerun after the installed CLI exposes a documented no-session-persistence mode or after an explicitly approved isolated session-store boundary is available." diff --git a/docs/changes/20260710-journal-reliability-foundation/research/u8-cursor-agent-candidate-preflight.md b/docs/changes/20260710-journal-reliability-foundation/research/cursor-agent-2026.05.09-0afadcc-isolation-preflight.md similarity index 50% rename from docs/changes/20260710-journal-reliability-foundation/research/u8-cursor-agent-candidate-preflight.md rename to docs/changes/20260710-journal-reliability-foundation/research/cursor-agent-2026.05.09-0afadcc-isolation-preflight.md index 78447366..aae9b4cf 100644 --- a/docs/changes/20260710-journal-reliability-foundation/research/u8-cursor-agent-candidate-preflight.md +++ b/docs/changes/20260710-journal-reliability-foundation/research/cursor-agent-2026.05.09-0afadcc-isolation-preflight.md @@ -1,11 +1,11 @@ -# Cursor Agent U8 candidate preflight +# Cursor Agent 2026.05.09-0afadcc isolation preflight This receipt records an unavailable preflight on `darwin-arm64`, not installed model-visible support. The installed Cursor Agent identity is `2026.05.09-0afadcc`; `cursor-agent --help` does not expose `--no-session-persistence`, so no model-visible prompt is invoked that could persist a session outside the disposable project. The separate `cursor-agent status --format json` check was not retained because it includes account-state output; no account data is part of the evidence. Rerun from the repository root: ```bash -node cli/scripts/u8-cursor-smoke.mjs +node cli/scripts/preflight-cursor-agent-context.mjs --client cursor-agent --expected-version 2026.05.09-0afadcc --receipt docs/changes/20260710-journal-reliability-foundation/research/cursor-agent-2026.05.09-0afadcc-isolation-preflight.json ``` -The script records the exact candidate `dist/cursor/hooks.json` and native binary SHA-256 values, the sanitized preflight facts, and the explicit blocker in [u8-cursor-agent-candidate-preflight.json](u8-cursor-agent-candidate-preflight.json). No global Cursor configuration, install, session store, or Loaf database is modified. The Cursor Agent `new-composer` capability therefore remains `candidate`; IDE `3.11.19` remains a separate `candidate` identity. +The script records the exact candidate `dist/cursor/hooks.json` and native binary SHA-256 values, the sanitized preflight facts, and the explicit blocker in [cursor-agent-2026.05.09-0afadcc-isolation-preflight.json](cursor-agent-2026.05.09-0afadcc-isolation-preflight.json). No global Cursor configuration, install, session store, or Loaf database is modified. The Cursor Agent `new-composer` capability therefore remains `candidate`; IDE `3.11.19` remains a separate `candidate` identity. diff --git a/docs/changes/20260710-journal-reliability-foundation/research/u8-opencode-1.18.4-isolated-smoke.json b/docs/changes/20260710-journal-reliability-foundation/research/opencode-1.18.4-isolated-request-smoke.json similarity index 93% rename from docs/changes/20260710-journal-reliability-foundation/research/u8-opencode-1.18.4-isolated-smoke.json rename to docs/changes/20260710-journal-reliability-foundation/research/opencode-1.18.4-isolated-request-smoke.json index f0b42157..eb2ef226 100644 --- a/docs/changes/20260710-journal-reliability-foundation/research/u8-opencode-1.18.4-isolated-smoke.json +++ b/docs/changes/20260710-journal-reliability-foundation/research/opencode-1.18.4-isolated-request-smoke.json @@ -1,6 +1,6 @@ { "evidence_version": 2, - "timestamp": "2026-07-24T18:30:28.388Z", + "timestamp": "2026-07-24T22:32:15.899Z", "target": "opencode", "surface": "cli", "version": "1.18.4", @@ -39,7 +39,7 @@ "root_session_lookup_proven": true, "no_auth_supplied": true, "cleanup_succeeded": true, - "marker": "LOAF_OPENCODE_U8_7060016BD104", + "marker": "LOAF_OPENCODE_REQUEST_SMOKE_BB01462EC885", "candidate_artifacts": { "hooks_path": "dist/opencode/plugins/hooks.ts", "hooks_sha256": "b345ff7ec631d30f009c96d1d3161ba731fa57352a153e71bdf95583640c25e6", diff --git a/docs/changes/20260710-journal-reliability-foundation/research/u8-target-capability-survey.md b/docs/changes/20260710-journal-reliability-foundation/research/target-capability-survey.md similarity index 76% rename from docs/changes/20260710-journal-reliability-foundation/research/u8-target-capability-survey.md rename to docs/changes/20260710-journal-reliability-foundation/research/target-capability-survey.md index 3d19bd1d..013e139e 100644 --- a/docs/changes/20260710-journal-reliability-foundation/research/u8-target-capability-survey.md +++ b/docs/changes/20260710-journal-reliability-foundation/research/target-capability-survey.md @@ -1,4 +1,4 @@ -# U8 target capability survey +# Target capability survey This retained survey is source-level and installed-smoke evidence for the versioned capability registry. It does not claim an installed model-visible smoke for Cursor or Amp; OpenCode request delivery is proven only for the exact isolated-XDG smoke recorded below. @@ -8,11 +8,11 @@ Cursor IDE 3.11.19 and cursor-agent 2026.05.09-0afadcc expose the native new-com ## Codex -Codex CLI 0.144.1 retains `SessionStart` hook schemas with `additionalContext` and `systemMessage` fields. An isolated `CODEX_HOME` smoke on `darwin-arm64` now proves startup: it enables hooks, copies the installed auth file with mode 0600 outside the repository, installs the generated current-schema matcher group with a disposable mode-0700 observer that captures exact candidate stdout to a mode-0600 file, initializes an absolute disposable Loaf database, and runs `codex exec --ephemeral --ignore-rules --dangerously-bypass-hook-trust --sandbox read-only --json`; the native `hookSpecificOutput` contains the random marker and the model returns it exactly. This proves only the tested 0.144.1 isolated-home startup path on that exact platform; global user-home installation, resume, clear, compact, Windows runtime, and completion capture remain separately unproven. Primary tagged sources: [SessionStart input schema](https://github.com/openai/codex/blob/rust-v0.144.1/codex-rs/hooks/schema/generated/session-start.command.input.schema.json), [SessionStart output schema](https://github.com/openai/codex/blob/rust-v0.144.1/codex-rs/hooks/schema/generated/session-start.command.output.schema.json), [Codex hooks documentation](https://developers.openai.com/codex/hooks/), and the retained [isolated startup smoke receipt](u8-codex-0.144.1-isolated-smoke.json). +Codex CLI 0.144.1 retains `SessionStart` hook schemas with `additionalContext` and `systemMessage` fields. An isolated `CODEX_HOME` smoke on `darwin-arm64` now proves startup: it enables hooks, copies the installed auth file with mode 0600 outside the repository, installs the generated current-schema matcher group with a disposable mode-0700 observer that captures exact candidate stdout to a mode-0600 file, initializes an absolute disposable Loaf database, and runs `codex exec --ephemeral --ignore-rules --dangerously-bypass-hook-trust --sandbox read-only --json`; the native `hookSpecificOutput` contains the random marker and the model returns it exactly. This proves only the tested 0.144.1 isolated-home startup path on that exact platform; global user-home installation, resume, clear, compact, Windows runtime, and completion capture remain separately unproven. The retained proof can be refreshed with `node cli/scripts/smoke-codex-startup.mjs --client codex --expected-version 0.144.1 --receipt docs/changes/20260710-journal-reliability-foundation/research/codex-0.144.1-isolated-startup-smoke.json`. Primary tagged sources: [SessionStart input schema](https://github.com/openai/codex/blob/rust-v0.144.1/codex-rs/hooks/schema/generated/session-start.command.input.schema.json), [SessionStart output schema](https://github.com/openai/codex/blob/rust-v0.144.1/codex-rs/hooks/schema/generated/session-start.command.output.schema.json), [Codex hooks documentation](https://developers.openai.com/codex/hooks/), and the retained [isolated startup smoke receipt](codex-0.144.1-isolated-startup-smoke.json). ## OpenCode -OpenCode CLI 1.17.18 supports request context through the per-request plugin transformation `experimental.chat.system.transform`, returning the bounded digest in `output.system`; compaction context is appended through `experimental.session.compacting` in `output.context`. The isolated-XDG smoke invokes `opencode run --format json --model opencode/deepseek-v4-flash-free --dir ` with the exact marker prompt, loads only the candidate `dist/opencode/plugins/hooks.ts`, proves root-session lookup without `parentID` while child/background classification uses `parentID`, and observes the random marker returned exactly by the model. The strict receipt also proves zero exit, empty stderr, no auth supplied, cleanup, and current candidate artifact hashes. Request is therefore supported for this exact version/mode; startup has no distinct native signal, resume remains a transform candidate after reopen, and compact remains a candidate. Completion stays disabled: bash exit status is source-proven, but operation success and durable identity are not. Bypass cases are a missing/disabled plugin, an attached or remote server without the candidate plugin, missing state, and an untested version. Primary documentation: [OpenCode plugins](https://opencode.ai/docs/plugins/) and the retained [isolated-XDG request smoke receipt](u8-opencode-1.17.18-isolated-smoke.json). +OpenCode CLI 1.17.18 supports request context through the per-request plugin transformation `experimental.chat.system.transform`, returning the bounded digest in `output.system`; compaction context is appended through `experimental.session.compacting` in `output.context`. The isolated-XDG smoke invokes `opencode run --format json --model opencode/deepseek-v4-flash-free --dir ` with the exact marker prompt, loads only the candidate `dist/opencode/plugins/hooks.ts`, proves root-session lookup without `parentID` while child/background classification uses `parentID`, and observes the random marker returned exactly by the model. The strict receipt also proves zero exit, empty stderr, no auth supplied, cleanup, and current candidate artifact hashes. Request is therefore supported for this exact version/mode; startup has no distinct native signal, resume remains a transform candidate after reopen, and compact remains a candidate. Completion stays disabled: bash exit status is source-proven, but operation success and durable identity are not. Bypass cases are a missing/disabled plugin, an attached or remote server without the candidate plugin, missing state, and an untested version. The retained proof can be refreshed with `node cli/scripts/smoke-opencode-request-context.mjs --client opencode --expected-version 1.17.18 --receipt docs/changes/20260710-journal-reliability-foundation/research/opencode-1.17.18-isolated-request-smoke.json`. Primary documentation: [OpenCode plugins](https://opencode.ai/docs/plugins/) and the retained [isolated-XDG request smoke receipt](opencode-1.17.18-isolated-request-smoke.json). ## Amp diff --git a/internal/cli/target_capability_contract.go b/internal/cli/target_capability_contract.go index 5bec43ec..4c23b3f0 100644 --- a/internal/cli/target_capability_contract.go +++ b/internal/cli/target_capability_contract.go @@ -674,7 +674,7 @@ func validateInstalledSmokeEvidence(root string, record TargetCapabilityRecord, if smoke.HookObservation.EventName != "SessionStart:startup" || !smoke.HookObservation.NativeJSON || smoke.HookObservation.HookEventName != "SessionStart" || !smoke.HookObservation.AdditionalContextMarker { return errors.New("installed-smoke evidence lacks the expected native SessionStart marker observation") } - if smoke.ExitCode != 0 || !smoke.StderrEmpty || !smoke.ModelVisibleMarkerObserved || !smoke.AssistantMarkerMatch || !regexp.MustCompile(`^LOAF_U8_SMOKE_[A-F0-9]{12}$`).MatchString(smoke.Marker) { + if smoke.ExitCode != 0 || !smoke.StderrEmpty || !smoke.ModelVisibleMarkerObserved || !smoke.AssistantMarkerMatch || !regexp.MustCompile(`^LOAF_CLAUDE_STARTUP_SMOKE_[A-F0-9]{12}$`).MatchString(smoke.Marker) { return errors.New("installed-smoke evidence does not prove a successful model-visible marker result") } artifacts := smoke.CandidateArtifacts @@ -792,8 +792,8 @@ func validateOpenCodeInstalledSmokeEvidence(root string, record TargetCapability if !smoke.ModelVisibleMarkerObserved || !smoke.AssistantMarkerMatch || !smoke.PluginLoaded || !smoke.RootSessionLookupProven || !smoke.NoAuthSupplied || !smoke.CleanupSucceeded { return errors.New("OpenCode installed-smoke evidence does not prove the required model-visible plugin observations") } - if !regexp.MustCompile(`^LOAF_OPENCODE_U8_[A-F0-9]{12}$`).MatchString(smoke.Marker) { - return errors.New("OpenCode installed-smoke evidence marker is not a valid U8 marker") + if !regexp.MustCompile(`^LOAF_OPENCODE_REQUEST_SMOKE_[A-F0-9]{12}$`).MatchString(smoke.Marker) { + return errors.New("OpenCode installed-smoke evidence marker is not a valid request-smoke marker") } artifacts := smoke.CandidateArtifacts if artifacts.HooksPath != "dist/opencode/plugins/hooks.ts" { @@ -894,7 +894,7 @@ func validateCodexInstalledSmokeEvidence(root string, record TargetCapabilityRec if len(smoke.Setup) == 0 || smoke.HookObservation.EventName != "SessionStart:startup" || !smoke.HookObservation.NativeJSON || smoke.HookObservation.HookEventName != "SessionStart" || !smoke.HookObservation.AdditionalContextMarker { return errors.New("Codex installed-smoke evidence lacks the expected native SessionStart marker observation") } - if smoke.ExitCode != 0 || !smoke.ModelVisibleMarkerObserved || !smoke.AssistantMarkerMatch || !regexp.MustCompile(`^LOAF_CODEX_U8_[A-F0-9]{12}$`).MatchString(smoke.Marker) { + if smoke.ExitCode != 0 || !smoke.ModelVisibleMarkerObserved || !smoke.AssistantMarkerMatch || !regexp.MustCompile(`^LOAF_CODEX_STARTUP_SMOKE_[A-F0-9]{12}$`).MatchString(smoke.Marker) { return errors.New("Codex installed-smoke evidence does not prove a successful model-visible marker result") } if smoke.Stderr != "" && smoke.Stderr != "Reading additional input from stdin..." { diff --git a/internal/cli/target_capability_contract_test.go b/internal/cli/target_capability_contract_test.go index 6c6f9cb6..c4898f7c 100644 --- a/internal/cli/target_capability_contract_test.go +++ b/internal/cli/target_capability_contract_test.go @@ -329,7 +329,7 @@ func TestTargetCapabilityEvidenceLoadRequiresRetainedRegularSources(t *testing.T "traversal": "../outside.md", "missing": "docs/changes/20260710-journal-reliability-foundation/research/missing.md", "directory": "internal/cli", - "anchor": "docs/changes/20260710-journal-reliability-foundation/research/u8-target-capability-survey.md#survey", + "anchor": "docs/changes/20260710-journal-reliability-foundation/research/target-capability-survey.md#survey", } { t.Run(name, func(t *testing.T) { var contract TargetCapabilityEvidenceContract @@ -337,7 +337,7 @@ func TestTargetCapabilityEvidenceLoadRequiresRetainedRegularSources(t *testing.T t.Fatal(err) } contract.Records[1].Completion.Evidence.Source = source - path := filepath.Join(filepath.Dir(configPath), ".u8-source-test-"+strings.ReplaceAll(name, " ", "-")+".json") + path := filepath.Join(filepath.Dir(configPath), ".capability-source-test-"+strings.ReplaceAll(name, " ", "-")+".json") encoded, err := json.Marshal(contract) if err != nil { t.Fatal(err) @@ -361,7 +361,7 @@ func TestTargetCapabilityEvidenceLoadRequiresRetainedRegularSources(t *testing.T if err := os.WriteFile(outside, []byte("outside"), 0o600); err != nil { t.Fatal(err) } - relativeLink := filepath.Join("internal", "cli", ".u8-source-symlink") + relativeLink := filepath.Join("internal", "cli", ".capability-source-symlink") link := filepath.Join(filepath.Dir(filepath.Dir(configPath)), relativeLink) if err := os.MkdirAll(filepath.Dir(link), 0o755); err != nil { t.Fatal(err) @@ -375,7 +375,7 @@ func TestTargetCapabilityEvidenceLoadRequiresRetainedRegularSources(t *testing.T t.Fatal(err) } contract.Records[1].Completion.Evidence.Source = filepath.ToSlash(relativeLink) - path := filepath.Join(filepath.Dir(configPath), ".u8-source-test-symlink.json") + path := filepath.Join(filepath.Dir(configPath), ".capability-source-test-symlink.json") encoded, err := json.Marshal(contract) if err != nil { t.Fatal(err) @@ -394,7 +394,7 @@ func TestTargetCapabilityEvidenceLoadRequiresRetainedRegularSources(t *testing.T if err := os.WriteFile(outside, []byte("outside"), 0o600); err != nil { t.Fatal(err) } - relativeLink := "internal/.u8-source-linkdir" + relativeLink := "internal/.capability-source-linkdir" link := filepath.Join(filepath.Dir(filepath.Dir(configPath)), filepath.FromSlash(relativeLink)) if err := os.Symlink(outsideDir, link); err != nil { t.Fatal(err) @@ -405,7 +405,7 @@ func TestTargetCapabilityEvidenceLoadRequiresRetainedRegularSources(t *testing.T t.Fatal(err) } contract.Records[1].Completion.Evidence.Source = filepath.ToSlash(filepath.Join(relativeLink, "evidence.md")) - path := filepath.Join(filepath.Dir(configPath), ".u8-source-test-intermediate-symlink.json") + path := filepath.Join(filepath.Dir(configPath), ".capability-source-test-intermediate-symlink.json") encoded, err := json.Marshal(contract) if err != nil { t.Fatal(err) @@ -421,7 +421,7 @@ func TestTargetCapabilityEvidenceLoadRequiresRetainedRegularSources(t *testing.T } func TestInstalledSmokeEvidenceRejectsUnknownVersionsAndHashDrift(t *testing.T) { - data, err := os.ReadFile(filepath.Join(filepath.Dir(filepath.Dir(testTargetCapabilityEvidencePath(t))), "docs/changes/20260710-journal-reliability-foundation/research/u8-claude-code-2.1.218-candidate-smoke.json")) + data, err := os.ReadFile(filepath.Join(filepath.Dir(filepath.Dir(testTargetCapabilityEvidencePath(t))), "docs/changes/20260710-journal-reliability-foundation/research/claude-code-2.1.218-plugin-startup-smoke.json")) if err != nil { t.Fatal(err) } @@ -483,7 +483,7 @@ func TestOpenCodeInstalledSmokeEvidenceAcceptsFixture(t *testing.T) { } func TestOpenCodeInstalledSmokeEvidenceRejectsFalseBooleansIdentityInvocationHashAndCleanup(t *testing.T) { - receiptPath := filepath.Join(testRepositoryRoot(t), "docs/changes/20260710-journal-reliability-foundation/research/u8-opencode-1.18.4-isolated-smoke.json") + receiptPath := filepath.Join(testRepositoryRoot(t), "docs/changes/20260710-journal-reliability-foundation/research/opencode-1.18.4-isolated-request-smoke.json") data, err := os.ReadFile(receiptPath) if err != nil { t.Fatal(err) @@ -546,7 +546,7 @@ func TestOpenCodeInstalledSmokeEvidenceRejectsFalseBooleansIdentityInvocationHas } func TestClaudeInstalledSmokeEvidenceRejectsPlatformSwappedNativeBinaryPath(t *testing.T) { - receiptPath := filepath.Join(testRepositoryRoot(t), "docs/changes/20260710-journal-reliability-foundation/research/u8-claude-code-2.1.218-candidate-smoke.json") + receiptPath := filepath.Join(testRepositoryRoot(t), "docs/changes/20260710-journal-reliability-foundation/research/claude-code-2.1.218-plugin-startup-smoke.json") raw := readSmokeReceiptRaw(t, receiptPath) artifacts := raw["candidate_artifacts"].(map[string]any) sourceNativePath := artifacts["native_binary_path"].(string) @@ -568,7 +568,7 @@ func TestClaudeInstalledSmokeEvidenceRejectsPlatformSwappedNativeBinaryPath(t *t } func TestCodexInstalledSmokeEvidenceRejectsPlatformSwappedNativeBinaryPath(t *testing.T) { - receiptPath := filepath.Join(testRepositoryRoot(t), "docs/changes/20260710-journal-reliability-foundation/research/u8-codex-0.145.0-isolated-smoke.json") + receiptPath := filepath.Join(testRepositoryRoot(t), "docs/changes/20260710-journal-reliability-foundation/research/codex-0.145.0-isolated-startup-smoke.json") raw := readSmokeReceiptRaw(t, receiptPath) artifacts := raw["candidate_artifacts"].(map[string]any) sourceNativePath := artifacts["native_binary_path"].(string) @@ -601,7 +601,7 @@ func TestInstalledSmokeEvidenceRejectsCrossTargetNativeBinaryPaths(t *testing.T) { name: "claude-receives-codex-path", target: "claude-code", - receipt: "u8-claude-code-2.1.218-candidate-smoke.json", + receipt: "claude-code-2.1.218-plugin-startup-smoke.json", modeName: "startup", wrongPath: "bin/native/darwin-arm64/loaf", validateFn: validateInstalledSmokeEvidence, @@ -609,7 +609,7 @@ func TestInstalledSmokeEvidenceRejectsCrossTargetNativeBinaryPaths(t *testing.T) { name: "codex-receives-claude-path", target: "codex", - receipt: "u8-codex-0.145.0-isolated-smoke.json", + receipt: "codex-0.145.0-isolated-startup-smoke.json", modeName: "startup", wrongPath: "plugins/loaf/bin/native/darwin-arm64/loaf", validateFn: validateCodexInstalledSmokeEvidence, diff --git a/package.json b/package.json index fabfb81e..70af050f 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "typecheck": "go test ./... -run=^$", "test": "go test ./...", "test:smoke": "node cli/scripts/smoke-test.js", + "test:capability-runners": "node --test cli/scripts/smoke-claude-code-startup.test.mjs cli/scripts/smoke-codex-startup.test.mjs cli/scripts/smoke-opencode-request-context.test.mjs cli/scripts/preflight-cursor-agent-context.test.mjs", "eval:routing": "node cli/scripts/eval-skill-routing.mjs", "prepare": "npm run build:go", "prepublishOnly": "npm run build:release"